tuwunel_api/client/
threads.rs1use std::collections::BTreeSet;
2
3use axum::extract::State;
4use futures::{StreamExt, TryStreamExt};
5use ruma::{
6 OwnedUserId,
7 api::client::threads::get_threads,
8 events::{GlobalAccountDataEventType, ignored_user_list::IgnoredUserListEvent},
9};
10use tuwunel_core::{
11 Err, Result, at,
12 matrix::{
13 Event,
14 pdu::{PduCount, PduEvent},
15 },
16 result::{FlatOk, LogErr},
17 utils::stream::TryWidebandExt,
18};
19use tuwunel_service::rooms::pdu_metadata::IgnoredThreadView;
20
21use crate::Ruma;
22
23pub(crate) async fn get_threads_route(
25 State(services): State<crate::State>,
26 ref body: Ruma<get_threads::v1::Request>,
27) -> Result<get_threads::v1::Response> {
28 let sender_user = body.sender_user();
29 let room_id = &body.room_id;
30
31 if !services.metadata.exists(room_id).await {
32 return Err!(Request(Forbidden("Room does not exist to this server")));
33 }
34
35 if !services
36 .state_accessor
37 .user_can_see_room(sender_user, room_id)
38 .await
39 {
40 return Err!(Request(Forbidden("You don't have permission to view this room.")));
41 }
42
43 let limit = body
45 .limit
46 .map(usize::try_from)
47 .flat_ok()
48 .unwrap_or(10)
49 .min(100);
50
51 let from: PduCount = body
52 .from
53 .as_deref()
54 .map(str::parse)
55 .transpose()?
56 .unwrap_or_else(PduCount::max);
57
58 let ignored: BTreeSet<OwnedUserId> = services
60 .account_data
61 .get_global(sender_user, GlobalAccountDataEventType::IgnoredUserList)
62 .await
63 .map(|event: IgnoredUserListEvent| event.content.ignored_users.into_keys().collect())
64 .unwrap_or_default();
65
66 let mut threads: Vec<(PduCount, PduEvent)> = services
68 .threads
69 .threads_until(sender_user, room_id, from, &body.include)
70 .try_filter_map(async |(count, pdu)| {
71 Ok(services
72 .state_accessor
73 .user_can_see_event(sender_user, room_id, &pdu.event_id)
74 .await
75 .then_some((count, pdu)))
76 })
77 .try_filter_map(async |(count, pdu)| {
78 let view = match ignored.is_empty() {
79 | true => IgnoredThreadView::Unchanged,
80 | false =>
81 services
82 .pdu_metadata
83 .ignored_thread_view(sender_user, &ignored, &pdu)
84 .await,
85 };
86
87 Ok(match view {
88 | IgnoredThreadView::Omitted => None,
89 | view => Some((count, pdu, view)),
90 })
91 })
92 .take(limit.saturating_add(1))
93 .wide_and_then(async |(count, pdu, view)| {
94 let pdu = services
95 .pdu_metadata
96 .bundle_aggregations(sender_user, pdu)
97 .await;
98
99 Ok((count, apply_ignored_view(pdu, view)))
100 })
101 .try_collect()
102 .await?;
103
104 let more = threads.len() > limit;
105
106 threads.truncate(limit);
107
108 Ok(get_threads::v1::Response {
109 next_batch: threads
110 .last()
111 .filter(|_| more)
112 .map(at!(0))
113 .as_ref()
114 .map(ToString::to_string),
115
116 chunk: threads
117 .into_iter()
118 .map(at!(1))
119 .map(Event::into_format)
120 .collect(),
121 })
122}
123
124fn apply_ignored_view(mut pdu: PduEvent, view: IgnoredThreadView) -> PduEvent {
129 let IgnoredThreadView::Adjusted { root, count, latest } = view else {
130 return pdu;
131 };
132
133 if let Some(count) = count {
134 pdu.set_thread_count(count).log_err().ok();
135 }
136
137 if let Some(latest) = latest {
138 pdu.set_thread_latest_event(&latest)
139 .log_err()
140 .ok();
141 }
142
143 match root {
144 | None => pdu,
145 | Some(mut root) => {
146 root.unsigned = pdu.unsigned;
147 root.remove_replacement_bundle().log_err().ok();
148
149 *root
150 },
151 }
152}