Skip to main content

tuwunel_api/client/admin/federation/
destination_rooms.rs

1use 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
12/// # `GET /_synapse/admin/v1/federation/destinations/{destination}/rooms`
13///
14/// The last-sent PDU stream ordering is not tracked per room; every row
15/// reports 0.
16pub(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
50/// The `next_token` is the stringified next offset, emitted only while rows
51/// remain past the returned page (Synapse's `str(from + len)` convention).
52fn 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}