Skip to main content

tuwunel_admin/room/
purge_user.rs

1use futures::{Stream, StreamExt, TryStreamExt};
2use regex::Regex;
3use ruma::{OwnedRoomId, RoomId};
4use tuwunel_core::{Result, utils::stream::ReadyExt};
5use tuwunel_service::Services;
6
7use crate::{Context, admin_command, get_room_info, utils::parse_user_id};
8
9#[admin_command]
10pub(super) async fn room_purge_user(
11	&self,
12	user_id: String,
13	regex: bool,
14	sole_member: bool,
15	dry_run: bool,
16) -> Result {
17	let services = self.services;
18
19	if dry_run {
20		self.write_str("Matching rooms:\n```\n").await?;
21	}
22
23	let count = if regex {
24		let pattern = &Regex::new(&user_id)?;
25		let rooms = services
26			.metadata
27			.iter_ids()
28			.map(ToOwned::to_owned)
29			.filter_map(async |room_id| {
30				(!services.admin.is_admin_room(&room_id).await
31					&& room_has_matching_member(services, &room_id, pattern, sole_member).await)
32					.then_some(room_id)
33			});
34
35		purge_stream(self, rooms, dry_run).await?
36	} else {
37		let user_id = parse_user_id(services, &user_id)?;
38		let rooms = services
39			.state_cache
40			.rooms_joined(&user_id)
41			.map(ToOwned::to_owned)
42			.filter_map(async |room_id| {
43				(!services.admin.is_admin_room(&room_id).await
44					&& (!sole_member || is_sole_joined_member(services, &room_id).await))
45					.then_some(room_id)
46			});
47
48		purge_stream(self, rooms, dry_run).await?
49	};
50
51	match (dry_run, count) {
52		| (true, _) => write!(self, "```\nMatched {count} rooms."),
53		| (false, 0) => write!(self, "No rooms matched."),
54		| (false, _) => write!(self, "Deleted {count} rooms from our database."),
55	}
56	.await
57}
58
59async fn room_has_matching_member(
60	services: &Services,
61	room_id: &RoomId,
62	pattern: &Regex,
63	sole_member: bool,
64) -> bool {
65	let sole_ok = !sole_member || is_sole_joined_member(services, room_id).await;
66
67	sole_ok
68		&& services
69			.state_cache
70			.room_members(room_id)
71			.ready_any(|user| pattern.is_match(user.as_str()))
72			.await
73}
74
75async fn is_sole_joined_member(services: &Services, room_id: &RoomId) -> bool {
76	services
77		.state_cache
78		.room_joined_count(room_id)
79		.await
80		.is_ok_and(|count| count == 1)
81}
82
83/// Lists (dry run) or deletes each matched room, returning the count.
84async fn purge_stream<S>(context: &Context<'_>, rooms: S, dry_run: bool) -> Result<usize>
85where
86	S: Stream<Item = OwnedRoomId> + Send,
87{
88	let services = context.services;
89
90	rooms
91		.map(Ok)
92		.try_fold(0_usize, async |count, room_id: OwnedRoomId| {
93			if dry_run {
94				let (id, members, name) = get_room_info(services, &room_id).await;
95
96				writeln!(context, "{id}\tMembers: {members}\tName: {name}").await?;
97			} else {
98				let state_lock = services.state.mutex.lock(&room_id).await;
99
100				// Non-forced: preserves local users' left-membership records.
101				services
102					.delete
103					.delete_room(&room_id, false, state_lock)
104					.await?;
105			}
106
107			Ok(count.saturating_add(1))
108		})
109		.await
110}