1use reqwest::Method;
2use serde::{Deserialize, Serialize};
3use serde_json::{Value, json};
4
5use crate::client::MatrixClient;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct WhoisInfo {
9 pub user_id: String,
10 pub devices: std::collections::HashMap<String, WhoisDevice>,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct WhoisDevice {
15 pub sessions: Vec<WhoisSession>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct WhoisSession {
20 pub connections: Vec<WhoisConnectionInfo>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct WhoisConnectionInfo {
25 pub ip: String,
26 pub last_seen: u64,
27 pub user_agent: String,
28}
29
30#[derive(Clone)]
31pub struct AdminApis {
32 client: MatrixClient,
33}
34
35impl AdminApis {
36 pub fn new(client: MatrixClient) -> Self {
37 Self { client }
38 }
39
40 pub fn synapse(&self) -> SynapseAdminApis {
41 SynapseAdminApis::new(self.client.clone())
42 }
43
44 pub async fn whois_user(&self, user_id: &str) -> anyhow::Result<WhoisInfo> {
45 let encoded = encode_path(user_id);
46 let endpoint = format!("/_matrix/client/v3/admin/whois/{encoded}");
47 let response = self.client.raw_json(Method::GET, &endpoint, None).await?;
48 serde_json::from_value(response).map_err(Into::into)
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct UserListResponse {
54 pub users: Vec<UserInfo>,
55 pub next_token: Option<String>,
56 pub total: Option<u64>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct UserInfo {
61 pub name: String,
62 #[serde(default)]
63 pub displayname: Option<String>,
64 #[serde(default)]
65 pub avatar_url: Option<String>,
66 #[serde(default)]
67 pub admin: Option<bool>,
68 #[serde(default)]
69 pub deactivated: Option<bool>,
70 #[serde(default)]
71 pub user_type: Option<String>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct RoomListResponse {
76 pub rooms: Vec<RoomInfo>,
77 #[serde(default)]
78 pub offset: Option<String>,
79 pub next_batch: Option<String>,
80 #[serde(default)]
81 pub prev_batch: Option<String>,
82 pub total_rooms: Option<u64>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct RoomInfo {
87 pub room_id: String,
88 pub name: Option<String>,
89 pub canonical_alias: Option<String>,
90 pub joined_members: Option<u64>,
91 pub joined_local_members: Option<u64>,
92 pub version: Option<String>,
93 pub creator: Option<String>,
94 pub encryption: Option<String>,
95 pub federatable: Option<bool>,
96 pub public: Option<bool>,
97 pub join_rules: Option<String>,
98 pub guest_access: Option<String>,
99 pub history_visibility: Option<String>,
100 pub state_events: Option<u64>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
104#[serde(rename_all = "snake_case")]
105pub enum SynapseRoomProperty {
106 Name,
107 CanonicalAlias,
108 JoinedMembers,
109 JoinedLocalMembers,
110 Version,
111 Creator,
112 Encryption,
113 Federatable,
114 Public,
115 JoinRules,
116 GuestAccess,
117 HistoryVisibility,
118 StateEvents,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ServerStatsResponse {
123 pub total_users: Option<u64>,
124 pub total_nonbridged_users: Option<u64>,
125 pub total_room_count: Option<u64>,
126 pub daily_active_users: Option<u64>,
127 pub monthly_active_users: Option<u64>,
128 #[serde(default)]
129 pub r30_users: Option<u64>,
130 #[serde(default)]
131 pub r30v2_users: Option<u64>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize, Default)]
135pub struct SynapseUserProperties {
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub displayname: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub avatar_url: Option<String>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub admin: Option<bool>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub deactivated: Option<bool>,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub locked: Option<bool>,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub password: Option<String>,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub logout_devices: Option<bool>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct SynapseUser {
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub displayname: Option<String>,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub avatar_url: Option<String>,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub admin: Option<bool>,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub deactivated: Option<bool>,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub locked: Option<bool>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct SynapseRegistrationToken {
168 pub token: String,
169 pub uses_allowed: Option<u64>,
170 pub pending: u64,
171 pub completed: u64,
172 pub expiry_time: Option<u64>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, Default)]
176pub struct SynapseRegistrationTokenUpdateOptions {
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub uses_allowed: Option<u64>,
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub expiry_time: Option<u64>,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize, Default)]
184pub struct SynapseRegistrationTokenOptions {
185 #[serde(flatten)]
186 pub update_options: SynapseRegistrationTokenUpdateOptions,
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub token: Option<String>,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub length: Option<u64>,
191}
192
193#[derive(Clone)]
194pub struct SynapseAdminApis {
195 client: MatrixClient,
196}
197
198impl SynapseAdminApis {
199 pub fn new(client: MatrixClient) -> Self {
200 Self { client }
201 }
202
203 pub async fn get_user(&self, user_id: &str) -> anyhow::Result<SynapseUser> {
204 let encoded = encode_path(user_id);
205 let endpoint = format!("/_synapse/admin/v2/users/{encoded}");
206 let response = self.client.raw_json(Method::GET, &endpoint, None).await?;
207 serde_json::from_value(response).map_err(Into::into)
208 }
209
210 pub async fn upsert_user(
211 &self,
212 user_id: &str,
213 opts: &SynapseUserProperties,
214 ) -> anyhow::Result<SynapseUser> {
215 let encoded = encode_path(user_id);
216 let endpoint = format!("/_synapse/admin/v2/users/{encoded}");
217 let body = serde_json::to_value(opts)?;
218 let response = self
219 .client
220 .raw_json(Method::PUT, &endpoint, Some(body))
221 .await?;
222 serde_json::from_value(response).map_err(Into::into)
223 }
224
225 pub async fn list_users(
226 &self,
227 from: Option<&str>,
228 limit: Option<u64>,
229 name: Option<&str>,
230 guests: bool,
231 deactivated: bool,
232 ) -> anyhow::Result<UserListResponse> {
233 let mut params = vec![
234 format!("guests={guests}"),
235 format!("deactivated={deactivated}"),
236 ];
237 if let Some(v) = from {
238 params.push(format!(
239 "from={}",
240 percent_encoding::utf8_percent_encode(v, percent_encoding::NON_ALPHANUMERIC)
241 ));
242 }
243 if let Some(v) = limit {
244 params.push(format!("limit={v}"));
245 }
246 if let Some(v) = name {
247 params.push(format!(
248 "name={}",
249 percent_encoding::utf8_percent_encode(v, percent_encoding::NON_ALPHANUMERIC)
250 ));
251 }
252 let endpoint = format!("/_synapse/admin/v2/users?{}", params.join("&"));
253 let response = self.client.raw_json(Method::GET, &endpoint, None).await?;
254 serde_json::from_value(response).map_err(Into::into)
255 }
256
257 pub async fn is_admin(&self, user_id: &str) -> anyhow::Result<bool> {
258 let encoded = encode_path(user_id);
259 let endpoint = format!("/_synapse/admin/v1/users/{encoded}/admin");
260 let response = self.client.raw_json(Method::GET, &endpoint, None).await?;
261 Ok(response
262 .get("admin")
263 .and_then(|v| v.as_bool())
264 .unwrap_or(false))
265 }
266
267 pub async fn is_self_admin(&self) -> anyhow::Result<bool> {
268 let user_id = self.client.get_user_id().await?;
269 match self.is_admin(&user_id).await {
270 Ok(is_admin) => Ok(is_admin),
271 Err(e) => {
272 if e.to_string().contains("M_FORBIDDEN") {
274 Ok(false)
275 } else {
276 Err(e)
277 }
278 }
279 }
280 }
281
282 pub async fn get_room_members(&self, room_id: &str) -> anyhow::Result<Value> {
283 let encoded = encode_path(room_id);
284 let endpoint = format!("/_synapse/admin/v1/rooms/{encoded}/members");
285 self.client.raw_json(Method::GET, &endpoint, None).await
286 }
287
288 pub async fn list_rooms(
289 &self,
290 search_term: Option<&str>,
291 from: Option<&str>,
292 limit: Option<u64>,
293 order_by: Option<SynapseRoomProperty>,
294 reverse_order: bool,
295 ) -> anyhow::Result<RoomListResponse> {
296 let mut params = Vec::new();
297 if let Some(v) = from {
298 params.push(format!(
299 "from={}",
300 percent_encoding::utf8_percent_encode(v, percent_encoding::NON_ALPHANUMERIC)
301 ));
302 }
303 if let Some(v) = limit {
304 params.push(format!("limit={v}"));
305 }
306 if let Some(v) = search_term {
307 params.push(format!(
308 "search_term={}",
309 percent_encoding::utf8_percent_encode(v, percent_encoding::NON_ALPHANUMERIC)
310 ));
311 }
312 if let Some(v) = order_by {
313 let field = serde_json::to_value(v)?
314 .as_str()
315 .ok_or_else(|| anyhow::anyhow!("invalid order_by field"))?
316 .to_owned();
317 params.push(format!(
318 "order_by={}",
319 percent_encoding::utf8_percent_encode(&field, percent_encoding::NON_ALPHANUMERIC)
320 ));
321 }
322 params.push(format!("dir={}", if reverse_order { "b" } else { "f" }));
323
324 let endpoint = format!("/_synapse/admin/v1/rooms?{}", params.join("&"));
325 let response = self.client.raw_json(Method::GET, &endpoint, None).await?;
326 serde_json::from_value(response).map_err(Into::into)
327 }
328
329 pub async fn get_room_state(&self, room_id: &str) -> anyhow::Result<Value> {
330 let encoded = encode_path(room_id);
331 let endpoint = format!("/_synapse/admin/v1/rooms/{encoded}/state");
332 self.client.raw_json(Method::GET, &endpoint, None).await
333 }
334
335 pub async fn make_room_admin(
336 &self,
337 room_id: &str,
338 user_id: Option<&str>,
339 ) -> anyhow::Result<()> {
340 let encoded = encode_path(room_id);
341 let endpoint = format!("/_synapse/admin/v1/rooms/{encoded}/make_room_admin");
342 let body = if let Some(uid) = user_id {
343 json!({ "user_id": uid })
344 } else {
345 json!({})
346 };
347 self.client
348 .raw_json(Method::POST, &endpoint, Some(body))
349 .await?;
350 Ok(())
351 }
352
353 pub async fn get_server_stats(&self) -> anyhow::Result<ServerStatsResponse> {
354 let response = self
355 .client
356 .raw_json(Method::GET, "/_synapse/admin/v1/statistics", None)
357 .await?;
358 serde_json::from_value(response).map_err(Into::into)
359 }
360
361 pub async fn get_room_details(&self, room_id: &str) -> anyhow::Result<Value> {
362 let encoded = encode_path(room_id);
363 let endpoint = format!("/_synapse/admin/v1/rooms/{encoded}");
364 self.client.raw_json(Method::GET, &endpoint, None).await
365 }
366
367 pub async fn delete_room(
368 &self,
369 room_id: &str,
370 purge: bool,
371 force_purge: bool,
372 block: bool,
373 ) -> anyhow::Result<String> {
374 let encoded = encode_path(room_id);
375 let endpoint = format!("/_synapse/admin/v2/rooms/{encoded}/delete");
376 let body = json!({
377 "purge": purge,
378 "force_purge": force_purge,
379 "block": block,
380 });
381 let response = self
382 .client
383 .raw_json(Method::POST, &endpoint, Some(body))
384 .await?;
385 response
386 .get("delete_id")
387 .and_then(|v| v.as_str())
388 .map(ToOwned::to_owned)
389 .ok_or_else(|| anyhow::anyhow!("missing delete_id in response"))
390 }
391
392 pub async fn get_delete_room_status(&self, delete_id: &str) -> anyhow::Result<Value> {
393 let encoded = encode_path(delete_id);
394 let endpoint = format!("/_synapse/admin/v2/rooms/delete/{encoded}/status");
395 self.client.raw_json(Method::GET, &endpoint, None).await
396 }
397
398 pub async fn get_delete_room_state(&self, room_id: &str) -> anyhow::Result<Vec<Value>> {
399 let encoded = encode_path(room_id);
400 let endpoint = format!("/_synapse/admin/v2/rooms/{encoded}/delete_status");
401 let response = self.client.raw_json(Method::GET, &endpoint, None).await?;
402 Ok(response
403 .get("results")
404 .and_then(Value::as_array)
405 .cloned()
406 .unwrap_or_default())
407 }
408
409 pub async fn get_user_media(
410 &self,
411 user_id: &str,
412 limit: Option<u64>,
413 from: Option<&str>,
414 ) -> anyhow::Result<Value> {
415 let encoded = encode_path(user_id);
416 let mut endpoint = format!("/_synapse/admin/v1/users/{encoded}/media");
417 let mut params = Vec::new();
418 if let Some(l) = limit {
419 params.push(format!("limit={}", l));
420 }
421 if let Some(f) = from {
422 params.push(format!("from={}", f));
423 }
424 if !params.is_empty() {
425 endpoint.push('?');
426 endpoint.push_str(¶ms.join("&"));
427 }
428 self.client.raw_json(Method::GET, &endpoint, None).await
429 }
430
431 pub async fn get_user_joined_rooms(&self, user_id: &str) -> anyhow::Result<Value> {
432 let encoded = encode_path(user_id);
433 let endpoint = format!("/_synapse/admin/v1/users/{encoded}/joined_rooms");
434 self.client.raw_json(Method::GET, &endpoint, None).await
435 }
436
437 pub async fn shadow_ban(&self, user_id: &str) -> anyhow::Result<()> {
438 let encoded = encode_path(user_id);
439 let endpoint = format!("/_synapse/admin/v1/users/{encoded}/shadow_ban");
440 self.client
441 .raw_json(Method::POST, &endpoint, Some(json!({})))
442 .await?;
443 Ok(())
444 }
445
446 pub async fn unshadow_ban(&self, user_id: &str) -> anyhow::Result<()> {
447 let encoded = encode_path(user_id);
448 let endpoint = format!("/_synapse/admin/v1/users/{encoded}/shadow_ban/unban");
449 self.client
450 .raw_json(Method::POST, &endpoint, Some(json!({})))
451 .await?;
452 Ok(())
453 }
454
455 pub async fn get_server_version(&self) -> anyhow::Result<Value> {
456 self.client
457 .raw_json(Method::GET, "/_synapse/admin/v1/server_version", None)
458 .await
459 }
460
461 pub async fn purge_history(
462 &self,
463 room_id: &str,
464 purge_up_to_ts: Option<u64>,
465 delete_local_events: bool,
466 ) -> anyhow::Result<String> {
467 let encoded = encode_path(room_id);
468 let endpoint = format!("/_synapse/admin/v1/purge_history/{encoded}");
469 let mut body = json!({ "delete_local_events": delete_local_events });
470 if let Some(ts) = purge_up_to_ts {
471 body["purge_up_to_ts"] = json!(ts);
472 }
473 let response = self
474 .client
475 .raw_json(Method::POST, &endpoint, Some(body))
476 .await?;
477 response
478 .get("purge_id")
479 .and_then(Value::as_str)
480 .map(ToOwned::to_owned)
481 .ok_or_else(|| anyhow::anyhow!("missing purge_id in response"))
482 }
483
484 pub async fn get_purge_status(&self, purge_id: &str) -> anyhow::Result<Value> {
485 let encoded = encode_path(purge_id);
486 let endpoint = format!("/_synapse/admin/v1/purge_history_status/{encoded}");
487 self.client.raw_json(Method::GET, &endpoint, None).await
488 }
489
490 pub async fn list_registration_tokens(
491 &self,
492 valid: Option<bool>,
493 ) -> anyhow::Result<Vec<SynapseRegistrationToken>> {
494 let endpoint = "/_synapse/admin/v1/registration_tokens";
495 let endpoint = if let Some(v) = valid {
496 format!("{endpoint}?valid={v}")
497 } else {
498 endpoint.to_string()
499 };
500 let response = self.client.raw_json(Method::GET, &endpoint, None).await?;
501 Ok(response
502 .get("registration_tokens")
503 .and_then(Value::as_array)
504 .cloned()
505 .unwrap_or_default()
506 .into_iter()
507 .filter_map(|v| serde_json::from_value(v).ok())
508 .collect())
509 }
510
511 pub async fn get_registration_token(
512 &self,
513 token: &str,
514 ) -> anyhow::Result<Option<SynapseRegistrationToken>> {
515 let encoded = encode_path(token);
516 let endpoint = format!("/_synapse/admin/v1/registration_tokens/{encoded}");
517 match self.client.raw_json(Method::GET, &endpoint, None).await {
518 Ok(response) => Ok(Some(serde_json::from_value(response)?)),
519 Err(e) if e.to_string().contains("404") => Ok(None),
520 Err(e) => Err(e),
521 }
522 }
523
524 pub async fn create_registration_token(
525 &self,
526 options: &SynapseRegistrationTokenOptions,
527 ) -> anyhow::Result<SynapseRegistrationToken> {
528 let endpoint = "/_synapse/admin/v1/registration_tokens/new";
529 let body = serde_json::to_value(options)?;
530 let response = self
531 .client
532 .raw_json(Method::POST, endpoint, Some(body))
533 .await?;
534 serde_json::from_value(response).map_err(Into::into)
535 }
536
537 pub async fn update_registration_token(
538 &self,
539 token: &str,
540 options: &SynapseRegistrationTokenUpdateOptions,
541 ) -> anyhow::Result<SynapseRegistrationToken> {
542 let encoded = encode_path(token);
543 let endpoint = format!("/_synapse/admin/v1/registration_tokens/{encoded}");
544 let body = serde_json::to_value(options)?;
545 let response = self
546 .client
547 .raw_json(Method::PUT, &endpoint, Some(body))
548 .await?;
549 serde_json::from_value(response).map_err(Into::into)
550 }
551
552 pub async fn delete_registration_token(&self, token: &str) -> anyhow::Result<()> {
553 let encoded = encode_path(token);
554 let endpoint = format!("/_synapse/admin/v1/registration_tokens/{encoded}");
555 self.client
556 .raw_json(Method::DELETE, &endpoint, None)
557 .await?;
558 Ok(())
559 }
560
561 pub async fn get_event_nearest_to_timestamp(
562 &self,
563 room_id: &str,
564 ts: u64,
565 dir: &str,
566 ) -> anyhow::Result<Value> {
567 let encoded = encode_path(room_id);
568 let endpoint =
569 format!("/_synapse/admin/v1/rooms/{encoded}/timestamp_to_event?ts={ts}&dir={dir}");
570 self.client.raw_json(Method::GET, &endpoint, None).await
571 }
572}
573
574fn encode_path(value: &str) -> String {
575 percent_encoding::utf8_percent_encode(value, percent_encoding::NON_ALPHANUMERIC).to_string()
576}