Skip to main content

max / makenotwork

4.9 KB · 137 lines History Blame Raw
1 //! Thread tracking handlers, track, untrack, tracked threads list.
2
3 use axum::{
4 extract::{Path, Query},
5 response::{IntoResponse, Redirect, Response},
6 };
7 use tower_sessions::Session;
8
9 use crate::AppState;
10 use crate::auth::{MaybeUser, RequireUser};
11 use crate::csrf;
12 use crate::templates::{
13 Pagination, TrackedThreadViewRow, TrackedThreadsTemplate, TrackingInfoTemplate,
14 };
15
16 use super::{CommunityScope, check_community_access, db_error, template_user};
17 use mt_db::queries::ThreadWithBreadcrumb;
18
19 /// POST /p/{slug}/{cat}/{thread_id}/track
20 #[tracing::instrument(skip_all)]
21 pub(super) async fn track_thread_handler(
22 axum::extract::State(state): axum::extract::State<AppState>,
23 Path((slug, category_slug, thread_id_str)): Path<(String, String, String)>,
24 RequireUser(user): RequireUser,
25 ) -> Result<Redirect, Response> {
26 // C1: prove the thread belongs to the slug's community (mismatch/bad id →
27 // 404), and refuse banned/suspended users so they can't accrue tracking
28 // state via a foreign slug. Tracking is read-adjacent, so a mute doesn't gate
29 // it, use `check_community_access`, not write access.
30 let scope =
31 CommunityScope::<ThreadWithBreadcrumb>::resolve(&state.db, &slug, &thread_id_str).await?;
32 check_community_access(&state.db, &scope.community, Some(user.user_id)).await?;
33
34 mt_db::mutations::track_thread(&state.db, user.user_id, scope.resource.id)
35 .await
36 .map_err(db_error)?;
37
38 Ok(Redirect::to(&format!(
39 "/p/{slug}/{category_slug}/{thread_id_str}?toast=Thread+tracked"
40 )))
41 }
42
43 /// POST /p/{slug}/{cat}/{thread_id}/untrack
44 #[tracing::instrument(skip_all)]
45 pub(super) async fn untrack_thread_handler(
46 axum::extract::State(state): axum::extract::State<AppState>,
47 Path((slug, category_slug, thread_id_str)): Path<(String, String, String)>,
48 RequireUser(user): RequireUser,
49 ) -> Result<Redirect, Response> {
50 // C1: same scope proof as track (mismatch/bad id → 404). No ban check here,
51 // removing your own tracking state is always allowed.
52 let scope =
53 CommunityScope::<ThreadWithBreadcrumb>::resolve(&state.db, &slug, &thread_id_str).await?;
54
55 mt_db::mutations::untrack_thread(&state.db, user.user_id, scope.resource.id)
56 .await
57 .map_err(db_error)?;
58
59 Ok(Redirect::to(&format!(
60 "/p/{slug}/{category_slug}/{thread_id_str}?toast=Thread+untracked"
61 )))
62 }
63
64 /// POST /tracked/stop-all
65 #[tracing::instrument(skip_all)]
66 pub(super) async fn untrack_all_handler(
67 axum::extract::State(state): axum::extract::State<AppState>,
68 RequireUser(user): RequireUser,
69 ) -> Result<Redirect, Response> {
70 mt_db::mutations::untrack_all(&state.db, user.user_id)
71 .await
72 .map_err(db_error)?;
73
74 Ok(Redirect::to("/tracked?toast=Stopped+tracking+all"))
75 }
76
77 /// GET /about/tracking, privacy/tracking info page
78 #[tracing::instrument(skip_all)]
79 pub(super) async fn tracking_info_page(
80 axum::extract::State(state): axum::extract::State<AppState>,
81 session: Session,
82 MaybeUser(session_user): MaybeUser,
83 ) -> Result<impl IntoResponse, Response> {
84 let csrf_token = Some(csrf::get_or_create_token(&session).await?);
85 let session_user = session_user
86 .as_ref()
87 .map(|u| template_user(u, state.config.platform_admin_id));
88 Ok(TrackingInfoTemplate {
89 csrf_token,
90 session_user,
91 mnw_base_url: state.config.mnw_base_url.clone(),
92 })
93 }
94
95 /// GET /tracked, tracked threads page
96 #[tracing::instrument(skip_all)]
97 pub(super) async fn tracked_threads_page(
98 axum::extract::State(state): axum::extract::State<AppState>,
99 session: Session,
100 Query(query): Query<super::ForumDirectoryQuery>,
101 RequireUser(user): RequireUser,
102 ) -> Result<impl IntoResponse, Response> {
103 let csrf_token = Some(csrf::get_or_create_token(&session).await?);
104 const PER_PAGE: i64 = 50;
105 let total = mt_db::queries::count_tracked_threads(&state.db, user.user_id)
106 .await
107 .map_err(db_error)?;
108 let pagination = Pagination::new(query.page.unwrap_or(1).max(1), total, PER_PAGE);
109 let offset = pagination.offset(PER_PAGE);
110
111 let db_tracked =
112 mt_db::queries::list_tracked_threads(&state.db, user.user_id, PER_PAGE, offset)
113 .await
114 .map_err(db_error)?;
115
116 let threads = db_tracked
117 .into_iter()
118 .map(|t| TrackedThreadViewRow {
119 thread_id: t.thread_id.to_string(),
120 thread_title: t.thread_title,
121 community_name: t.community_name,
122 community_slug: t.community_slug,
123 category_slug: t.category_slug,
124 unread_count: t.unread_count.max(0) as u32,
125 has_mention: t.has_mention,
126 })
127 .collect();
128
129 Ok(TrackedThreadsTemplate {
130 csrf_token,
131 session_user: Some(template_user(&user, state.config.platform_admin_id)),
132 mnw_base_url: state.config.mnw_base_url.clone(),
133 threads,
134 pagination,
135 })
136 }
137