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 struct HierarchyArgs<'a> {
41 pub sender_user: &'a UserId,
43
44 pub room_id: &'a RoomId,
46
47 pub limit: usize,
49
50 pub max_depth: usize,
52
53 pub suggested_only: bool,
55
56 pub skip_room_ids: &'a [ShortRoomId],
58
59 pub bypass_visibility: bool,
62}
63
64pub(crate) async fn get_hierarchy_route(
69 State(services): State<crate::State>,
70 body: Ruma<get_hierarchy::v1::Request>,
71) -> Result<get_hierarchy::v1::Response> {
72 let limit = body
73 .limit
74 .unwrap_or_else(|| UInt::from(10_u32))
75 .min(UInt::from(100_u32));
76
77 let max_depth = body
78 .max_depth
79 .unwrap_or_else(|| UInt::from(3_u32))
80 .min(UInt::from(10_u32));
81
82 let key = body
83 .from
84 .as_ref()
85 .and_then(|s| PaginationToken::from_str(s).ok());
86
87 if let Some(ref token) = key
89 && (token.suggested_only != body.suggested_only || token.max_depth != max_depth)
90 {
91 return Err!(Request(InvalidParam(
92 "suggested_only and max_depth cannot change on paginated requests"
93 )));
94 }
95
96 get_client_hierarchy(&services, HierarchyArgs {
97 sender_user: body.sender_user(),
98 room_id: &body.room_id,
99 limit: limit.try_into().unwrap_or(10),
100 max_depth: max_depth.try_into().unwrap_or(usize::MAX),
101 suggested_only: body.suggested_only,
102 skip_room_ids: key
103 .as_ref()
104 .map(|t| t.short_room_ids.as_slice())
105 .unwrap_or_default(),
106 bypass_visibility: false,
107 })
108 .await
109}
110
111pub async fn get_client_hierarchy(
115 services: &Services,
116 args: HierarchyArgs<'_>,
117) -> Result<get_hierarchy::v1::Response> {
118 type Via = SmallVec<[OwnedServerName; 1]>;
119 type QueueItem = (OwnedRoomId, Via, usize);
120
121 let HierarchyArgs {
122 sender_user,
123 room_id,
124 limit,
125 max_depth,
126 suggested_only,
127 skip_room_ids,
128 bypass_visibility,
129 } = args;
130
131 let sender = match bypass_visibility {
134 | true => Identifier::ServerName(services.globals.server_name()),
135 | false => Identifier::UserId(sender_user),
136 };
137
138 let root_via: Via = match room_id.server_name() {
141 | _ if bypass_visibility => Via::new(),
142 | Some(server) => [server.to_owned()].into(),
143 | None =>
144 services
145 .state_cache
146 .servers_invite_via(room_id)
147 .map(ToOwned::to_owned)
148 .collect()
149 .await,
150 };
151
152 let root_summary = match services
155 .spaces
156 .get_summary_and_children(room_id, &sender, &root_via)
157 .await
158 {
159 | Err(e) => {
160 debug_error!(?room_id, "space hierarchy root: {e}");
161 return Err(e);
162 },
163 | Ok(Accessibility::Inaccessible) => {
164 return Err!(Request(Forbidden(debug_error!("The requested room is inaccessible."))));
165 },
166 | Ok(Accessibility::Accessible(s)) => s,
167 };
168
169 let initial_queue: VecDeque<QueueItem> = max_depth
172 .gt(&0)
173 .then(|| {
174 get_parent_children_via(&root_summary, suggested_only)
175 .filter(|(room_id_, _)| room_id.ne(room_id_))
176 .map(|(room_id, via)| {
177 let via = match bypass_visibility {
178 | true => Via::new(),
179 | false => via.collect(),
180 };
181
182 (room_id, via, 1_usize)
183 })
184 })
185 .into_iter()
186 .flatten()
187 .collect();
188
189 let skip_ids: BTreeSet<ShortRoomId> = skip_room_ids.iter().copied().collect();
192
193 let initial_state = (initial_queue, BTreeSet::from([room_id.to_owned()]));
194
195 let rooms = once(ready(Some(root_summary)))
198 .chain(unfold(initial_state, async |(mut queue, mut visited)| {
199 let (current_room, via, depth) = queue.pop_front()?;
200
201 if visited.contains(¤t_room) {
204 return Some((None, (queue, visited)));
205 }
206
207 match services
208 .spaces
209 .get_summary_and_children(¤t_room, &sender, &via)
210 .await
211 {
212 | Err(e) if !e.is_not_found() => {
213 error!(?current_room, ?depth, "space child error: {e}");
214
215 Some((None, (queue, visited)))
216 },
217 | Err(_) | Ok(Accessibility::Inaccessible) => {
218 trace!(?current_room, ?depth, "child inaccessible or not found");
219
220 Some((None, (queue, visited)))
221 },
222 | Ok(Accessibility::Accessible(s)) => {
223 visited.insert(current_room);
224
225 if depth < max_depth {
227 get_parent_children_via(&s, suggested_only)
228 .filter(|(child, _)| !visited.contains(child))
229 .for_each(|(child, via)| {
230 let via = match bypass_visibility {
231 | true => Via::new(),
232 | false => via.collect(),
233 };
234
235 queue.push_back((child, via, depth.saturating_add(1)));
236 });
237 }
238
239 Some((Some(s), (queue, visited)))
240 },
241 }
242 }))
243 .ready_filter_map(identity)
244 .wide_filter_map(async |summary| {
245 skip_ids
246 .is_empty()
247 .is_false()
248 .then_async(async || {
249 services
250 .short
251 .get_shortroomid(&summary.summary.room_id)
252 .await
253 .ok()
254 .filter(|shortid| skip_ids.contains(shortid))
255 })
256 .await
257 .flatten()
258 .is_none()
259 .then_some(summary)
260 .filter(is_summary_serializable)
261 .map(summary_to_chunk)
262 })
263 .take(limit)
264 .collect::<Vec<_>>()
265 .await;
266
267 let next_batch = (limit > 0 && rooms.len() >= limit)
271 .then_async(async || {
272 let next_skip = skip_room_ids
273 .iter()
274 .copied()
275 .stream()
276 .chain(rooms.iter().stream().then(async |chunk| {
277 services
283 .short
284 .get_or_create_shortroomid(&chunk.summary.room_id)
285 .await
286 }))
287 .collect::<Vec<_>>()
288 .await;
289
290 (next_skip.len() > skip_room_ids.len()).then_some(PaginationToken {
294 suggested_only,
295 short_room_ids: next_skip,
296 limit: limit.try_into().unwrap_or_default(),
297 max_depth: max_depth.try_into().unwrap_or_default(),
298 })
299 })
300 .await
301 .flatten()
302 .as_ref()
303 .map(ToString::to_string);
304
305 Ok(get_hierarchy::v1::Response { rooms, next_batch })
306}