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,137 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Annotated, Literal
|
|
3
|
+
|
|
4
|
+
from fastapi.exceptions import RequestValidationError
|
|
5
|
+
from pydantic import BaseModel, Field, RootModel, ValidationError
|
|
6
|
+
from ulid import ULID
|
|
7
|
+
|
|
8
|
+
from core_framework.api.schemas import BasePatchRequest
|
|
9
|
+
from core_framework.api.users.shared.schemas import (
|
|
10
|
+
AvatarMixin,
|
|
11
|
+
BannerMixin,
|
|
12
|
+
Bio,
|
|
13
|
+
DisplayName,
|
|
14
|
+
UserID,
|
|
15
|
+
Username,
|
|
16
|
+
UserReference,
|
|
17
|
+
UserStatus,
|
|
18
|
+
)
|
|
19
|
+
from core_framework.domains.moderation import RestrictionCategory, RestrictionType
|
|
20
|
+
from core_framework.domains.user import ProfileVisibility, UserRole
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UserSummaryResponse(AvatarMixin):
|
|
24
|
+
user_id: UserID
|
|
25
|
+
username: str
|
|
26
|
+
display_name: DisplayName | None = None
|
|
27
|
+
role: UserRole
|
|
28
|
+
created_at: datetime
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UserIdentity(AvatarMixin):
|
|
32
|
+
user_id: UserID
|
|
33
|
+
username: str
|
|
34
|
+
display_name: DisplayName | None = None
|
|
35
|
+
role: UserRole
|
|
36
|
+
created_at: datetime
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class UserRestriction(BaseModel):
|
|
40
|
+
status: RestrictionType
|
|
41
|
+
category: RestrictionCategory
|
|
42
|
+
expires_at: datetime | None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class InternalNoteRequest(BaseModel):
|
|
46
|
+
content: Annotated[str, Field(..., min_length=3, max_length=200)]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class InternalNoteResponse(BaseModel):
|
|
50
|
+
id: int
|
|
51
|
+
content: str
|
|
52
|
+
created_at: datetime
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class InternalNoteDetailResponse(BaseModel):
|
|
56
|
+
id: int
|
|
57
|
+
content: str
|
|
58
|
+
created_at: datetime
|
|
59
|
+
actor: UserReference
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class UserModeration(BaseModel):
|
|
63
|
+
restriction: UserRestriction
|
|
64
|
+
notes: list[InternalNoteResponse]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AdminAccountResponse(BaseModel):
|
|
68
|
+
user_id: UserID
|
|
69
|
+
username: str
|
|
70
|
+
role: UserRole
|
|
71
|
+
email: str | None = None
|
|
72
|
+
email_verified: bool
|
|
73
|
+
deletion_scheduled_for: datetime | None = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class AdminProfileResponse(AvatarMixin, BannerMixin):
|
|
77
|
+
display_name: DisplayName | None = None
|
|
78
|
+
bio: Bio | None = None
|
|
79
|
+
status: UserStatus | None = None
|
|
80
|
+
social_links: Annotated[dict[str, str], Field(default_factory=dict)]
|
|
81
|
+
profile_visibility: ProfileVisibility
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class UserDetailResponse(BaseModel):
|
|
85
|
+
account: AdminAccountResponse
|
|
86
|
+
profile: AdminProfileResponse
|
|
87
|
+
moderation: UserModeration
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class UserSearchQueryParams(BaseModel):
|
|
91
|
+
username: Annotated[str | None, Field(default=None, min_length=1, max_length=50)]
|
|
92
|
+
role: Annotated[UserRole | None, Field(default=None)]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class UsernameChangeRequest(BaseModel):
|
|
96
|
+
username: Username
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class RoleChangeRequest(BaseModel):
|
|
100
|
+
role: Literal["member", "moderator"]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AdminProfileUpdateRequest(BasePatchRequest):
|
|
104
|
+
display_name: DisplayName | None = None
|
|
105
|
+
avatar_id: ULID | None = None
|
|
106
|
+
banner_id: ULID | None = None
|
|
107
|
+
bio: Bio | None = None
|
|
108
|
+
status: UserStatus | None = None
|
|
109
|
+
social_links: dict[str, str] | None = None
|
|
110
|
+
profile_visibility: ProfileVisibility | None = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class UserChangeHistoryItem(BaseModel):
|
|
114
|
+
user_id: UserID
|
|
115
|
+
actor: UserReference | None
|
|
116
|
+
entity_type: str
|
|
117
|
+
field: str
|
|
118
|
+
old_value: str | None
|
|
119
|
+
new_value: str | None
|
|
120
|
+
created_at: datetime
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class NoteID(RootModel[int]):
|
|
124
|
+
root: Annotated[
|
|
125
|
+
int,
|
|
126
|
+
Field(
|
|
127
|
+
description="The ID of the internal note",
|
|
128
|
+
ge=1,
|
|
129
|
+
),
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def validate_note_id(*, note_id: int) -> NoteID:
|
|
134
|
+
try:
|
|
135
|
+
return NoteID(root=note_id)
|
|
136
|
+
except ValidationError as e:
|
|
137
|
+
raise RequestValidationError(e.errors()) from e
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, status
|
|
4
|
+
|
|
5
|
+
from core_framework.api.auth.schemas import CreatedUserResponse
|
|
6
|
+
from core_framework.api.dependencies import RequiredFirebaseUser
|
|
7
|
+
from core_framework.application.auth.auth_service import register_user
|
|
8
|
+
|
|
9
|
+
router = APIRouter(
|
|
10
|
+
prefix="/auth",
|
|
11
|
+
tags=["auth"],
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@router.post(
|
|
16
|
+
"/register",
|
|
17
|
+
status_code=status.HTTP_201_CREATED,
|
|
18
|
+
response_model=CreatedUserResponse,
|
|
19
|
+
)
|
|
20
|
+
async def post_register(firebase_user: RequiredFirebaseUser) -> Any:
|
|
21
|
+
return await register_user(user_id=firebase_user.user_id.root)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, EmailStr, Field, RootModel
|
|
4
|
+
|
|
5
|
+
from core_framework.api.users.shared.schemas import UserID, Username
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CountryCode(RootModel[str]):
|
|
9
|
+
root: Annotated[
|
|
10
|
+
str,
|
|
11
|
+
Field(
|
|
12
|
+
description="ISO 3166-1 alpha-2 country code",
|
|
13
|
+
pattern="^[A-Z]{2}$",
|
|
14
|
+
examples=["US", "GB"],
|
|
15
|
+
),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CreatedUserResponse(BaseModel):
|
|
20
|
+
user_id: UserID
|
|
21
|
+
username: Username
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FirebaseUser(BaseModel):
|
|
25
|
+
user_id: UserID
|
|
26
|
+
email: EmailStr
|
|
27
|
+
email_verified: bool
|
|
28
|
+
sign_in_provider: str
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from fastapi import APIRouter, BackgroundTasks, Depends, status
|
|
2
|
+
from ulid import ULID
|
|
3
|
+
|
|
4
|
+
from core_framework.api.comments.authenticated.schemas import (
|
|
5
|
+
CommentReportRequest,
|
|
6
|
+
CreateCommentRequest,
|
|
7
|
+
CreateReplyRequest,
|
|
8
|
+
UpdateCommentRequest,
|
|
9
|
+
)
|
|
10
|
+
from core_framework.api.dependencies import RequiredUserID, check_not_banned
|
|
11
|
+
from core_framework.application.comments.authenticated_service import (
|
|
12
|
+
add_comment_report,
|
|
13
|
+
create_comment,
|
|
14
|
+
create_reply,
|
|
15
|
+
delete_comment,
|
|
16
|
+
edit_comment,
|
|
17
|
+
like_comment,
|
|
18
|
+
mark_comment_stats_dirty,
|
|
19
|
+
remove_comment_report,
|
|
20
|
+
unlike_comment,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
router = APIRouter(
|
|
24
|
+
prefix="/comments",
|
|
25
|
+
tags=["comments - authenticated"],
|
|
26
|
+
dependencies=[Depends(check_not_banned)],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.put("/{comment_id}/like", status_code=status.HTTP_204_NO_CONTENT)
|
|
31
|
+
async def put_comment_like(
|
|
32
|
+
comment_id: ULID,
|
|
33
|
+
user_id: RequiredUserID,
|
|
34
|
+
background_tasks: BackgroundTasks,
|
|
35
|
+
) -> None:
|
|
36
|
+
await like_comment(comment_id=str(comment_id), user_id=user_id.root)
|
|
37
|
+
background_tasks.add_task(mark_comment_stats_dirty, comment_id=str(comment_id))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@router.delete("/{comment_id}/like", status_code=status.HTTP_204_NO_CONTENT)
|
|
41
|
+
async def delete_comment_like(
|
|
42
|
+
comment_id: ULID,
|
|
43
|
+
user_id: RequiredUserID,
|
|
44
|
+
background_tasks: BackgroundTasks,
|
|
45
|
+
) -> None:
|
|
46
|
+
await unlike_comment(comment_id=str(comment_id), user_id=user_id.root)
|
|
47
|
+
background_tasks.add_task(mark_comment_stats_dirty, comment_id=str(comment_id))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@router.post("/{comment_id}/report", status_code=status.HTTP_204_NO_CONTENT)
|
|
51
|
+
async def post_comment_report(
|
|
52
|
+
comment_id: ULID,
|
|
53
|
+
user_id: RequiredUserID,
|
|
54
|
+
report_request: CommentReportRequest,
|
|
55
|
+
background_tasks: BackgroundTasks,
|
|
56
|
+
) -> None:
|
|
57
|
+
await add_comment_report(
|
|
58
|
+
reporter_id=user_id.root,
|
|
59
|
+
comment_id=str(comment_id),
|
|
60
|
+
category=report_request.category,
|
|
61
|
+
reason=report_request.reason,
|
|
62
|
+
)
|
|
63
|
+
background_tasks.add_task(mark_comment_stats_dirty, comment_id=str(comment_id))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@router.delete("/{comment_id}/report", status_code=status.HTTP_204_NO_CONTENT)
|
|
67
|
+
async def delete_comment_report(
|
|
68
|
+
comment_id: ULID,
|
|
69
|
+
user_id: RequiredUserID,
|
|
70
|
+
background_tasks: BackgroundTasks,
|
|
71
|
+
) -> None:
|
|
72
|
+
await remove_comment_report(reporter_id=user_id.root, comment_id=str(comment_id))
|
|
73
|
+
background_tasks.add_task(mark_comment_stats_dirty, comment_id=str(comment_id))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@router.patch("/{comment_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
77
|
+
async def patch_comment(
|
|
78
|
+
comment_id: ULID,
|
|
79
|
+
user_id: RequiredUserID,
|
|
80
|
+
request_body: UpdateCommentRequest,
|
|
81
|
+
) -> None:
|
|
82
|
+
validated_update_request = request_body.model_dump(mode="json", exclude_unset=True)
|
|
83
|
+
await edit_comment(
|
|
84
|
+
comment_id=str(comment_id),
|
|
85
|
+
author_id=user_id.root,
|
|
86
|
+
validated_update_request=validated_update_request,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@router.delete("/{comment_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
91
|
+
async def delete_comment_route(
|
|
92
|
+
comment_id: ULID,
|
|
93
|
+
user_id: RequiredUserID,
|
|
94
|
+
background_tasks: BackgroundTasks,
|
|
95
|
+
) -> None:
|
|
96
|
+
parent_comment_id = await delete_comment(
|
|
97
|
+
comment_id=str(comment_id),
|
|
98
|
+
author_id=user_id.root,
|
|
99
|
+
)
|
|
100
|
+
if parent_comment_id is not None:
|
|
101
|
+
background_tasks.add_task(mark_comment_stats_dirty, comment_id=parent_comment_id)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@router.post("", status_code=status.HTTP_201_CREATED)
|
|
105
|
+
async def post_comment(user_id: RequiredUserID, request_body: CreateCommentRequest) -> None:
|
|
106
|
+
await create_comment(
|
|
107
|
+
author_id=user_id.root,
|
|
108
|
+
content=request_body.content.root,
|
|
109
|
+
subject_type=request_body.subject_type,
|
|
110
|
+
subject_id=request_body.subject_id,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@router.post("/{comment_id}/replies", status_code=status.HTTP_201_CREATED)
|
|
115
|
+
async def post_reply(
|
|
116
|
+
comment_id: ULID,
|
|
117
|
+
user_id: RequiredUserID,
|
|
118
|
+
request_body: CreateReplyRequest,
|
|
119
|
+
background_tasks: BackgroundTasks,
|
|
120
|
+
) -> None:
|
|
121
|
+
await create_reply(
|
|
122
|
+
author_id=user_id.root,
|
|
123
|
+
content=request_body.content.root,
|
|
124
|
+
parent_comment_id=str(comment_id),
|
|
125
|
+
)
|
|
126
|
+
background_tasks.add_task(mark_comment_stats_dirty, comment_id=str(comment_id))
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from core_framework.api.comments.schemas import CommentContent
|
|
6
|
+
from core_framework.api.schemas import BasePatchRequest
|
|
7
|
+
from core_framework.domains.comment import CommentSubjectType
|
|
8
|
+
from core_framework.domains.moderation import ReportCategory
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UpdateCommentRequest(BasePatchRequest):
|
|
12
|
+
content: CommentContent | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CommentReportRequest(BaseModel):
|
|
16
|
+
category: ReportCategory
|
|
17
|
+
reason: Annotated[str, Field(default="", max_length=250)]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CreateCommentRequest(BaseModel):
|
|
21
|
+
content: CommentContent
|
|
22
|
+
subject_type: CommentSubjectType
|
|
23
|
+
subject_id: Annotated[str, Field(min_length=1)]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CreateReplyRequest(BaseModel):
|
|
27
|
+
content: CommentContent
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from typing import Annotated, Any
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
|
4
|
+
from ulid import ULID
|
|
5
|
+
|
|
6
|
+
from core_framework.api.comments.public.schemas import CommentResponse
|
|
7
|
+
from core_framework.api.dependencies import OptionalUserID
|
|
8
|
+
from core_framework.api.users.shared.schemas import validate_user_id
|
|
9
|
+
from core_framework.application.comments.public_service import (
|
|
10
|
+
retrieve_comment_by_id,
|
|
11
|
+
retrieve_comments,
|
|
12
|
+
retrieve_comments_by_user_id,
|
|
13
|
+
retrieve_replies,
|
|
14
|
+
)
|
|
15
|
+
from core_framework.core.pagination import (
|
|
16
|
+
PaginationCursorType,
|
|
17
|
+
PaginationResponse,
|
|
18
|
+
TokenCursorQueryParams,
|
|
19
|
+
paginate_cursor,
|
|
20
|
+
)
|
|
21
|
+
from core_framework.domains.comment import CommentSubjectType
|
|
22
|
+
|
|
23
|
+
router = APIRouter(
|
|
24
|
+
prefix="/comments",
|
|
25
|
+
tags=["comments - public"],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@router.get("/users/{user_id}", response_model=PaginationResponse[CommentResponse])
|
|
30
|
+
async def get_comments_by_user_id(
|
|
31
|
+
user_id: str,
|
|
32
|
+
request: Request,
|
|
33
|
+
viewer_id: OptionalUserID,
|
|
34
|
+
pagination_params: Annotated[TokenCursorQueryParams, Depends()],
|
|
35
|
+
) -> Any:
|
|
36
|
+
validated_user_id = validate_user_id(user_id=user_id)
|
|
37
|
+
comments = await retrieve_comments_by_user_id(
|
|
38
|
+
user_id=validated_user_id.root,
|
|
39
|
+
cursor=pagination_params.cursor,
|
|
40
|
+
limit=pagination_params.limit,
|
|
41
|
+
viewer_id=viewer_id.root if viewer_id else None,
|
|
42
|
+
)
|
|
43
|
+
return paginate_cursor(
|
|
44
|
+
request=request,
|
|
45
|
+
items=comments,
|
|
46
|
+
limit=pagination_params.limit,
|
|
47
|
+
cursor_field=PaginationCursorType.CREATED_AT,
|
|
48
|
+
retrieved=pagination_params.retrieved,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@router.get("", response_model=PaginationResponse[CommentResponse])
|
|
53
|
+
async def get_comments(
|
|
54
|
+
request: Request,
|
|
55
|
+
subject_type: CommentSubjectType,
|
|
56
|
+
subject_id: Annotated[str, Query(min_length=1)],
|
|
57
|
+
pagination_params: Annotated[TokenCursorQueryParams, Depends()],
|
|
58
|
+
viewer_id: OptionalUserID,
|
|
59
|
+
) -> Any:
|
|
60
|
+
comments = await retrieve_comments(
|
|
61
|
+
subject_type=subject_type,
|
|
62
|
+
subject_id=subject_id,
|
|
63
|
+
cursor=pagination_params.cursor,
|
|
64
|
+
limit=pagination_params.limit,
|
|
65
|
+
viewer_id=viewer_id.root if viewer_id else None,
|
|
66
|
+
)
|
|
67
|
+
return paginate_cursor(
|
|
68
|
+
request=request,
|
|
69
|
+
items=comments,
|
|
70
|
+
limit=pagination_params.limit,
|
|
71
|
+
cursor_field=PaginationCursorType.CREATED_AT,
|
|
72
|
+
retrieved=pagination_params.retrieved,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@router.get("/{comment_id}/replies", response_model=PaginationResponse[CommentResponse])
|
|
77
|
+
async def get_replies(
|
|
78
|
+
comment_id: ULID,
|
|
79
|
+
request: Request,
|
|
80
|
+
pagination_params: Annotated[TokenCursorQueryParams, Depends()],
|
|
81
|
+
viewer_id: OptionalUserID,
|
|
82
|
+
) -> Any:
|
|
83
|
+
replies = await retrieve_replies(
|
|
84
|
+
parent_comment_id=str(comment_id),
|
|
85
|
+
cursor=pagination_params.cursor,
|
|
86
|
+
limit=pagination_params.limit,
|
|
87
|
+
viewer_id=viewer_id.root if viewer_id else None,
|
|
88
|
+
)
|
|
89
|
+
return paginate_cursor(
|
|
90
|
+
request=request,
|
|
91
|
+
items=replies,
|
|
92
|
+
limit=pagination_params.limit,
|
|
93
|
+
cursor_field=PaginationCursorType.CREATED_AT,
|
|
94
|
+
retrieved=pagination_params.retrieved,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@router.get("/{comment_id}", response_model=CommentResponse)
|
|
99
|
+
async def get_comment_by_id(comment_id: ULID, viewer_id: OptionalUserID) -> Any:
|
|
100
|
+
comment = await retrieve_comment_by_id(comment_id=str(comment_id), viewer_id=viewer_id.root if viewer_id else None)
|
|
101
|
+
if comment is None:
|
|
102
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Comment not found")
|
|
103
|
+
return comment
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
from ulid import ULID
|
|
6
|
+
|
|
7
|
+
from core_framework.api.comments.schemas import CommentContent
|
|
8
|
+
from core_framework.api.users.shared.schemas import UserReference
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CommentStatsResponse(BaseModel):
|
|
12
|
+
like_count: Annotated[int, Field(ge=0)] = 0
|
|
13
|
+
reply_count: Annotated[int, Field(ge=0)] = 0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CommentViewerContext(BaseModel):
|
|
17
|
+
is_liked: bool
|
|
18
|
+
is_reported: bool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthorContext(BaseModel):
|
|
22
|
+
can_edit: bool
|
|
23
|
+
can_delete: bool
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CommentResponse(BaseModel):
|
|
27
|
+
id: ULID
|
|
28
|
+
author: UserReference
|
|
29
|
+
content: CommentContent
|
|
30
|
+
stats: CommentStatsResponse
|
|
31
|
+
can_reply: bool
|
|
32
|
+
engagement_allowed: bool
|
|
33
|
+
edited_at: datetime | None
|
|
34
|
+
created_at: datetime
|
|
35
|
+
viewer_context: CommentViewerContext | None
|
|
36
|
+
author_context: AuthorContext | None
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
|
|
3
|
+
from core_framework.api.comments.authenticated.router import router as authenticated_router
|
|
4
|
+
from core_framework.api.comments.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,17 @@
|
|
|
1
|
+
from typing import Annotated, Final
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, RootModel, field_validator
|
|
4
|
+
|
|
5
|
+
MAX_COMMENT_CONTENT_LENGTH: Final[int] = 5000
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CommentContent(RootModel[str]):
|
|
9
|
+
root: Annotated[str, Field(min_length=1, max_length=MAX_COMMENT_CONTENT_LENGTH)]
|
|
10
|
+
|
|
11
|
+
@field_validator("root", mode="after")
|
|
12
|
+
@classmethod
|
|
13
|
+
def validate_not_blank(cls, v: str) -> str:
|
|
14
|
+
s = v.strip()
|
|
15
|
+
if not s:
|
|
16
|
+
raise ValueError("Comment content must not be blank")
|
|
17
|
+
return s
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from typing import Annotated, Any, Final
|
|
2
|
+
|
|
3
|
+
from async_lru import alru_cache
|
|
4
|
+
from asyncer import asyncify
|
|
5
|
+
from fastapi import Depends, Header, Security
|
|
6
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
7
|
+
from firebase_admin import auth
|
|
8
|
+
|
|
9
|
+
from core_framework.api.auth.schemas import CountryCode, FirebaseUser
|
|
10
|
+
from core_framework.api.users.shared.schemas import UserID
|
|
11
|
+
from core_framework.application.auth.access_service import get_user_detail
|
|
12
|
+
from core_framework.application.auth.models import UserDetail
|
|
13
|
+
from core_framework.application.shared.exceptions import ForbiddenException, UnauthorizedException
|
|
14
|
+
from core_framework.application.shared.user_agent import parse_user_agent
|
|
15
|
+
from core_framework.application.users.admin_service import retrieve_admin_user_ids
|
|
16
|
+
from core_framework.core.context import user_id
|
|
17
|
+
from core_framework.domains.moderation import RestrictionType
|
|
18
|
+
from core_framework.domains.user import UserIdentity
|
|
19
|
+
|
|
20
|
+
MAX_REFERRER_LENGTH: Final[int] = 512
|
|
21
|
+
|
|
22
|
+
bearer_scheme = HTTPBearer(auto_error=False)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@alru_cache(maxsize=5000, ttl=300) # ~10MB per worker, 5 minutes TTL
|
|
26
|
+
async def _verify_id_token(token: str) -> dict[str, Any]:
|
|
27
|
+
return await asyncify(auth.verify_id_token)(token)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _build_firebase_user_from_claims(user_dict: dict[str, Any]) -> FirebaseUser | None:
|
|
31
|
+
uid = user_dict.get("uid")
|
|
32
|
+
email = user_dict.get("email")
|
|
33
|
+
email_verified = user_dict.get("email_verified")
|
|
34
|
+
firebase_claim = user_dict.get("firebase")
|
|
35
|
+
|
|
36
|
+
if not isinstance(uid, str):
|
|
37
|
+
return None
|
|
38
|
+
if not isinstance(email, str):
|
|
39
|
+
return None
|
|
40
|
+
if not isinstance(email_verified, bool):
|
|
41
|
+
return None
|
|
42
|
+
if not isinstance(firebase_claim, dict):
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
sign_in_provider = firebase_claim.get("sign_in_provider")
|
|
46
|
+
if not isinstance(sign_in_provider, str):
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
return FirebaseUser(
|
|
51
|
+
user_id=UserID(uid),
|
|
52
|
+
email=email,
|
|
53
|
+
email_verified=email_verified,
|
|
54
|
+
sign_in_provider=sign_in_provider,
|
|
55
|
+
)
|
|
56
|
+
except TypeError, ValueError:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def _get_bearer_token(
|
|
61
|
+
creds: Annotated[HTTPAuthorizationCredentials | None, Security(bearer_scheme)],
|
|
62
|
+
) -> str | None:
|
|
63
|
+
if not creds:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
if creds.scheme.lower() != "bearer":
|
|
67
|
+
raise UnauthorizedException("Invalid token scheme")
|
|
68
|
+
|
|
69
|
+
return creds.credentials
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def _get_firebase_user(token: Annotated[str | None, Depends(_get_bearer_token)]) -> FirebaseUser | None:
|
|
73
|
+
if not token:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
user_dict = await _verify_id_token(token)
|
|
78
|
+
except (
|
|
79
|
+
ValueError,
|
|
80
|
+
auth.InvalidIdTokenError,
|
|
81
|
+
auth.ExpiredIdTokenError,
|
|
82
|
+
auth.RevokedIdTokenError,
|
|
83
|
+
auth.CertificateFetchError,
|
|
84
|
+
auth.UserDisabledError,
|
|
85
|
+
):
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
firebase_user = _build_firebase_user_from_claims(user_dict)
|
|
89
|
+
if firebase_user is None:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
user_id.set(firebase_user.user_id.root)
|
|
93
|
+
return firebase_user
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def _get_required_firebase_user(
|
|
97
|
+
user: Annotated[FirebaseUser | None, Depends(_get_firebase_user)],
|
|
98
|
+
) -> FirebaseUser:
|
|
99
|
+
if not user:
|
|
100
|
+
raise UnauthorizedException("Unauthorized")
|
|
101
|
+
return user
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def _get_user_id(user: Annotated[FirebaseUser | None, Depends(_get_firebase_user)]) -> UserID | None:
|
|
105
|
+
if not user:
|
|
106
|
+
return None
|
|
107
|
+
return user.user_id
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def _get_required_user_id(user_id: Annotated[UserID | None, Depends(_get_user_id)]) -> UserID:
|
|
111
|
+
if not user_id:
|
|
112
|
+
raise UnauthorizedException("Unauthorized")
|
|
113
|
+
return user_id
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def check_admin_role(user_id: Annotated[UserID, Depends(_get_required_user_id)]) -> None:
|
|
117
|
+
if user_id.root not in await retrieve_admin_user_ids():
|
|
118
|
+
raise ForbiddenException("Admin access required")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def _get_user_detail(user_id: Annotated[UserID, Depends(_get_required_user_id)]) -> UserDetail:
|
|
122
|
+
return await get_user_detail(user_id=user_id.root)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def check_not_banned(user_detail: Annotated[UserDetail, Depends(_get_user_detail)]) -> None:
|
|
126
|
+
if (
|
|
127
|
+
user_detail.identity == UserIdentity.DEFAULT
|
|
128
|
+
or user_detail.moderation.restriction.status == RestrictionType.BANNED
|
|
129
|
+
):
|
|
130
|
+
raise ForbiddenException()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def _get_request_context(
|
|
134
|
+
firebase_user: Annotated[FirebaseUser | None, Depends(_get_firebase_user)],
|
|
135
|
+
user_agent: Annotated[str | None, Header()] = None,
|
|
136
|
+
cf_ipcountry: Annotated[CountryCode | None, Header()] = None,
|
|
137
|
+
referrer: Annotated[str | None, Header(alias="Referer")] = None,
|
|
138
|
+
) -> dict[str, str]:
|
|
139
|
+
metadata: dict[str, str] = {}
|
|
140
|
+
|
|
141
|
+
if firebase_user:
|
|
142
|
+
metadata["viewer_id"] = firebase_user.user_id.root
|
|
143
|
+
metadata["provider"] = firebase_user.sign_in_provider
|
|
144
|
+
if cf_ipcountry:
|
|
145
|
+
metadata["country_code"] = cf_ipcountry.root
|
|
146
|
+
if referrer:
|
|
147
|
+
metadata["referrer"] = referrer[:MAX_REFERRER_LENGTH]
|
|
148
|
+
|
|
149
|
+
client_info = parse_user_agent(user_agent)
|
|
150
|
+
metadata.update(client_info)
|
|
151
|
+
|
|
152
|
+
return {k: v for k, v in metadata.items() if v is not None}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
RequiredFirebaseUser = Annotated[FirebaseUser, Depends(_get_required_firebase_user)]
|
|
156
|
+
OptionalUserID = Annotated[UserID | None, Depends(_get_user_id)]
|
|
157
|
+
RequiredUserID = Annotated[UserID, Depends(_get_required_user_id)]
|
|
158
|
+
RequestContext = Annotated[dict[str, str], Depends(_get_request_context)]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
__all__ = [
|
|
162
|
+
"RequestContext",
|
|
163
|
+
"RequiredFirebaseUser",
|
|
164
|
+
"OptionalUserID",
|
|
165
|
+
"RequiredUserID",
|
|
166
|
+
"check_admin_role",
|
|
167
|
+
"check_not_banned",
|
|
168
|
+
]
|