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,791 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any, Literal
|
|
3
|
+
|
|
4
|
+
import orjson
|
|
5
|
+
from asyncpg import Record
|
|
6
|
+
|
|
7
|
+
from core_framework.core.database import Postgres
|
|
8
|
+
from core_framework.domains.post.constants import REDACTED_CONTENT_PLACEHOLDER
|
|
9
|
+
from core_framework.domains.post.enums import PostStatus, PostVisibility
|
|
10
|
+
from core_framework.domains.post.exceptions import (
|
|
11
|
+
EditLimitReachedException,
|
|
12
|
+
PostNotFoundException,
|
|
13
|
+
PostUpdateNotFoundException,
|
|
14
|
+
)
|
|
15
|
+
from core_framework.domains.post.models import Post, PostPreview, PostStats, PostWithMetadata
|
|
16
|
+
from core_framework.domains.user import REDACTED_AUTHOR_ID
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PostRepository:
|
|
20
|
+
def __init__(self, write_database: Postgres, read_database: Postgres, strong_read_database: Postgres):
|
|
21
|
+
self.write_database = write_database
|
|
22
|
+
self.read_database = read_database
|
|
23
|
+
self.strong_read_database = strong_read_database
|
|
24
|
+
|
|
25
|
+
async def insert_post(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
post_id: str,
|
|
29
|
+
author_id: str,
|
|
30
|
+
content: str,
|
|
31
|
+
visibility: PostVisibility,
|
|
32
|
+
hashtags: list[str] | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
insert_post_query = """
|
|
35
|
+
insert into "post".posts (id, author_id, content, visibility)
|
|
36
|
+
values ($1, $2, $3, $4::"post".post_visibility)
|
|
37
|
+
"""
|
|
38
|
+
insert_post_stats_query = """
|
|
39
|
+
insert into "post".post_stats (post_id)
|
|
40
|
+
values ($1)
|
|
41
|
+
"""
|
|
42
|
+
insert_post_hashtags_query = """
|
|
43
|
+
insert into "post".post_hashtags (post_id, hashtag)
|
|
44
|
+
select $1, unnest($2::varchar[])
|
|
45
|
+
"""
|
|
46
|
+
async with self.write_database.get_transaction() as connection:
|
|
47
|
+
await connection.execute(insert_post_query, post_id, author_id, content, visibility.value)
|
|
48
|
+
await connection.execute(insert_post_stats_query, post_id)
|
|
49
|
+
if hashtags:
|
|
50
|
+
await connection.execute(insert_post_hashtags_query, post_id, hashtags)
|
|
51
|
+
|
|
52
|
+
async def update_post(
|
|
53
|
+
self,
|
|
54
|
+
*,
|
|
55
|
+
post_id: str,
|
|
56
|
+
author_id: str,
|
|
57
|
+
post_updates: dict[str, Any],
|
|
58
|
+
hashtags: list[str] | None,
|
|
59
|
+
max_edit_count: int,
|
|
60
|
+
) -> None:
|
|
61
|
+
allowed_fields = {
|
|
62
|
+
"content": "content",
|
|
63
|
+
"visibility": "visibility",
|
|
64
|
+
}
|
|
65
|
+
set_clauses = []
|
|
66
|
+
params: list[Any] = [post_id, author_id, max_edit_count]
|
|
67
|
+
content_param_index: int | None = None
|
|
68
|
+
|
|
69
|
+
for field, value in post_updates.items():
|
|
70
|
+
if field not in allowed_fields:
|
|
71
|
+
continue
|
|
72
|
+
if value is None:
|
|
73
|
+
continue
|
|
74
|
+
column = allowed_fields[field]
|
|
75
|
+
params.append(value)
|
|
76
|
+
if field == "visibility":
|
|
77
|
+
set_clauses.append(f'{column} = ${len(params)}::"post".post_visibility')
|
|
78
|
+
else:
|
|
79
|
+
content_param_index = len(params)
|
|
80
|
+
set_clauses.append(f"{column} = ${len(params)}")
|
|
81
|
+
|
|
82
|
+
if not set_clauses and hashtags is None:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
if content_param_index is not None:
|
|
86
|
+
if hashtags is not None:
|
|
87
|
+
# Content and hashtags both present: bump when content changed or hashtags updated
|
|
88
|
+
params.append(True)
|
|
89
|
+
hashtags_param_index = len(params)
|
|
90
|
+
set_clauses.append(
|
|
91
|
+
f"edited_count = case when p.content is distinct from ${content_param_index} or ${hashtags_param_index} "
|
|
92
|
+
"then p.edited_count + 1 else p.edited_count end"
|
|
93
|
+
)
|
|
94
|
+
set_clauses.append(
|
|
95
|
+
f"edited_at = case when p.content is distinct from ${content_param_index} or ${hashtags_param_index} then now() else p.edited_at end"
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
# Content only: bump when content actually changed
|
|
99
|
+
set_clauses.append(
|
|
100
|
+
f"edited_count = case when p.content is distinct from ${content_param_index} "
|
|
101
|
+
"then p.edited_count + 1 else p.edited_count end"
|
|
102
|
+
)
|
|
103
|
+
set_clauses.append(
|
|
104
|
+
f"edited_at = case when p.content is distinct from ${content_param_index} then now() else p.edited_at end"
|
|
105
|
+
)
|
|
106
|
+
elif hashtags is not None:
|
|
107
|
+
# Hashtags only: always bump
|
|
108
|
+
set_clauses.append("edited_count = p.edited_count + 1")
|
|
109
|
+
set_clauses.append("edited_at = now()")
|
|
110
|
+
set_clauses.append("updated_at = now()")
|
|
111
|
+
|
|
112
|
+
update_posts_query = f"""
|
|
113
|
+
with post_check as (
|
|
114
|
+
select
|
|
115
|
+
id,
|
|
116
|
+
edited_count
|
|
117
|
+
from "post".posts
|
|
118
|
+
where true
|
|
119
|
+
and id = $1
|
|
120
|
+
and author_id = $2
|
|
121
|
+
and status = 'active'::"post".post_status
|
|
122
|
+
),
|
|
123
|
+
update_result as (
|
|
124
|
+
update "post".posts p
|
|
125
|
+
set {", ".join(set_clauses)}
|
|
126
|
+
from post_check pc
|
|
127
|
+
where true
|
|
128
|
+
and p.id = pc.id
|
|
129
|
+
and pc.edited_count < $3
|
|
130
|
+
returning 1
|
|
131
|
+
)
|
|
132
|
+
select
|
|
133
|
+
(select count(*) from post_check) as found,
|
|
134
|
+
(select count(*) from update_result) as rows_updated
|
|
135
|
+
"""
|
|
136
|
+
replace_post_hashtags_query = """
|
|
137
|
+
with target as (
|
|
138
|
+
select 1
|
|
139
|
+
from "post".posts p
|
|
140
|
+
where true
|
|
141
|
+
and p.id = $1
|
|
142
|
+
and p.author_id = $2
|
|
143
|
+
and p.status = 'active'::"post".post_status
|
|
144
|
+
and p.edited_count < $4
|
|
145
|
+
),
|
|
146
|
+
deleted as (
|
|
147
|
+
delete from "post".post_hashtags ph
|
|
148
|
+
using target
|
|
149
|
+
where true
|
|
150
|
+
and ph.post_id = $1
|
|
151
|
+
and ph.hashtag <> all($3::varchar[])
|
|
152
|
+
returning 1
|
|
153
|
+
),
|
|
154
|
+
inserted as (
|
|
155
|
+
insert into "post".post_hashtags (post_id, hashtag)
|
|
156
|
+
select $1, t
|
|
157
|
+
from target, unnest($3::varchar[]) as t
|
|
158
|
+
on conflict (post_id, hashtag) do nothing
|
|
159
|
+
returning 1
|
|
160
|
+
)
|
|
161
|
+
select 1
|
|
162
|
+
from target
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
async with self.write_database.get_transaction() as connection:
|
|
166
|
+
row = await connection.fetchrow(update_posts_query, *params)
|
|
167
|
+
if row["found"] == 0:
|
|
168
|
+
raise PostUpdateNotFoundException()
|
|
169
|
+
if row["rows_updated"] == 0:
|
|
170
|
+
raise EditLimitReachedException()
|
|
171
|
+
if hashtags is not None:
|
|
172
|
+
await connection.execute(
|
|
173
|
+
replace_post_hashtags_query,
|
|
174
|
+
post_id,
|
|
175
|
+
author_id,
|
|
176
|
+
hashtags,
|
|
177
|
+
max_edit_count,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
async def update_post_status_by_id_and_author(
|
|
181
|
+
self,
|
|
182
|
+
*,
|
|
183
|
+
post_id: str,
|
|
184
|
+
author_id: str,
|
|
185
|
+
status: PostStatus,
|
|
186
|
+
) -> None:
|
|
187
|
+
query = """
|
|
188
|
+
update "post".posts
|
|
189
|
+
set
|
|
190
|
+
status = $3::"post".post_status,
|
|
191
|
+
updated_at = now()
|
|
192
|
+
where true
|
|
193
|
+
and id = $1
|
|
194
|
+
and author_id = $2
|
|
195
|
+
and status != $3::"post".post_status
|
|
196
|
+
"""
|
|
197
|
+
async with self.write_database.get_connection() as connection:
|
|
198
|
+
await connection.execute(query, post_id, author_id, status.value)
|
|
199
|
+
|
|
200
|
+
async def update_post_status_by_id(self, *, post_id: str, status: PostStatus) -> bool:
|
|
201
|
+
query = """
|
|
202
|
+
update "post".posts
|
|
203
|
+
set
|
|
204
|
+
status = $2::"post".post_status,
|
|
205
|
+
updated_at = now()
|
|
206
|
+
where id = $1
|
|
207
|
+
returning 1
|
|
208
|
+
"""
|
|
209
|
+
async with self.write_database.get_connection() as connection:
|
|
210
|
+
row = await connection.fetchrow(query, post_id, status.value)
|
|
211
|
+
return row is not None
|
|
212
|
+
|
|
213
|
+
async def delete_post_by_id(self, *, post_id: str) -> bool:
|
|
214
|
+
query = """
|
|
215
|
+
delete from "post".posts
|
|
216
|
+
where id = $1
|
|
217
|
+
returning 1
|
|
218
|
+
"""
|
|
219
|
+
async with self.write_database.get_connection() as connection:
|
|
220
|
+
row = await connection.fetchrow(query, post_id)
|
|
221
|
+
return row is not None
|
|
222
|
+
|
|
223
|
+
async def insert_user_restriction_lookup(
|
|
224
|
+
self,
|
|
225
|
+
*,
|
|
226
|
+
user_id: str,
|
|
227
|
+
restriction_type: Literal["muted", "banned"],
|
|
228
|
+
) -> None:
|
|
229
|
+
query = """
|
|
230
|
+
merge into "post".user_restrictions_lookup as target
|
|
231
|
+
using (values ($1, $2)) as source(user_id, restriction_type)
|
|
232
|
+
on target.user_id = source.user_id
|
|
233
|
+
when matched then
|
|
234
|
+
update set restriction_type = source.restriction_type
|
|
235
|
+
when not matched then
|
|
236
|
+
insert (user_id, restriction_type)
|
|
237
|
+
values (source.user_id, source.restriction_type)
|
|
238
|
+
"""
|
|
239
|
+
async with self.write_database.get_connection() as connection:
|
|
240
|
+
await connection.execute(query, user_id, restriction_type)
|
|
241
|
+
|
|
242
|
+
async def delete_user_restriction_lookup(self, *, user_id: str) -> None:
|
|
243
|
+
query = """
|
|
244
|
+
delete from "post".user_restrictions_lookup
|
|
245
|
+
where true
|
|
246
|
+
and user_id = $1
|
|
247
|
+
"""
|
|
248
|
+
async with self.write_database.get_connection() as connection:
|
|
249
|
+
await connection.execute(query, user_id)
|
|
250
|
+
|
|
251
|
+
async def insert_user_block_lookup(self, *, blocker_id: str, blocked_id: str) -> None:
|
|
252
|
+
query = """
|
|
253
|
+
merge into "post".user_blocks_lookup as target
|
|
254
|
+
using (values ($1, $2)) as source(blocker_id, blocked_id)
|
|
255
|
+
on target.blocker_id = source.blocker_id
|
|
256
|
+
and target.blocked_id = source.blocked_id
|
|
257
|
+
when not matched then
|
|
258
|
+
insert (blocker_id, blocked_id)
|
|
259
|
+
values (source.blocker_id, source.blocked_id)
|
|
260
|
+
"""
|
|
261
|
+
async with self.write_database.get_connection() as connection:
|
|
262
|
+
await connection.execute(query, blocker_id, blocked_id)
|
|
263
|
+
|
|
264
|
+
async def delete_user_block_lookup(self, *, blocker_id: str, blocked_id: str) -> None:
|
|
265
|
+
query = """
|
|
266
|
+
delete from "post".user_blocks_lookup
|
|
267
|
+
where true
|
|
268
|
+
and blocker_id = $1
|
|
269
|
+
and blocked_id = $2
|
|
270
|
+
"""
|
|
271
|
+
async with self.write_database.get_connection() as connection:
|
|
272
|
+
await connection.execute(query, blocker_id, blocked_id)
|
|
273
|
+
|
|
274
|
+
async def delete_user(self, *, user_id: str) -> None:
|
|
275
|
+
delete_hashtags_for_author_query = """
|
|
276
|
+
delete from "post".post_hashtags ph
|
|
277
|
+
using "post".posts p
|
|
278
|
+
where true
|
|
279
|
+
and ph.post_id = p.id
|
|
280
|
+
and p.author_id = $1
|
|
281
|
+
"""
|
|
282
|
+
redact_posts_query = """
|
|
283
|
+
update "post".posts
|
|
284
|
+
set
|
|
285
|
+
content = $2,
|
|
286
|
+
author_id = $3,
|
|
287
|
+
updated_at = now()
|
|
288
|
+
where true
|
|
289
|
+
and author_id = $1
|
|
290
|
+
"""
|
|
291
|
+
delete_restrictions_query = """
|
|
292
|
+
delete from "post".user_restrictions_lookup
|
|
293
|
+
where true
|
|
294
|
+
and user_id = $1
|
|
295
|
+
"""
|
|
296
|
+
delete_blocks_query = """
|
|
297
|
+
delete from "post".user_blocks_lookup
|
|
298
|
+
where true
|
|
299
|
+
and (
|
|
300
|
+
blocker_id = $1
|
|
301
|
+
or blocked_id = $1
|
|
302
|
+
)
|
|
303
|
+
"""
|
|
304
|
+
async with self.write_database.get_transaction() as connection:
|
|
305
|
+
await connection.execute(delete_hashtags_for_author_query, user_id)
|
|
306
|
+
await connection.execute(
|
|
307
|
+
redact_posts_query,
|
|
308
|
+
user_id,
|
|
309
|
+
REDACTED_CONTENT_PLACEHOLDER,
|
|
310
|
+
REDACTED_AUTHOR_ID,
|
|
311
|
+
)
|
|
312
|
+
await connection.execute(delete_restrictions_query, user_id)
|
|
313
|
+
await connection.execute(delete_blocks_query, user_id)
|
|
314
|
+
|
|
315
|
+
async def select_post_with_metadata_by_id(self, *, post_id: str) -> PostWithMetadata:
|
|
316
|
+
query = """
|
|
317
|
+
select
|
|
318
|
+
id,
|
|
319
|
+
author_id,
|
|
320
|
+
content,
|
|
321
|
+
visibility,
|
|
322
|
+
status,
|
|
323
|
+
edited_count,
|
|
324
|
+
edited_at,
|
|
325
|
+
created_at
|
|
326
|
+
from "post".posts
|
|
327
|
+
where id = $1
|
|
328
|
+
"""
|
|
329
|
+
async with self.read_database.get_connection() as connection:
|
|
330
|
+
row = await connection.fetchrow(query, post_id)
|
|
331
|
+
if row is None:
|
|
332
|
+
raise PostNotFoundException()
|
|
333
|
+
return PostWithMetadata(
|
|
334
|
+
id=row["id"],
|
|
335
|
+
author_id=row["author_id"],
|
|
336
|
+
content=row["content"],
|
|
337
|
+
visibility=PostVisibility(row["visibility"]),
|
|
338
|
+
status=PostStatus(row["status"]),
|
|
339
|
+
edited_count=row["edited_count"],
|
|
340
|
+
edited_at=row["edited_at"],
|
|
341
|
+
created_at=row["created_at"],
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
async def select_post_by_id(self, *, post_id: str, viewer_id: str | None) -> Post | None:
|
|
345
|
+
query = """
|
|
346
|
+
select
|
|
347
|
+
p.id,
|
|
348
|
+
p.author_id,
|
|
349
|
+
p.content,
|
|
350
|
+
p.visibility,
|
|
351
|
+
p.edited_count,
|
|
352
|
+
p.edited_at,
|
|
353
|
+
p.created_at
|
|
354
|
+
from "post".posts p
|
|
355
|
+
where true
|
|
356
|
+
and p.id = $1
|
|
357
|
+
and p.status = 'active'::"post".post_status
|
|
358
|
+
and (
|
|
359
|
+
$2::varchar is null
|
|
360
|
+
or not exists(
|
|
361
|
+
select 1
|
|
362
|
+
from "post".user_blocks_lookup b
|
|
363
|
+
where true
|
|
364
|
+
and b.blocker_id = $2
|
|
365
|
+
and b.blocked_id = p.author_id
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
"""
|
|
369
|
+
async with self.read_database.get_connection() as connection:
|
|
370
|
+
result = await connection.fetchrow(query, post_id, viewer_id)
|
|
371
|
+
if result is None:
|
|
372
|
+
return None
|
|
373
|
+
return self._map_post_row(result)
|
|
374
|
+
|
|
375
|
+
async def select_posts(
|
|
376
|
+
self,
|
|
377
|
+
*,
|
|
378
|
+
viewer_id: str | None,
|
|
379
|
+
cursor: datetime,
|
|
380
|
+
limit: int,
|
|
381
|
+
) -> list[Post]:
|
|
382
|
+
query = """
|
|
383
|
+
with viewer_context as (
|
|
384
|
+
select exists(
|
|
385
|
+
select 1
|
|
386
|
+
from "post".user_restrictions_lookup
|
|
387
|
+
where true
|
|
388
|
+
and user_id = $1
|
|
389
|
+
and restriction_type = 'muted'
|
|
390
|
+
) as is_viewer_muted
|
|
391
|
+
)
|
|
392
|
+
select
|
|
393
|
+
p.id,
|
|
394
|
+
p.author_id,
|
|
395
|
+
p.content,
|
|
396
|
+
p.visibility,
|
|
397
|
+
p.edited_count,
|
|
398
|
+
p.edited_at,
|
|
399
|
+
p.created_at
|
|
400
|
+
from "post".posts p
|
|
401
|
+
left join "post".user_restrictions_lookup r
|
|
402
|
+
on r.user_id = p.author_id
|
|
403
|
+
cross join viewer_context vc
|
|
404
|
+
where true
|
|
405
|
+
and p.status = 'active'::"post".post_status
|
|
406
|
+
and p.created_at <= $2
|
|
407
|
+
and (
|
|
408
|
+
r.user_id is null
|
|
409
|
+
or (
|
|
410
|
+
r.restriction_type = 'muted'
|
|
411
|
+
and $1 is not null
|
|
412
|
+
and vc.is_viewer_muted
|
|
413
|
+
and p.author_id = $1
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
and (
|
|
417
|
+
$1 is null
|
|
418
|
+
or not exists(
|
|
419
|
+
select 1
|
|
420
|
+
from "post".user_blocks_lookup b
|
|
421
|
+
where true
|
|
422
|
+
and b.blocker_id = $1
|
|
423
|
+
and b.blocked_id = p.author_id
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
order by p.created_at desc
|
|
427
|
+
limit $3
|
|
428
|
+
"""
|
|
429
|
+
async with self.read_database.get_connection() as connection:
|
|
430
|
+
result = await connection.fetch(query, viewer_id, cursor, limit)
|
|
431
|
+
return [self._map_post_row(row) for row in result]
|
|
432
|
+
|
|
433
|
+
async def select_posts_by_user_id(
|
|
434
|
+
self,
|
|
435
|
+
*,
|
|
436
|
+
user_id: str,
|
|
437
|
+
cursor: datetime,
|
|
438
|
+
limit: int,
|
|
439
|
+
viewer_id: str | None,
|
|
440
|
+
) -> list[Post]:
|
|
441
|
+
query = """
|
|
442
|
+
with viewer_context as (
|
|
443
|
+
select exists(
|
|
444
|
+
select 1
|
|
445
|
+
from "post".user_restrictions_lookup
|
|
446
|
+
where true
|
|
447
|
+
and user_id = $4
|
|
448
|
+
and restriction_type = 'muted'
|
|
449
|
+
) as is_viewer_muted
|
|
450
|
+
)
|
|
451
|
+
select
|
|
452
|
+
p.id,
|
|
453
|
+
p.author_id,
|
|
454
|
+
p.content,
|
|
455
|
+
p.visibility,
|
|
456
|
+
p.edited_count,
|
|
457
|
+
p.edited_at,
|
|
458
|
+
p.created_at
|
|
459
|
+
from "post".posts p
|
|
460
|
+
left join "post".user_restrictions_lookup r
|
|
461
|
+
on r.user_id = p.author_id
|
|
462
|
+
cross join viewer_context vc
|
|
463
|
+
where true
|
|
464
|
+
and p.author_id = $1
|
|
465
|
+
and p.status = 'active'::"post".post_status
|
|
466
|
+
and p.created_at <= $2
|
|
467
|
+
and (
|
|
468
|
+
r.user_id is null
|
|
469
|
+
or (
|
|
470
|
+
r.restriction_type = 'muted'
|
|
471
|
+
and $4 is not null
|
|
472
|
+
and vc.is_viewer_muted
|
|
473
|
+
and p.author_id = $4
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
and (
|
|
477
|
+
$4::varchar is null
|
|
478
|
+
or not exists(
|
|
479
|
+
select 1
|
|
480
|
+
from "post".user_blocks_lookup
|
|
481
|
+
where true
|
|
482
|
+
and blocker_id = $4
|
|
483
|
+
and blocked_id = $1
|
|
484
|
+
)
|
|
485
|
+
)
|
|
486
|
+
order by p.created_at desc
|
|
487
|
+
limit $3
|
|
488
|
+
"""
|
|
489
|
+
async with self.read_database.get_connection() as connection:
|
|
490
|
+
result = await connection.fetch(query, user_id, cursor, limit, viewer_id)
|
|
491
|
+
return [self._map_post_row(row) for row in result]
|
|
492
|
+
|
|
493
|
+
async def select_posts_by_hashtag(
|
|
494
|
+
self,
|
|
495
|
+
*,
|
|
496
|
+
hashtag: str,
|
|
497
|
+
cursor: datetime,
|
|
498
|
+
limit: int,
|
|
499
|
+
viewer_id: str | None,
|
|
500
|
+
) -> list[Post]:
|
|
501
|
+
query = """
|
|
502
|
+
with viewer_context as (
|
|
503
|
+
select exists(
|
|
504
|
+
select 1
|
|
505
|
+
from "post".user_restrictions_lookup
|
|
506
|
+
where true
|
|
507
|
+
and user_id = $4
|
|
508
|
+
and restriction_type = 'muted'
|
|
509
|
+
) as is_viewer_muted
|
|
510
|
+
)
|
|
511
|
+
select
|
|
512
|
+
p.id,
|
|
513
|
+
p.author_id,
|
|
514
|
+
p.content,
|
|
515
|
+
p.visibility,
|
|
516
|
+
p.edited_count,
|
|
517
|
+
p.edited_at,
|
|
518
|
+
p.created_at
|
|
519
|
+
from "post".posts p
|
|
520
|
+
join "post".post_hashtags ph on ph.post_id = p.id and ph.hashtag = $1
|
|
521
|
+
left join "post".user_restrictions_lookup r on r.user_id = p.author_id
|
|
522
|
+
cross join viewer_context vc
|
|
523
|
+
where true
|
|
524
|
+
and p.status = 'active'::"post".post_status
|
|
525
|
+
and p.created_at <= $2
|
|
526
|
+
and (
|
|
527
|
+
r.user_id is null
|
|
528
|
+
or (
|
|
529
|
+
r.restriction_type = 'muted'
|
|
530
|
+
and $4 is not null
|
|
531
|
+
and vc.is_viewer_muted
|
|
532
|
+
and p.author_id = $4
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
and (
|
|
536
|
+
$4::varchar is null
|
|
537
|
+
or not exists(
|
|
538
|
+
select 1
|
|
539
|
+
from "post".user_blocks_lookup b
|
|
540
|
+
where true
|
|
541
|
+
and b.blocker_id = $4
|
|
542
|
+
and b.blocked_id = p.author_id
|
|
543
|
+
)
|
|
544
|
+
)
|
|
545
|
+
order by p.created_at desc
|
|
546
|
+
limit $3
|
|
547
|
+
"""
|
|
548
|
+
async with self.read_database.get_connection() as connection:
|
|
549
|
+
result = await connection.fetch(query, hashtag, cursor, limit, viewer_id)
|
|
550
|
+
return [self._map_post_row(row) for row in result]
|
|
551
|
+
|
|
552
|
+
async def select_posts_unfiltered(
|
|
553
|
+
self,
|
|
554
|
+
*,
|
|
555
|
+
cursor: datetime,
|
|
556
|
+
limit: int,
|
|
557
|
+
) -> list[PostWithMetadata]:
|
|
558
|
+
query = """
|
|
559
|
+
select
|
|
560
|
+
id,
|
|
561
|
+
author_id,
|
|
562
|
+
content,
|
|
563
|
+
visibility,
|
|
564
|
+
status,
|
|
565
|
+
edited_count,
|
|
566
|
+
edited_at,
|
|
567
|
+
created_at
|
|
568
|
+
from "post".posts
|
|
569
|
+
where true
|
|
570
|
+
and created_at <= $1
|
|
571
|
+
order by created_at desc
|
|
572
|
+
limit $2
|
|
573
|
+
"""
|
|
574
|
+
async with self.read_database.get_connection() as connection:
|
|
575
|
+
result = await connection.fetch(query, cursor, limit)
|
|
576
|
+
return [
|
|
577
|
+
PostWithMetadata(
|
|
578
|
+
id=row["id"],
|
|
579
|
+
author_id=row["author_id"],
|
|
580
|
+
content=row["content"],
|
|
581
|
+
visibility=PostVisibility(row["visibility"]),
|
|
582
|
+
status=PostStatus(row["status"]),
|
|
583
|
+
edited_count=row["edited_count"],
|
|
584
|
+
edited_at=row["edited_at"],
|
|
585
|
+
created_at=row["created_at"],
|
|
586
|
+
)
|
|
587
|
+
for row in result
|
|
588
|
+
]
|
|
589
|
+
|
|
590
|
+
async def insert_post_like(self, *, post_id: str, liker_id: str) -> None:
|
|
591
|
+
query = """
|
|
592
|
+
insert into "post".post_likes (post_id, liker_id)
|
|
593
|
+
select
|
|
594
|
+
$1::varchar,
|
|
595
|
+
$2::varchar
|
|
596
|
+
where exists (
|
|
597
|
+
select 1
|
|
598
|
+
from "post".posts
|
|
599
|
+
where true
|
|
600
|
+
and id = $1::varchar
|
|
601
|
+
and status = 'active'::"post".post_status
|
|
602
|
+
)
|
|
603
|
+
on conflict (post_id, liker_id) do nothing
|
|
604
|
+
"""
|
|
605
|
+
async with self.write_database.get_connection() as connection:
|
|
606
|
+
await connection.execute(query, post_id, liker_id)
|
|
607
|
+
|
|
608
|
+
async def delete_post_like(self, *, post_id: str, liker_id: str) -> None:
|
|
609
|
+
query = """
|
|
610
|
+
delete from "post".post_likes
|
|
611
|
+
where true
|
|
612
|
+
and post_id = $1
|
|
613
|
+
and liker_id = $2
|
|
614
|
+
"""
|
|
615
|
+
async with self.write_database.get_connection() as connection:
|
|
616
|
+
await connection.execute(query, post_id, liker_id)
|
|
617
|
+
|
|
618
|
+
async def select_post_like_count(self, *, post_id: str) -> int:
|
|
619
|
+
query = """
|
|
620
|
+
select count(*)
|
|
621
|
+
from "post".post_likes
|
|
622
|
+
where post_id = $1
|
|
623
|
+
"""
|
|
624
|
+
async with self.read_database.get_connection() as connection:
|
|
625
|
+
return await connection.fetchval(query, post_id)
|
|
626
|
+
|
|
627
|
+
async def select_post_ids_liked_by_user(self, *, liker_id: str, post_ids: set[str]) -> list[str]:
|
|
628
|
+
if not post_ids:
|
|
629
|
+
return []
|
|
630
|
+
query = """
|
|
631
|
+
select post_id
|
|
632
|
+
from "post".post_likes
|
|
633
|
+
where true
|
|
634
|
+
and liker_id = $1
|
|
635
|
+
and post_id = any($2)
|
|
636
|
+
"""
|
|
637
|
+
async with self.read_database.get_connection() as connection:
|
|
638
|
+
rows = await connection.fetch(query, liker_id, list(post_ids))
|
|
639
|
+
return [row["post_id"] for row in rows]
|
|
640
|
+
|
|
641
|
+
async def insert_post_view(
|
|
642
|
+
self,
|
|
643
|
+
*,
|
|
644
|
+
post_id: str,
|
|
645
|
+
token: str,
|
|
646
|
+
request_context: dict[str, str],
|
|
647
|
+
) -> None:
|
|
648
|
+
query = """
|
|
649
|
+
insert into "post".post_views (post_id, token, request_context)
|
|
650
|
+
values ($1, $2, $3::jsonb)
|
|
651
|
+
on conflict (post_id, token) do nothing
|
|
652
|
+
"""
|
|
653
|
+
json_value = orjson.dumps(request_context).decode()
|
|
654
|
+
async with self.write_database.get_connection() as connection:
|
|
655
|
+
await connection.execute(query, post_id, token, json_value)
|
|
656
|
+
|
|
657
|
+
async def aggregate_post_view_counts(self) -> list[str]:
|
|
658
|
+
query = """
|
|
659
|
+
with new_counts as (
|
|
660
|
+
select
|
|
661
|
+
pv.post_id,
|
|
662
|
+
count(*) as new_views
|
|
663
|
+
from "post".post_views pv
|
|
664
|
+
join "post".post_stats ps on ps.post_id = pv.post_id
|
|
665
|
+
where pv.created_at > coalesce(ps.view_count_aggregated_at, '1970-01-01'::timestamptz)
|
|
666
|
+
group by pv.post_id
|
|
667
|
+
)
|
|
668
|
+
update "post".post_stats ps
|
|
669
|
+
set
|
|
670
|
+
view_count = ps.view_count + nc.new_views,
|
|
671
|
+
view_count_aggregated_at = now()
|
|
672
|
+
from new_counts nc
|
|
673
|
+
where ps.post_id = nc.post_id
|
|
674
|
+
returning ps.post_id
|
|
675
|
+
"""
|
|
676
|
+
async with self.write_database.get_connection() as connection:
|
|
677
|
+
rows = await connection.fetch(query)
|
|
678
|
+
return [row["post_id"] for row in rows]
|
|
679
|
+
|
|
680
|
+
async def update_post_stats(
|
|
681
|
+
self,
|
|
682
|
+
*,
|
|
683
|
+
post_id: str,
|
|
684
|
+
like_count: int,
|
|
685
|
+
comment_count: int,
|
|
686
|
+
report_count: int,
|
|
687
|
+
) -> None:
|
|
688
|
+
query = """
|
|
689
|
+
update "post".post_stats
|
|
690
|
+
set
|
|
691
|
+
like_count = $2,
|
|
692
|
+
comment_count = $3,
|
|
693
|
+
report_count = $4,
|
|
694
|
+
updated_at = now()
|
|
695
|
+
where post_id = $1
|
|
696
|
+
"""
|
|
697
|
+
async with self.write_database.get_connection() as connection:
|
|
698
|
+
await connection.execute(
|
|
699
|
+
query,
|
|
700
|
+
post_id,
|
|
701
|
+
like_count,
|
|
702
|
+
comment_count,
|
|
703
|
+
report_count,
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
async def select_post_stats_mapping(self, *, post_ids: set[str]) -> dict[str, PostStats]:
|
|
707
|
+
if not post_ids:
|
|
708
|
+
return {}
|
|
709
|
+
|
|
710
|
+
query = """
|
|
711
|
+
select
|
|
712
|
+
post_id,
|
|
713
|
+
like_count,
|
|
714
|
+
comment_count,
|
|
715
|
+
view_count,
|
|
716
|
+
report_count
|
|
717
|
+
from "post".post_stats
|
|
718
|
+
where post_id = any($1::varchar[])
|
|
719
|
+
"""
|
|
720
|
+
async with self.read_database.get_connection() as connection:
|
|
721
|
+
result = await connection.fetch(query, list(post_ids))
|
|
722
|
+
return {
|
|
723
|
+
row["post_id"]: PostStats(
|
|
724
|
+
like_count=row["like_count"],
|
|
725
|
+
comment_count=row["comment_count"],
|
|
726
|
+
view_count=row["view_count"],
|
|
727
|
+
report_count=row["report_count"],
|
|
728
|
+
)
|
|
729
|
+
for row in result
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async def select_post_content_mapping(self, *, post_ids: set[str]) -> dict[str, str]:
|
|
733
|
+
if not post_ids:
|
|
734
|
+
return {}
|
|
735
|
+
query = """
|
|
736
|
+
select id, content
|
|
737
|
+
from "post".posts
|
|
738
|
+
where id = any($1::varchar[])
|
|
739
|
+
"""
|
|
740
|
+
async with self.read_database.get_connection() as connection:
|
|
741
|
+
result = await connection.fetch(query, list(post_ids))
|
|
742
|
+
return {row["id"]: row["content"] or "" for row in result}
|
|
743
|
+
|
|
744
|
+
async def select_post_preview_mapping(self, *, post_ids: set[str]) -> dict[str, PostPreview]:
|
|
745
|
+
if not post_ids:
|
|
746
|
+
return {}
|
|
747
|
+
query = """
|
|
748
|
+
select
|
|
749
|
+
id,
|
|
750
|
+
author_id,
|
|
751
|
+
content
|
|
752
|
+
from "post".posts
|
|
753
|
+
where id = any($1::varchar[])
|
|
754
|
+
"""
|
|
755
|
+
async with self.read_database.get_connection() as connection:
|
|
756
|
+
result = await connection.fetch(query, list(post_ids))
|
|
757
|
+
return {
|
|
758
|
+
row["id"]: PostPreview(
|
|
759
|
+
id=row["id"],
|
|
760
|
+
author_id=row["author_id"],
|
|
761
|
+
content=row["content"],
|
|
762
|
+
)
|
|
763
|
+
for row in result
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async def select_hashtags_mapping(self, *, post_ids: set[str]) -> dict[str, list[str]]:
|
|
767
|
+
if not post_ids:
|
|
768
|
+
return {}
|
|
769
|
+
|
|
770
|
+
query = """
|
|
771
|
+
select
|
|
772
|
+
post_id,
|
|
773
|
+
array_agg(hashtag order by created_at) as hashtags
|
|
774
|
+
from "post".post_hashtags
|
|
775
|
+
where post_id = any($1::varchar[])
|
|
776
|
+
group by post_id
|
|
777
|
+
"""
|
|
778
|
+
async with self.read_database.get_connection() as connection:
|
|
779
|
+
result = await connection.fetch(query, list(post_ids))
|
|
780
|
+
return {row["post_id"]: list(row["hashtags"]) for row in result}
|
|
781
|
+
|
|
782
|
+
def _map_post_row(self, row: Record) -> Post:
|
|
783
|
+
return Post(
|
|
784
|
+
id=row["id"],
|
|
785
|
+
author_id=row["author_id"],
|
|
786
|
+
content=row["content"],
|
|
787
|
+
visibility=PostVisibility(row["visibility"]),
|
|
788
|
+
edited_count=row["edited_count"],
|
|
789
|
+
edited_at=row["edited_at"],
|
|
790
|
+
created_at=row["created_at"],
|
|
791
|
+
)
|