Skip to main content

tuwunel_api/client/admin/rooms/
forward_extremities.rs

1use axum::extract::State;
2use futures::{
3	StreamExt,
4	future::{join, join_all},
5};
6use ruma::{EventId, OwnedEventId, UInt};
7use synapse_admin_api::rooms::forward_extremities::{
8	delete::{Request as DeleteRequest, Response as DeleteResponse},
9	get::{ForwardExtremity, Request as GetRequest, Response as GetResponse},
10};
11use tuwunel_core::{Result, smallvec::SmallVec};
12
13use crate::{Ruma, client::admin::require_admin};
14
15type Extremities = SmallVec<[OwnedEventId; 1]>;
16
17/// # `GET /_synapse/admin/v1/rooms/{room_id_or_alias}/forward_extremities`
18///
19/// Lists the room's forward extremities. `received_ts` stands in as the event's
20/// `origin_server_ts` (arrival time is not stored) and `state_group` is the
21/// event's state hash or null.
22pub(crate) async fn admin_get_forward_extremities_route(
23	State(services): State<crate::State>,
24	body: Ruma<GetRequest>,
25) -> Result<GetResponse> {
26	require_admin(&services, body.sender_user()).await?;
27
28	let (room_id, _) = services
29		.alias
30		.maybe_resolve_with_servers(&body.room_id_or_alias, None)
31		.await?;
32
33	let extremities = services
34		.state
35		.get_forward_extremities(&room_id)
36		.map(ToOwned::to_owned)
37		.collect::<Extremities>()
38		.await;
39
40	let results = join_all(
41		extremities
42			.iter()
43			.map(|event_id| forward_extremity(&services, event_id)),
44	)
45	.await;
46
47	Ok(GetResponse {
48		count: super::usize_to_uint(results.len()),
49		results,
50	})
51}
52
53/// # `DELETE /_synapse/admin/v1/rooms/{room_id_or_alias}/forward_extremities`
54///
55/// Collapses the room to a single forward extremity, keeping the one furthest
56/// along in stream order, and reports how many were removed.
57pub(crate) async fn admin_delete_forward_extremities_route(
58	State(services): State<crate::State>,
59	body: Ruma<DeleteRequest>,
60) -> Result<DeleteResponse> {
61	require_admin(&services, body.sender_user()).await?;
62
63	let (room_id, _) = services
64		.alias
65		.maybe_resolve_with_servers(&body.room_id_or_alias, None)
66		.await?;
67
68	let state_lock = services.state.mutex.lock(&room_id).await;
69
70	let deleted = services
71		.state
72		.collapse_forward_extremities(&room_id, &state_lock)
73		.await;
74
75	Ok(DeleteResponse { deleted: super::usize_to_uint(deleted) })
76}
77
78async fn forward_extremity(services: &crate::State, event_id: &EventId) -> ForwardExtremity {
79	let (pdu, shortstatehash) =
80		join(services.timeline.get_pdu(event_id), services.state.pdu_shortstatehash(event_id))
81			.await;
82
83	let (depth, received_ts) = pdu
84		.map(|pdu| (pdu.depth, pdu.origin_server_ts))
85		.unwrap_or_default();
86
87	forward_extremity_row(event_id, depth, received_ts, shortstatehash.ok())
88}
89
90/// Assembles a forward-extremity row. `state_group` carries the event's state
91/// hash when it fits `UInt`, else null; `depth` and `received_ts` pass through.
92fn forward_extremity_row(
93	event_id: &EventId,
94	depth: UInt,
95	received_ts: UInt,
96	shortstatehash: Option<u64>,
97) -> ForwardExtremity {
98	ForwardExtremity {
99		event_id: event_id.to_owned(),
100		state_group: shortstatehash.and_then(|hash| UInt::try_from(hash).ok()),
101		depth,
102		received_ts,
103	}
104}
105
106#[cfg(test)]
107mod tests {
108	use ruma::{event_id, uint};
109	use serde_json::json;
110
111	use super::forward_extremity_row;
112
113	#[test]
114	fn state_group_is_the_state_hash_when_it_fits() {
115		let row =
116			forward_extremity_row(event_id!("$abc:example.org"), uint!(7), uint!(1000), Some(42));
117
118		let value = serde_json::to_value(row).unwrap();
119
120		assert_eq!(value["state_group"], json!(42));
121		assert_eq!(value["depth"], json!(7));
122		assert_eq!(value["received_ts"], json!(1000));
123	}
124
125	#[test]
126	fn state_group_is_null_when_the_hash_overflows_uint() {
127		let row = forward_extremity_row(
128			event_id!("$abc:example.org"),
129			uint!(0),
130			uint!(0),
131			Some(u64::MAX),
132		);
133
134		let value = serde_json::to_value(row).unwrap();
135
136		assert_eq!(value["state_group"], json!(null));
137	}
138}