1use std::{
2 collections::{BTreeSet, VecDeque},
3 convert::identity,
4 str::FromStr,
5};
6
7use axum::extract::State;
8use futures::{
9 StreamExt,
10 future::ready,
11 stream::{once, unfold},
12};
13use ruma::{
14 OwnedRoomId, OwnedServerName, RoomId, UInt, UserId, api::client::space::get_hierarchy,
15};
16use tuwunel_core::{
17 Err, Result, debug_error, error,
18 smallvec::SmallVec,
19 trace,
20 utils::{
21 BoolExt,
22 stream::{IterStream, ReadyExt, WidebandExt},
23 },
24};
25use tuwunel_service::{
26 Services,
27 rooms::{
28 short::ShortRoomId,
29 spaces::{
30 Accessibility, Identifier, PaginationToken, get_parent_children_via,
31 is_summary_serializable, summary_to_chunk,
32 },
33 },
34};
35
36use crate::Ruma;
37
38pub(crate) struct HierarchyArgs<'a> {
41 pub sender_user: &'a UserId,
42 pub room_id: &'a RoomId,
43 pub limit: usize,
44 pub max_depth: usize,
45 pub suggested_only: bool,
46 pub skip_room_ids: &'a [ShortRoomId],
47
48 pub bypass_visibility: bool,
51}
52
53pub(crate) async fn get_hierarchy_route(
58 State(services): State<crate::State>,
59 body: Ruma<get_hierarchy::v1::Request>,
60) -> Result<get_hierarchy::v1::Response> {
61 let limit = body
62 .limit
63 .unwrap_or_else(|| UInt::from(10_u32))
64 .min(UInt::from(100_u32));
65
66 let max_depth = body
67 .max_depth
68 .unwrap_or_else(|| UInt::from(3_u32))
69 .min(UInt::from(10_u32));
70
71 let key = body
72 .from
73 .as_ref()
74 .and_then(|s| PaginationToken::from_str(s).ok());
75
76 if let Some(ref token) = key
78 && (token.suggested_only != body.suggested_only || token.max_depth != max_depth)
79 {
80 return Err!(Request(InvalidParam(
81 "suggested_only and max_depth cannot change on paginated requests"
82 )));
83 }
84
85 get_client_hierarchy(&services, HierarchyArgs {
86 sender_user: body.sender_user(),
87 room_id: &body.room_id,
88 limit: limit.try_into().unwrap_or(10),
89 max_depth: max_depth.try_into().unwrap_or(usize::MAX),
90 suggested_only: body.suggested_only,
91 skip_room_ids: key
92 .as_ref()
93 .map(|t| t.short_room_ids.as_slice())
94 .unwrap_or_default(),
95 bypass_visibility: false,
96 })
97 .await
98}
99
100pub(crate) async fn get_client_hierarchy(
101 services: &Services,
102 args: HierarchyArgs<'_>,
103) -> Result<get_hierarchy::v1::Response> {
104 type Via = SmallVec<[OwnedServerName; 1]>;
105 type QueueItem = (OwnedRoomId, Via, usize);
106
107 let HierarchyArgs {
108 sender_user,
109 room_id,
110 limit,
111 max_depth,
112 suggested_only,
113 skip_room_ids,
114 bypass_visibility,
115 } = args;
116
117 let sender = match bypass_visibility {
120 | true => Identifier::ServerName(services.globals.server_name()),
121 | false => Identifier::UserId(sender_user),
122 };
123
124 let root_via: Via = match bypass_visibility {
127 | true => Via::new(),
128 | false => room_id
129 .server_name()
130 .map(ToOwned::to_owned)
131 .into_iter()
132 .collect(),
133 };
134
135 let root_summary = match services
136 .spaces
137 .get_summary_and_children(room_id, &sender, &root_via)
138 .await
139 {
140 | Err(e) => {
141 debug_error!(?room_id, "space hierarchy root: {e}");
142 return Err(e);
143 },
144 | Ok(Accessibility::Inaccessible) => {
145 return Err!(Request(Forbidden(debug_error!("The requested room is inaccessible."))));
146 },
147 | Ok(Accessibility::Accessible(s)) => s,
148 };
149
150 let initial_queue: VecDeque<QueueItem> = max_depth
153 .gt(&0)
154 .then(|| {
155 get_parent_children_via(&root_summary, suggested_only)
156 .filter(|(room_id_, _)| room_id.ne(room_id_))
157 .map(|(room_id, via)| {
158 let via = match bypass_visibility {
159 | true => Via::new(),
160 | false => via.collect(),
161 };
162
163 (room_id, via, 1_usize)
164 })
165 })
166 .into_iter()
167 .flatten()
168 .collect();
169
170 let skip_ids: BTreeSet<ShortRoomId> = skip_room_ids.iter().copied().collect();
173
174 let initial_state = (initial_queue, BTreeSet::from([room_id.to_owned()]));
175
176 let rooms = once(ready(Some(root_summary)))
179 .chain(unfold(initial_state, async |(mut queue, mut visited)| {
180 let (current_room, via, depth) = queue.pop_front()?;
181
182 if visited.contains(¤t_room) {
185 return Some((None, (queue, visited)));
186 }
187
188 match services
189 .spaces
190 .get_summary_and_children(¤t_room, &sender, &via)
191 .await
192 {
193 | Err(e) if !e.is_not_found() => {
194 error!(?current_room, ?depth, "space child error: {e}");
195
196 Some((None, (queue, visited)))
197 },
198 | Err(_) | Ok(Accessibility::Inaccessible) => {
199 trace!(?current_room, ?depth, "child inaccessible or not found");
200
201 Some((None, (queue, visited)))
202 },
203 | Ok(Accessibility::Accessible(s)) => {
204 visited.insert(current_room);
205
206 if depth < max_depth {
208 get_parent_children_via(&s, suggested_only)
209 .filter(|(child, _)| !visited.contains(child))
210 .for_each(|(child, via)| {
211 let via = match bypass_visibility {
212 | true => Via::new(),
213 | false => via.collect(),
214 };
215
216 queue.push_back((child, via, depth.saturating_add(1)));
217 });
218 }
219
220 Some((Some(s), (queue, visited)))
221 },
222 }
223 }))
224 .ready_filter_map(identity)
225 .wide_filter_map(async |summary| {
226 skip_ids
227 .is_empty()
228 .is_false()
229 .then_async(async || {
230 services
231 .short
232 .get_shortroomid(&summary.summary.room_id)
233 .await
234 .ok()
235 .filter(|shortid| skip_ids.contains(shortid))
236 })
237 .await
238 .flatten()
239 .is_none()
240 .then_some(summary)
241 .filter(is_summary_serializable)
242 .map(summary_to_chunk)
243 })
244 .take(limit)
245 .collect::<Vec<_>>()
246 .await;
247
248 let next_batch = (limit > 0 && rooms.len() >= limit)
252 .then_async(async || {
253 let next_skip = skip_room_ids
254 .iter()
255 .copied()
256 .stream()
257 .chain(rooms.iter().stream().then(async |chunk| {
258 services
264 .short
265 .get_or_create_shortroomid(&chunk.summary.room_id)
266 .await
267 }))
268 .collect::<Vec<_>>()
269 .await;
270
271 (next_skip.len() > skip_room_ids.len()).then_some(PaginationToken {
275 suggested_only,
276 short_room_ids: next_skip,
277 limit: limit.try_into().unwrap_or_default(),
278 max_depth: max_depth.try_into().unwrap_or_default(),
279 })
280 })
281 .await
282 .flatten()
283 .as_ref()
284 .map(ToString::to_string);
285
286 Ok(get_hierarchy::v1::Response { rooms, next_batch })
287}