Skip to main content

tuwunel_admin/room/
moderation.rs

1use clap::Subcommand;
2use futures::{FutureExt, StreamExt};
3use ruma::{OwnedRoomId, OwnedRoomOrAliasId, RoomId, RoomOrAliasId};
4use tuwunel_core::{
5	Err, Result, debug, is_equal_to,
6	utils::{IterStream, ReadyExt},
7	warn,
8};
9use tuwunel_service::Services;
10
11use crate::{admin_command, admin_command_dispatch, get_room_info};
12
13#[admin_command_dispatch]
14#[derive(Debug, Subcommand)]
15pub(crate) enum RoomModerationCommand {
16	/// - Bans a room from local users joining and evicts all our local users
17	///   (including server
18	/// admins)
19	///   from the room. Also blocks any invites (local and remote) for the
20	///   banned room, and disables federation entirely with it.
21	BanRoom {
22		/// The room in the format of `!roomid:example.com` or a room alias in
23		/// the format of `#roomalias:example.com`
24		room: OwnedRoomOrAliasId,
25	},
26
27	/// - Bans a list of rooms (room IDs and room aliases) from a newline
28	///   delimited codeblock similar to `user deactivate-all`. Applies the same
29	///   steps as ban-room
30	BanListOfRooms,
31
32	/// - Unbans a room to allow local users to join again
33	UnbanRoom {
34		/// The room in the format of `!roomid:example.com` or a room alias in
35		/// the format of `#roomalias:example.com`
36		room: OwnedRoomOrAliasId,
37	},
38
39	/// - List of all rooms we have banned
40	ListBannedRooms {
41		#[arg(long)]
42		/// Whether to only output room IDs without supplementary room
43		/// information
44		no_details: bool,
45	},
46}
47
48async fn do_ban_room(services: &Services, room_id: &RoomId) {
49	services.metadata.ban_room(room_id);
50
51	debug!("Banned {room_id} successfully");
52
53	debug!("Making all users leave the room {room_id} and forgetting it");
54	let mut users = services
55		.state_cache
56		.room_members(room_id)
57		.ready_filter(|user| services.globals.user_is_local(user))
58		.map(ToOwned::to_owned)
59		.boxed();
60
61	while let Some(ref user_id) = users.next().await {
62		debug!(
63			"Attempting leave for user {user_id} in room {room_id} (ignoring all errors, \
64			 evicting admins too)",
65		);
66
67		let state_lock = services.state.mutex.lock(room_id).await;
68
69		if let Err(e) = services
70			.membership
71			.leave(user_id, room_id, None, false, &state_lock)
72			.boxed()
73			.await
74		{
75			warn!("Failed to leave room: {e}");
76		}
77
78		drop(state_lock);
79
80		services.state_cache.forget(room_id, user_id);
81	}
82
83	// remove any local aliases, ignore errors
84	services
85		.alias
86		.local_aliases_for_room(room_id)
87		.map(ToOwned::to_owned)
88		.for_each(async |local_alias| {
89			if let Err(e) = services.alias.remove_alias(&local_alias).await {
90				warn!("Error removing alias {local_alias} for {room_id}: {e}");
91			}
92		})
93		.await;
94
95	// unpublish from room directory, ignore errors
96	services.directory.set_not_public(room_id);
97
98	services.metadata.disable_room(room_id);
99}
100
101#[admin_command]
102async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
103	debug!("Got room alias or ID: {}", room);
104
105	let admin_room_alias = &self.services.admin.admin_alias;
106
107	if let Ok(admin_room_id) = self.services.admin.get_admin_room().await
108		&& (room.to_string().eq(&admin_room_id) || room.to_string().eq(admin_room_alias))
109	{
110		return Err!("Not allowed to ban the admin room.");
111	}
112
113	let room_id = self.services.alias.maybe_resolve(&room).await?;
114
115	do_ban_room(self.services, &room_id).await;
116
117	self.write_str(
118		"Room banned, removed all our local users, and disabled incoming federation with room.",
119	)
120	.await
121}
122
123#[admin_command]
124async fn ban_list_of_rooms(&self) -> Result {
125	if self.body.len() < 2
126		|| !self.body[0].trim().starts_with("```")
127		|| self.body.last().unwrap_or(&"").trim() != "```"
128	{
129		return Err!("Expected code block in command body. Add --help for details.",);
130	}
131
132	let rooms_s = self
133		.body
134		.to_vec()
135		.drain(1..self.body.len().saturating_sub(1))
136		.collect::<Vec<_>>();
137
138	let admin_room_id = self.services.admin.get_admin_room().await.ok();
139
140	let mut room_ids: Vec<OwnedRoomId> = Vec::with_capacity(rooms_s.len());
141
142	for room in rooms_s {
143		let room_alias_or_id = match <&RoomOrAliasId>::try_from(room) {
144			| Ok(room_alias_or_id) => room_alias_or_id,
145			| Err(e) => {
146				warn!("Error parsing room {room} during bulk room banning, ignoring: {e}");
147				continue;
148			},
149		};
150
151		let room_id = match self
152			.services
153			.alias
154			.maybe_resolve(room_alias_or_id)
155			.await
156		{
157			| Ok(room_id) => room_id,
158			| Err(e) => {
159				warn!("Failed to resolve room alias {room_alias_or_id} to a room ID: {e}");
160				continue;
161			},
162		};
163
164		if admin_room_id
165			.as_ref()
166			.is_some_and(is_equal_to!(&room_id))
167		{
168			warn!("User specified admin room in bulk ban list, ignoring");
169			continue;
170		}
171
172		room_ids.push(room_id);
173	}
174
175	let rooms_len = room_ids.len();
176
177	for room_id in room_ids {
178		do_ban_room(self.services, &room_id).await;
179	}
180
181	self.write_str(&format!(
182		"Finished bulk room ban, banned {rooms_len} total rooms, evicted all users, and \
183		 disabled incoming federation with the room."
184	))
185	.await
186}
187
188#[admin_command]
189async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
190	let room_id = self.services.alias.maybe_resolve(&room).await?;
191
192	self.services.metadata.unban_room(&room_id);
193	self.services.metadata.enable_room(&room_id);
194	self.write_str("Room unbanned and federation re-enabled.")
195		.await
196}
197
198#[admin_command]
199async fn list_banned_rooms(&self, no_details: bool) -> Result {
200	let room_ids: Vec<OwnedRoomId> = self
201		.services
202		.metadata
203		.list_banned_rooms()
204		.map(Into::into)
205		.collect()
206		.await;
207
208	if room_ids.is_empty() {
209		return Err!("No rooms are banned.");
210	}
211
212	let mut rooms = room_ids
213		.iter()
214		.stream()
215		.then(|room_id| get_room_info(self.services, room_id))
216		.collect::<Vec<_>>()
217		.await;
218
219	rooms.sort_by_key(|r| r.1);
220	rooms.reverse();
221
222	let num = rooms.len();
223
224	let body = rooms
225		.iter()
226		.map(|(id, members, name)| {
227			if no_details {
228				format!("{id}")
229			} else {
230				format!("{id}\tMembers: {members}\tName: {name}")
231			}
232		})
233		.collect::<Vec<_>>()
234		.join("\n");
235
236	self.write_str(&format!("Rooms Banned ({num}):\n```\n{body}\n```"))
237		.await
238}