Skip to main content

matrix_bot_sdk/
admin.rs

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 the error is a MatrixError M_FORBIDDEN (403), return false instead of failing
273                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(&params.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}