Skip to main content

tuwunel_api/client/admin/users/
list_users.rs

1use axum::extract::State;
2use futures::StreamExt;
3use ruma::{MilliSecondsSinceUnixEpoch, UInt, UserId, api::Direction};
4use synapse_admin_api::users::list_users::{
5	v2::{self, UserMinorDetails},
6	v3,
7};
8use tuwunel_core::{
9	Result,
10	utils::{
11		IterStream, ReadyExt,
12		math::{ruma_from_usize, usize_from_ruma},
13		stream::WidebandExt,
14	},
15};
16
17use crate::{Ruma, client::admin::require_admin};
18
19/// The `deactivated` query filter, whose semantics differ between v2 and v3.
20#[derive(Clone, Copy)]
21enum DeactivatedFilter {
22	/// Include both active and deactivated users (v3 absent).
23	Any,
24
25	/// Only deactivated users (v3 `true`).
26	Only,
27
28	/// Exclude deactivated users (v2 absent/false, v3 `false`).
29	Exclude,
30
31	/// Include deactivated users (v2 `true`).
32	Include,
33}
34
35/// Filter and pagination parameters shared by the v2 and v3 list endpoints.
36struct ListParams<'a> {
37	from: usize,
38	limit: usize,
39	name: Option<&'a str>,
40	user_id: Option<&'a str>,
41	admins: Option<bool>,
42	locked: bool,
43	deactivated: DeactivatedFilter,
44	dir: Direction,
45}
46
47/// # `GET /_synapse/admin/v2/users`
48pub(crate) async fn admin_list_users_v2_route(
49	State(services): State<crate::State>,
50	body: Ruma<v2::Request>,
51) -> Result<v2::Response> {
52	require_admin(&services, body.sender_user()).await?;
53
54	let deactivated = match body.deactivated {
55		| true => DeactivatedFilter::Include,
56		| false => DeactivatedFilter::Exclude,
57	};
58
59	let params = ListParams {
60		from: usize_from_ruma(body.from),
61		limit: body.limit.map_or(100, usize_from_ruma),
62		name: body.name.as_deref(),
63		user_id: body.user_id.as_deref(),
64		admins: body.admins,
65		locked: body.locked,
66		deactivated,
67		dir: body.dir.unwrap_or(Direction::Forward),
68	};
69
70	let (users, next_token, total) = list_users(services, &params).await;
71
72	Ok(v2::Response { users, next_token, total })
73}
74
75/// # `GET /_synapse/admin/v3/users`
76pub(crate) async fn admin_list_users_v3_route(
77	State(services): State<crate::State>,
78	body: Ruma<v3::Request>,
79) -> Result<v3::Response> {
80	require_admin(&services, body.sender_user()).await?;
81
82	let deactivated = match body.deactivated {
83		| None => DeactivatedFilter::Any,
84		| Some(true) => DeactivatedFilter::Only,
85		| Some(false) => DeactivatedFilter::Exclude,
86	};
87
88	let params = ListParams {
89		from: usize_from_ruma(body.from),
90		limit: body.limit.map_or(100, usize_from_ruma),
91		name: body.name.as_deref(),
92		user_id: body.user_id.as_deref(),
93		admins: body.admins,
94		locked: body.locked,
95		deactivated,
96		dir: body.dir.unwrap_or(Direction::Forward),
97	};
98
99	let (users, next_token, total) = list_users(services, &params).await;
100
101	Ok(v3::Response { users, next_token, total })
102}
103
104/// Returns the filtered, name-ordered and paginated user page, the `next_token`
105/// (present only while the page does not reach the end of the filtered set) and
106/// the filtered total. `order_by` beyond `name` is not backed by stored fields,
107/// so the name ordering (reversed for `dir=b`) is the only one applied.
108async fn list_users(
109	services: crate::State,
110	params: &ListParams<'_>,
111) -> (Vec<UserMinorDetails>, Option<String>, UInt) {
112	let mut names: Vec<String> = services
113		.users
114		.stream()
115		.map(ToString::to_string)
116		.collect()
117		.await;
118
119	names.sort_unstable();
120
121	if matches!(params.dir, Direction::Backward) {
122		names.reverse();
123	}
124
125	let matched: Vec<UserMinorDetails> = names
126		.iter()
127		.map(String::as_str)
128		.stream()
129		.wide_filter_map(async |name| user_minor_details(services, name, params).await)
130		.collect()
131		.await;
132
133	let matched_count = matched.len();
134	let total = ruma_from_usize(matched_count);
135
136	let page: Vec<UserMinorDetails> = matched
137		.into_iter()
138		.skip(params.from)
139		.take(params.limit)
140		.collect();
141
142	let end = params.from.saturating_add(page.len());
143	let next_token = (end < matched_count).then(|| end.to_string());
144
145	(page, next_token, total)
146}
147
148/// Applies the substring, admin, locked and deactivated filters to one user and
149/// builds its `UserMinorDetails`, or returns `None` when the user is filtered
150/// out.
151async fn user_minor_details(
152	services: crate::State,
153	name: &str,
154	params: &ListParams<'_>,
155) -> Option<UserMinorDetails> {
156	let user_id = UserId::parse(name).ok()?;
157
158	let displayname = services.profile.displayname(&user_id).await.ok();
159
160	if let Some(needle) = params.user_id.filter(|_| params.name.is_none())
161		&& !name.contains(needle)
162	{
163		return None;
164	}
165
166	if let Some(needle) = params.name {
167		let in_localpart = user_id.localpart().contains(needle);
168		let in_displayname = displayname
169			.as_deref()
170			.is_some_and(|display| display.contains(needle));
171
172		if !in_localpart && !in_displayname {
173			return None;
174		}
175	}
176
177	let admin = services.admin.user_is_admin(&user_id).await;
178	if let Some(want_admin) = params.admins
179		&& want_admin != admin
180	{
181		return None;
182	}
183
184	let locked = services.users.is_locked(&user_id).await;
185	if locked && !params.locked {
186		return None;
187	}
188
189	let deactivated = services
190		.users
191		.is_deactivated(&user_id)
192		.await
193		.unwrap_or(false);
194
195	let keep = match params.deactivated {
196		| DeactivatedFilter::Any | DeactivatedFilter::Include => true,
197		| DeactivatedFilter::Only => deactivated,
198		| DeactivatedFilter::Exclude => !deactivated,
199	};
200
201	if !keep {
202		return None;
203	}
204
205	let avatar_url = services
206		.profile
207		.avatar_url(&user_id)
208		.await
209		.ok()
210		.map(|url| url.to_string());
211
212	let erased = services.users.is_erased(&user_id).await;
213
214	let last_seen_ts = services
215		.users
216		.all_devices_metadata(&user_id)
217		.ready_fold(None, |max, device| max.max(device.last_seen_ts))
218		.await;
219
220	Some(UserMinorDetails {
221		displayname,
222		avatar_url,
223		admin,
224		deactivated,
225		locked,
226		erased,
227		last_seen_ts,
228		// tuwunel has no creation timestamp; emit a 0 sentinel (strict clients reject null).
229		creation_ts: Some(MilliSecondsSinceUnixEpoch(UInt::from(0_u32))),
230		..UserMinorDetails::new(name.to_owned())
231	})
232}