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,236 @@
|
|
|
1
|
+
from asyncio import TaskGroup
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import core_framework.domains.comment.dependencies as comment_deps
|
|
8
|
+
import core_framework.domains.moderation.dependencies as moderation_deps
|
|
9
|
+
import core_framework.domains.post.dependencies as post_deps
|
|
10
|
+
import core_framework.domains.user.dependencies as user_deps
|
|
11
|
+
from core_framework.domains.comment import (
|
|
12
|
+
CommentPreview,
|
|
13
|
+
CommentStats,
|
|
14
|
+
CommentStatus,
|
|
15
|
+
CommentSubjectType,
|
|
16
|
+
CommentWithMetadata,
|
|
17
|
+
)
|
|
18
|
+
from core_framework.domains.post import PostPreview
|
|
19
|
+
from core_framework.domains.user import UserIdentity
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def retrieve_admin_comment_by_id(*, comment_id: str) -> dict[str, Any]:
|
|
23
|
+
comment = await comment_deps.comment_service.retrieve_comment_with_metadata_by_id(comment_id=comment_id)
|
|
24
|
+
comment_ids = {comment.id}
|
|
25
|
+
author_ids = {comment.author_id}
|
|
26
|
+
post_subject_ids = {comment.subject_id} if comment.subject_type == CommentSubjectType.POST else set()
|
|
27
|
+
user_wall_subject_ids = {comment.subject_id} if comment.subject_type == CommentSubjectType.USER_WALL else set()
|
|
28
|
+
parent_comment_ids = {comment.parent_comment_id} if comment.parent_comment_id is not None else set()
|
|
29
|
+
|
|
30
|
+
async with TaskGroup() as tg:
|
|
31
|
+
stats_task = tg.create_task(
|
|
32
|
+
comment_deps.comment_service.retrieve_comment_stats_mapping(comment_ids=comment_ids)
|
|
33
|
+
)
|
|
34
|
+
post_subject_task = tg.create_task(
|
|
35
|
+
post_deps.post_service.retrieve_post_preview_mapping(post_ids=post_subject_ids)
|
|
36
|
+
)
|
|
37
|
+
parent_comment_task = tg.create_task(
|
|
38
|
+
comment_deps.comment_service.retrieve_comment_preview_mapping(comment_ids=parent_comment_ids)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
comment_stats_mapping = stats_task.result()
|
|
42
|
+
post_subject_mapping = post_subject_task.result()
|
|
43
|
+
parent_comment_mapping = parent_comment_task.result()
|
|
44
|
+
user_identity_mapping = await user_deps.user_service.retrieve_user_identity_mapping(
|
|
45
|
+
user_ids=(
|
|
46
|
+
author_ids
|
|
47
|
+
| user_wall_subject_ids
|
|
48
|
+
| {item.author_id for item in post_subject_mapping.values()}
|
|
49
|
+
| {item.author_id for item in parent_comment_mapping.values()}
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
return _serialize_admin_comment(
|
|
53
|
+
comment=comment,
|
|
54
|
+
comment_stats_mapping=comment_stats_mapping,
|
|
55
|
+
user_identity_mapping=user_identity_mapping,
|
|
56
|
+
post_subject_mapping=post_subject_mapping,
|
|
57
|
+
parent_comment_mapping=parent_comment_mapping,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def retrieve_admin_comments(
|
|
62
|
+
*,
|
|
63
|
+
cursor: datetime,
|
|
64
|
+
limit: int,
|
|
65
|
+
) -> list[dict[str, Any]]:
|
|
66
|
+
comments = await comment_deps.comment_service.retrieve_comments_with_metadata(cursor=cursor, limit=limit)
|
|
67
|
+
if not comments:
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
comment_ids = {comment.id for comment in comments}
|
|
71
|
+
author_ids = {comment.author_id for comment in comments}
|
|
72
|
+
post_subject_ids = {comment.subject_id for comment in comments if comment.subject_type == CommentSubjectType.POST}
|
|
73
|
+
user_wall_subject_ids = {
|
|
74
|
+
comment.subject_id for comment in comments if comment.subject_type == CommentSubjectType.USER_WALL
|
|
75
|
+
}
|
|
76
|
+
parent_comment_ids = {comment.parent_comment_id for comment in comments if comment.parent_comment_id is not None}
|
|
77
|
+
|
|
78
|
+
async with TaskGroup() as tg:
|
|
79
|
+
stats_task = tg.create_task(
|
|
80
|
+
comment_deps.comment_service.retrieve_comment_stats_mapping(comment_ids=comment_ids)
|
|
81
|
+
)
|
|
82
|
+
post_subject_task = tg.create_task(
|
|
83
|
+
post_deps.post_service.retrieve_post_preview_mapping(post_ids=post_subject_ids)
|
|
84
|
+
)
|
|
85
|
+
parent_comment_task = tg.create_task(
|
|
86
|
+
comment_deps.comment_service.retrieve_comment_preview_mapping(comment_ids=parent_comment_ids)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
comment_stats_mapping = stats_task.result()
|
|
90
|
+
post_subject_mapping = post_subject_task.result()
|
|
91
|
+
parent_comment_mapping = parent_comment_task.result()
|
|
92
|
+
user_identity_mapping = await user_deps.user_service.retrieve_user_identity_mapping(
|
|
93
|
+
user_ids=(
|
|
94
|
+
author_ids
|
|
95
|
+
| user_wall_subject_ids
|
|
96
|
+
| {item.author_id for item in post_subject_mapping.values()}
|
|
97
|
+
| {item.author_id for item in parent_comment_mapping.values()}
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
return [
|
|
101
|
+
_serialize_admin_comment(
|
|
102
|
+
comment=comment,
|
|
103
|
+
comment_stats_mapping=comment_stats_mapping,
|
|
104
|
+
user_identity_mapping=user_identity_mapping,
|
|
105
|
+
post_subject_mapping=post_subject_mapping,
|
|
106
|
+
parent_comment_mapping=parent_comment_mapping,
|
|
107
|
+
)
|
|
108
|
+
for comment in comments
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def set_comment_inactive(*, comment_id: str) -> str | None:
|
|
113
|
+
return await comment_deps.comment_service.set_comment_status_by_id(
|
|
114
|
+
comment_id=comment_id,
|
|
115
|
+
status=CommentStatus.DELETED,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def restore_comment(*, comment_id: str) -> str | None:
|
|
120
|
+
return await comment_deps.comment_service.set_comment_status_by_id(
|
|
121
|
+
comment_id=comment_id,
|
|
122
|
+
status=CommentStatus.ACTIVE,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def remove_comment(*, comment_id: str) -> str | None:
|
|
127
|
+
parent_comment_id = await comment_deps.comment_service.retrieve_parent_comment_id_strong(comment_id=comment_id)
|
|
128
|
+
comment_ids = await comment_deps.comment_service.retrieve_comment_subtree_ids_strong(root_comment_id=comment_id)
|
|
129
|
+
if not comment_ids:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
await moderation_deps.moderation_service.remove_comment_reports_by_target_ids(target_ids=comment_ids)
|
|
133
|
+
await comment_deps.comment_service.remove_comment_subtree(root_comment_id=comment_id)
|
|
134
|
+
return parent_comment_id
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _serialize_admin_comment(
|
|
138
|
+
*,
|
|
139
|
+
comment: CommentWithMetadata,
|
|
140
|
+
comment_stats_mapping: defaultdict[str, CommentStats],
|
|
141
|
+
user_identity_mapping: defaultdict[str, UserIdentity],
|
|
142
|
+
post_subject_mapping: Mapping[str, PostPreview],
|
|
143
|
+
parent_comment_mapping: Mapping[str, CommentPreview],
|
|
144
|
+
) -> dict[str, Any]:
|
|
145
|
+
stats = comment_stats_mapping[comment.id]
|
|
146
|
+
return {
|
|
147
|
+
"id": comment.id,
|
|
148
|
+
"author": _serialize_user_reference(user_identity_mapping=user_identity_mapping, user_id=comment.author_id),
|
|
149
|
+
"content": comment.content,
|
|
150
|
+
"subject": _serialize_subject(
|
|
151
|
+
comment=comment,
|
|
152
|
+
user_identity_mapping=user_identity_mapping,
|
|
153
|
+
post_subject_mapping=post_subject_mapping,
|
|
154
|
+
),
|
|
155
|
+
"parent_comment": _serialize_parent_comment(
|
|
156
|
+
comment=comment,
|
|
157
|
+
user_identity_mapping=user_identity_mapping,
|
|
158
|
+
parent_comment_mapping=parent_comment_mapping,
|
|
159
|
+
),
|
|
160
|
+
"status": comment.status,
|
|
161
|
+
"edited_count": comment.edited_count,
|
|
162
|
+
"stats": {
|
|
163
|
+
"like_count": stats.like_count,
|
|
164
|
+
"reply_count": stats.reply_count,
|
|
165
|
+
"report_count": stats.report_count,
|
|
166
|
+
},
|
|
167
|
+
"edited_at": comment.edited_at,
|
|
168
|
+
"created_at": comment.created_at,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _serialize_subject(
|
|
173
|
+
*,
|
|
174
|
+
comment: CommentWithMetadata,
|
|
175
|
+
user_identity_mapping: defaultdict[str, UserIdentity],
|
|
176
|
+
post_subject_mapping: Mapping[str, PostPreview],
|
|
177
|
+
) -> dict[str, Any]:
|
|
178
|
+
if comment.subject_type == CommentSubjectType.POST:
|
|
179
|
+
post_subject = post_subject_mapping[comment.subject_id]
|
|
180
|
+
return {
|
|
181
|
+
"subject_type": comment.subject_type,
|
|
182
|
+
"post_id": comment.subject_id,
|
|
183
|
+
"author": _serialize_user_reference(
|
|
184
|
+
user_identity_mapping=user_identity_mapping,
|
|
185
|
+
user_id=post_subject.author_id,
|
|
186
|
+
),
|
|
187
|
+
"content_preview": _serialize_content_preview(post_subject.content),
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
"subject_type": comment.subject_type,
|
|
192
|
+
"user": _serialize_user_reference(
|
|
193
|
+
user_identity_mapping=user_identity_mapping,
|
|
194
|
+
user_id=comment.subject_id,
|
|
195
|
+
),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _serialize_parent_comment(
|
|
200
|
+
*,
|
|
201
|
+
comment: CommentWithMetadata,
|
|
202
|
+
user_identity_mapping: defaultdict[str, UserIdentity],
|
|
203
|
+
parent_comment_mapping: Mapping[str, CommentPreview],
|
|
204
|
+
) -> dict[str, Any] | None:
|
|
205
|
+
if comment.parent_comment_id is None:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
parent_comment = parent_comment_mapping[comment.parent_comment_id]
|
|
209
|
+
return {
|
|
210
|
+
"comment_id": comment.parent_comment_id,
|
|
211
|
+
"author": _serialize_user_reference(
|
|
212
|
+
user_identity_mapping=user_identity_mapping,
|
|
213
|
+
user_id=parent_comment.author_id,
|
|
214
|
+
),
|
|
215
|
+
"content_preview": _serialize_content_preview(parent_comment.content),
|
|
216
|
+
"status": parent_comment.status,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _serialize_user_reference(
|
|
221
|
+
*,
|
|
222
|
+
user_identity_mapping: defaultdict[str, UserIdentity],
|
|
223
|
+
user_id: str,
|
|
224
|
+
) -> dict[str, Any]:
|
|
225
|
+
identity = user_identity_mapping[user_id]
|
|
226
|
+
return {
|
|
227
|
+
"user_id": user_id,
|
|
228
|
+
"username": identity.username,
|
|
229
|
+
"display_name": identity.display_name,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _serialize_content_preview(content: str) -> str:
|
|
234
|
+
if content.strip():
|
|
235
|
+
return content
|
|
236
|
+
return "[preview unavailable]"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from asyncio import TaskGroup
|
|
2
|
+
|
|
3
|
+
import core_framework.domains.comment.dependencies as comment_deps
|
|
4
|
+
import core_framework.domains.moderation.dependencies as moderation_deps
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def aggregate_comment_stats(*, comment_id: str) -> None:
|
|
8
|
+
async with TaskGroup() as tg:
|
|
9
|
+
like_task = tg.create_task(comment_deps.comment_service.retrieve_comment_like_count(comment_id=comment_id))
|
|
10
|
+
reply_task = tg.create_task(comment_deps.comment_service.retrieve_comment_reply_count(comment_id=comment_id))
|
|
11
|
+
report_task = tg.create_task(
|
|
12
|
+
moderation_deps.moderation_service.retrieve_comment_report_count(comment_id=comment_id)
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
await comment_deps.comment_service.update_comment_stats(
|
|
16
|
+
comment_id=comment_id,
|
|
17
|
+
like_count=like_task.result(),
|
|
18
|
+
reply_count=reply_task.result(),
|
|
19
|
+
report_count=report_task.result(),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def claim_comment_ids_dirty() -> list[str]:
|
|
24
|
+
return await comment_deps.comment_service.claim_comment_ids_dirty()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def mark_comment_stats_dirty(*, comment_id: str) -> None:
|
|
28
|
+
await comment_deps.comment_service.insert_comment_stats_dirty(comment_id=comment_id)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import core_framework.domains.comment.dependencies as comment_deps
|
|
4
|
+
import core_framework.domains.moderation.dependencies as moderation_deps
|
|
5
|
+
from core_framework.application.comments import aggregation_service as comment_aggregation_service
|
|
6
|
+
from core_framework.domains.comment import CommentSubjectType
|
|
7
|
+
from core_framework.domains.moderation import ReportCategory
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def create_comment(
|
|
11
|
+
*,
|
|
12
|
+
author_id: str,
|
|
13
|
+
content: str,
|
|
14
|
+
subject_type: CommentSubjectType,
|
|
15
|
+
subject_id: str,
|
|
16
|
+
) -> None:
|
|
17
|
+
await comment_deps.comment_service.add_comment(
|
|
18
|
+
author_id=author_id,
|
|
19
|
+
content=content,
|
|
20
|
+
subject_type=subject_type,
|
|
21
|
+
subject_id=subject_id,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def create_reply(
|
|
26
|
+
*,
|
|
27
|
+
author_id: str,
|
|
28
|
+
content: str,
|
|
29
|
+
parent_comment_id: str,
|
|
30
|
+
) -> None:
|
|
31
|
+
await comment_deps.comment_service.add_reply(
|
|
32
|
+
author_id=author_id,
|
|
33
|
+
content=content,
|
|
34
|
+
parent_comment_id=parent_comment_id,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def edit_comment(
|
|
39
|
+
*,
|
|
40
|
+
comment_id: str,
|
|
41
|
+
author_id: str,
|
|
42
|
+
validated_update_request: dict[str, Any],
|
|
43
|
+
) -> None:
|
|
44
|
+
await comment_deps.comment_service.edit_comment(
|
|
45
|
+
comment_id=comment_id,
|
|
46
|
+
author_id=author_id,
|
|
47
|
+
validated_update_request=validated_update_request,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def like_comment(*, comment_id: str, user_id: str) -> None:
|
|
52
|
+
await comment_deps.comment_service.add_comment_like(comment_id=comment_id, liker_id=user_id)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def unlike_comment(*, comment_id: str, user_id: str) -> None:
|
|
56
|
+
await comment_deps.comment_service.remove_comment_like(comment_id=comment_id, liker_id=user_id)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def add_comment_report(
|
|
60
|
+
*,
|
|
61
|
+
reporter_id: str,
|
|
62
|
+
comment_id: str,
|
|
63
|
+
category: ReportCategory,
|
|
64
|
+
reason: str,
|
|
65
|
+
) -> None:
|
|
66
|
+
await moderation_deps.moderation_service.add_comment_report(
|
|
67
|
+
reporter_id=reporter_id,
|
|
68
|
+
target_id=comment_id,
|
|
69
|
+
category=category,
|
|
70
|
+
reason=reason,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def remove_comment_report(*, reporter_id: str, comment_id: str) -> None:
|
|
75
|
+
await moderation_deps.moderation_service.remove_comment_report_by_reporter(
|
|
76
|
+
reporter_id=reporter_id,
|
|
77
|
+
target_id=comment_id,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def delete_comment(*, comment_id: str, author_id: str) -> str | None:
|
|
82
|
+
return await comment_deps.comment_service.remove_comment(
|
|
83
|
+
comment_id=comment_id,
|
|
84
|
+
author_id=author_id,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def mark_comment_stats_dirty(*, comment_id: str) -> None:
|
|
89
|
+
await comment_aggregation_service.mark_comment_stats_dirty(comment_id=comment_id)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from asyncio import TaskGroup
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import core_framework.domains.comment.dependencies as comment_deps
|
|
7
|
+
import core_framework.domains.moderation.dependencies as moderation_deps
|
|
8
|
+
import core_framework.domains.user.dependencies as user_deps
|
|
9
|
+
from core_framework.domains.comment import Comment, CommentStats, CommentSubjectType
|
|
10
|
+
from core_framework.domains.user import REDACTED_AUTHOR_ID, UserIdentity
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def _retrieve_viewer_context_ids(
|
|
14
|
+
*,
|
|
15
|
+
viewer_id: str | None,
|
|
16
|
+
comment_ids: set[str],
|
|
17
|
+
) -> tuple[frozenset[str] | None, frozenset[str] | None]:
|
|
18
|
+
if viewer_id is None or not comment_ids:
|
|
19
|
+
return None, None
|
|
20
|
+
async with TaskGroup() as tg:
|
|
21
|
+
liked_task = tg.create_task(
|
|
22
|
+
comment_deps.comment_service.retrieve_comment_ids_liked_by_user(
|
|
23
|
+
liker_id=viewer_id,
|
|
24
|
+
comment_ids=comment_ids,
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
reported_task = tg.create_task(
|
|
28
|
+
moderation_deps.moderation_service.retrieve_comment_ids_reported_by_user(
|
|
29
|
+
reporter_id=viewer_id,
|
|
30
|
+
comment_ids=comment_ids,
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
return liked_task.result(), reported_task.result()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _serialize_comment(
|
|
37
|
+
*,
|
|
38
|
+
comment: Comment,
|
|
39
|
+
comment_stats_mapping: defaultdict[str, CommentStats],
|
|
40
|
+
user_identity_mapping: defaultdict[str, UserIdentity],
|
|
41
|
+
viewer_id: str | None = None,
|
|
42
|
+
liked_ids: frozenset[str] | None = None,
|
|
43
|
+
reported_ids: frozenset[str] | None = None,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
stats = comment_stats_mapping[comment.id]
|
|
46
|
+
author_identity = user_identity_mapping[comment.author_id]
|
|
47
|
+
viewer_context = None
|
|
48
|
+
if liked_ids is not None and reported_ids is not None:
|
|
49
|
+
viewer_context = {
|
|
50
|
+
"is_liked": comment.id in liked_ids,
|
|
51
|
+
"is_reported": comment.id in reported_ids,
|
|
52
|
+
}
|
|
53
|
+
author_context = None
|
|
54
|
+
if viewer_id is not None and viewer_id == comment.author_id:
|
|
55
|
+
author_context = {
|
|
56
|
+
"can_edit": comment.edited_count < comment_deps.comment_service.MAX_EDIT_COUNT,
|
|
57
|
+
"can_delete": True,
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
"id": comment.id,
|
|
61
|
+
"author": {
|
|
62
|
+
"user_id": comment.author_id,
|
|
63
|
+
"username": author_identity.username,
|
|
64
|
+
"display_name": author_identity.display_name,
|
|
65
|
+
},
|
|
66
|
+
"content": comment.content,
|
|
67
|
+
"stats": {
|
|
68
|
+
"like_count": stats.like_count,
|
|
69
|
+
"reply_count": stats.reply_count,
|
|
70
|
+
},
|
|
71
|
+
"can_reply": comment.level < comment_deps.comment_service.MAX_REPLY_LEVEL,
|
|
72
|
+
"engagement_allowed": comment.author_id != REDACTED_AUTHOR_ID,
|
|
73
|
+
"edited_at": comment.edited_at,
|
|
74
|
+
"created_at": comment.created_at,
|
|
75
|
+
"viewer_context": viewer_context,
|
|
76
|
+
"author_context": author_context,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def retrieve_comments(
|
|
81
|
+
*,
|
|
82
|
+
subject_type: CommentSubjectType,
|
|
83
|
+
subject_id: str,
|
|
84
|
+
cursor: datetime,
|
|
85
|
+
limit: int,
|
|
86
|
+
viewer_id: str | None,
|
|
87
|
+
) -> list[dict[str, Any]]:
|
|
88
|
+
comments = await comment_deps.comment_service.retrieve_comments_by_subject(
|
|
89
|
+
subject_type=subject_type,
|
|
90
|
+
subject_id=subject_id,
|
|
91
|
+
cursor=cursor,
|
|
92
|
+
limit=limit,
|
|
93
|
+
viewer_id=viewer_id,
|
|
94
|
+
)
|
|
95
|
+
comment_ids = {c.id for c in comments}
|
|
96
|
+
author_ids = {c.author_id for c in comments}
|
|
97
|
+
async with TaskGroup() as tg:
|
|
98
|
+
liked_reported_task = tg.create_task(_retrieve_viewer_context_ids(viewer_id=viewer_id, comment_ids=comment_ids))
|
|
99
|
+
stats_task = tg.create_task(
|
|
100
|
+
comment_deps.comment_service.retrieve_comment_stats_mapping(comment_ids=comment_ids)
|
|
101
|
+
)
|
|
102
|
+
identity_task = tg.create_task(user_deps.user_service.retrieve_user_identity_mapping(user_ids=author_ids))
|
|
103
|
+
liked_ids, reported_ids = liked_reported_task.result()
|
|
104
|
+
comment_stats_mapping = stats_task.result()
|
|
105
|
+
user_identity_mapping = identity_task.result()
|
|
106
|
+
return [
|
|
107
|
+
_serialize_comment(
|
|
108
|
+
comment=comment,
|
|
109
|
+
comment_stats_mapping=comment_stats_mapping,
|
|
110
|
+
user_identity_mapping=user_identity_mapping,
|
|
111
|
+
viewer_id=viewer_id,
|
|
112
|
+
liked_ids=liked_ids,
|
|
113
|
+
reported_ids=reported_ids,
|
|
114
|
+
)
|
|
115
|
+
for comment in comments
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def retrieve_comments_by_user_id(
|
|
120
|
+
*,
|
|
121
|
+
user_id: str,
|
|
122
|
+
cursor: datetime,
|
|
123
|
+
limit: int,
|
|
124
|
+
viewer_id: str | None,
|
|
125
|
+
) -> list[dict[str, Any]]:
|
|
126
|
+
comments = await comment_deps.comment_service.retrieve_comments_by_author_id(
|
|
127
|
+
author_id=user_id,
|
|
128
|
+
cursor=cursor,
|
|
129
|
+
limit=limit,
|
|
130
|
+
viewer_id=viewer_id,
|
|
131
|
+
)
|
|
132
|
+
comment_ids = {c.id for c in comments}
|
|
133
|
+
author_ids = {c.author_id for c in comments}
|
|
134
|
+
async with TaskGroup() as tg:
|
|
135
|
+
liked_reported_task = tg.create_task(_retrieve_viewer_context_ids(viewer_id=viewer_id, comment_ids=comment_ids))
|
|
136
|
+
stats_task = tg.create_task(
|
|
137
|
+
comment_deps.comment_service.retrieve_comment_stats_mapping(comment_ids=comment_ids)
|
|
138
|
+
)
|
|
139
|
+
identity_task = tg.create_task(user_deps.user_service.retrieve_user_identity_mapping(user_ids=author_ids))
|
|
140
|
+
liked_ids, reported_ids = liked_reported_task.result()
|
|
141
|
+
comment_stats_mapping = stats_task.result()
|
|
142
|
+
user_identity_mapping = identity_task.result()
|
|
143
|
+
return [
|
|
144
|
+
_serialize_comment(
|
|
145
|
+
comment=comment,
|
|
146
|
+
comment_stats_mapping=comment_stats_mapping,
|
|
147
|
+
user_identity_mapping=user_identity_mapping,
|
|
148
|
+
viewer_id=viewer_id,
|
|
149
|
+
liked_ids=liked_ids,
|
|
150
|
+
reported_ids=reported_ids,
|
|
151
|
+
)
|
|
152
|
+
for comment in comments
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def retrieve_replies(
|
|
157
|
+
*,
|
|
158
|
+
parent_comment_id: str,
|
|
159
|
+
cursor: datetime,
|
|
160
|
+
limit: int,
|
|
161
|
+
viewer_id: str | None,
|
|
162
|
+
) -> list[dict[str, Any]]:
|
|
163
|
+
comments = await comment_deps.comment_service.retrieve_replies_by_parent_id(
|
|
164
|
+
parent_comment_id=parent_comment_id,
|
|
165
|
+
cursor=cursor,
|
|
166
|
+
limit=limit,
|
|
167
|
+
viewer_id=viewer_id,
|
|
168
|
+
)
|
|
169
|
+
comment_ids = {c.id for c in comments}
|
|
170
|
+
author_ids = {c.author_id for c in comments}
|
|
171
|
+
async with TaskGroup() as tg:
|
|
172
|
+
liked_reported_task = tg.create_task(_retrieve_viewer_context_ids(viewer_id=viewer_id, comment_ids=comment_ids))
|
|
173
|
+
stats_task = tg.create_task(
|
|
174
|
+
comment_deps.comment_service.retrieve_comment_stats_mapping(comment_ids=comment_ids)
|
|
175
|
+
)
|
|
176
|
+
identity_task = tg.create_task(user_deps.user_service.retrieve_user_identity_mapping(user_ids=author_ids))
|
|
177
|
+
liked_ids, reported_ids = liked_reported_task.result()
|
|
178
|
+
comment_stats_mapping = stats_task.result()
|
|
179
|
+
user_identity_mapping = identity_task.result()
|
|
180
|
+
return [
|
|
181
|
+
_serialize_comment(
|
|
182
|
+
comment=comment,
|
|
183
|
+
comment_stats_mapping=comment_stats_mapping,
|
|
184
|
+
user_identity_mapping=user_identity_mapping,
|
|
185
|
+
viewer_id=viewer_id,
|
|
186
|
+
liked_ids=liked_ids,
|
|
187
|
+
reported_ids=reported_ids,
|
|
188
|
+
)
|
|
189
|
+
for comment in comments
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
async def retrieve_comment_by_id(*, comment_id: str, viewer_id: str | None) -> dict[str, Any] | None:
|
|
194
|
+
comment = await comment_deps.comment_service.retrieve_comment_by_id(
|
|
195
|
+
comment_id=comment_id,
|
|
196
|
+
viewer_id=viewer_id,
|
|
197
|
+
)
|
|
198
|
+
if comment is None:
|
|
199
|
+
return None
|
|
200
|
+
comment_ids = {comment.id}
|
|
201
|
+
author_ids = {comment.author_id}
|
|
202
|
+
async with TaskGroup() as tg:
|
|
203
|
+
liked_reported_task = tg.create_task(_retrieve_viewer_context_ids(viewer_id=viewer_id, comment_ids=comment_ids))
|
|
204
|
+
stats_task = tg.create_task(
|
|
205
|
+
comment_deps.comment_service.retrieve_comment_stats_mapping(comment_ids=comment_ids)
|
|
206
|
+
)
|
|
207
|
+
identity_task = tg.create_task(user_deps.user_service.retrieve_user_identity_mapping(user_ids=author_ids))
|
|
208
|
+
liked_ids, reported_ids = liked_reported_task.result()
|
|
209
|
+
comment_stats_mapping = stats_task.result()
|
|
210
|
+
user_identity_mapping = identity_task.result()
|
|
211
|
+
return _serialize_comment(
|
|
212
|
+
comment=comment,
|
|
213
|
+
comment_stats_mapping=comment_stats_mapping,
|
|
214
|
+
user_identity_mapping=user_identity_mapping,
|
|
215
|
+
viewer_id=viewer_id,
|
|
216
|
+
liked_ids=liked_ids,
|
|
217
|
+
reported_ids=reported_ids,
|
|
218
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Events Application Layer
|
|
2
|
+
|
|
3
|
+
Handles fire-and-forget event recording for analytics and telemetry.
|
|
4
|
+
|
|
5
|
+
## Event Types
|
|
6
|
+
|
|
7
|
+
### LOGIN
|
|
8
|
+
|
|
9
|
+
- Records user login telemetry in `user_login_events`
|
|
10
|
+
- **user_id source**: Authenticated Firebase user (`viewer_id` from request context), not the event payload
|
|
11
|
+
- Unauthenticated requests are accepted (202) but no event is recorded
|
|
12
|
+
- Fire-and-forget: errors are logged, not raised
|
|
13
|
+
|
|
14
|
+
### VIEW
|
|
15
|
+
|
|
16
|
+
- Records post view events in `post_views`
|
|
17
|
+
- Uses `subject` from each event as `post_id`
|
|
18
|
+
- Fire-and-forget: errors are logged, not raised
|
|
19
|
+
|
|
20
|
+
## Flow
|
|
21
|
+
|
|
22
|
+
1. Client obtains a token via `GET /events/token`
|
|
23
|
+
1. Client posts events via `POST /events` with `type`, `token`, and `events`
|
|
24
|
+
1. Token is verified; invalid tokens cause early return (still 202)
|
|
25
|
+
1. Handler runs in background task
|
|
26
|
+
1. Handlers are keyed by `EventType`; unknown types are ignored
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
|
|
6
|
+
import core_framework.domains.post.dependencies as post_deps
|
|
7
|
+
import core_framework.domains.user.dependencies as user_deps
|
|
8
|
+
from core_framework.application.events.models import Event
|
|
9
|
+
from core_framework.application.shared.enums import EventType, SubjectType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def record_login_events(
|
|
13
|
+
*,
|
|
14
|
+
events: list[Event],
|
|
15
|
+
token: str,
|
|
16
|
+
request_context: dict[str, str],
|
|
17
|
+
) -> None:
|
|
18
|
+
viewer_id = request_context.get("viewer_id")
|
|
19
|
+
if not viewer_id:
|
|
20
|
+
return
|
|
21
|
+
try:
|
|
22
|
+
await user_deps.user_service.add_login_event(
|
|
23
|
+
user_id=viewer_id,
|
|
24
|
+
request_context=request_context,
|
|
25
|
+
)
|
|
26
|
+
except Exception:
|
|
27
|
+
logger.warning("Login event recording failed (fire-and-forget, acceptable to ignore)")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def record_view_events(
|
|
31
|
+
*,
|
|
32
|
+
events: list[Event],
|
|
33
|
+
token: str,
|
|
34
|
+
request_context: dict[str, str],
|
|
35
|
+
) -> None:
|
|
36
|
+
for event in events:
|
|
37
|
+
try:
|
|
38
|
+
if event.subject_type == SubjectType.POST:
|
|
39
|
+
await post_deps.post_service.add_post_view(
|
|
40
|
+
post_id=event.subject,
|
|
41
|
+
token=token,
|
|
42
|
+
request_context=request_context,
|
|
43
|
+
)
|
|
44
|
+
except Exception:
|
|
45
|
+
logger.warning("View event recording failed (fire-and-forget, acceptable to ignore)")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
EVENT_HANDLERS: dict[EventType, Callable[..., Any]] = {
|
|
49
|
+
EventType.VIEW: record_view_events,
|
|
50
|
+
EventType.LOGIN: record_login_events,
|
|
51
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer
|
|
2
|
+
from loguru import logger
|
|
3
|
+
|
|
4
|
+
from core_framework.application.shared.enums import EventType
|
|
5
|
+
|
|
6
|
+
EVENT_TOKEN_MAX_AGE_VIEW_SECONDS: int = 600 # 10 minutes
|
|
7
|
+
_configured_secret_key: str | None = None
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def configure_event_token(*, secret_key: str) -> None:
|
|
11
|
+
global _configured_secret_key
|
|
12
|
+
_configured_secret_key = secret_key
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _build_serializer(secret_key: str) -> URLSafeTimedSerializer:
|
|
16
|
+
return URLSafeTimedSerializer(secret_key, salt="event-token")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_serializer() -> URLSafeTimedSerializer:
|
|
20
|
+
if _configured_secret_key is None:
|
|
21
|
+
msg = "Event token is not configured. Call configure_event_token() during application bootstrap."
|
|
22
|
+
raise RuntimeError(msg)
|
|
23
|
+
return _build_serializer(_configured_secret_key)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_EVENT_MAX_AGE: dict[EventType, int | None] = {
|
|
27
|
+
EventType.VIEW: EVENT_TOKEN_MAX_AGE_VIEW_SECONDS,
|
|
28
|
+
EventType.LOGIN: None,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def generate_token() -> str:
|
|
33
|
+
return _get_serializer().dumps("")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def verify_token(token: str, *, event_type: EventType) -> bool:
|
|
37
|
+
max_age = _EVENT_MAX_AGE.get(event_type)
|
|
38
|
+
try:
|
|
39
|
+
_get_serializer().loads(token, max_age=max_age)
|
|
40
|
+
return True
|
|
41
|
+
except SignatureExpired:
|
|
42
|
+
logger.warning("Event token verification failed: token expired")
|
|
43
|
+
return False
|
|
44
|
+
except BadSignature:
|
|
45
|
+
logger.warning("Event token verification failed: invalid or tampered token")
|
|
46
|
+
return False
|