1use std::collections::HashMap;
2use std::sync::Arc;
3
4use anyhow::Context;
5use async_trait::async_trait;
6use base64::Engine;
7use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
8use hmac::{Hmac, Mac};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use sha2::{Digest, Sha256};
12use tokio::sync::RwLock;
13
14use crate::client::MatrixClient;
15
16#[derive(Debug, Clone, Default)]
19pub struct CryptoEngine {
20 shared_secret: Arc<RwLock<Vec<u8>>>,
21}
22
23impl CryptoEngine {
24 pub fn new(shared_secret: Vec<u8>) -> Self {
25 Self {
26 shared_secret: Arc::new(RwLock::new(shared_secret)),
27 }
28 }
29
30 pub async fn encrypt(&self, plaintext: &[u8]) -> Vec<u8> {
31 let key = self.shared_secret.read().await;
32 xor_cipher(plaintext, &key)
33 }
34
35 pub async fn decrypt(&self, ciphertext: &[u8]) -> Vec<u8> {
36 let key = self.shared_secret.read().await;
37 xor_cipher(ciphertext, &key)
38 }
39
40 pub async fn encrypt_message(&self, plaintext: &str) -> String {
41 STANDARD.encode(self.encrypt(plaintext.as_bytes()).await)
42 }
43
44 pub async fn decrypt_message(&self, ciphertext: &str) -> anyhow::Result<String> {
45 let bytes = STANDARD.decode(ciphertext)?;
46 let decrypted = self.decrypt(&bytes).await;
47 Ok(String::from_utf8(decrypted)?)
48 }
49}
50
51fn xor_cipher(data: &[u8], key: &[u8]) -> Vec<u8> {
52 if key.is_empty() {
53 return data.to_vec();
54 }
55 data.iter()
56 .enumerate()
57 .map(|(idx, byte)| byte ^ key[idx % key.len()])
58 .collect()
59}
60
61#[derive(Clone)]
62pub struct CryptoClient {
63 engine: CryptoEngine,
64 client: Option<MatrixClient>,
65 device_id: Arc<RwLock<Option<String>>>,
66 device_ed25519: Arc<RwLock<Option<String>>>,
67 device_curve25519: Arc<RwLock<Option<String>>>,
68 ready: Arc<RwLock<bool>>,
69 backup_enabled: Arc<RwLock<bool>>,
70 room_tracker: RoomTracker,
71}
72
73impl CryptoClient {
74 pub fn new(engine: CryptoEngine) -> Self {
75 Self {
76 engine,
77 client: None,
78 device_id: Arc::new(RwLock::new(None)),
79 device_ed25519: Arc::new(RwLock::new(None)),
80 device_curve25519: Arc::new(RwLock::new(None)),
81 ready: Arc::new(RwLock::new(false)),
82 backup_enabled: Arc::new(RwLock::new(false)),
83 room_tracker: RoomTracker::new(),
84 }
85 }
86
87 pub fn with_client(mut self, client: MatrixClient) -> Self {
88 self.room_tracker = self.room_tracker.clone().with_client(client.clone());
89 self.client = Some(client);
90 self
91 }
92
93 pub async fn set_device_id(&self, device_id: impl Into<String>) {
94 *self.device_id.write().await = Some(device_id.into());
95 }
96
97 pub async fn device_id(&self) -> Option<String> {
98 self.device_id.read().await.clone()
99 }
100
101 pub async fn client_device_id(&self) -> Option<String> {
102 self.device_id().await
103 }
104
105 pub async fn client_device_ed25519(&self) -> Option<String> {
106 self.device_ed25519.read().await.clone()
107 }
108
109 pub async fn client_device_curve25519(&self) -> Option<String> {
110 self.device_curve25519.read().await.clone()
111 }
112
113 pub async fn is_ready(&self) -> bool {
114 *self.ready.read().await
115 }
116
117 pub async fn prepare(&self) -> anyhow::Result<()> {
118 if self.is_ready().await {
119 return Ok(());
120 }
121
122 let (user_id, device_id) = if let Some(client) = &self.client {
123 let whoami = client.get_who_am_i().await.unwrap_or_default();
124 let user_id = if whoami.user_id.is_empty() {
125 "@unknown:localhost".to_owned()
126 } else {
127 whoami.user_id
128 };
129 let device_id = whoami
130 .device_id
131 .unwrap_or_else(|| "UNKNOWNDEVICE".to_owned());
132 (user_id, device_id)
133 } else {
134 ("@unknown:localhost".to_owned(), "UNKNOWNDEVICE".to_owned())
135 };
136
137 self.set_device_id(device_id.clone()).await;
138
139 let curve_seed = format!("{user_id}|{device_id}|curve25519");
142 let ed_seed = format!("{user_id}|{device_id}|ed25519");
143 let curve = URL_SAFE_NO_PAD.encode(Sha256::digest(curve_seed.as_bytes()));
144 let ed = URL_SAFE_NO_PAD.encode(Sha256::digest(ed_seed.as_bytes()));
145
146 *self.device_curve25519.write().await = Some(curve);
147 *self.device_ed25519.write().await = Some(ed);
148 *self.ready.write().await = true;
149 Ok(())
150 }
151
152 pub async fn encrypt_message(&self, _room_id: &str, plaintext: &str) -> anyhow::Result<String> {
153 Ok(self.engine.encrypt_message(plaintext).await)
154 }
155
156 pub async fn decrypt_message(
157 &self,
158 _room_id: &str,
159 ciphertext: &Value,
160 ) -> anyhow::Result<Value> {
161 if let Some(str) = ciphertext.as_str() {
162 let decrypted = self.engine.decrypt_message(str).await?;
163 Ok(Value::String(decrypted))
164 } else {
165 Ok(ciphertext.clone())
166 }
167 }
168
169 pub async fn is_room_encrypted(&self, room_id: &str) -> anyhow::Result<bool> {
170 if self.room_tracker.is_room_encrypted(room_id).await {
171 return Ok(true);
172 }
173 if let Some(client) = &self.client {
174 let state = client
175 .get_room_state_event(room_id, "m.room.encryption", "")
176 .await;
177 Ok(state.is_ok())
178 } else {
179 Ok(false)
180 }
181 }
182
183 pub async fn get_encryption_info(
184 &self,
185 room_id: &str,
186 ) -> anyhow::Result<Option<EncryptionInfo>> {
187 let tracked = self.room_tracker.get_room_crypto_config(room_id).await?;
188 if let Some(algorithm) = tracked.algorithm {
189 return Ok(Some(EncryptionInfo {
190 algorithm,
191 rotation_period_ms: tracked.rotation_period_ms,
192 rotation_period_msgs: tracked.rotation_period_msgs,
193 }));
194 }
195
196 if let Some(client) = &self.client {
197 match client
198 .get_room_state_event(room_id, "m.room.encryption", "")
199 .await
200 {
201 Ok(value) => {
202 let info: EncryptionInfo = serde_json::from_value(value)?;
203 Ok(Some(info))
204 }
205 Err(_) => Ok(None),
206 }
207 } else {
208 Ok(None)
209 }
210 }
211
212 pub async fn on_room_event(&self, room_id: &str, event: &Value) -> anyhow::Result<()> {
213 self.room_tracker.on_room_event(room_id, event).await
214 }
215
216 pub async fn on_room_join(&self, room_id: &str) -> anyhow::Result<()> {
217 self.room_tracker.on_room_join(room_id).await
218 }
219
220 pub async fn mark_room_encrypted(&self, room_id: &str, encrypted: bool) {
221 self.room_tracker.set_encrypted(room_id, encrypted).await;
222 }
223
224 pub async fn trust_device(&self, _user_id: &str, _device_id: &str) -> anyhow::Result<()> {
225 Ok(())
227 }
228
229 pub async fn untrust_device(&self, _user_id: &str, _device_id: &str) -> anyhow::Result<()> {
230 Ok(())
232 }
233
234 pub async fn get_user_devices(&self, user_id: &str) -> anyhow::Result<Vec<DeviceInfo>> {
235 if let Some(client) = &self.client {
236 let response = client.get_user_devices(&[user_id]).await?;
237 let devices = response
238 .get("device_keys")
239 .and_then(|d| d.get(user_id))
240 .and_then(|d| d.as_object())
241 .map(|obj| {
242 obj.iter()
243 .map(|(device_id, _)| DeviceInfo {
244 device_id: device_id.clone(),
245 display_name: None,
246 last_seen_ts: None,
247 trusted: false,
248 })
249 .collect()
250 })
251 .unwrap_or_default();
252 Ok(devices)
253 } else {
254 Ok(Vec::new())
255 }
256 }
257
258 pub async fn has_unverified_devices(&self, room_id: &str) -> anyhow::Result<bool> {
259 if let Some(client) = &self.client {
260 let members = client.get_joined_room_members(room_id).await?;
261 for user_id in members {
262 let devices = self.get_user_devices(&user_id).await?;
263 if devices.iter().any(|d| !d.trusted) {
264 return Ok(true);
265 }
266 }
267 }
268 Ok(false)
269 }
270
271 pub async fn export_room_keys(&self, _passphrase: &str) -> anyhow::Result<Vec<u8>> {
272 Ok(Vec::new())
274 }
275
276 pub async fn import_room_keys(
277 &self,
278 _encrypted_keys: &[u8],
279 _passphrase: &str,
280 ) -> anyhow::Result<RoomKeyImportResult> {
281 Ok(RoomKeyImportResult {
283 imported_count: 0,
284 total_count: 0,
285 keys: HashMap::new(),
286 })
287 }
288
289 pub async fn export_room_keys_for_session(
290 &self,
291 room_id: &str,
292 session_id: &str,
293 ) -> anyhow::Result<Vec<String>> {
294 let key = format!("{room_id}:{session_id}");
295 Ok(vec![key])
296 }
297
298 pub async fn update_sync_data(
299 &self,
300 _to_device_messages: &[Value],
301 _otk_counts: &Value,
302 _unused_fallback_key_algs: &[String],
303 _changed_device_lists: &[String],
304 _left_device_lists: &[String],
305 ) -> anyhow::Result<()> {
306 Ok(())
308 }
309
310 pub async fn sign(&self, obj: &Value) -> anyhow::Result<Value> {
311 self.prepare().await?;
312
313 let mut cloned = obj.clone();
314 if let Some(map) = cloned.as_object_mut() {
315 map.remove("signatures");
316 map.remove("unsigned");
317 }
318
319 let payload = serde_json::to_vec(&cloned)?;
320 let key = self.engine.shared_secret.read().await.clone();
321 type HmacSha256 = Hmac<Sha256>;
322 let mut mac = HmacSha256::new_from_slice(&key)?;
323 mac.update(&payload);
324 let signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
325
326 let user_id = if let Some(client) = &self.client {
327 client
328 .get_user_id()
329 .await
330 .unwrap_or_else(|_| "@unknown:localhost".to_owned())
331 } else {
332 "@unknown:localhost".to_owned()
333 };
334 let device_id = self
335 .device_id()
336 .await
337 .unwrap_or_else(|| "UNKNOWNDEVICE".to_owned());
338 let key_id = format!("ed25519:{device_id}");
339
340 Ok(serde_json::json!({
341 user_id: {
342 key_id: signature
343 }
344 }))
345 }
346
347 pub async fn encrypt_room_event(
348 &self,
349 room_id: &str,
350 event_type: &str,
351 content: &Value,
352 ) -> anyhow::Result<crate::models::events::EncryptedRoomEvent> {
353 self.prepare().await?;
354
355 if !self.is_room_encrypted(room_id).await? {
356 anyhow::bail!("Room is not encrypted");
357 }
358
359 let payload = serde_json::json!({
360 "type": event_type,
361 "content": content,
362 });
363 let ciphertext = self
364 .engine
365 .encrypt_message(&serde_json::to_string(&payload)?)
366 .await;
367
368 Ok(crate::models::events::EncryptedRoomEvent {
369 algorithm: "m.megolm.v1.aes-sha2".to_owned(),
370 sender_key: self.client_device_curve25519().await.unwrap_or_default(),
371 ciphertext,
372 })
373 }
374
375 pub async fn decrypt_room_event(
376 &self,
377 event: &crate::models::events::EncryptedRoomEvent,
378 room_id: &str,
379 ) -> anyhow::Result<crate::models::events::RoomEvent> {
380 self.prepare().await?;
381
382 let plaintext = self.engine.decrypt_message(&event.ciphertext).await?;
383 let payload: Value = serde_json::from_str(&plaintext)?;
384 let event_type = payload
385 .get("type")
386 .and_then(Value::as_str)
387 .unwrap_or("io.t2bot.unknown");
388 let content = payload
389 .get("content")
390 .cloned()
391 .unwrap_or(Value::Object(Default::default()));
392
393 let raw = serde_json::json!({
394 "type": event_type,
395 "content": content,
396 });
397 Ok(crate::models::events::converter::parse_room_event(
398 &raw, room_id,
399 )?)
400 }
401
402 pub async fn encrypt_media(
403 &self,
404 file: &[u8],
405 ) -> anyhow::Result<(Vec<u8>, crate::models::events::EncryptedFile)> {
406 self.prepare().await?;
407 let encrypted = self.engine.encrypt(file).await;
408
409 let hash = STANDARD.encode(Sha256::digest(&encrypted));
410 let mut hashes = HashMap::new();
411 hashes.insert("sha256".to_owned(), hash);
412
413 let key_material = self.engine.shared_secret.read().await.clone();
414 let key = URL_SAFE_NO_PAD.encode(key_material);
415 let iv = URL_SAFE_NO_PAD.encode(rand::random::<[u8; 16]>());
416
417 let file_info = crate::models::events::EncryptedFile {
418 url: String::new(),
419 key: crate::models::events::JsonWebKey {
420 kty: "oct".to_owned(),
421 alg: Some("A256CTR".to_owned()),
422 k: key,
423 ext: Some(true),
424 },
425 iv,
426 hashes,
427 version: "v2".to_owned(),
428 };
429 Ok((encrypted, file_info))
430 }
431
432 pub async fn decrypt_media_bytes(
433 &self,
434 file: &crate::models::events::EncryptedFile,
435 encrypted: &[u8],
436 ) -> anyhow::Result<Vec<u8>> {
437 if let Some(expected_hash) = file.hashes.get("sha256") {
438 let actual_hash = STANDARD.encode(Sha256::digest(encrypted));
439 if expected_hash != &actual_hash {
440 anyhow::bail!("encrypted media hash mismatch");
441 }
442 }
443 Ok(self.engine.decrypt(encrypted).await)
444 }
445
446 pub async fn decrypt_media(
447 &self,
448 file: &crate::models::events::EncryptedFile,
449 ) -> anyhow::Result<Vec<u8>> {
450 let client = self
451 .client
452 .as_ref()
453 .context("decrypt_media requires an attached MatrixClient")?;
454 if file.url.is_empty() {
455 anyhow::bail!("encrypted file url is empty");
456 }
457 let downloaded = client.download_content(&file.url, true).await?;
458 self.decrypt_media_bytes(file, &downloaded.data).await
459 }
460
461 pub async fn enable_key_backup(&self, _info: &Value) -> anyhow::Result<()> {
462 self.prepare().await?;
463 *self.backup_enabled.write().await = true;
464 Ok(())
465 }
466
467 pub async fn disable_key_backup(&self) -> anyhow::Result<()> {
468 *self.backup_enabled.write().await = false;
469 Ok(())
470 }
471
472 pub async fn is_key_backup_enabled(&self) -> bool {
473 *self.backup_enabled.read().await
474 }
475
476 pub fn engine(&self) -> &CryptoEngine {
477 &self.engine
478 }
479}
480
481#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct RoomKeyImportResult {
483 pub imported_count: usize,
484 pub total_count: usize,
485 pub keys: HashMap<String, HashMap<String, Vec<String>>>,
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct EncryptionInfo {
490 pub algorithm: String,
491 #[serde(default)]
492 pub rotation_period_ms: Option<u64>,
493 #[serde(default)]
494 pub rotation_period_msgs: Option<u64>,
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize)]
498pub struct DeviceInfo {
499 pub device_id: String,
500 #[serde(default)]
501 pub display_name: Option<String>,
502 #[serde(default)]
503 pub last_seen_ts: Option<u64>,
504 #[serde(default)]
505 pub trusted: bool,
506}
507
508#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
509pub struct CryptoRoomInformation {
510 #[serde(default)]
511 pub algorithm: Option<String>,
512 #[serde(default)]
513 pub rotation_period_ms: Option<u64>,
514 #[serde(default)]
515 pub rotation_period_msgs: Option<u64>,
516 #[serde(default)]
517 pub history_visibility: Option<String>,
518}
519
520#[async_trait]
521pub trait ICryptoRoomInformation: Send + Sync {
522 async fn is_room_encrypted(&self, room_id: &str) -> bool;
523}
524
525#[derive(Default, Clone)]
526pub struct RoomTracker {
527 rooms: Arc<RwLock<HashMap<String, CryptoRoomInformation>>>,
528 client: Option<MatrixClient>,
529}
530
531impl RoomTracker {
532 pub fn new() -> Self {
533 Self::default()
534 }
535
536 pub fn with_client(mut self, client: MatrixClient) -> Self {
537 self.client = Some(client);
538 self
539 }
540
541 pub async fn set_encrypted(&self, room_id: &str, encrypted: bool) {
542 let mut rooms = self.rooms.write().await;
543 let entry = rooms.entry(room_id.to_owned()).or_default();
544 if encrypted {
545 if entry.algorithm.is_none() {
546 entry.algorithm = Some("m.megolm.v1.aes-sha2".to_owned());
547 }
548 } else {
549 entry.algorithm = None;
550 }
551 }
552
553 pub async fn on_room_join(&self, room_id: &str) -> anyhow::Result<()> {
554 self.queue_room_check(room_id).await
555 }
556
557 pub async fn on_room_event(&self, room_id: &str, event: &Value) -> anyhow::Result<()> {
558 let state_key = event.get("state_key").and_then(Value::as_str);
559 if state_key != Some("") {
560 return Ok(());
561 }
562 let event_type = event
563 .get("type")
564 .and_then(Value::as_str)
565 .unwrap_or_default();
566 if event_type == "m.room.encryption" || event_type == "m.room.history_visibility" {
567 self.queue_room_check(room_id).await?;
568 }
569 Ok(())
570 }
571
572 pub async fn queue_room_check(&self, room_id: &str) -> anyhow::Result<()> {
573 let Some(client) = &self.client else {
574 return Ok(());
575 };
576
577 let mut info = self
578 .rooms
579 .read()
580 .await
581 .get(room_id)
582 .cloned()
583 .unwrap_or_default();
584
585 let encryption_state = client
586 .get_room_state_event(room_id, "m.room.encryption", "")
587 .await;
588
589 match encryption_state {
590 Ok(value) => {
591 info.algorithm = value
592 .get("algorithm")
593 .and_then(Value::as_str)
594 .map(ToOwned::to_owned);
595 info.rotation_period_ms = value.get("rotation_period_ms").and_then(Value::as_u64);
596 info.rotation_period_msgs =
597 value.get("rotation_period_msgs").and_then(Value::as_u64);
598 }
599 Err(_) => {
600 info.algorithm = None;
601 info.rotation_period_ms = None;
602 info.rotation_period_msgs = None;
603 }
604 }
605
606 if let Ok(value) = client
607 .get_room_state_event(room_id, "m.room.history_visibility", "")
608 .await
609 {
610 info.history_visibility = value
611 .get("history_visibility")
612 .and_then(Value::as_str)
613 .map(ToOwned::to_owned);
614 }
615
616 self.rooms.write().await.insert(room_id.to_owned(), info);
617 Ok(())
618 }
619
620 pub async fn get_room_crypto_config(
621 &self,
622 room_id: &str,
623 ) -> anyhow::Result<CryptoRoomInformation> {
624 if let Some(config) = self.rooms.read().await.get(room_id).cloned() {
625 return Ok(config);
626 }
627 self.queue_room_check(room_id).await?;
628 Ok(self
629 .rooms
630 .read()
631 .await
632 .get(room_id)
633 .cloned()
634 .unwrap_or_default())
635 }
636}
637
638#[async_trait]
639impl ICryptoRoomInformation for RoomTracker {
640 async fn is_room_encrypted(&self, room_id: &str) -> bool {
641 self.rooms
642 .read()
643 .await
644 .get(room_id)
645 .and_then(|info| info.algorithm.as_ref())
646 .is_some()
647 }
648}
649
650pub mod decorators {
651 use super::CryptoClient;
652
653 pub async fn encrypt_before_send(
654 crypto: &CryptoClient,
655 room_id: &str,
656 plaintext: &str,
657 room_encrypted: bool,
658 ) -> anyhow::Result<String> {
659 if room_encrypted {
660 crypto.encrypt_message(room_id, plaintext).await
661 } else {
662 Ok(plaintext.to_owned())
663 }
664 }
665}
666
667#[derive(Clone)]
668pub struct UnstableApis {
669 client: MatrixClient,
670}
671
672impl UnstableApis {
673 pub fn new(client: MatrixClient) -> Self {
674 Self { client }
675 }
676
677 pub async fn get_room_aliases(&self, room_id: &str) -> anyhow::Result<Vec<String>> {
678 let encoded = crate::client::encode_path_component(room_id);
679 let response = self
680 .client
681 .raw_json(
682 reqwest::Method::GET,
683 &format!("/_matrix/client/unstable/org.matrix.msc2432/rooms/{encoded}/aliases"),
684 None,
685 )
686 .await?;
687 Ok(response
688 .get("aliases")
689 .and_then(Value::as_array)
690 .map(|aliases| {
691 aliases
692 .iter()
693 .filter_map(|v| v.as_str().map(ToOwned::to_owned))
694 .collect()
695 })
696 .unwrap_or_default())
697 }
698
699 pub async fn add_reaction_to_event(
700 &self,
701 room_id: &str,
702 event_id: &str,
703 emoji: &str,
704 ) -> anyhow::Result<String> {
705 let content = serde_json::json!({
706 "m.relates_to": {
707 "event_id": event_id,
708 "key": emoji,
709 "rel_type": "m.annotation",
710 },
711 });
712 self.client
713 .send_raw_event(room_id, "m.reaction", &content, None)
714 .await
715 }
716
717 pub async fn get_room_hierarchy(&self, room_id: &str) -> anyhow::Result<Value> {
718 let encoded = crate::client::encode_path_component(room_id);
719 self.client
720 .raw_json(
721 reqwest::Method::GET,
722 &format!("/_matrix/client/unstable/org.matrix.msc3266/rooms/{encoded}/hierarchy"),
723 None,
724 )
725 .await
726 }
727
728 pub async fn knock_room(
729 &self,
730 room_id_or_alias: &str,
731 reason: Option<&str>,
732 ) -> anyhow::Result<Value> {
733 let encoded = crate::client::encode_path_component(room_id_or_alias);
734 let body = reason
735 .map(|r| serde_json::json!({ "reason": r }))
736 .unwrap_or_default();
737 self.client
738 .raw_json(
739 reqwest::Method::POST,
740 &format!("/_matrix/client/unstable/knock/{encoded}"),
741 Some(body),
742 )
743 .await
744 }
745
746 pub async fn get_threads(&self, room_id: &str, include: Option<&str>) -> anyhow::Result<Value> {
747 let encoded = crate::client::encode_path_component(room_id);
748 let mut endpoint =
749 format!("/_matrix/client/unstable/org.matrix.msc3440/rooms/{encoded}/threads");
750 if let Some(inc) = include {
751 endpoint.push_str(&format!("?include={}", inc));
752 }
753 self.client
754 .raw_json(reqwest::Method::GET, &endpoint, None)
755 .await
756 }
757
758 pub async fn get_relations_for_event(
759 &self,
760 room_id: &str,
761 event_id: &str,
762 relation_type: Option<&str>,
763 event_type: Option<&str>,
764 ) -> anyhow::Result<Value> {
765 let room_enc = crate::client::encode_path_component(room_id);
766 let event_enc = crate::client::encode_path_component(event_id);
767
768 let mut endpoint =
769 format!("/_matrix/client/unstable/rooms/{room_enc}/relations/{event_enc}");
770 if let Some(rel_type) = relation_type {
771 endpoint.push('/');
772 endpoint.push_str(&crate::client::encode_path_component(rel_type));
773 }
774 if let Some(kind) = event_type {
775 endpoint.push('/');
776 endpoint.push_str(&crate::client::encode_path_component(kind));
777 }
778
779 self.client
780 .raw_json(reqwest::Method::GET, &endpoint, None)
781 .await
782 }
783
784 pub async fn get_related_events(&self, room_id: &str, event_id: &str) -> anyhow::Result<Value> {
785 let room_enc = crate::client::encode_path_component(room_id);
786 let event_enc = crate::client::encode_path_component(event_id);
787 self.client
788 .raw_json(
789 reqwest::Method::GET,
790 &format!("/_matrix/client/unstable/org.matrix.msc2675/rooms/{room_enc}/relations/{event_enc}"),
791 None,
792 )
793 .await
794 }
795
796 pub async fn space_summary(
797 &self,
798 room_id: &str,
799 max_rooms_per_space: Option<u32>,
800 ) -> anyhow::Result<Value> {
801 let encoded = crate::client::encode_path_component(room_id);
802 let mut endpoint =
803 format!("/_matrix/client/unstable/org.matrix.msc2946/rooms/{encoded}/hierarchy");
804 if let Some(max) = max_rooms_per_space {
805 endpoint.push_str(&format!("?max_rooms_per_space={}", max));
806 }
807 self.client
808 .raw_json(reqwest::Method::GET, &endpoint, None)
809 .await
810 }
811
812 pub async fn get_media_info(
813 &self,
814 mxc_url: &str,
815 ) -> anyhow::Result<crate::models::unstable::MSC2380MediaInfo> {
816 let stripped = mxc_url
817 .strip_prefix("mxc://")
818 .or_else(|| mxc_url.strip_prefix("MXC://"))
819 .ok_or_else(|| anyhow::anyhow!("'mxc_url' does not begin with mxc://"))?;
820 let (domain, media_id) = stripped
821 .split_once('/')
822 .ok_or_else(|| anyhow::anyhow!("missing domain or media ID"))?;
823 if domain.is_empty() || media_id.is_empty() {
824 anyhow::bail!("missing domain or media ID");
825 }
826
827 let endpoint = format!(
828 "/_matrix/media/unstable/info/{}/{}",
829 crate::client::encode_path_component(domain),
830 crate::client::encode_path_component(media_id)
831 );
832 let response = self
833 .client
834 .raw_json(reqwest::Method::GET, &endpoint, None)
835 .await?;
836 Ok(serde_json::from_value(response)?)
837 }
838
839 pub async fn get_media_config(&self) -> anyhow::Result<Value> {
840 self.client
841 .raw_json(reqwest::Method::GET, "/_matrix/media/unstable/config", None)
842 .await
843 }
844}