Skip to main content

tuwunel_api_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, 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		.ready_filter(|user_id| services.globals.user_is_local(user_id))
116		.map(ToString::to_string)
117		.collect()
118		.await;
119
120	names.sort_unstable();
121
122	if matches!(params.dir, Direction::Backward) {
123		names.reverse();
124	}
125
126	let matched: Vec<UserMinorDetails> = names
127		.iter()
128		.map(String::as_str)
129		.stream()
130		.wide_filter_map(async |name| user_minor_details(services, name, params).await)
131		.collect()
132		.await;
133
134	let matched_count = matched.len();
135	let total = ruma_from_usize(matched_count);
136
137	let page: Vec<UserMinorDetails> = matched
138		.into_iter()
139		.skip(params.from)
140		.take(params.limit)
141		.collect();
142
143	let end = params.from.saturating_add(page.len());
144	let next_token = (end < matched_count).then(|| end.to_string());
145
146	(page, next_token, total)
147}
148
149/// Applies the substring, admin, locked and deactivated filters to one user and
150/// builds its `UserMinorDetails`, or returns `None` when the user is filtered
151/// out.
152async fn user_minor_details(
153	services: crate::State,
154	name: &str,
155	params: &ListParams<'_>,
156) -> Option<UserMinorDetails> {
157	let user_id = UserId::parse(name).ok()?;
158
159	let displayname = services.profile.displayname(&user_id).await.ok();
160
161	if let Some(needle) = params.user_id.filter(|_| params.name.is_none())
162		&& !name.contains(needle)
163	{
164		return None;
165	}
166
167	if let Some(needle) = params.name {
168		let in_localpart = user_id.localpart().contains(needle);
169		let in_displayname = displayname
170			.as_deref()
171			.is_some_and(|display| display.contains(needle));
172
173		if !in_localpart && !in_displayname {
174			return None;
175		}
176	}
177
178	let admin = services.admin.user_is_admin(&user_id).await;
179	if let Some(want_admin) = params.admins
180		&& want_admin != admin
181	{
182		return None;
183	}
184
185	let locked = services.users.is_locked(&user_id).await;
186	if locked && !params.locked {
187		return None;
188	}
189
190	let deactivated = services
191		.users
192		.is_deactivated(&user_id)
193		.await
194		.unwrap_or(false);
195
196	let keep = match params.deactivated {
197		| DeactivatedFilter::Any | DeactivatedFilter::Include => true,
198		| DeactivatedFilter::Only => deactivated,
199		| DeactivatedFilter::Exclude => !deactivated,
200	};
201
202	if !keep {
203		return None;
204	}
205
206	let avatar_url = services
207		.profile
208		.avatar_url(&user_id)
209		.await
210		.ok()
211		.map(|url| url.to_string());
212
213	let erased = services.users.is_erased(&user_id).await;
214
215	let last_seen_ts = services
216		.users
217		.all_devices_metadata(&user_id)
218		.ready_fold(None, |max, device| max.max(device.last_seen_ts))
219		.await;
220
221	Some(UserMinorDetails {
222		displayname,
223		avatar_url,
224		admin,
225		deactivated,
226		locked,
227		erased,
228		last_seen_ts,
229		// tuwunel has no creation timestamp; emit a 0 sentinel (strict clients reject null).
230		creation_ts: Some(MilliSecondsSinceUnixEpoch(UInt::from(0_u32))),
231		..UserMinorDetails::new(name.to_owned())
232	})
233}