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,334 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from typing import Any, Final
|
|
5
|
+
|
|
6
|
+
from core_framework.domains.moderation.enums import (
|
|
7
|
+
AppealDecision,
|
|
8
|
+
ModerationActionType,
|
|
9
|
+
ReportCategory,
|
|
10
|
+
RestrictionCategory,
|
|
11
|
+
)
|
|
12
|
+
from core_framework.domains.moderation.exceptions import SelfReportException
|
|
13
|
+
from core_framework.domains.moderation.models import (
|
|
14
|
+
Appeal,
|
|
15
|
+
InternalNote,
|
|
16
|
+
ModerationAction,
|
|
17
|
+
Report,
|
|
18
|
+
RestrictionHistory,
|
|
19
|
+
UserModeration,
|
|
20
|
+
UserRestriction,
|
|
21
|
+
)
|
|
22
|
+
from core_framework.domains.moderation.repository import ModerationRepository
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ModerationService:
|
|
26
|
+
DEFAULT_BAN_EXPIRATION_DAYS: Final[int] = 4000
|
|
27
|
+
DEFAULT_MUTE_EXPIRATION_DAYS: Final[int] = 7
|
|
28
|
+
USER_DETAIL_NOTES_LIMIT: Final[int] = 5
|
|
29
|
+
|
|
30
|
+
def __init__(self, repository: ModerationRepository):
|
|
31
|
+
self.repository = repository
|
|
32
|
+
|
|
33
|
+
# Restrictions
|
|
34
|
+
async def ban_user(self, *, actor_id: str, user_id: str, reason: str, category: RestrictionCategory) -> None:
|
|
35
|
+
expires_at = datetime.now(timezone.utc) + timedelta(days=self.DEFAULT_BAN_EXPIRATION_DAYS)
|
|
36
|
+
await self.repository.upsert_ban_state(
|
|
37
|
+
actor_id=actor_id,
|
|
38
|
+
user_id=user_id,
|
|
39
|
+
reason=reason,
|
|
40
|
+
category=category,
|
|
41
|
+
expires_at=expires_at,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
async def mute_user(self, *, actor_id: str, user_id: str, reason: str, category: RestrictionCategory) -> None:
|
|
45
|
+
expires_at = datetime.now(timezone.utc) + timedelta(days=self.DEFAULT_MUTE_EXPIRATION_DAYS)
|
|
46
|
+
await self.repository.upsert_mute_state(
|
|
47
|
+
actor_id=actor_id,
|
|
48
|
+
user_id=user_id,
|
|
49
|
+
reason=reason,
|
|
50
|
+
category=category,
|
|
51
|
+
expires_at=expires_at,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
async def warn_user(self, *, actor_id: str, user_id: str, reason: str, category: RestrictionCategory) -> None:
|
|
55
|
+
await self.repository.upsert_warn_state(actor_id=actor_id, user_id=user_id, reason=reason, category=category)
|
|
56
|
+
|
|
57
|
+
async def clear_user_restriction(self, *, actor_id: str, user_id: str) -> None:
|
|
58
|
+
await self.repository.delete_user_restriction(actor_id=actor_id, user_id=user_id)
|
|
59
|
+
|
|
60
|
+
async def retrieve_expired_mute_user_ids(self) -> list[str]:
|
|
61
|
+
return await self.repository.select_expired_mute_user_ids()
|
|
62
|
+
|
|
63
|
+
# Reports
|
|
64
|
+
async def add_user_report(self, *, reporter_id: str, target_id: str, category: ReportCategory, reason: str) -> None:
|
|
65
|
+
if reporter_id == target_id:
|
|
66
|
+
raise SelfReportException()
|
|
67
|
+
|
|
68
|
+
await self.repository.insert_user_report(
|
|
69
|
+
reporter_id=reporter_id,
|
|
70
|
+
target_id=target_id,
|
|
71
|
+
category=category,
|
|
72
|
+
reason=reason,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
async def retrieve_user_reports(
|
|
76
|
+
self,
|
|
77
|
+
*,
|
|
78
|
+
reporter_id: str | None,
|
|
79
|
+
target_id: str | None,
|
|
80
|
+
cursor: datetime,
|
|
81
|
+
limit: int,
|
|
82
|
+
) -> list[Report]:
|
|
83
|
+
return await self.repository.select_user_reports(
|
|
84
|
+
reporter_id=reporter_id,
|
|
85
|
+
target_id=target_id,
|
|
86
|
+
cursor=cursor,
|
|
87
|
+
limit=limit,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
async def remove_user_report(self, *, report_id: int) -> None:
|
|
91
|
+
await self.repository.delete_user_report_by_id(report_id=report_id)
|
|
92
|
+
|
|
93
|
+
async def remove_user_report_by_reporter(self, *, reporter_id: str, target_id: str) -> None:
|
|
94
|
+
await self.repository.delete_user_report_by_reporter_and_target(
|
|
95
|
+
reporter_id=reporter_id,
|
|
96
|
+
target_id=target_id,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
async def add_post_report(
|
|
100
|
+
self,
|
|
101
|
+
*,
|
|
102
|
+
reporter_id: str,
|
|
103
|
+
target_id: str,
|
|
104
|
+
category: ReportCategory,
|
|
105
|
+
reason: str,
|
|
106
|
+
) -> None:
|
|
107
|
+
await self.repository.insert_post_report(
|
|
108
|
+
reporter_id=reporter_id,
|
|
109
|
+
target_id=target_id,
|
|
110
|
+
category=category,
|
|
111
|
+
reason=reason,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
async def retrieve_post_reports(
|
|
115
|
+
self,
|
|
116
|
+
*,
|
|
117
|
+
reporter_id: str | None,
|
|
118
|
+
target_id: str | None,
|
|
119
|
+
cursor: datetime,
|
|
120
|
+
limit: int,
|
|
121
|
+
) -> list[Report]:
|
|
122
|
+
return await self.repository.select_post_reports(
|
|
123
|
+
reporter_id=reporter_id,
|
|
124
|
+
target_id=target_id,
|
|
125
|
+
cursor=cursor,
|
|
126
|
+
limit=limit,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
async def retrieve_comment_reports(
|
|
130
|
+
self,
|
|
131
|
+
*,
|
|
132
|
+
reporter_id: str | None,
|
|
133
|
+
target_id: str | None,
|
|
134
|
+
cursor: datetime,
|
|
135
|
+
limit: int,
|
|
136
|
+
) -> list[Report]:
|
|
137
|
+
return await self.repository.select_comment_reports(
|
|
138
|
+
reporter_id=reporter_id,
|
|
139
|
+
target_id=target_id,
|
|
140
|
+
cursor=cursor,
|
|
141
|
+
limit=limit,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
async def retrieve_post_report_count(self, *, post_id: str) -> int:
|
|
145
|
+
return await self.repository.select_post_report_count(post_id=post_id)
|
|
146
|
+
|
|
147
|
+
async def retrieve_comment_report_count(self, *, comment_id: str) -> int:
|
|
148
|
+
return await self.repository.select_comment_report_count(comment_id=comment_id)
|
|
149
|
+
|
|
150
|
+
async def remove_post_report(self, *, report_id: int) -> None:
|
|
151
|
+
await self.repository.delete_post_report_by_id(report_id=report_id)
|
|
152
|
+
|
|
153
|
+
async def remove_comment_report(self, *, report_id: int) -> None:
|
|
154
|
+
await self.repository.delete_comment_report_by_id(report_id=report_id)
|
|
155
|
+
|
|
156
|
+
async def remove_post_report_by_reporter(self, *, reporter_id: str, target_id: str) -> None:
|
|
157
|
+
await self.repository.delete_post_report_by_reporter_and_target(
|
|
158
|
+
reporter_id=reporter_id,
|
|
159
|
+
target_id=target_id,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
async def remove_post_reports_by_target_id(self, *, target_id: str) -> None:
|
|
163
|
+
await self.repository.delete_post_reports_by_target_id(target_id=target_id)
|
|
164
|
+
|
|
165
|
+
async def retrieve_post_ids_reported_by_user(self, *, reporter_id: str, post_ids: set[str]) -> frozenset[str]:
|
|
166
|
+
ids = await self.repository.select_post_ids_reported_by_user(reporter_id=reporter_id, post_ids=post_ids)
|
|
167
|
+
return frozenset(ids)
|
|
168
|
+
|
|
169
|
+
async def retrieve_comment_ids_reported_by_user(self, *, reporter_id: str, comment_ids: set[str]) -> frozenset[str]:
|
|
170
|
+
ids = await self.repository.select_comment_ids_reported_by_user(
|
|
171
|
+
reporter_id=reporter_id, comment_ids=comment_ids
|
|
172
|
+
)
|
|
173
|
+
return frozenset(ids)
|
|
174
|
+
|
|
175
|
+
async def add_comment_report(
|
|
176
|
+
self,
|
|
177
|
+
*,
|
|
178
|
+
reporter_id: str,
|
|
179
|
+
target_id: str,
|
|
180
|
+
category: ReportCategory,
|
|
181
|
+
reason: str,
|
|
182
|
+
) -> None:
|
|
183
|
+
await self.repository.insert_comment_report(
|
|
184
|
+
reporter_id=reporter_id,
|
|
185
|
+
target_id=target_id,
|
|
186
|
+
category=category,
|
|
187
|
+
reason=reason,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
async def remove_comment_report_by_reporter(self, *, reporter_id: str, target_id: str) -> None:
|
|
191
|
+
await self.repository.delete_comment_report_by_reporter_and_target(
|
|
192
|
+
reporter_id=reporter_id,
|
|
193
|
+
target_id=target_id,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
async def remove_comment_reports_by_target_ids(self, *, target_ids: set[str]) -> None:
|
|
197
|
+
await self.repository.delete_comment_reports_by_target_ids(target_ids=target_ids)
|
|
198
|
+
|
|
199
|
+
# Appeals
|
|
200
|
+
async def add_appeal(self, *, user_id: str, justification: str) -> None:
|
|
201
|
+
await self.repository.insert_appeal(user_id=user_id, justification=justification)
|
|
202
|
+
|
|
203
|
+
async def decide_appeal(self, *, actor_id: str, appeal_id: int, decision: AppealDecision, reason: str) -> Appeal:
|
|
204
|
+
await self.repository.update_appeal_decision(
|
|
205
|
+
actor_id=actor_id,
|
|
206
|
+
appeal_id=appeal_id,
|
|
207
|
+
decision=decision,
|
|
208
|
+
reason=reason,
|
|
209
|
+
)
|
|
210
|
+
return await self.repository.select_appeal_strong(appeal_id=appeal_id)
|
|
211
|
+
|
|
212
|
+
async def retrieve_appeals(self, *, status: AppealDecision | None, cursor: datetime, limit: int) -> list[Appeal]:
|
|
213
|
+
return await self.repository.select_appeals(status=status, cursor=cursor, limit=limit)
|
|
214
|
+
|
|
215
|
+
async def retrieve_appeals_of_user(
|
|
216
|
+
self,
|
|
217
|
+
*,
|
|
218
|
+
user_id: str,
|
|
219
|
+
status: AppealDecision | None,
|
|
220
|
+
cursor: datetime,
|
|
221
|
+
limit: int,
|
|
222
|
+
) -> list[Appeal]:
|
|
223
|
+
return await self.repository.select_appeals(user_id=user_id, status=status, cursor=cursor, limit=limit)
|
|
224
|
+
|
|
225
|
+
async def remove_current_appeal_of_user(self, *, user_id: str) -> None:
|
|
226
|
+
await self.repository.delete_current_appeal_of_user(user_id=user_id)
|
|
227
|
+
|
|
228
|
+
# Internal Notes
|
|
229
|
+
async def add_internal_note(
|
|
230
|
+
self,
|
|
231
|
+
*,
|
|
232
|
+
actor_id: str,
|
|
233
|
+
target_user_id: str,
|
|
234
|
+
content: str,
|
|
235
|
+
) -> InternalNote:
|
|
236
|
+
return await self.repository.insert_internal_note(
|
|
237
|
+
actor_id=actor_id,
|
|
238
|
+
target_user_id=target_user_id,
|
|
239
|
+
content=content,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
async def retrieve_internal_note_strong(self, *, note_id: int, target_user_id: str) -> InternalNote | None:
|
|
243
|
+
return await self.repository.select_internal_note_by_id_strong(
|
|
244
|
+
note_id=note_id,
|
|
245
|
+
target_user_id=target_user_id,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
async def retrieve_internal_notes(self, *, target_user_id: str) -> list[InternalNote]:
|
|
249
|
+
return await self.repository.select_internal_notes(target_user_id=target_user_id)
|
|
250
|
+
|
|
251
|
+
async def retrieve_internal_notes_paginated(
|
|
252
|
+
self,
|
|
253
|
+
*,
|
|
254
|
+
target_user_id: str,
|
|
255
|
+
cursor: datetime,
|
|
256
|
+
limit: int,
|
|
257
|
+
) -> list[InternalNote]:
|
|
258
|
+
return await self.repository.select_internal_notes_paginated(
|
|
259
|
+
target_user_id=target_user_id,
|
|
260
|
+
cursor=cursor,
|
|
261
|
+
limit=limit,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
async def remove_internal_note(self, *, note_id: int, target_user_id: str) -> None:
|
|
265
|
+
await self.repository.delete_internal_note(note_id=note_id, target_user_id=target_user_id)
|
|
266
|
+
|
|
267
|
+
# User Moderation
|
|
268
|
+
async def retrieve_user_restriction(self, *, user_id: str) -> UserRestriction:
|
|
269
|
+
mapping = await self.repository.select_user_restriction_mapping(user_ids={user_id})
|
|
270
|
+
return mapping.get(user_id, UserRestriction.DEFAULT)
|
|
271
|
+
|
|
272
|
+
async def retrieve_user_restriction_strong(self, *, user_id: str) -> UserRestriction:
|
|
273
|
+
mapping = await self.repository.select_user_restriction_mapping_strong(user_ids={user_id})
|
|
274
|
+
return mapping.get(user_id, UserRestriction.DEFAULT)
|
|
275
|
+
|
|
276
|
+
async def retrieve_user_moderation_mapping(self, *, user_ids: set[str]) -> defaultdict[str, UserModeration]:
|
|
277
|
+
user_restriction_mapping = await self.repository.select_user_restriction_mapping(user_ids=user_ids)
|
|
278
|
+
result: defaultdict[str, UserModeration] = defaultdict(lambda: UserModeration.DEFAULT)
|
|
279
|
+
for user_id, restriction in user_restriction_mapping.items():
|
|
280
|
+
result[user_id] = UserModeration(restriction=restriction, notes=())
|
|
281
|
+
return result
|
|
282
|
+
|
|
283
|
+
async def retrieve_user_moderation_for_detail(self, *, user_id: str) -> UserModeration:
|
|
284
|
+
far_future = datetime(3000, 1, 1, tzinfo=timezone.utc)
|
|
285
|
+
async with asyncio.TaskGroup() as tg:
|
|
286
|
+
restriction_task = tg.create_task(self.repository.select_user_restriction_mapping(user_ids={user_id}))
|
|
287
|
+
notes_task = tg.create_task(
|
|
288
|
+
self.repository.select_internal_notes_paginated(
|
|
289
|
+
target_user_id=user_id,
|
|
290
|
+
cursor=far_future,
|
|
291
|
+
limit=self.USER_DETAIL_NOTES_LIMIT,
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
restriction_mapping = restriction_task.result()
|
|
295
|
+
notes = notes_task.result()
|
|
296
|
+
restriction = restriction_mapping.get(user_id, UserRestriction.DEFAULT)
|
|
297
|
+
return UserModeration(restriction=restriction, notes=tuple(notes))
|
|
298
|
+
|
|
299
|
+
async def remove_user(self, *, user_id: str) -> None:
|
|
300
|
+
await self.repository.delete_user(user_id=user_id)
|
|
301
|
+
|
|
302
|
+
async def retrieve_restriction_history(
|
|
303
|
+
self,
|
|
304
|
+
*,
|
|
305
|
+
user_id: str,
|
|
306
|
+
cursor: datetime,
|
|
307
|
+
limit: int,
|
|
308
|
+
) -> list[RestrictionHistory]:
|
|
309
|
+
return await self.repository.select_restriction_history(user_id=user_id, cursor=cursor, limit=limit)
|
|
310
|
+
|
|
311
|
+
# Moderation Actions
|
|
312
|
+
async def retrieve_moderation_actions_strong(
|
|
313
|
+
self, *, actor_id: str, cursor: datetime, limit: int
|
|
314
|
+
) -> list[ModerationAction]:
|
|
315
|
+
return await self.repository.select_moderation_actions_strong(
|
|
316
|
+
actor_id=actor_id,
|
|
317
|
+
cursor=cursor,
|
|
318
|
+
limit=limit,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
async def record_moderation_action(
|
|
322
|
+
self,
|
|
323
|
+
*,
|
|
324
|
+
actor_id: str,
|
|
325
|
+
action_type: ModerationActionType,
|
|
326
|
+
target_user_id: str | None = None,
|
|
327
|
+
action_metadata: dict[str, Any] | None = None,
|
|
328
|
+
) -> None:
|
|
329
|
+
await self.repository.insert_moderation_action(
|
|
330
|
+
actor_id=actor_id,
|
|
331
|
+
action_type=action_type,
|
|
332
|
+
target_user_id=target_user_id,
|
|
333
|
+
action_metadata=action_metadata,
|
|
334
|
+
)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Post Domain
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
Owns post content lifecycle, audience visibility rules, media attachments, post likes, hashtags, mentions, and engagement analytics. Provides the authoritative source for post existence and current content state.
|
|
6
|
+
|
|
7
|
+
## Owns
|
|
8
|
+
|
|
9
|
+
### Post Content
|
|
10
|
+
|
|
11
|
+
- Post creation, retrieval, editing, and deletion state
|
|
12
|
+
- Author-owned content fields (body and optional metadata)
|
|
13
|
+
- Post timestamps (created/updated/edited)
|
|
14
|
+
- Edit limit (max 5 edits) and `edited_count` for author capability display; retrieval responses include `author_context` (can_edit, can_delete) when viewer is author—see `docs/flows/posts/author_context.md`
|
|
15
|
+
|
|
16
|
+
### Post Visibility State
|
|
17
|
+
|
|
18
|
+
- Audience visibility state of a post (public, followers, private, unlisted)
|
|
19
|
+
- Soft-delete state for post lifecycle management
|
|
20
|
+
- Readability/editability rules based on state
|
|
21
|
+
|
|
22
|
+
### Media Attachments
|
|
23
|
+
|
|
24
|
+
- Post-linked media metadata for images and videos
|
|
25
|
+
- Attachment ordering and per-post attachment limits
|
|
26
|
+
- Media state lifecycle (processing, ready, failed, removed)
|
|
27
|
+
|
|
28
|
+
### Post Likes
|
|
29
|
+
|
|
30
|
+
- User-to-post like relationships
|
|
31
|
+
- Idempotent like/unlike behavior as relationship state
|
|
32
|
+
- Aggregate like counts derived from post_like rows
|
|
33
|
+
|
|
34
|
+
### Hashtags and Discovery
|
|
35
|
+
|
|
36
|
+
- Hashtag extraction and canonicalization from post content
|
|
37
|
+
- Hashtag-to-post indexing for discovery and retrieval
|
|
38
|
+
- Search/discovery query support over post content and hashtags
|
|
39
|
+
|
|
40
|
+
### Mentions
|
|
41
|
+
|
|
42
|
+
- Mention extraction from post content (e.g., @username)
|
|
43
|
+
- Mention-to-post mapping for retrieval and mention inbox use cases
|
|
44
|
+
- Mention events for downstream notification workflows
|
|
45
|
+
|
|
46
|
+
### Engagement Analytics
|
|
47
|
+
|
|
48
|
+
- Post engagement counters and derived analytics (views, likes, comments, reports)
|
|
49
|
+
- View tracking records and aggregation strategy for post metrics
|
|
50
|
+
- Retrieval of post-level metrics for authors and admins
|
|
51
|
+
|
|
52
|
+
## Does Not Own
|
|
53
|
+
|
|
54
|
+
- User identity, authentication, or profile data
|
|
55
|
+
- User relationship graph (follows/friends/blocks)
|
|
56
|
+
- Post reports (owned by moderation domain; moderation is the policy enforcement system)
|
|
57
|
+
- Enforcement decisions (ban/mute/warn) and appeal lifecycle - owned by moderation domain
|
|
58
|
+
- Notification delivery mechanisms (domain may emit mention events, but delivery is external)
|
|
59
|
+
- Feed ranking, recommendation, or timeline assembly in API layer
|
|
60
|
+
|
|
61
|
+
## User removal and redacted authors (design)
|
|
62
|
+
|
|
63
|
+
When an account is removed, **authored posts are not hard-deleted**: **retain** `posts.id`, **redact** content to an agreed placeholder, and set **`author_id`** to the same **sentinel** used for redacted comments (see `docs/flows/users/user_removal.md`). **Post reports and post likes** are kept; **`reporter_id` / `liker_id` are not rewritten** to the sentinel. Public post **GET** responses set **`engagement_allowed`** to `false` for redacted-author posts so clients can hide like/report actions; **write** endpoints do not enforce that flag.
|
|
64
|
+
|
|
65
|
+
## Hard delete post (design)
|
|
66
|
+
|
|
67
|
+
**Hard delete** is a separate operation from user removal. For `DELETE /admin/posts/{post_id}` the **target is physical removal** of the `posts` row and **all post-domain rows** that reference that `post_id` (e.g. `post_likes`, `post_stats`, `post_attachments`, `post_hashtags`, `post_mentions`, `post_views`), plus **cross-domain cleanup**: every comment on that post (full subtree), their likes/stats/attachments/dirty markers, and **moderation** `post_reports` and the relevant `comment_reports`.
|
|
68
|
+
|
|
69
|
+
Authoritative checklist and HTTP flow notes: `docs/flows/posts/admin_posts.md` (Hard delete post). Keep repository and application delete paths aligned with that contract whenever hard-delete behavior changes.
|
|
70
|
+
|
|
71
|
+
## Invariants
|
|
72
|
+
|
|
73
|
+
- A post has exactly one author_id
|
|
74
|
+
- A post in deleted state is not editable
|
|
75
|
+
- Attachments are immutable once published on an active post
|
|
76
|
+
- Attachment type must be image or video
|
|
77
|
+
- A user can like a given post at most once
|
|
78
|
+
- Post visibility must be one of (public, followers, private, unlisted)
|
|
79
|
+
- A user can be mentioned at most once per post
|
|
80
|
+
- Engagement counters are non-negative
|
|
81
|
+
- Domain stores only IDs for external entities (no cross-domain foreign keys)
|
|
82
|
+
- Post reads/writes only touch the post schema
|
|
83
|
+
|
|
84
|
+
## Feed Visibility Filtering Rules
|
|
85
|
+
|
|
86
|
+
- Post domain may keep local lookup mirrors for cross-domain visibility checks:
|
|
87
|
+
- `user_restrictions_lookup` (restricted authors: muted/banned)
|
|
88
|
+
- `user_blocks_lookup` (directed block edge: blocker -> blocked)
|
|
89
|
+
- For viewer `A` and post author `B`, hide the post when either:
|
|
90
|
+
- `A` has blocked `B` (exists in `user_blocks_lookup`)
|
|
91
|
+
- `B` is restricted as muted/banned (exists in `user_restrictions_lookup`)
|
|
92
|
+
- Special rule: if viewer `A` is muted, `A` can still see their own posts.
|
|
93
|
+
- These lookup tables are mirrors for read-time filtering only; source-of-truth remains outside post domain.
|
|
94
|
+
|
|
95
|
+
## Lookup Sync Contract
|
|
96
|
+
|
|
97
|
+
- Source-of-truth for restriction state is the moderation domain.
|
|
98
|
+
- Source-of-truth for user-to-user blocks is the user domain.
|
|
99
|
+
- Post domain mirrors (`user_restrictions_lookup`, `user_blocks_lookup`) exist only to support read-time filtering.
|
|
100
|
+
- Sync operations should be idempotent and safe to run repeatedly.
|
|
101
|
+
- No-op moderation operations (for example, banning an already banned user or clearing an already active user)
|
|
102
|
+
should still reconcile post-domain lookup rows to heal prior cross-domain partial failures.
|
|
103
|
+
|
|
104
|
+
## Persistence Model
|
|
105
|
+
|
|
106
|
+
### Tables
|
|
107
|
+
|
|
108
|
+
#### posts
|
|
109
|
+
|
|
110
|
+
- Canonical post record (author_id, content, visibility/state, timestamps)
|
|
111
|
+
- Supports soft deletion and edit tracking
|
|
112
|
+
- Indexed for recent-first listing and author timeline queries
|
|
113
|
+
|
|
114
|
+
#### post_likes
|
|
115
|
+
|
|
116
|
+
- Tracks user likes on posts as a relationship table
|
|
117
|
+
- Stores liker_id, post_id, and created_at
|
|
118
|
+
- One row per (liker_id, post_id) pair to enforce idempotent likes
|
|
119
|
+
|
|
120
|
+
#### post_stats
|
|
121
|
+
|
|
122
|
+
- Denormalized engagement counters per post
|
|
123
|
+
- Stores post_id, like_count, comment_count, view_count, report_count, view_count_aggregated_at, timestamps
|
|
124
|
+
- like_count, comment_count, report_count from post_likes, comments, moderation domain post_reports; view_count from incremental aggregation of post_views
|
|
125
|
+
- One row per post; created when post is created
|
|
126
|
+
- Aggregated periodically by background worker (every 15 min); see `docs/flows/posts/post_stats_aggregation.md`
|
|
127
|
+
|
|
128
|
+
#### post_attachments
|
|
129
|
+
|
|
130
|
+
- Tracks media attached to posts (image/video)
|
|
131
|
+
- Stores post_id, media_id/storage_key, media_type, order_index, and processing status
|
|
132
|
+
- Supports deterministic ordering for multi-attachment posts
|
|
133
|
+
|
|
134
|
+
#### post_hashtags
|
|
135
|
+
|
|
136
|
+
- Tracks normalized hashtag labels attached to posts
|
|
137
|
+
- Supports hashtag-based retrieval and discovery
|
|
138
|
+
- One row per (post_id, hashtag) pair
|
|
139
|
+
|
|
140
|
+
#### post_mentions
|
|
141
|
+
|
|
142
|
+
- Tracks mentioned users per post
|
|
143
|
+
- Stores post_id, mentioned_user_id, and mention metadata
|
|
144
|
+
- One row per (post_id, mentioned_user_id) pair
|
|
145
|
+
|
|
146
|
+
#### post_views
|
|
147
|
+
|
|
148
|
+
- Append-only view events for analytics
|
|
149
|
+
- Stores post_id, token (deduplication), created_at, and request_context (JSONB)
|
|
150
|
+
- request_context holds optional fields: viewer_id, country_code, client_type, browser_name, os_name, referrer
|
|
151
|
+
- Unique (post_id, token) prevents duplicate views per token
|
|
152
|
+
- Used to derive view_count via incremental aggregation (every 15 min). For each affected post, a task also aggregates like/comment counts (from post_likes, comments) and report counts (from moderation domain post_reports)
|
|
153
|
+
|
|
154
|
+
#### user_restrictions_lookup
|
|
155
|
+
|
|
156
|
+
- Read-time mirror for restricted users (muted/banned). Source-of-truth: moderation domain.
|
|
157
|
+
- Stores user_id, restriction_type (muted, banned)
|
|
158
|
+
- Used in feed queries to hide posts from restricted authors. Sync operations reconcile from moderation domain; see Lookup Sync Contract above.
|
|
159
|
+
|
|
160
|
+
#### user_blocks_lookup
|
|
161
|
+
|
|
162
|
+
- Read-time mirror for user-to-user blocks. Source-of-truth: user domain.
|
|
163
|
+
- Stores blocker_id, blocked_id (directed edge: blocker has blocked blocked)
|
|
164
|
+
- Used in feed queries to hide posts from authors the viewer has blocked. Sync operations reconcile from user domain; see Lookup Sync Contract above.
|
|
165
|
+
|
|
166
|
+
### Enums
|
|
167
|
+
|
|
168
|
+
#### post_status
|
|
169
|
+
|
|
170
|
+
- Lifecycle state of a post (active, deleted)
|
|
171
|
+
|
|
172
|
+
#### post_visibility
|
|
173
|
+
|
|
174
|
+
- Post visibility state (public, followers, private, unlisted)
|
|
175
|
+
|
|
176
|
+
#### post_media_type
|
|
177
|
+
|
|
178
|
+
- Attachment media type (image, video)
|
|
179
|
+
|
|
180
|
+
#### post_attachment_status
|
|
181
|
+
|
|
182
|
+
- Attachment processing lifecycle (processing, ready, failed, removed)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from core_framework.domains.post.enums import AttachmentType, PostStatus, PostVisibility
|
|
2
|
+
from core_framework.domains.post.exceptions import (
|
|
3
|
+
BasePostException,
|
|
4
|
+
EditLimitReachedException,
|
|
5
|
+
PostNotFoundException,
|
|
6
|
+
PostUpdateNotFoundException,
|
|
7
|
+
)
|
|
8
|
+
from core_framework.domains.post.models import Post, PostPreview, PostStats, PostWithMetadata
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AttachmentType",
|
|
12
|
+
"PostStatus",
|
|
13
|
+
"PostVisibility",
|
|
14
|
+
"BasePostException",
|
|
15
|
+
"EditLimitReachedException",
|
|
16
|
+
"Post",
|
|
17
|
+
"PostNotFoundException",
|
|
18
|
+
"PostPreview",
|
|
19
|
+
"PostStats",
|
|
20
|
+
"PostUpdateNotFoundException",
|
|
21
|
+
"PostWithMetadata",
|
|
22
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from core_framework.core.runtime import CoreRuntime, unconfigured_dependency
|
|
2
|
+
from core_framework.domains.post.repository import PostRepository
|
|
3
|
+
from core_framework.domains.post.service import PostService
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_post_repository(runtime: CoreRuntime) -> PostRepository:
|
|
7
|
+
return PostRepository(runtime.write_postgres, runtime.read_postgres, runtime.write_postgres)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_post_service(runtime: CoreRuntime) -> PostService:
|
|
11
|
+
return PostService(build_post_repository(runtime))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def configure_post_dependencies(runtime: CoreRuntime) -> None:
|
|
15
|
+
global post_repository, post_service
|
|
16
|
+
post_repository = build_post_repository(runtime)
|
|
17
|
+
post_service = build_post_service(runtime)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
post_repository = unconfigured_dependency("PostRepository")
|
|
21
|
+
post_service = unconfigured_dependency("PostService")
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"build_post_repository",
|
|
25
|
+
"build_post_service",
|
|
26
|
+
"configure_post_dependencies",
|
|
27
|
+
"post_repository",
|
|
28
|
+
"post_service",
|
|
29
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PostVisibility(StrEnum):
|
|
5
|
+
PUBLIC = "public"
|
|
6
|
+
FOLLOWERS = "followers"
|
|
7
|
+
PRIVATE = "private"
|
|
8
|
+
UNLISTED = "unlisted"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AttachmentType(StrEnum):
|
|
12
|
+
IMAGE = "image"
|
|
13
|
+
VIDEO = "video"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PostStatus(StrEnum):
|
|
17
|
+
ACTIVE = "active"
|
|
18
|
+
DELETED = "deleted"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class BasePostException(Exception):
|
|
2
|
+
message: str
|
|
3
|
+
|
|
4
|
+
def __init__(self, message: str):
|
|
5
|
+
self.message = message
|
|
6
|
+
super().__init__(self.message)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PostUpdateNotFoundException(BasePostException):
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
super().__init__("Unable to process request")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EditLimitReachedException(BasePostException):
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
super().__init__("Edit limit reached. Maximum edits allowed per post.")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PostNotFoundException(BasePostException):
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
super().__init__("Post not found")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from core_framework.domains.post.enums import PostStatus, PostVisibility
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
9
|
+
class Post:
|
|
10
|
+
id: str
|
|
11
|
+
author_id: str
|
|
12
|
+
content: str
|
|
13
|
+
visibility: PostVisibility
|
|
14
|
+
edited_count: int
|
|
15
|
+
edited_at: datetime | None
|
|
16
|
+
created_at: datetime
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
20
|
+
class PostWithMetadata:
|
|
21
|
+
id: str
|
|
22
|
+
author_id: str
|
|
23
|
+
content: str
|
|
24
|
+
visibility: PostVisibility
|
|
25
|
+
status: PostStatus
|
|
26
|
+
edited_count: int
|
|
27
|
+
edited_at: datetime | None
|
|
28
|
+
created_at: datetime
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
32
|
+
class PostPreview:
|
|
33
|
+
id: str
|
|
34
|
+
author_id: str
|
|
35
|
+
content: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
39
|
+
class PostStats:
|
|
40
|
+
like_count: int
|
|
41
|
+
comment_count: int
|
|
42
|
+
view_count: int
|
|
43
|
+
report_count: int
|
|
44
|
+
|
|
45
|
+
DEFAULT: ClassVar[PostStats]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
PostStats.DEFAULT = PostStats(
|
|
49
|
+
like_count=0,
|
|
50
|
+
comment_count=0,
|
|
51
|
+
view_count=0,
|
|
52
|
+
report_count=0,
|
|
53
|
+
)
|