tuwunel_admin/room/
moderation.rs1use 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 BanRoom {
22 room: OwnedRoomOrAliasId,
25 },
26
27 BanListOfRooms,
31
32 UnbanRoom {
34 room: OwnedRoomOrAliasId,
37 },
38
39 ListBannedRooms {
41 #[arg(long)]
42 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 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 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}