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#[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 .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
149async 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 creation_ts: Some(MilliSecondsSinceUnixEpoch(UInt::from(0_u32))),
231 ..UserMinorDetails::new(name.to_owned())
232 })
233}