tuwunel_api/client/admin/federation/
destination_rooms.rs1use axum::extract::State;
2use futures::StreamExt;
3use ruma::{OwnedRoomId, UInt, api::Direction};
4use synapse_admin_api::federation::destination_rooms::v1::{DestinationRoom, Request, Response};
5use tuwunel_core::{
6 Err, Result,
7 utils::math::{ruma_from_usize, usize_from_ruma},
8};
9
10use crate::{Ruma, client::admin::require_admin};
11
12pub(crate) async fn admin_destination_rooms_route(
17 State(services): State<crate::State>,
18 body: Ruma<Request>,
19) -> Result<Response> {
20 require_admin(&services, body.sender_user()).await?;
21
22 if services.globals.server_is_ours(&body.destination) {
23 return Err!(Request(NotFound("Unknown destination")));
24 }
25
26 let rooms: Vec<OwnedRoomId> = services
27 .state_cache
28 .server_rooms(&body.destination)
29 .map(ToOwned::to_owned)
30 .collect()
31 .await;
32
33 if rooms.is_empty()
34 && !services
35 .federation
36 .peer_has_failures(&body.destination)
37 .await
38 {
39 return Err!(Request(NotFound("Unknown destination")));
40 }
41
42 let from = body.from.map_or(0, usize_from_ruma);
43 let limit = body.limit.map_or(100, usize_from_ruma);
44 let dir = body.dir.unwrap_or(Direction::Forward);
45 let (rooms, total, next_token) = paginate(rooms, dir, from, limit);
46
47 Ok(Response { rooms, total, next_token })
48}
49
50fn paginate(
53 mut rooms: Vec<OwnedRoomId>,
54 dir: Direction,
55 from: usize,
56 limit: usize,
57) -> (Vec<DestinationRoom>, UInt, Option<String>) {
58 rooms.sort_unstable();
59
60 if matches!(dir, Direction::Backward) {
61 rooms.reverse();
62 }
63
64 let total = rooms.len();
65
66 let page: Vec<DestinationRoom> = rooms
67 .into_iter()
68 .skip(from)
69 .take(limit)
70 .map(|room_id| DestinationRoom {
71 room_id,
72 stream_ordering: UInt::from(0_u32),
73 })
74 .collect();
75
76 let end = from.saturating_add(page.len());
77 let next_token = (end < total).then(|| end.to_string());
78
79 (page, ruma_from_usize(total), next_token)
80}
81
82#[cfg(test)]
83mod tests {
84 use ruma::{OwnedRoomId, api::Direction, owned_room_id, uint};
85
86 use super::{DestinationRoom, paginate};
87
88 fn rooms() -> Vec<OwnedRoomId> {
89 vec![
90 owned_room_id!("!c:example.com"),
91 owned_room_id!("!a:example.com"),
92 owned_room_id!("!b:example.com"),
93 ]
94 }
95
96 fn ids(page: &[DestinationRoom]) -> impl Iterator<Item = &str> {
97 page.iter().map(|room| room.room_id.as_str())
98 }
99
100 #[test]
101 fn forward_mid_window_emits_token() {
102 let (page, total, next_token) = paginate(rooms(), Direction::Forward, 0, 2);
103
104 assert!(ids(&page).eq(["!a:example.com", "!b:example.com"]));
105 assert!(
106 page.iter()
107 .all(|room| room.stream_ordering == uint!(0))
108 );
109 assert_eq!(total, uint!(3));
110 assert_eq!(next_token.as_deref(), Some("2"));
111 }
112
113 #[test]
114 fn final_window_omits_token() {
115 let (page, _, next_token) = paginate(rooms(), Direction::Forward, 2, 2);
116
117 assert!(ids(&page).eq(["!c:example.com"]));
118 assert_eq!(next_token, None);
119 }
120
121 #[test]
122 fn from_past_end_is_empty() {
123 let (page, total, next_token) = paginate(rooms(), Direction::Forward, 5, 2);
124
125 assert!(page.is_empty());
126 assert_eq!(total, uint!(3));
127 assert_eq!(next_token, None);
128 }
129
130 #[test]
131 fn zero_limit_holds_position() {
132 let (page, _, next_token) = paginate(rooms(), Direction::Forward, 1, 0);
133
134 assert!(page.is_empty());
135 assert_eq!(next_token.as_deref(), Some("1"));
136 }
137
138 #[test]
139 fn backward_reverses_before_windowing() {
140 let (page, _, next_token) = paginate(rooms(), Direction::Backward, 0, 2);
141
142 assert!(ids(&page).eq(["!c:example.com", "!b:example.com"]));
143 assert_eq!(next_token.as_deref(), Some("2"));
144 }
145}