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,259 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, Final, Literal
|
|
4
|
+
|
|
5
|
+
from ulid import ULID
|
|
6
|
+
|
|
7
|
+
from core_framework.domains.comment import Comment, CommentPreview, CommentStats, CommentStatus, CommentWithMetadata
|
|
8
|
+
from core_framework.domains.comment.enums import CommentSubjectType
|
|
9
|
+
from core_framework.domains.comment.repository import CommentRepository
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CommentService:
|
|
13
|
+
MAX_EDIT_COUNT: Final[int] = 5
|
|
14
|
+
MAX_REPLY_LEVEL: Final[int] = 4
|
|
15
|
+
|
|
16
|
+
def __init__(self, repository: CommentRepository):
|
|
17
|
+
self.repository = repository
|
|
18
|
+
|
|
19
|
+
async def add_comment(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
author_id: str,
|
|
23
|
+
content: str,
|
|
24
|
+
subject_type: CommentSubjectType,
|
|
25
|
+
subject_id: str,
|
|
26
|
+
) -> None:
|
|
27
|
+
await self.repository.insert_comment(
|
|
28
|
+
comment_id=str(ULID()),
|
|
29
|
+
author_id=author_id,
|
|
30
|
+
content=content,
|
|
31
|
+
subject_type=subject_type,
|
|
32
|
+
subject_id=subject_id,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
async def add_reply(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
author_id: str,
|
|
39
|
+
content: str,
|
|
40
|
+
parent_comment_id: str,
|
|
41
|
+
) -> None:
|
|
42
|
+
await self.repository.insert_reply(
|
|
43
|
+
comment_id=str(ULID()),
|
|
44
|
+
author_id=author_id,
|
|
45
|
+
content=content,
|
|
46
|
+
parent_comment_id=parent_comment_id,
|
|
47
|
+
max_reply_level=self.MAX_REPLY_LEVEL,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def retrieve_comment_content_mapping(self, *, comment_ids: set[str]) -> defaultdict[str, str]:
|
|
51
|
+
comment_content_mapping = await self.repository.select_comment_content_mapping(comment_ids=comment_ids)
|
|
52
|
+
return defaultdict(str, comment_content_mapping)
|
|
53
|
+
|
|
54
|
+
async def retrieve_comments_with_metadata(
|
|
55
|
+
self,
|
|
56
|
+
*,
|
|
57
|
+
cursor: datetime,
|
|
58
|
+
limit: int,
|
|
59
|
+
) -> list[CommentWithMetadata]:
|
|
60
|
+
return await self.repository.select_comments_with_metadata(cursor=cursor, limit=limit)
|
|
61
|
+
|
|
62
|
+
async def retrieve_comment_with_metadata_by_id(self, *, comment_id: str) -> CommentWithMetadata:
|
|
63
|
+
return await self.repository.select_comment_with_metadata_by_id(comment_id=comment_id)
|
|
64
|
+
|
|
65
|
+
async def retrieve_comment_preview_mapping(self, *, comment_ids: set[str]) -> dict[str, CommentPreview]:
|
|
66
|
+
return await self.repository.select_comment_preview_mapping(comment_ids=comment_ids)
|
|
67
|
+
|
|
68
|
+
async def retrieve_post_comment_count(self, *, post_id: str) -> int:
|
|
69
|
+
return await self.repository.select_active_comment_count_for_post(post_id=post_id)
|
|
70
|
+
|
|
71
|
+
async def retrieve_comments_by_subject(
|
|
72
|
+
self,
|
|
73
|
+
*,
|
|
74
|
+
subject_type: CommentSubjectType,
|
|
75
|
+
subject_id: str,
|
|
76
|
+
cursor: datetime,
|
|
77
|
+
limit: int,
|
|
78
|
+
viewer_id: str | None,
|
|
79
|
+
) -> list[Comment]:
|
|
80
|
+
return await self.repository.select_comments_by_subject(
|
|
81
|
+
subject_type=subject_type,
|
|
82
|
+
subject_id=subject_id,
|
|
83
|
+
cursor=cursor,
|
|
84
|
+
limit=limit,
|
|
85
|
+
viewer_id=viewer_id,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
async def retrieve_comments_by_author_id(
|
|
89
|
+
self,
|
|
90
|
+
*,
|
|
91
|
+
author_id: str,
|
|
92
|
+
cursor: datetime,
|
|
93
|
+
limit: int,
|
|
94
|
+
viewer_id: str | None,
|
|
95
|
+
) -> list[Comment]:
|
|
96
|
+
return await self.repository.select_comments_by_author_id(
|
|
97
|
+
author_id=author_id,
|
|
98
|
+
cursor=cursor,
|
|
99
|
+
limit=limit,
|
|
100
|
+
viewer_id=viewer_id,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
async def retrieve_replies_by_parent_id(
|
|
104
|
+
self,
|
|
105
|
+
*,
|
|
106
|
+
parent_comment_id: str,
|
|
107
|
+
cursor: datetime,
|
|
108
|
+
limit: int,
|
|
109
|
+
viewer_id: str | None,
|
|
110
|
+
) -> list[Comment]:
|
|
111
|
+
return await self.repository.select_replies_by_parent_id(
|
|
112
|
+
parent_comment_id=parent_comment_id,
|
|
113
|
+
cursor=cursor,
|
|
114
|
+
limit=limit,
|
|
115
|
+
viewer_id=viewer_id,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
async def retrieve_comment_by_id(
|
|
119
|
+
self,
|
|
120
|
+
*,
|
|
121
|
+
comment_id: str,
|
|
122
|
+
viewer_id: str | None,
|
|
123
|
+
) -> Comment | None:
|
|
124
|
+
return await self.repository.select_comment_by_id(
|
|
125
|
+
comment_id=comment_id,
|
|
126
|
+
viewer_id=viewer_id,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
async def add_user_restriction_lookup(self, *, user_id: str, restriction_type: Literal["muted", "banned"]) -> None:
|
|
130
|
+
await self.repository.insert_user_restriction_lookup(
|
|
131
|
+
user_id=user_id,
|
|
132
|
+
restriction_type=restriction_type,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
async def remove_user_restriction_lookup(self, *, user_id: str) -> None:
|
|
136
|
+
await self.repository.delete_user_restriction_lookup(user_id=user_id)
|
|
137
|
+
|
|
138
|
+
async def add_user_block_lookup(self, *, blocker_id: str, blocked_id: str) -> None:
|
|
139
|
+
await self.repository.insert_user_block_lookup(
|
|
140
|
+
blocker_id=blocker_id,
|
|
141
|
+
blocked_id=blocked_id,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
async def remove_user_block_lookup(self, *, blocker_id: str, blocked_id: str) -> None:
|
|
145
|
+
await self.repository.delete_user_block_lookup(
|
|
146
|
+
blocker_id=blocker_id,
|
|
147
|
+
blocked_id=blocked_id,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
async def remove_user(self, *, user_id: str) -> None:
|
|
151
|
+
await self.repository.delete_user(user_id=user_id)
|
|
152
|
+
|
|
153
|
+
async def add_comment_like(self, *, comment_id: str, liker_id: str) -> None:
|
|
154
|
+
await self.repository.insert_comment_like(
|
|
155
|
+
comment_id=comment_id,
|
|
156
|
+
liker_id=liker_id,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
async def remove_comment_like(self, *, comment_id: str, liker_id: str) -> None:
|
|
160
|
+
await self.repository.delete_comment_like(
|
|
161
|
+
comment_id=comment_id,
|
|
162
|
+
liker_id=liker_id,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
async def edit_comment(
|
|
166
|
+
self,
|
|
167
|
+
*,
|
|
168
|
+
comment_id: str,
|
|
169
|
+
author_id: str,
|
|
170
|
+
validated_update_request: dict[str, Any],
|
|
171
|
+
) -> None:
|
|
172
|
+
await self.repository.update_comment(
|
|
173
|
+
comment_id=comment_id,
|
|
174
|
+
author_id=author_id,
|
|
175
|
+
comment_updates=validated_update_request,
|
|
176
|
+
max_edit_count=self.MAX_EDIT_COUNT,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
async def remove_comment(self, *, comment_id: str, author_id: str) -> str | None:
|
|
180
|
+
return await self.repository.update_comment_status_by_id_and_author(
|
|
181
|
+
comment_id=comment_id,
|
|
182
|
+
author_id=author_id,
|
|
183
|
+
status=CommentStatus.DELETED,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
async def set_comment_status_by_id(self, *, comment_id: str, status: CommentStatus) -> str | None:
|
|
187
|
+
return await self.repository.update_comment_status_by_id(
|
|
188
|
+
comment_id=comment_id,
|
|
189
|
+
status=status,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
async def retrieve_comment_ids_by_subject_strong(
|
|
193
|
+
self,
|
|
194
|
+
*,
|
|
195
|
+
subject_type: CommentSubjectType,
|
|
196
|
+
subject_id: str,
|
|
197
|
+
) -> set[str]:
|
|
198
|
+
return await self.repository.select_comment_ids_by_subject_strong(
|
|
199
|
+
subject_type=subject_type,
|
|
200
|
+
subject_id=subject_id,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
async def retrieve_comment_subtree_ids_strong(self, *, root_comment_id: str) -> set[str]:
|
|
204
|
+
return await self.repository.select_comment_subtree_ids_strong(root_comment_id=root_comment_id)
|
|
205
|
+
|
|
206
|
+
async def retrieve_parent_comment_id_strong(self, *, comment_id: str) -> str | None:
|
|
207
|
+
return await self.repository.select_parent_comment_id_strong(comment_id=comment_id)
|
|
208
|
+
|
|
209
|
+
async def remove_comment_subtree(self, *, root_comment_id: str) -> None:
|
|
210
|
+
await self.repository.delete_comment_subtree(root_comment_id=root_comment_id)
|
|
211
|
+
|
|
212
|
+
async def remove_comments_by_subject(
|
|
213
|
+
self,
|
|
214
|
+
*,
|
|
215
|
+
subject_type: CommentSubjectType,
|
|
216
|
+
subject_id: str,
|
|
217
|
+
) -> None:
|
|
218
|
+
await self.repository.delete_comments_by_subject(
|
|
219
|
+
subject_type=subject_type,
|
|
220
|
+
subject_id=subject_id,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
async def retrieve_comment_ids_liked_by_user(self, *, liker_id: str, comment_ids: set[str]) -> frozenset[str]:
|
|
224
|
+
ids = await self.repository.select_comment_ids_liked_by_user(liker_id=liker_id, comment_ids=comment_ids)
|
|
225
|
+
return frozenset(ids)
|
|
226
|
+
|
|
227
|
+
async def retrieve_comment_stats_mapping(self, *, comment_ids: set[str]) -> defaultdict[str, CommentStats]:
|
|
228
|
+
comment_stats_mapping = await self.repository.select_comment_stats_mapping(comment_ids=comment_ids)
|
|
229
|
+
return defaultdict(lambda: CommentStats.DEFAULT, comment_stats_mapping)
|
|
230
|
+
|
|
231
|
+
async def insert_comment_stats_dirty(self, *, comment_id: str) -> None:
|
|
232
|
+
await self.repository.insert_comment_stats_dirty(comment_id=comment_id)
|
|
233
|
+
|
|
234
|
+
async def claim_comment_ids_dirty(self) -> list[str]:
|
|
235
|
+
return await self.repository.claim_comment_ids_dirty()
|
|
236
|
+
|
|
237
|
+
async def delete_comment_stats_dirty(self, *, comment_ids: set[str]) -> None:
|
|
238
|
+
await self.repository.delete_comment_stats_dirty(comment_ids=comment_ids)
|
|
239
|
+
|
|
240
|
+
async def update_comment_stats(
|
|
241
|
+
self,
|
|
242
|
+
*,
|
|
243
|
+
comment_id: str,
|
|
244
|
+
like_count: int,
|
|
245
|
+
reply_count: int,
|
|
246
|
+
report_count: int,
|
|
247
|
+
) -> None:
|
|
248
|
+
await self.repository.update_comment_stats(
|
|
249
|
+
comment_id=comment_id,
|
|
250
|
+
like_count=like_count,
|
|
251
|
+
reply_count=reply_count,
|
|
252
|
+
report_count=report_count,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
async def retrieve_comment_like_count(self, *, comment_id: str) -> int:
|
|
256
|
+
return await self.repository.select_comment_like_count(comment_id=comment_id)
|
|
257
|
+
|
|
258
|
+
async def retrieve_comment_reply_count(self, *, comment_id: str) -> int:
|
|
259
|
+
return await self.repository.select_comment_reply_count(comment_id=comment_id)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Moderation Domain
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
Moderation is the **policy enforcement system**. It owns report intake (user, post, comment), review workflow, enforcement actions (restrictions), appeals, and audit. Provides the authoritative source for whether a user is restricted and why.
|
|
6
|
+
|
|
7
|
+
**Policy enforcement lifecycle**: Intake (reports) → Review (moderation queue) → Enforcement (restrictions) → Appeals → Audit (history and actions).
|
|
8
|
+
|
|
9
|
+
## Owns
|
|
10
|
+
|
|
11
|
+
### Reports
|
|
12
|
+
|
|
13
|
+
- **User reports**: User-to-user reports for policy violations
|
|
14
|
+
- **Post reports**: User-to-post reports for policy violations
|
|
15
|
+
- **Comment reports**: User-to-comment reports for policy violations
|
|
16
|
+
- Report reason/category and freeform details
|
|
17
|
+
- Reports are for analytics and moderation intake; reporters do not receive outcome notifications
|
|
18
|
+
|
|
19
|
+
### Restriction History
|
|
20
|
+
|
|
21
|
+
- Append-only audit log of all restriction changes
|
|
22
|
+
- Tracks insert, update, and delete operations on restrictions
|
|
23
|
+
- Each entry includes actor, target, action type, and restriction details
|
|
24
|
+
- Entries are immutable once created
|
|
25
|
+
|
|
26
|
+
### Restrictions
|
|
27
|
+
|
|
28
|
+
- Current effective restriction state for each user (active, muted, banned)
|
|
29
|
+
- Derived from enforcement actions or set directly by moderators
|
|
30
|
+
- Includes reason and expiration (if applicable)
|
|
31
|
+
|
|
32
|
+
### Appeals
|
|
33
|
+
|
|
34
|
+
- User-initiated appeals (can only be submitted when muted or banned)
|
|
35
|
+
- Appeal status lifecycle (pending, approved, denied)
|
|
36
|
+
- User justification and admin decision reasoning
|
|
37
|
+
- One pending appeal per user at a time
|
|
38
|
+
|
|
39
|
+
### Audit Trail
|
|
40
|
+
|
|
41
|
+
- All restriction changes are logged via restriction_history
|
|
42
|
+
- Includes actor, target, action type, timestamp, and restriction details
|
|
43
|
+
|
|
44
|
+
### Moderation Actions
|
|
45
|
+
|
|
46
|
+
- Append-only audit log of all moderation actions (ban, mute, warn, unrestrict, add_note, change_username, delete_user, etc.)
|
|
47
|
+
- Per-moderator retrieval for accountability; see [Moderation Flow: Moderator Actions](../../../docs/flows/moderation/moderator_actions.md)
|
|
48
|
+
|
|
49
|
+
## Does Not Own
|
|
50
|
+
|
|
51
|
+
- User identity, authentication, or profile data
|
|
52
|
+
- User-to-user blocks (personal preference, owned by user domain)
|
|
53
|
+
- Notification delivery mechanisms
|
|
54
|
+
- Post/comment content and lifecycle (post and comment domains own content; moderation owns reports and enforcement; content domains perform removal when moderators decide)
|
|
55
|
+
|
|
56
|
+
## Cross-Domain Sync
|
|
57
|
+
|
|
58
|
+
- Moderation remains the source-of-truth for user restriction state.
|
|
59
|
+
- Post-domain `user_restrictions_lookup` is a read-time mirror maintained from moderation application flows.
|
|
60
|
+
- Restriction commands are idempotent and may run on no-op paths to reconcile mirror drift after partial failures.
|
|
61
|
+
|
|
62
|
+
## Invariants
|
|
63
|
+
|
|
64
|
+
- A user can have at most one active restriction at a time
|
|
65
|
+
- Restriction history entries are append-only and immutable
|
|
66
|
+
- Appeals can only be submitted when user is muted or banned
|
|
67
|
+
- Appeals become read-only once decided (approved or denied)
|
|
68
|
+
- Only users with moderator or admin roles can modify restrictions
|
|
69
|
+
- Users can submit new appeals immediately after denial (only one pending appeal at a time)
|
|
70
|
+
|
|
71
|
+
## Persistence Model
|
|
72
|
+
|
|
73
|
+
### Tables
|
|
74
|
+
|
|
75
|
+
#### user_reports
|
|
76
|
+
|
|
77
|
+
- Tracks user-to-user reports for policy violations
|
|
78
|
+
- Stores reporter_id, target_id, category, reason, and timestamp
|
|
79
|
+
- One report per (reporter_id, target_id); duplicate reports are idempotent (no-op)
|
|
80
|
+
- Reports are append-only and used for analytics; no status tracking
|
|
81
|
+
|
|
82
|
+
#### post_reports
|
|
83
|
+
|
|
84
|
+
- Tracks user-to-post reports for policy violations
|
|
85
|
+
- Stores reporter_id, target_id (post ID), category, reason, and timestamp
|
|
86
|
+
- One report per (reporter_id, target_id); duplicate reports are idempotent (no-op)
|
|
87
|
+
- Append-only; used for moderation intake and analytics
|
|
88
|
+
|
|
89
|
+
#### comment_reports
|
|
90
|
+
|
|
91
|
+
- Tracks user-to-comment reports for policy violations
|
|
92
|
+
- Stores reporter_id, target_id (comment ID), category, reason, and timestamp
|
|
93
|
+
- Append-only; used for moderation intake and analytics
|
|
94
|
+
|
|
95
|
+
#### restriction_history
|
|
96
|
+
|
|
97
|
+
- Append-only audit log of all changes to user_restrictions
|
|
98
|
+
- Tracks action type (inserted, updated, deleted) and restriction details
|
|
99
|
+
- Each entry references the target user and acting moderator
|
|
100
|
+
- Populated automatically via trigger on user_restrictions
|
|
101
|
+
|
|
102
|
+
#### user_restrictions
|
|
103
|
+
|
|
104
|
+
- Current effective restriction state per user
|
|
105
|
+
- Stores restriction type (warned, muted, banned), reason, and expiry
|
|
106
|
+
- Updated when new enforcement actions are applied
|
|
107
|
+
- A user with no restriction has no row (or state = active)
|
|
108
|
+
|
|
109
|
+
#### appeals
|
|
110
|
+
|
|
111
|
+
- User appeals submitted when muted or banned
|
|
112
|
+
- Stores user justification and admin decision reason
|
|
113
|
+
- Tracks appeal status and reviewer
|
|
114
|
+
- Rows become read-only once decided (enforced by trigger)
|
|
115
|
+
|
|
116
|
+
#### moderation_actions
|
|
117
|
+
|
|
118
|
+
- Append-only audit log of all moderation actions (ban, mute, warn, add_note, delete_user, etc.)
|
|
119
|
+
- Stores actor_id, action_type, target_user_id (optional), action_metadata, created_at
|
|
120
|
+
- Rows are never deleted when actors or targets are deleted (audit trail preserved)
|
|
121
|
+
|
|
122
|
+
### Enums
|
|
123
|
+
|
|
124
|
+
#### history_action
|
|
125
|
+
|
|
126
|
+
- Types of history entries (inserted, updated, deleted)
|
|
127
|
+
|
|
128
|
+
#### restriction_type
|
|
129
|
+
|
|
130
|
+
- Types of restrictions (warned, muted, banned)
|
|
131
|
+
|
|
132
|
+
#### appeal_status
|
|
133
|
+
|
|
134
|
+
- Appeal lifecycle states (pending, approved, denied)
|
|
135
|
+
|
|
136
|
+
#### report_category
|
|
137
|
+
|
|
138
|
+
- Report categories used for policy classification (shared by user_reports, post_reports, comment_reports)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from core_framework.domains.moderation.enums import (
|
|
2
|
+
AppealDecision,
|
|
3
|
+
HistoryAction,
|
|
4
|
+
ModerationActionType,
|
|
5
|
+
ReportCategory,
|
|
6
|
+
ReportType,
|
|
7
|
+
RestrictionCategory,
|
|
8
|
+
RestrictionType,
|
|
9
|
+
)
|
|
10
|
+
from core_framework.domains.moderation.exceptions import (
|
|
11
|
+
AppealAlreadyDecidedException,
|
|
12
|
+
AppealNotFoundException,
|
|
13
|
+
AppealRequirementException,
|
|
14
|
+
BaseModerationException,
|
|
15
|
+
ExistingPendingAppealException,
|
|
16
|
+
SelfReportException,
|
|
17
|
+
)
|
|
18
|
+
from core_framework.domains.moderation.models import (
|
|
19
|
+
InternalNote,
|
|
20
|
+
ModerationAction,
|
|
21
|
+
Report,
|
|
22
|
+
RestrictionHistory,
|
|
23
|
+
UserModeration,
|
|
24
|
+
UserRestriction,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ModerationActionType",
|
|
29
|
+
"ReportType",
|
|
30
|
+
"RestrictionType",
|
|
31
|
+
"AppealDecision",
|
|
32
|
+
"ReportCategory",
|
|
33
|
+
"RestrictionCategory",
|
|
34
|
+
"HistoryAction",
|
|
35
|
+
"Report",
|
|
36
|
+
"UserModeration",
|
|
37
|
+
"RestrictionHistory",
|
|
38
|
+
"InternalNote",
|
|
39
|
+
"ModerationAction",
|
|
40
|
+
"AppealAlreadyDecidedException",
|
|
41
|
+
"AppealNotFoundException",
|
|
42
|
+
"SelfReportException",
|
|
43
|
+
"ExistingPendingAppealException",
|
|
44
|
+
"AppealRequirementException",
|
|
45
|
+
"BaseModerationException",
|
|
46
|
+
"UserRestriction",
|
|
47
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from core_framework.core.runtime import CoreRuntime, unconfigured_dependency
|
|
2
|
+
from core_framework.domains.moderation.repository import ModerationRepository
|
|
3
|
+
from core_framework.domains.moderation.service import ModerationService
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_moderation_repository(runtime: CoreRuntime) -> ModerationRepository:
|
|
7
|
+
return ModerationRepository(runtime.write_postgres, runtime.read_postgres, runtime.write_postgres)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_moderation_service(runtime: CoreRuntime) -> ModerationService:
|
|
11
|
+
return ModerationService(build_moderation_repository(runtime))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def configure_moderation_dependencies(runtime: CoreRuntime) -> None:
|
|
15
|
+
global moderation_repository, moderation_service
|
|
16
|
+
moderation_repository = build_moderation_repository(runtime)
|
|
17
|
+
moderation_service = build_moderation_service(runtime)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
moderation_repository = unconfigured_dependency("ModerationRepository")
|
|
21
|
+
moderation_service = unconfigured_dependency("ModerationService")
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"build_moderation_repository",
|
|
25
|
+
"build_moderation_service",
|
|
26
|
+
"configure_moderation_dependencies",
|
|
27
|
+
"moderation_repository",
|
|
28
|
+
"moderation_service",
|
|
29
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RestrictionType(StrEnum):
|
|
5
|
+
ACTIVE = "active"
|
|
6
|
+
WARNED = "warned"
|
|
7
|
+
MUTED = "muted"
|
|
8
|
+
BANNED = "banned"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReportType(StrEnum):
|
|
12
|
+
USER = "user"
|
|
13
|
+
POST = "post"
|
|
14
|
+
COMMENT = "comment"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ReportCategory(StrEnum):
|
|
18
|
+
SPAM = "spam"
|
|
19
|
+
HARASSMENT = "harassment"
|
|
20
|
+
INAPPROPRIATE_CONTENT = "inappropriate_content"
|
|
21
|
+
IMPERSONATION = "impersonation"
|
|
22
|
+
SELF_HARM = "self_harm"
|
|
23
|
+
HATE_SPEECH = "hate_speech"
|
|
24
|
+
VIOLENCE = "violence"
|
|
25
|
+
SCAM = "scam"
|
|
26
|
+
OTHER = "other"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RestrictionCategory(StrEnum):
|
|
30
|
+
SPAM = "spam"
|
|
31
|
+
HARASSMENT = "harassment"
|
|
32
|
+
INAPPROPRIATE_CONTENT = "inappropriate_content"
|
|
33
|
+
IMPERSONATION = "impersonation"
|
|
34
|
+
HARMFUL_CONTENT = "harmful_content"
|
|
35
|
+
SCAM = "scam"
|
|
36
|
+
OTHER = "other"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class HistoryAction(StrEnum):
|
|
40
|
+
INSERTED = "inserted"
|
|
41
|
+
UPDATED = "updated"
|
|
42
|
+
DELETED = "deleted"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AppealDecision(StrEnum):
|
|
46
|
+
PENDING = "pending"
|
|
47
|
+
APPROVED = "approved"
|
|
48
|
+
DENIED = "denied"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ModerationActionType(StrEnum):
|
|
52
|
+
BAN = "ban"
|
|
53
|
+
MUTE = "mute"
|
|
54
|
+
WARN = "warn"
|
|
55
|
+
UNRESTRICT = "unrestrict"
|
|
56
|
+
ADD_NOTE = "add_note"
|
|
57
|
+
DELETE_NOTE = "delete_note"
|
|
58
|
+
CHANGE_USERNAME = "change_username"
|
|
59
|
+
CHANGE_ROLE = "change_role"
|
|
60
|
+
CHANGE_PROFILE = "change_profile"
|
|
61
|
+
DECIDE_APPEAL = "decide_appeal"
|
|
62
|
+
DELETE_USER = "delete_user"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class BaseModerationException(Exception):
|
|
2
|
+
message: str
|
|
3
|
+
|
|
4
|
+
def __init__(self, message: str):
|
|
5
|
+
self.message = message
|
|
6
|
+
super().__init__(self.message)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SelfReportException(BaseModerationException):
|
|
10
|
+
def __init__(self):
|
|
11
|
+
super().__init__("Self report is not allowed")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ExistingPendingAppealException(BaseModerationException):
|
|
15
|
+
def __init__(self):
|
|
16
|
+
super().__init__("An existing pending appeal already exists for this user")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AppealNotFoundException(BaseModerationException):
|
|
20
|
+
def __init__(self):
|
|
21
|
+
super().__init__("Appeal not found")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AppealAlreadyDecidedException(BaseModerationException):
|
|
25
|
+
def __init__(self):
|
|
26
|
+
super().__init__("Appeal already decided")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AppealRequirementException(BaseModerationException):
|
|
30
|
+
def __init__(self):
|
|
31
|
+
super().__init__("User must be muted or banned to submit an appeal")
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from core_framework.domains.moderation.enums import (
|
|
6
|
+
AppealDecision,
|
|
7
|
+
HistoryAction,
|
|
8
|
+
ModerationActionType,
|
|
9
|
+
ReportCategory,
|
|
10
|
+
RestrictionCategory,
|
|
11
|
+
RestrictionType,
|
|
12
|
+
)
|
|
13
|
+
from core_framework.domains.user import DEFAULT_USER_ID
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
17
|
+
class Report:
|
|
18
|
+
id: int
|
|
19
|
+
reporter_id: str
|
|
20
|
+
target_id: str
|
|
21
|
+
category: ReportCategory
|
|
22
|
+
reason: str
|
|
23
|
+
created_at: datetime
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
27
|
+
class UserRestriction:
|
|
28
|
+
user_id: str
|
|
29
|
+
status: RestrictionType
|
|
30
|
+
category: RestrictionCategory
|
|
31
|
+
expires_at: datetime | None
|
|
32
|
+
|
|
33
|
+
DEFAULT: ClassVar[UserRestriction]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
UserRestriction.DEFAULT = UserRestriction(
|
|
37
|
+
user_id=DEFAULT_USER_ID,
|
|
38
|
+
status=RestrictionType.ACTIVE,
|
|
39
|
+
category=RestrictionCategory.OTHER,
|
|
40
|
+
expires_at=None,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
45
|
+
class InternalNote:
|
|
46
|
+
id: int
|
|
47
|
+
target_user_id: str
|
|
48
|
+
actor_id: str
|
|
49
|
+
content: str
|
|
50
|
+
created_at: datetime
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
54
|
+
class UserModeration:
|
|
55
|
+
restriction: UserRestriction
|
|
56
|
+
notes: tuple[InternalNote, ...]
|
|
57
|
+
|
|
58
|
+
DEFAULT: ClassVar[UserModeration]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
UserModeration.DEFAULT = UserModeration(restriction=UserRestriction.DEFAULT, notes=())
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
65
|
+
class Appeal:
|
|
66
|
+
id: int
|
|
67
|
+
user_id: str
|
|
68
|
+
justification: str
|
|
69
|
+
decision_reason: str | None
|
|
70
|
+
reviewer_id: str | None
|
|
71
|
+
status: AppealDecision
|
|
72
|
+
created_at: datetime
|
|
73
|
+
updated_at: datetime
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
77
|
+
class RestrictionHistory:
|
|
78
|
+
user_id: str
|
|
79
|
+
action: HistoryAction
|
|
80
|
+
restriction_type: RestrictionType
|
|
81
|
+
category: RestrictionCategory
|
|
82
|
+
reason: str
|
|
83
|
+
actor_id: str | None
|
|
84
|
+
created_at: datetime
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
88
|
+
class ModerationAction:
|
|
89
|
+
id: int
|
|
90
|
+
actor_id: str
|
|
91
|
+
action_type: ModerationActionType
|
|
92
|
+
target_user_id: str | None
|
|
93
|
+
action_metadata: dict
|
|
94
|
+
created_at: datetime
|