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 room_id.server_name() {
127 | _ if bypass_visibility => Via::new(),
128 | Some(server) => [server.to_owned()].into(),
129 | None =>
130 services
131 .state_cache
132 .servers_invite_via(room_id)
133 .map(ToOwned::to_owned)
134 .collect()
135 .await,
136 };
137
138 let root_summary = match services
141 .spaces
142 .get_summary_and_children(room_id, &sender, &root_via)
143 .await
144 {
145 | Err(e) => {
146 debug_error!(?room_id, "space hierarchy root: {e}");
147 return Err(e);
148 },
149 | Ok(Accessibility::Inaccessible) => {
150 return Err!(Request(Forbidden(debug_error!("The requested room is inaccessible."))));
151 },
152 | Ok(Accessibility::Accessible(s)) => s,
153 };
154
155 let initial_queue: VecDeque<QueueItem> = max_depth
158 .gt(&0)
159 .then(|| {
160 get_parent_children_via(&root_summary, suggested_only)
161 .filter(|(room_id_, _)| room_id.ne(room_id_))
162 .map(|(room_id, via)| {
163 let via = match bypass_visibility {
164 | true => Via::new(),
165 | false => via.collect(),
166 };
167
168 (room_id, via, 1_usize)
169 })
170 })
171 .into_iter()
172 .flatten()
173 .collect();
174
175 let skip_ids: BTreeSet<ShortRoomId> = skip_room_ids.iter().copied().collect();
178
179 let initial_state = (initial_queue, BTreeSet::from([room_id.to_owned()]));
180
181 let rooms = once(ready(Some(root_summary)))
184 .chain(unfold(initial_state, async |(mut queue, mut visited)| {
185 let (current_room, via, depth) = queue.pop_front()?;
186
187 if visited.contains(¤t_room) {
190 return Some((None, (queue, visited)));
191 }
192
193 match services
194 .spaces
195 .get_summary_and_children(¤t_room, &sender, &via)
196 .await
197 {
198 | Err(e) if !e.is_not_found() => {
199 error!(?current_room, ?depth, "space child error: {e}");
200
201 Some((None, (queue, visited)))
202 },
203 | Err(_) | Ok(Accessibility::Inaccessible) => {
204 trace!(?current_room, ?depth, "child inaccessible or not found");
205
206 Some((None, (queue, visited)))
207 },
208 | Ok(Accessibility::Accessible(s)) => {
209 visited.insert(current_room);
210
211 if depth < max_depth {
213 get_parent_children_via(&s, suggested_only)
214 .filter(|(child, _)| !visited.contains(child))
215 .for_each(|(child, via)| {
216 let via = match bypass_visibility {
217 | true => Via::new(),
218 | false => via.collect(),
219 };
220
221 queue.push_back((child, via, depth.saturating_add(1)));
222 });
223 }
224
225 Some((Some(s), (queue, visited)))
226 },
227 }
228 }))
229 .ready_filter_map(identity)
230 .wide_filter_map(async |summary| {
231 skip_ids
232 .is_empty()
233 .is_false()
234 .then_async(async || {
235 services
236 .short
237 .get_shortroomid(&summary.summary.room_id)
238 .await
239 .ok()
240 .filter(|shortid| skip_ids.contains(shortid))
241 })
242 .await
243 .flatten()
244 .is_none()
245 .then_some(summary)
246 .filter(is_summary_serializable)
247 .map(summary_to_chunk)
248 })
249 .take(limit)
250 .collect::<Vec<_>>()
251 .await;
252
253 let next_batch = (limit > 0 && rooms.len() >= limit)
257 .then_async(async || {
258 let next_skip = skip_room_ids
259 .iter()
260 .copied()
261 .stream()
262 .chain(rooms.iter().stream().then(async |chunk| {
263 services
269 .short
270 .get_or_create_shortroomid(&chunk.summary.room_id)
271 .await
272 }))
273 .collect::<Vec<_>>()
274 .await;
275
276 (next_skip.len() > skip_room_ids.len()).then_some(PaginationToken {
280 suggested_only,
281 short_room_ids: next_skip,
282 limit: limit.try_into().unwrap_or_default(),
283 max_depth: max_depth.try_into().unwrap_or_default(),
284 })
285 })
286 .await
287 .flatten()
288 .as_ref()
289 .map(ToString::to_string);
290
291 Ok(get_hierarchy::v1::Response { rooms, next_batch })
292}