Skip to main content

tuwunel_api/client/admin/rooms/
delete_room.rs

1use axum::extract::State;
2use ruma::{RoomId, UserId};
3use synapse_admin_api::rooms::delete_room::{
4	v1::{Request as V1Request, Response as V1Response, ShutdownRoom},
5	v2::{Request as V2Request, Response as V2Response},
6};
7use tuwunel_core::Result;
8use tuwunel_service::rooms::delete::ShutdownRoom as Summary;
9
10use crate::{Ruma, client::admin::require_admin};
11
12/// # `DELETE /_synapse/admin/v1/rooms/{room_id}`
13///
14/// Synchronously evicts the room's local users, optionally purges its storage
15/// (`purge`, default true) and blocks it (`block`), returning the shutdown
16/// outcome. The replacement-room fields are accepted and ignored, so no
17/// redirect room is created (`new_room_id` is always null).
18pub(crate) async fn admin_delete_room_v1_route(
19	State(services): State<crate::State>,
20	body: Ruma<V1Request>,
21) -> Result<V1Response> {
22	require_admin(&services, body.sender_user()).await?;
23
24	let summary =
25		run_shutdown(&services, &body.room_id, body.sender_user(), body.block, body.purge).await;
26
27	Ok(V1Response { result: into_response(summary) })
28}
29
30/// # `DELETE /_synapse/admin/v2/rooms/{room_id}`
31///
32/// Schedules the same shutdown as a background task and returns its id at once;
33/// the outcome is retrieved from the delete-status endpoints.
34pub(crate) async fn admin_delete_room_v2_route(
35	State(services): State<crate::State>,
36	body: Ruma<V2Request>,
37) -> Result<V2Response> {
38	require_admin(&services, body.sender_user()).await?;
39
40	let room_id = body.room_id.clone();
41	let sender = body.sender_user().to_owned();
42	let (block, purge) = (body.block, body.purge);
43
44	let work = async move {
45		let summary = run_shutdown(&services, &room_id, &sender, block, purge).await;
46
47		Ok(serde_json::to_value(summary)?)
48	};
49
50	let delete_id = services
51		.tasks
52		.spawn(super::DELETE_ROOM_ACTION, body.room_id.to_string(), work)
53		.to_string();
54
55	Ok(V2Response { delete_id })
56}
57
58/// Runs the shutdown, purging storage when asked and recording the room as
59/// blocked by the requesting admin. `force_purge` is not mapped: tuwunel always
60/// evicts local users before purging.
61async fn run_shutdown(
62	services: &crate::State,
63	room_id: &RoomId,
64	sender: &UserId,
65	block: bool,
66	purge: bool,
67) -> Summary {
68	let state_lock = services.state.mutex.lock(room_id).await;
69
70	let summary = if purge {
71		services
72			.delete
73			.delete_room(room_id, false, state_lock)
74			.await
75			.unwrap_or_default()
76	} else {
77		services
78			.delete
79			.shutdown_room(room_id, &state_lock)
80			.await
81	};
82
83	if block {
84		services.metadata.block_room(room_id, sender);
85	}
86
87	summary
88}
89
90fn into_response(summary: Summary) -> ShutdownRoom {
91	ShutdownRoom {
92		kicked_users: summary.kicked_users,
93		failed_to_kick_users: summary.failed_to_kick_users,
94		local_aliases: summary
95			.local_aliases
96			.iter()
97			.map(ToString::to_string)
98			.collect(),
99		new_room_id: summary.new_room_id,
100	}
101}
102
103#[cfg(test)]
104mod tests {
105	use ruma::{room_alias_id, user_id};
106	use tuwunel_service::rooms::delete::ShutdownRoom as Summary;
107
108	use super::{ShutdownRoom, into_response};
109
110	/// The synchronous v1 delete maps the summary's fields directly, while the
111	/// async v2 delete stores the summary as JSON and the status endpoint
112	/// deserializes it back. Both must yield the same wire shape.
113	#[test]
114	fn v1_field_map_agrees_with_v2_json_round_trip() {
115		let summary = Summary {
116			kicked_users: vec![user_id!("@alice:example.org").to_owned()],
117			failed_to_kick_users: vec![user_id!("@bob:example.org").to_owned()],
118			local_aliases: vec![room_alias_id!("#lounge:example.org").to_owned()],
119			new_room_id: None,
120		};
121
122		let via_field_map = into_response(summary.clone());
123
124		let via_json: ShutdownRoom =
125			serde_json::from_value(serde_json::to_value(&summary).unwrap()).unwrap();
126
127		assert_eq!(
128			serde_json::to_value(&via_field_map).unwrap(),
129			serde_json::to_value(&via_json).unwrap(),
130		);
131	}
132}