tuwunel_api/client/admin/users/
list_users.rs1use 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#[derive(Clone, Copy)]
21enum DeactivatedFilter {
22 Any,
24
25 Only,
27
28 Exclude,
30
31 Include,
33}
34
35struct 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
47pub(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, ¶ms).await;
71
72 Ok(v2::Response { users, next_token, total })
73}
74
75pub(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, ¶ms).await;
100
101 Ok(v3::Response { users, next_token, total })
102}
103
104async 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
148async 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 creation_ts: Some(MilliSecondsSinceUnixEpoch(UInt::from(0_u32))),
230 ..UserMinorDetails::new(name.to_owned())
231 })
232}