1use std::collections::BTreeMap;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, Ordering};
4
5use anyhow::Context;
6use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
7use reqwest::Method;
8use serde::Deserialize;
9use serde_json::{Value, json};
10use tokio::runtime::Runtime;
11use tokio::sync::RwLock;
12use url::Url;
13use uuid::Uuid;
14
15use crate::http::MatrixHttp;
16use crate::models::events::{Event, Membership, MembershipEvent, converter};
17use crate::models::{Account, CreateRoom, MXCUrl, MatrixProfile, Presence};
18use crate::preprocessors::IPreprocessor;
19use crate::storage::IStorageProvider;
20
21pub trait IFilter: Send + Sync {
22 fn matches(&self, event: &Event) -> bool;
23}
24
25#[derive(Debug, Clone, Default)]
26pub struct MatrixAuth {
27 pub access_token: String,
28 pub user_id: Option<String>,
29 pub device_id: Option<String>,
30}
31
32impl MatrixAuth {
33 pub fn new(access_token: impl Into<String>) -> Self {
34 Self {
35 access_token: access_token.into(),
36 user_id: None,
37 device_id: None,
38 }
39 }
40
41 pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
42 self.user_id = Some(user_id.into());
43 self
44 }
45
46 pub fn with_device_id(mut self, device_id: impl Into<String>) -> Self {
47 self.device_id = Some(device_id.into());
48 self
49 }
50}
51
52#[derive(Clone)]
53pub struct MatrixClient {
54 http: MatrixHttp,
55 auth: Arc<RwLock<MatrixAuth>>,
56 storage: Option<Arc<dyn IStorageProvider>>,
57 preprocessors: Arc<RwLock<Vec<Arc<dyn IPreprocessor>>>>,
58 filters: Arc<RwLock<Vec<Arc<dyn IFilter>>>>,
59 impersonated_user_id: Arc<RwLock<Option<String>>>,
60 impersonated_device_id: Arc<RwLock<Option<String>>>,
61 join_strategy: Arc<RwLock<Option<Arc<dyn crate::strategies::JoinRoomStrategy>>>>,
62 stop_syncing: Arc<AtomicBool>,
63}
64
65impl MatrixClient {
66 pub fn new(homeserver: Url, auth: MatrixAuth) -> Self {
67 Self {
68 http: MatrixHttp::new(homeserver),
69 auth: Arc::new(RwLock::new(auth)),
70 storage: None,
71 preprocessors: Arc::new(RwLock::new(Vec::new())),
72 filters: Arc::new(RwLock::new(Vec::new())),
73 impersonated_user_id: Arc::new(RwLock::new(None)),
74 impersonated_device_id: Arc::new(RwLock::new(None)),
75 join_strategy: Arc::new(RwLock::new(None)),
76 stop_syncing: Arc::new(AtomicBool::new(false)),
77 }
78 }
79
80 pub fn with_storage(mut self, storage: Arc<dyn IStorageProvider>) -> Self {
81 self.storage = Some(storage);
82 self
83 }
84
85 pub fn homeserver(&self) -> &Url {
86 self.http.homeserver()
87 }
88
89 pub async fn access_token(&self) -> Option<String> {
90 let auth = self.auth.read().await;
91 if auth.access_token.is_empty() {
92 None
93 } else {
94 Some(auth.access_token.clone())
95 }
96 }
97
98 pub async fn auth(&self) -> MatrixAuth {
99 self.auth.read().await.clone()
100 }
101
102 pub async fn set_auth(&self, auth: MatrixAuth) {
103 *self.auth.write().await = auth;
104 }
105
106 pub async fn add_preprocessor(&self, preprocessor: Arc<dyn IPreprocessor>) {
107 self.preprocessors.write().await.push(preprocessor);
108 }
109
110 pub async fn add_filter(&self, filter: Arc<dyn IFilter>) {
111 self.filters.write().await.push(filter);
112 }
113
114 pub async fn impersonate_user_id(
115 &self,
116 user_id: Option<impl Into<String>>,
117 device_id: Option<impl Into<String>>,
118 ) {
119 let mut uid = self.impersonated_user_id.write().await;
120 let mut did = self.impersonated_device_id.write().await;
121 *uid = user_id.map(Into::into);
122 *did = device_id.map(Into::into);
123 }
124
125 pub async fn set_join_strategy(&self, strategy: Arc<dyn crate::strategies::JoinRoomStrategy>) {
126 *self.join_strategy.write().await = Some(strategy);
127 }
128
129 pub fn unstable_apis(&self) -> crate::e2ee::UnstableApis {
130 crate::e2ee::UnstableApis::new(self.clone())
131 }
132
133 pub async fn get_identity_server_client(
134 &self,
135 identity_server_name: &str,
136 ) -> anyhow::Result<crate::identity::IdentityClient> {
137 let oidc_token = self.get_openid_connect_token().await?;
138 let access_token = oidc_token
139 .get("access_token")
140 .and_then(Value::as_str)
141 .context("missing access_token in OpenID token response")?;
142 let server_url = Url::parse(&format!("https://{identity_server_name}"))?;
143 Ok(crate::identity::IdentityClient::new(
144 access_token,
145 server_url,
146 Arc::new(self.clone()),
147 ))
148 }
149
150 pub async fn raw_json(
151 &self,
152 method: Method,
153 endpoint: &str,
154 body: Option<Value>,
155 ) -> anyhow::Result<Value> {
156 self.http
157 .send_json(method, endpoint, Some(&self.token().await), body.as_ref())
158 .await
159 }
160
161 pub async fn do_request(
162 &self,
163 method: Method,
164 endpoint: &str,
165 query: Option<&BTreeMap<String, String>>,
166 body: Option<&Value>,
167 ) -> anyhow::Result<Value> {
168 let url = if let Some(q) = query {
169 let qs = q
170 .iter()
171 .map(|(k, v)| format!("{}={}", k, v))
172 .collect::<Vec<_>>()
173 .join("&");
174 format!("{}?{}", endpoint, qs)
175 } else {
176 endpoint.to_owned()
177 };
178 self.raw_json(method, &url, body.cloned()).await
179 }
180
181 async fn token(&self) -> String {
182 self.auth.read().await.access_token.clone()
183 }
184}
185
186impl MatrixClient {
187 pub async fn password_login(
188 &self,
189 username: &str,
190 password: &str,
191 device_name: Option<&str>,
192 ) -> anyhow::Result<MatrixAuth> {
193 let mut body = json!({
194 "type": "m.login.password",
195 "identifier": {
196 "type": "m.id.user",
197 "user": username
198 },
199 "password": password,
200 });
201 if let Some(name) = device_name {
202 body["initial_device_display_name"] = json!(name);
203 }
204 let response = self
205 .http
206 .send_json(Method::POST, "/_matrix/client/v3/login", None, Some(&body))
207 .await?;
208
209 let access_token = response
210 .get("access_token")
211 .and_then(Value::as_str)
212 .context("missing access_token in login response")?
213 .to_owned();
214 let user_id = response
215 .get("user_id")
216 .and_then(Value::as_str)
217 .map(ToOwned::to_owned);
218 let device_id = response
219 .get("device_id")
220 .and_then(Value::as_str)
221 .map(ToOwned::to_owned);
222
223 Ok(MatrixAuth {
224 access_token,
225 user_id,
226 device_id,
227 })
228 }
229
230 pub async fn password_register(
231 &self,
232 localpart: &str,
233 password: &str,
234 device_name: Option<&str>,
235 ) -> anyhow::Result<MatrixAuth> {
236 let mut body = json!({
237 "username": localpart,
238 "password": password,
239 });
240 if let Some(name) = device_name {
241 body["initial_device_display_name"] = json!(name);
242 }
243
244 let response = self
245 .http
246 .send_json(
247 Method::POST,
248 "/_matrix/client/v3/register",
249 None,
250 Some(&body),
251 )
252 .await?;
253
254 let access_token = response
255 .get("access_token")
256 .and_then(Value::as_str)
257 .context("missing access_token in register response")?
258 .to_owned();
259 let user_id = response
260 .get("user_id")
261 .and_then(Value::as_str)
262 .map(ToOwned::to_owned);
263 let device_id = response
264 .get("device_id")
265 .and_then(Value::as_str)
266 .map(ToOwned::to_owned);
267
268 Ok(MatrixAuth {
269 access_token,
270 user_id,
271 device_id,
272 })
273 }
274
275 pub async fn login_with_token(&self, auth: MatrixAuth) {
276 *self.auth.write().await = auth;
277 }
278}
279
280impl MatrixClient {
281 pub async fn sync_once(
282 &self,
283 since: Option<&str>,
284 ) -> anyhow::Result<Vec<crate::models::events::RoomEvent>> {
285 let (events, _) = self.sync_once_internal(since).await?;
286 Ok(events)
287 }
288
289 pub async fn start(&self) -> anyhow::Result<()> {
290 self.stop_syncing.store(false, Ordering::Relaxed);
291 let mut since: Option<String> = None;
292 while !self.stop_syncing.load(Ordering::Relaxed) {
293 let (_, next_batch) = self.sync_once_internal(since.as_deref()).await?;
294 if let Some(next_batch) = next_batch {
295 since = Some(next_batch);
296 }
297 }
298 Ok(())
299 }
300
301 pub fn stop(&self) {
302 self.stop_syncing.store(true, Ordering::Relaxed);
303 }
304
305 async fn sync_once_internal(
306 &self,
307 since: Option<&str>,
308 ) -> anyhow::Result<(Vec<crate::models::events::RoomEvent>, Option<String>)> {
309 let endpoint = match since {
310 Some(since_token) => format!("/_matrix/client/v3/sync?since={since_token}"),
311 None => "/_matrix/client/v3/sync".to_owned(),
312 };
313
314 let payload = self
315 .http
316 .send_json(Method::GET, &endpoint, Some(&self.token().await), None)
317 .await?;
318
319 let preprocessors = self.preprocessors.read().await.clone();
320 let filters = self.filters.read().await.clone();
321
322 let mut events = Vec::new();
323 let Some(joined_rooms) = payload
324 .get("rooms")
325 .and_then(|v| v.get("join"))
326 .and_then(Value::as_object)
327 else {
328 let next_batch = payload
329 .get("next_batch")
330 .and_then(Value::as_str)
331 .map(ToOwned::to_owned);
332 return Ok((events, next_batch));
333 };
334
335 for (room_id, room) in joined_rooms {
336 let Some(timeline) = room
337 .get("timeline")
338 .and_then(|t| t.get("events"))
339 .and_then(Value::as_array)
340 else {
341 continue;
342 };
343
344 for raw in timeline {
345 let mut mutable = raw.clone();
346 for preprocessor in &preprocessors {
347 preprocessor.process(&mut mutable).await?;
348 }
349 let room_event = converter::parse_room_event(&mutable, room_id.clone())?;
350 if filters
351 .iter()
352 .all(|filter| filter.matches(&room_event.event))
353 {
354 events.push(room_event);
355 }
356 }
357 }
358
359 let next_batch = payload
360 .get("next_batch")
361 .and_then(Value::as_str)
362 .map(ToOwned::to_owned);
363
364 Ok((events, next_batch))
365 }
366}
367
368impl MatrixClient {
369 pub async fn get_user_id(&self) -> anyhow::Result<String> {
370 let account = self.whoami().await?;
371 Ok(account.user_id)
372 }
373
374 pub async fn profile(&self, user_id: &str) -> anyhow::Result<MatrixProfile> {
375 let user_id_encoded = encode_path_component(user_id);
376 let response = self
377 .http
378 .send_json(
379 Method::GET,
380 &format!("/_matrix/client/v3/profile/{user_id_encoded}"),
381 Some(&self.token().await),
382 None,
383 )
384 .await?;
385
386 Ok(MatrixProfile {
387 user_id: user_id.to_owned(),
388 displayname: response
389 .get("displayname")
390 .and_then(Value::as_str)
391 .map(ToOwned::to_owned),
392 avatar_url: response
393 .get("avatar_url")
394 .and_then(Value::as_str)
395 .map(ToOwned::to_owned),
396 })
397 }
398
399 pub async fn set_display_name(&self, display_name: &str) -> anyhow::Result<()> {
400 let user_id = self.get_user_id().await?;
401 let user_id_encoded = encode_path_component(&user_id);
402 self.http
403 .send_json(
404 Method::PUT,
405 &format!("/_matrix/client/v3/profile/{user_id_encoded}/displayname"),
406 Some(&self.token().await),
407 Some(&json!({ "displayname": display_name })),
408 )
409 .await?;
410 Ok(())
411 }
412
413 pub async fn set_avatar_url(&self, avatar_url: &str) -> anyhow::Result<()> {
414 let user_id = self.get_user_id().await?;
415 let user_id_encoded = encode_path_component(&user_id);
416 self.http
417 .send_json(
418 Method::PUT,
419 &format!("/_matrix/client/v3/profile/{user_id_encoded}/avatar_url"),
420 Some(&self.token().await),
421 Some(&json!({ "avatar_url": avatar_url })),
422 )
423 .await?;
424 Ok(())
425 }
426
427 pub async fn get_presence_status(&self, user_id: &str) -> anyhow::Result<Presence> {
428 let user_id_encoded = encode_path_component(user_id);
429 let response = self
430 .http
431 .send_json(
432 Method::GET,
433 &format!("/_matrix/client/v3/presence/{user_id_encoded}/status"),
434 Some(&self.token().await),
435 None,
436 )
437 .await?;
438
439 let presence_str = response
440 .get("presence")
441 .and_then(Value::as_str)
442 .unwrap_or("offline");
443 match presence_str {
444 "online" => Ok(Presence::Online),
445 "unavailable" => Ok(Presence::Unavailable),
446 _ => Ok(Presence::Offline),
447 }
448 }
449
450 pub async fn get_presence_status_for(&self, user_id: &str) -> anyhow::Result<Presence> {
451 self.get_presence_status(user_id).await
452 }
453
454 pub async fn set_presence_status(
455 &self,
456 status: Presence,
457 status_msg: Option<&str>,
458 ) -> anyhow::Result<()> {
459 let user_id = self.get_user_id().await?;
460 let user_id_encoded = encode_path_component(&user_id);
461 let mut body = json!({ "presence": match status {
462 Presence::Online => "online",
463 Presence::Unavailable => "unavailable",
464 Presence::Offline => "offline",
465 }});
466 if let Some(msg) = status_msg {
467 body["status_msg"] = json!(msg);
468 }
469 self.http
470 .send_json(
471 Method::PUT,
472 &format!("/_matrix/client/v3/presence/{user_id_encoded}/status"),
473 Some(&self.token().await),
474 Some(&body),
475 )
476 .await?;
477 Ok(())
478 }
479
480 pub async fn whoami(&self) -> anyhow::Result<Account> {
481 let response = self
482 .http
483 .send_json(
484 Method::GET,
485 "/_matrix/client/v3/account/whoami",
486 Some(&self.token().await),
487 None,
488 )
489 .await?;
490
491 Ok(Account {
492 user_id: response
493 .get("user_id")
494 .and_then(Value::as_str)
495 .unwrap_or_default()
496 .to_owned(),
497 device_id: response
498 .get("device_id")
499 .and_then(Value::as_str)
500 .map(ToOwned::to_owned),
501 is_guest: response
502 .get("is_guest")
503 .and_then(Value::as_bool)
504 .unwrap_or(false),
505 })
506 }
507
508 pub async fn get_who_am_i(&self) -> anyhow::Result<Account> {
509 self.whoami().await
510 }
511
512 pub async fn get_user_profile(&self, user_id: &str) -> anyhow::Result<MatrixProfile> {
513 self.profile(user_id).await
514 }
515}
516
517impl MatrixClient {
518 pub async fn get_joined_rooms(&self) -> anyhow::Result<Vec<String>> {
519 let response = self
520 .http
521 .send_json(
522 Method::GET,
523 "/_matrix/client/v3/joined_rooms",
524 Some(&self.token().await),
525 None,
526 )
527 .await?;
528
529 let rooms = response
530 .get("joined_rooms")
531 .and_then(Value::as_array)
532 .map(|arr| {
533 arr.iter()
534 .filter_map(|v| v.as_str().map(ToOwned::to_owned))
535 .collect()
536 })
537 .unwrap_or_default();
538
539 Ok(rooms)
540 }
541
542 pub async fn get_joined_room_members(&self, room_id: &str) -> anyhow::Result<Vec<String>> {
543 let room_id = encode_path_component(room_id);
544 let response = self
545 .http
546 .send_json(
547 Method::GET,
548 &format!("/_matrix/client/v3/rooms/{room_id}/joined_members"),
549 Some(&self.token().await),
550 None,
551 )
552 .await?;
553
554 let members = response
555 .get("joined")
556 .and_then(Value::as_object)
557 .map(|obj| obj.keys().cloned().collect())
558 .unwrap_or_default();
559
560 Ok(members)
561 }
562
563 pub async fn get_joined_room_members_with_profiles(
564 &self,
565 room_id: &str,
566 ) -> anyhow::Result<BTreeMap<String, JoinedMemberProfile>> {
567 let room_id = encode_path_component(room_id);
568 let response = self
569 .http
570 .send_json(
571 Method::GET,
572 &format!("/_matrix/client/v3/rooms/{room_id}/joined_members"),
573 Some(&self.token().await),
574 None,
575 )
576 .await?;
577
578 let profiles = response
579 .get("joined")
580 .and_then(Value::as_object)
581 .map(|obj| {
582 obj.iter()
583 .map(|(user_id, profile)| {
584 let profile = JoinedMemberProfile {
585 display_name: profile
586 .get("display_name")
587 .and_then(Value::as_str)
588 .map(ToOwned::to_owned),
589 avatar_url: profile
590 .get("avatar_url")
591 .and_then(Value::as_str)
592 .map(ToOwned::to_owned),
593 };
594 (user_id.clone(), profile)
595 })
596 .collect()
597 })
598 .unwrap_or_default();
599
600 Ok(profiles)
601 }
602
603 pub async fn get_room_members(
604 &self,
605 room_id: &str,
606 membership: Option<Membership>,
607 not_membership: Option<Membership>,
608 ) -> anyhow::Result<Vec<MembershipEvent>> {
609 self.get_room_members_at(room_id, membership, not_membership, None)
610 .await
611 }
612
613 pub async fn get_all_room_members(
614 &self,
615 room_id: &str,
616 at_token: Option<&str>,
617 ) -> anyhow::Result<Vec<MembershipEvent>> {
618 self.get_room_members_at(room_id, None, None, at_token)
619 .await
620 }
621
622 pub async fn get_room_members_by_membership(
623 &self,
624 room_id: &str,
625 membership: Membership,
626 at_token: Option<&str>,
627 ) -> anyhow::Result<Vec<MembershipEvent>> {
628 self.get_room_members_at(room_id, Some(membership), None, at_token)
629 .await
630 }
631
632 pub async fn get_room_members_without_membership(
633 &self,
634 room_id: &str,
635 not_membership: Membership,
636 at_token: Option<&str>,
637 ) -> anyhow::Result<Vec<MembershipEvent>> {
638 self.get_room_members_at(room_id, None, Some(not_membership), at_token)
639 .await
640 }
641
642 pub async fn get_room_members_at(
643 &self,
644 room_id: &str,
645 membership: Option<Membership>,
646 not_membership: Option<Membership>,
647 at_token: Option<&str>,
648 ) -> anyhow::Result<Vec<MembershipEvent>> {
649 let room_id_enc = encode_path_component(room_id);
650 let mut query = Vec::new();
651 if let Some(m) = membership {
652 query.push(format!("membership={}", membership_to_query_value(m)));
653 }
654 if let Some(m) = not_membership {
655 query.push(format!("not_membership={}", membership_to_query_value(m)));
656 }
657 if let Some(token) = at_token {
658 query.push(format!("at={}", encode_path_component(token)));
659 }
660 let endpoint = if query.is_empty() {
661 format!("/_matrix/client/v3/rooms/{room_id_enc}/members")
662 } else {
663 format!(
664 "/_matrix/client/v3/rooms/{room_id_enc}/members?{}",
665 query.join("&")
666 )
667 };
668
669 let response = self
670 .http
671 .send_json(Method::GET, &endpoint, Some(&self.token().await), None)
672 .await?;
673
674 let members = response
675 .get("chunk")
676 .and_then(Value::as_array)
677 .map(|arr| {
678 arr.iter()
679 .filter_map(|v| {
680 let user_id = v.get("state_key").and_then(Value::as_str).or_else(|| {
681 v.get("content")
682 .and_then(|c| c.get("user_id"))
683 .and_then(Value::as_str)
684 })?;
685 let membership_str = v
686 .get("content")?
687 .get("membership")?
688 .as_str()
689 .unwrap_or("leave");
690 let membership = match membership_str {
691 "invite" => Membership::Invite,
692 "join" => Membership::Join,
693 "knock" => Membership::Knock,
694 "ban" => Membership::Ban,
695 _ => Membership::Leave,
696 };
697 Some(MembershipEvent {
698 user_id: user_id.to_owned(),
699 membership,
700 })
701 })
702 .collect()
703 })
704 .unwrap_or_default();
705
706 Ok(members)
707 }
708
709 pub async fn get_room_state(&self, room_id: &str) -> anyhow::Result<Vec<Value>> {
710 let room_id = encode_path_component(room_id);
711 let response = self
712 .http
713 .send_json(
714 Method::GET,
715 &format!("/_matrix/client/v3/rooms/{room_id}/state"),
716 Some(&self.token().await),
717 None,
718 )
719 .await?;
720
721 response
722 .as_array()
723 .cloned()
724 .map(Ok)
725 .unwrap_or_else(|| Ok(Vec::new()))
726 }
727
728 pub async fn get_room_state_events(
729 &self,
730 room_id: &str,
731 event_type: &str,
732 state_key: &str,
733 ) -> anyhow::Result<Value> {
734 self.get_room_state_event_content(room_id, event_type, state_key)
735 .await
736 }
737
738 pub async fn get_room_create_event(&self, room_id: &str) -> anyhow::Result<Value> {
739 self.get_room_state_event_body(room_id, "m.room.create", "")
740 .await
741 }
742
743 pub async fn user_has_power_level_for(
744 &self,
745 user_id: &str,
746 room_id: &str,
747 event_type: &str,
748 is_state: bool,
749 ) -> anyhow::Result<bool> {
750 let power_levels_value = self
751 .get_room_state_event_content(room_id, "m.room.power_levels", "")
752 .await?;
753 let power_levels: crate::models::PowerLevels = serde_json::from_value(power_levels_value)
754 .unwrap_or_else(|_| crate::models::PowerLevels::default());
755 let manager = crate::models::PLManager::new(power_levels);
756
757 let mut required_power = if is_state {
758 manager.power_levels.state_default
759 } else {
760 manager.power_levels.events_default
761 };
762 if let Some(required_event_level) = manager.power_levels.events.get(event_type) {
763 required_power = *required_event_level;
764 }
765
766 Ok(manager.user_level(user_id) >= required_power)
767 }
768
769 pub async fn user_has_power_level_for_action(
770 &self,
771 user_id: &str,
772 room_id: &str,
773 action: PowerLevelAction,
774 ) -> anyhow::Result<bool> {
775 let power_levels_value = self
776 .get_room_state_event_content(room_id, "m.room.power_levels", "")
777 .await?;
778 let power_levels: crate::models::PowerLevels = serde_json::from_value(power_levels_value)
779 .unwrap_or_else(|_| crate::models::PowerLevels::default());
780 let manager = crate::models::PLManager::new(power_levels);
781 let required_power = match action {
782 PowerLevelAction::Ban => manager.power_levels.ban,
783 PowerLevelAction::Invite => manager.power_levels.invite,
784 PowerLevelAction::Kick => manager.power_levels.kick,
785 PowerLevelAction::RedactEvents => manager.power_levels.redact,
786 PowerLevelAction::NotifyRoom => manager.power_levels.notify_whole_room_level(),
787 };
788 Ok(manager.user_level(user_id) >= required_power)
789 }
790
791 pub async fn calculate_power_level_change_bounds_on(
792 &self,
793 target_user_id: &str,
794 room_id: &str,
795 ) -> anyhow::Result<PowerLevelBounds> {
796 let my_user_id = self.get_user_id().await?;
797 let can_change = self
798 .user_has_power_level_for(&my_user_id, room_id, "m.room.power_levels", true)
799 .await?;
800 if !can_change {
801 return Ok(PowerLevelBounds {
802 can_modify: false,
803 maximum_possible_level: 0,
804 });
805 }
806
807 let power_levels_value = self
808 .get_room_state_event_content(room_id, "m.room.power_levels", "")
809 .await?;
810 let power_levels: crate::models::PowerLevels = serde_json::from_value(power_levels_value)
811 .unwrap_or_else(|_| crate::models::PowerLevels::default());
812 let manager = crate::models::PLManager::new(power_levels);
813
814 let target_user_power = manager.user_level(target_user_id);
815 let my_user_power = manager.user_level(&my_user_id);
816
817 if my_user_id == target_user_id {
818 return Ok(PowerLevelBounds {
819 can_modify: true,
820 maximum_possible_level: my_user_power,
821 });
822 }
823 if target_user_power >= my_user_power {
824 return Ok(PowerLevelBounds {
825 can_modify: false,
826 maximum_possible_level: my_user_power,
827 });
828 }
829 Ok(PowerLevelBounds {
830 can_modify: true,
831 maximum_possible_level: my_user_power,
832 })
833 }
834
835 pub async fn set_user_power_level(
836 &self,
837 user_id: &str,
838 room_id: &str,
839 new_level: i64,
840 ) -> anyhow::Result<()> {
841 let mut current_levels = self
842 .get_room_state_event_content(room_id, "m.room.power_levels", "")
843 .await
844 .unwrap_or_else(|_| json!({}));
845 if !current_levels.is_object() {
846 current_levels = json!({});
847 }
848 let Some(obj) = current_levels.as_object_mut() else {
849 anyhow::bail!("power levels state is not a JSON object");
850 };
851 let users = obj
852 .entry("users")
853 .or_insert_with(|| Value::Object(serde_json::Map::new()));
854 let Some(users_obj) = users.as_object_mut() else {
855 anyhow::bail!("power levels users is not a JSON object");
856 };
857 users_obj.insert(user_id.to_owned(), Value::from(new_level));
858
859 self.send_state_event(room_id, "m.room.power_levels", "", ¤t_levels)
860 .await?;
861 Ok(())
862 }
863
864 pub async fn get_room_state_event(
865 &self,
866 room_id: &str,
867 event_type: &str,
868 state_key: &str,
869 ) -> anyhow::Result<Value> {
870 let room_id = encode_path_component(room_id);
871 let event_type = encode_path_component(event_type);
872 let state_key = encode_path_component(state_key);
873 self.http
874 .send_json(
875 Method::GET,
876 &format!("/_matrix/client/v3/rooms/{room_id}/state/{event_type}/{state_key}"),
877 Some(&self.token().await),
878 None,
879 )
880 .await
881 }
882
883 pub async fn get_room_state_event_content(
884 &self,
885 room_id: &str,
886 event_type: &str,
887 state_key: &str,
888 ) -> anyhow::Result<Value> {
889 self.get_room_state_event(room_id, event_type, state_key)
890 .await
891 }
892
893 pub async fn get_room_state_event_body(
894 &self,
895 room_id: &str,
896 event_type: &str,
897 state_key: &str,
898 ) -> anyhow::Result<Value> {
899 if self
900 .does_server_support_version("v1.16")
901 .await
902 .unwrap_or(false)
903 {
904 let mut query = BTreeMap::new();
905 query.insert("format".to_owned(), "event".to_owned());
906 let room_id = encode_path_component(room_id);
907 let event_type = encode_path_component(event_type);
908 let state_key = encode_path_component(state_key);
909 return self
910 .do_request(
911 Method::GET,
912 &format!("/_matrix/client/v3/rooms/{room_id}/state/{event_type}/{state_key}"),
913 Some(&query),
914 None,
915 )
916 .await;
917 }
918
919 let state = self.get_room_state(room_id).await?;
920 state
921 .into_iter()
922 .find(|event| {
923 event
924 .get("type")
925 .and_then(Value::as_str)
926 .is_some_and(|kind| kind == event_type)
927 && event
928 .get("state_key")
929 .and_then(Value::as_str)
930 .unwrap_or_default()
931 == state_key
932 })
933 .ok_or_else(|| anyhow::anyhow!("state event not found"))
934 }
935
936 pub async fn send_state_event(
937 &self,
938 room_id: &str,
939 event_type: &str,
940 state_key: &str,
941 content: &Value,
942 ) -> anyhow::Result<String> {
943 let room_id = encode_path_component(room_id);
944 let event_type = encode_path_component(event_type);
945 let state_key = encode_path_component(state_key);
946 let response = self
947 .http
948 .send_json(
949 Method::PUT,
950 &format!("/_matrix/client/v3/rooms/{room_id}/state/{event_type}/{state_key}"),
951 Some(&self.token().await),
952 Some(content),
953 )
954 .await?;
955 response
956 .get("event_id")
957 .and_then(Value::as_str)
958 .map(ToOwned::to_owned)
959 .context("missing event_id in response")
960 }
961}
962
963#[derive(Debug, Clone, Deserialize)]
964pub struct RoomDirectoryLookupResponse {
965 pub room_id: String,
966 pub servers: Vec<String>,
967}
968
969#[derive(Debug, Clone, Default, PartialEq, Eq)]
970pub struct JoinedMemberProfile {
971 pub display_name: Option<String>,
972 pub avatar_url: Option<String>,
973}
974
975#[derive(Debug, Clone, Default, PartialEq, Eq)]
976pub struct DownloadedContent {
977 pub data: Vec<u8>,
978 pub content_type: Option<String>,
979}
980
981#[derive(Debug, Clone, Copy, PartialEq, Eq)]
982pub enum PowerLevelAction {
983 Ban,
984 Invite,
985 Kick,
986 RedactEvents,
987 NotifyRoom,
988}
989
990#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
991pub struct PowerLevelBounds {
992 pub can_modify: bool,
993 pub maximum_possible_level: i64,
994}
995
996impl MatrixClient {
997 pub async fn join_room(&self, room_id_or_alias: &str) -> anyhow::Result<String> {
998 let strategy = self.join_strategy.read().await.clone();
999 if let Some(strat) = strategy {
1000 strat.join_room(self, room_id_or_alias).await
1001 } else {
1002 let encoded = encode_path_component(room_id_or_alias);
1003 let response = self
1004 .http
1005 .send_json(
1006 Method::POST,
1007 &format!("/_matrix/client/v3/join/{encoded}"),
1008 Some(&self.token().await),
1009 Some(&json!({})),
1010 )
1011 .await?;
1012 response
1013 .get("room_id")
1014 .and_then(Value::as_str)
1015 .map(ToOwned::to_owned)
1016 .context("missing room_id in join response")
1017 }
1018 }
1019
1020 pub async fn create_space(&self, options: &CreateRoom) -> anyhow::Result<String> {
1021 let mut opt = options.clone();
1022 opt.creation_content = Some(json!({
1023 "type": "m.space"
1024 }));
1025
1026 let space_id = self.create_room(&opt).await?;
1028 Ok(space_id)
1029 }
1030
1031 pub async fn get_space(&self, room_id: &str) -> anyhow::Result<crate::models::Space> {
1032 let mut space = crate::models::Space::new(room_id);
1033
1034 let state = self.get_room_state(room_id).await?;
1035 for ev in state {
1036 if let Some(obj) = ev.as_object()
1037 && let (Some(ev_type), Some(state_key), Some(content)) = (
1038 obj.get("type").and_then(Value::as_str),
1039 obj.get("state_key").and_then(Value::as_str),
1040 obj.get("content").and_then(Value::as_object),
1041 )
1042 {
1043 if ev_type == "m.space.child" {
1044 if content.is_empty() || content.get("via").is_none() {
1045 space.remove_child(state_key);
1046 } else {
1047 let suggested = content
1048 .get("suggested")
1049 .and_then(Value::as_bool)
1050 .unwrap_or(false);
1051 space.add_child(state_key, suggested);
1052 }
1053 } else if ev_type == "m.space.parent"
1054 && !content.is_empty()
1055 && content.get("via").is_some()
1056 && !space.parents.contains(&state_key.to_string())
1057 {
1058 space.parents.push(state_key.to_string());
1059 }
1060 }
1061 }
1062
1063 Ok(space)
1064 }
1065
1066 pub async fn leave_room(&self, room_id: &str, reason: Option<&str>) -> anyhow::Result<()> {
1067 let room_id = encode_path_component(room_id);
1068 let body = reason.map(|r| json!({ "reason": r }));
1069 self.http
1070 .send_json(
1071 Method::POST,
1072 &format!("/_matrix/client/v3/rooms/{room_id}/leave"),
1073 Some(&self.token().await),
1074 body.as_ref(),
1075 )
1076 .await?;
1077 Ok(())
1078 }
1079
1080 pub async fn forget_room(&self, room_id: &str) -> anyhow::Result<()> {
1081 let room_id = encode_path_component(room_id);
1082 self.http
1083 .send_json(
1084 Method::POST,
1085 &format!("/_matrix/client/v3/rooms/{room_id}/forget"),
1086 Some(&self.token().await),
1087 None,
1088 )
1089 .await?;
1090 Ok(())
1091 }
1092
1093 pub async fn invite_user(&self, user_id: &str, room_id: &str) -> anyhow::Result<()> {
1094 let room_id = encode_path_component(room_id);
1095 self.http
1096 .send_json(
1097 Method::POST,
1098 &format!("/_matrix/client/v3/rooms/{room_id}/invite"),
1099 Some(&self.token().await),
1100 Some(&json!({ "user_id": user_id })),
1101 )
1102 .await?;
1103 Ok(())
1104 }
1105
1106 pub async fn kick_user(
1107 &self,
1108 user_id: &str,
1109 room_id: &str,
1110 reason: Option<&str>,
1111 ) -> anyhow::Result<()> {
1112 let room_id = encode_path_component(room_id);
1113 let mut body = json!({ "user_id": user_id });
1114 if let Some(r) = reason {
1115 body["reason"] = json!(r);
1116 }
1117 self.http
1118 .send_json(
1119 Method::POST,
1120 &format!("/_matrix/client/v3/rooms/{room_id}/kick"),
1121 Some(&self.token().await),
1122 Some(&body),
1123 )
1124 .await?;
1125 Ok(())
1126 }
1127
1128 pub async fn ban_user(
1129 &self,
1130 user_id: &str,
1131 room_id: &str,
1132 reason: Option<&str>,
1133 ) -> anyhow::Result<()> {
1134 let room_id = encode_path_component(room_id);
1135 let mut body = json!({ "user_id": user_id });
1136 if let Some(r) = reason {
1137 body["reason"] = json!(r);
1138 }
1139 self.http
1140 .send_json(
1141 Method::POST,
1142 &format!("/_matrix/client/v3/rooms/{room_id}/ban"),
1143 Some(&self.token().await),
1144 Some(&body),
1145 )
1146 .await?;
1147 Ok(())
1148 }
1149
1150 pub async fn unban_user(&self, user_id: &str, room_id: &str) -> anyhow::Result<()> {
1151 let room_id = encode_path_component(room_id);
1152 self.http
1153 .send_json(
1154 Method::POST,
1155 &format!("/_matrix/client/v3/rooms/{room_id}/unban"),
1156 Some(&self.token().await),
1157 Some(&json!({ "user_id": user_id })),
1158 )
1159 .await?;
1160 Ok(())
1161 }
1162
1163 pub async fn upgrade_room(&self, room_id: &str, new_version: &str) -> anyhow::Result<String> {
1164 let room_id = encode_path_component(room_id);
1165 let response = self
1166 .http
1167 .send_json(
1168 Method::POST,
1169 &format!("/_matrix/client/v3/rooms/{room_id}/upgrade"),
1170 Some(&self.token().await),
1171 Some(&json!({ "new_version": new_version })),
1172 )
1173 .await?;
1174
1175 response
1176 .get("replacement_room")
1177 .and_then(Value::as_str)
1178 .map(ToOwned::to_owned)
1179 .context("missing replacement_room in upgrade response")
1180 }
1181
1182 pub async fn get_room_upgrade_history(&self, room_id: &str) -> anyhow::Result<Vec<Value>> {
1183 let mut history = Vec::new();
1184 let mut current_room = room_id.to_string();
1185
1186 loop {
1187 let create_event = match self.get_room_create_event(¤t_room).await {
1188 Ok(ev) => ev,
1189 Err(_) => break, };
1191
1192 history.push(create_event.clone());
1193
1194 if let Some(predecessor) = create_event.get("predecessor").and_then(Value::as_object)
1195 && let Some(prev_room) = predecessor.get("room_id").and_then(Value::as_str)
1196 {
1197 current_room = prev_room.to_string();
1198 continue;
1199 }
1200 break;
1201 }
1202
1203 Ok(history)
1204 }
1205}
1206
1207impl MatrixClient {
1208 pub async fn send_message(&self, room_id: &str, body: &str) -> anyhow::Result<String> {
1209 self.send_text(room_id, body).await
1210 }
1211
1212 pub async fn send_text(&self, room_id: &str, body: &str) -> anyhow::Result<String> {
1213 let content = json!({
1214 "msgtype": "m.text",
1215 "body": body
1216 });
1217 self.send_event(room_id, "m.room.message", &content).await
1218 }
1219
1220 pub async fn send_html_text(
1221 &self,
1222 room_id: &str,
1223 body: &str,
1224 html: &str,
1225 ) -> anyhow::Result<String> {
1226 let content = json!({
1227 "msgtype": "m.text",
1228 "body": body,
1229 "format": "org.matrix.custom.html",
1230 "formatted_body": html
1231 });
1232 self.send_event(room_id, "m.room.message", &content).await
1233 }
1234
1235 pub async fn send_notice(&self, room_id: &str, body: &str) -> anyhow::Result<String> {
1236 let content = json!({
1237 "msgtype": "m.notice",
1238 "body": body
1239 });
1240 self.send_event(room_id, "m.room.message", &content).await
1241 }
1242
1243 pub async fn send_html_notice(
1244 &self,
1245 room_id: &str,
1246 body: &str,
1247 html: &str,
1248 ) -> anyhow::Result<String> {
1249 let content = json!({
1250 "msgtype": "m.notice",
1251 "body": body,
1252 "format": "org.matrix.custom.html",
1253 "formatted_body": html
1254 });
1255 self.send_event(room_id, "m.room.message", &content).await
1256 }
1257
1258 pub async fn reply_text(
1259 &self,
1260 room_id: &str,
1261 event: &Event,
1262 text: &str,
1263 ) -> anyhow::Result<String> {
1264 let mut content = json!({
1265 "msgtype": "m.text",
1266 "body": text,
1267 });
1268 let m_relates_to = json!({
1271 "m.in_reply_to": {
1272 "event_id": event.event_id
1273 }
1274 });
1275 content
1276 .as_object_mut()
1277 .unwrap()
1278 .insert("m.relates_to".to_string(), m_relates_to);
1279
1280 self.send_event(room_id, "m.room.message", &content).await
1281 }
1282
1283 pub async fn reply_html_text(
1284 &self,
1285 room_id: &str,
1286 event: &Event,
1287 html: &str,
1288 ) -> anyhow::Result<String> {
1289 let body = plain_text_from_html(html);
1290 let mut content = json!({
1291 "msgtype": "m.text",
1292 "body": body,
1293 "format": "org.matrix.custom.html",
1294 "formatted_body": html,
1295 });
1296 let m_relates_to = json!({
1297 "m.in_reply_to": {
1298 "event_id": event.event_id
1299 }
1300 });
1301 content
1302 .as_object_mut()
1303 .unwrap()
1304 .insert("m.relates_to".to_string(), m_relates_to);
1305
1306 self.send_event(room_id, "m.room.message", &content).await
1307 }
1308
1309 pub async fn reply_notice(
1310 &self,
1311 room_id: &str,
1312 event: &Event,
1313 text: &str,
1314 ) -> anyhow::Result<String> {
1315 let mut content = json!({
1316 "msgtype": "m.notice",
1317 "body": text,
1318 });
1319 let m_relates_to = json!({
1320 "m.in_reply_to": {
1321 "event_id": event.event_id
1322 }
1323 });
1324 content
1325 .as_object_mut()
1326 .unwrap()
1327 .insert("m.relates_to".to_string(), m_relates_to);
1328
1329 self.send_event(room_id, "m.room.message", &content).await
1330 }
1331
1332 pub async fn reply_html_notice(
1333 &self,
1334 room_id: &str,
1335 event: &Event,
1336 html: &str,
1337 ) -> anyhow::Result<String> {
1338 let body = plain_text_from_html(html);
1339 let mut content = json!({
1340 "msgtype": "m.notice",
1341 "body": body,
1342 "format": "org.matrix.custom.html",
1343 "formatted_body": html,
1344 });
1345 let m_relates_to = json!({
1346 "m.in_reply_to": {
1347 "event_id": event.event_id
1348 }
1349 });
1350 content
1351 .as_object_mut()
1352 .unwrap()
1353 .insert("m.relates_to".to_string(), m_relates_to);
1354
1355 self.send_event(room_id, "m.room.message", &content).await
1356 }
1357
1358 pub async fn send_event(
1359 &self,
1360 room_id: &str,
1361 event_type: &str,
1362 content: &Value,
1363 ) -> anyhow::Result<String> {
1364 let room_id = encode_path_component(room_id);
1365 let event_type = encode_path_component(event_type);
1366 let txn_id = Uuid::new_v4().to_string();
1367 let response = self
1368 .http
1369 .send_json(
1370 Method::PUT,
1371 &format!("/_matrix/client/v3/rooms/{room_id}/send/{event_type}/{txn_id}"),
1372 Some(&self.token().await),
1373 Some(content),
1374 )
1375 .await?;
1376 response
1377 .get("event_id")
1378 .and_then(Value::as_str)
1379 .map(ToOwned::to_owned)
1380 .context("missing event_id in send response")
1381 }
1382
1383 pub async fn send_raw_event(
1384 &self,
1385 room_id: &str,
1386 event_type: &str,
1387 content: &Value,
1388 txn_id: Option<&str>,
1389 ) -> anyhow::Result<String> {
1390 let room_id = encode_path_component(room_id);
1391 let event_type = encode_path_component(event_type);
1392 let txn_id = txn_id
1393 .map(ToOwned::to_owned)
1394 .unwrap_or_else(|| Uuid::new_v4().to_string());
1395 let response = self
1396 .http
1397 .send_json(
1398 Method::PUT,
1399 &format!("/_matrix/client/v3/rooms/{room_id}/send/{event_type}/{txn_id}"),
1400 Some(&self.token().await),
1401 Some(content),
1402 )
1403 .await?;
1404 response
1405 .get("event_id")
1406 .and_then(Value::as_str)
1407 .map(ToOwned::to_owned)
1408 .context("missing event_id in send response")
1409 }
1410
1411 pub async fn redact_event(
1412 &self,
1413 room_id: &str,
1414 event_id: &str,
1415 reason: Option<&str>,
1416 ) -> anyhow::Result<String> {
1417 let room_id = encode_path_component(room_id);
1418 let event_id = encode_path_component(event_id);
1419 let txn_id = Uuid::new_v4().to_string();
1420 let body = reason.map(|r| json!({ "reason": r }));
1421 let response = self
1422 .http
1423 .send_json(
1424 Method::PUT,
1425 &format!("/_matrix/client/v3/rooms/{room_id}/redact/{event_id}/{txn_id}"),
1426 Some(&self.token().await),
1427 body.as_ref(),
1428 )
1429 .await?;
1430 response
1431 .get("event_id")
1432 .and_then(Value::as_str)
1433 .map(ToOwned::to_owned)
1434 .context("missing event_id in redact response")
1435 }
1436
1437 pub async fn send_read_receipt(&self, room_id: &str, event_id: &str) -> anyhow::Result<()> {
1438 let room_id = encode_path_component(room_id);
1439 let event_id = encode_path_component(event_id);
1440 self.http
1441 .send_json(
1442 Method::POST,
1443 &format!("/_matrix/client/v3/rooms/{room_id}/receipt/m.read/{event_id}"),
1444 Some(&self.token().await),
1445 None,
1446 )
1447 .await?;
1448 Ok(())
1449 }
1450
1451 pub async fn set_typing(
1452 &self,
1453 room_id: &str,
1454 user_id: &str,
1455 typing: bool,
1456 timeout: Option<u64>,
1457 ) -> anyhow::Result<()> {
1458 let room_id = encode_path_component(room_id);
1459 let user_id = encode_path_component(user_id);
1460 let mut body = json!({ "typing": typing });
1461 if let Some(t) = timeout {
1462 body["timeout"] = json!(t);
1463 }
1464 self.http
1465 .send_json(
1466 Method::PUT,
1467 &format!("/_matrix/client/v3/rooms/{room_id}/typing/{user_id}"),
1468 Some(&self.token().await),
1469 Some(&body),
1470 )
1471 .await?;
1472 Ok(())
1473 }
1474
1475 pub async fn get_event(&self, room_id: &str, event_id: &str) -> anyhow::Result<Value> {
1476 let room_id = encode_path_component(room_id);
1477 let event_id = encode_path_component(event_id);
1478 self.http
1479 .send_json(
1480 Method::GET,
1481 &format!("/_matrix/client/v3/rooms/{room_id}/event/{event_id}"),
1482 Some(&self.token().await),
1483 None,
1484 )
1485 .await
1486 }
1487}
1488
1489impl MatrixClient {
1490 pub async fn create_room(&self, options: &CreateRoom) -> anyhow::Result<String> {
1491 let payload = serde_json::to_value(options)?;
1492 let response = self
1493 .http
1494 .send_json(
1495 Method::POST,
1496 "/_matrix/client/v3/createRoom",
1497 Some(&self.token().await),
1498 Some(&payload),
1499 )
1500 .await?;
1501 response
1502 .get("room_id")
1503 .and_then(Value::as_str)
1504 .map(ToOwned::to_owned)
1505 .context("missing room_id in createRoom response")
1506 }
1507
1508 pub async fn create_room_alias(&self, alias: &str, room_id: &str) -> anyhow::Result<()> {
1509 let alias = encode_path_component(alias);
1510 self.http
1511 .send_json(
1512 Method::PUT,
1513 &format!("/_matrix/client/v3/directory/room/{alias}"),
1514 Some(&self.token().await),
1515 Some(&json!({ "room_id": room_id })),
1516 )
1517 .await?;
1518 Ok(())
1519 }
1520
1521 pub async fn delete_room_alias(&self, alias: &str) -> anyhow::Result<()> {
1522 let alias = encode_path_component(alias);
1523 self.http
1524 .send_json(
1525 Method::DELETE,
1526 &format!("/_matrix/client/v3/directory/room/{alias}"),
1527 Some(&self.token().await),
1528 None,
1529 )
1530 .await?;
1531 Ok(())
1532 }
1533
1534 pub async fn lookup_room_alias(
1535 &self,
1536 alias: &str,
1537 ) -> anyhow::Result<RoomDirectoryLookupResponse> {
1538 let alias = encode_path_component(alias);
1539 let response = self
1540 .http
1541 .send_json(
1542 Method::GET,
1543 &format!("/_matrix/client/v3/directory/room/{alias}"),
1544 Some(&self.token().await),
1545 None,
1546 )
1547 .await?;
1548 serde_json::from_value(response).context("invalid room directory lookup response")
1549 }
1550
1551 pub async fn set_directory_visibility(
1552 &self,
1553 room_id: &str,
1554 visibility: &str,
1555 ) -> anyhow::Result<()> {
1556 let room_id = encode_path_component(room_id);
1557 self.http
1558 .send_json(
1559 Method::PUT,
1560 &format!("/_matrix/client/v3/directory/list/room/{room_id}"),
1561 Some(&self.token().await),
1562 Some(&json!({ "visibility": visibility })),
1563 )
1564 .await?;
1565 Ok(())
1566 }
1567
1568 pub async fn get_directory_visibility(&self, room_id: &str) -> anyhow::Result<String> {
1569 let room_id = encode_path_component(room_id);
1570 let response = self
1571 .http
1572 .send_json(
1573 Method::GET,
1574 &format!("/_matrix/client/v3/directory/list/room/{room_id}"),
1575 Some(&self.token().await),
1576 None,
1577 )
1578 .await?;
1579 response
1580 .get("visibility")
1581 .and_then(Value::as_str)
1582 .map(ToOwned::to_owned)
1583 .context("missing visibility in response")
1584 }
1585
1586 pub async fn resolve_room(&self, room_id_or_alias: &str) -> anyhow::Result<String> {
1587 if room_id_or_alias.starts_with('!') {
1588 Ok(room_id_or_alias.to_owned())
1589 } else if room_id_or_alias.starts_with('#') {
1590 let res = self.lookup_room_alias(room_id_or_alias).await?;
1591 Ok(res.room_id)
1592 } else {
1593 anyhow::bail!("Invalid room ID or alias: {}", room_id_or_alias)
1594 }
1595 }
1596
1597 pub async fn get_published_alias(
1598 &self,
1599 room_id_or_alias: &str,
1600 ) -> anyhow::Result<Option<String>> {
1601 let room_id = match self.resolve_room(room_id_or_alias).await {
1602 Ok(room_id) => room_id,
1603 Err(_) => return Ok(None),
1604 };
1605 let event = match self
1606 .get_room_state_event_content(&room_id, "m.room.canonical_alias", "")
1607 .await
1608 {
1609 Ok(event) => event,
1610 Err(_) => return Ok(None),
1611 };
1612 let canonical = event
1613 .get("alias")
1614 .and_then(Value::as_str)
1615 .map(ToOwned::to_owned);
1616 if canonical.is_some() {
1617 return Ok(canonical);
1618 }
1619 Ok(event
1620 .get("alt_aliases")
1621 .and_then(Value::as_array)
1622 .and_then(|aliases| {
1623 aliases
1624 .iter()
1625 .find_map(|a| a.as_str().map(ToOwned::to_owned))
1626 }))
1627 }
1628}
1629
1630impl MatrixClient {
1631 pub async fn set_account_data(&self, event_type: &str, content: &Value) -> anyhow::Result<()> {
1632 let user_id = self.get_user_id().await?;
1633 let user_id = encode_path_component(&user_id);
1634 let event_type = encode_path_component(event_type);
1635 self.http
1636 .send_json(
1637 Method::PUT,
1638 &format!("/_matrix/client/v3/user/{user_id}/account_data/{event_type}"),
1639 Some(&self.token().await),
1640 Some(content),
1641 )
1642 .await?;
1643 Ok(())
1644 }
1645
1646 pub async fn get_account_data(&self, event_type: &str) -> anyhow::Result<Value> {
1647 let user_id = self.get_user_id().await?;
1648 let user_id = encode_path_component(&user_id);
1649 let event_type = encode_path_component(event_type);
1650 self.http
1651 .send_json(
1652 Method::GET,
1653 &format!("/_matrix/client/v3/user/{user_id}/account_data/{event_type}"),
1654 Some(&self.token().await),
1655 None,
1656 )
1657 .await
1658 }
1659
1660 pub async fn set_room_account_data(
1661 &self,
1662 room_id: &str,
1663 event_type: &str,
1664 content: &Value,
1665 ) -> anyhow::Result<()> {
1666 let user_id = self.get_user_id().await?;
1667 let user_id = encode_path_component(&user_id);
1668 let room_id = encode_path_component(room_id);
1669 let event_type = encode_path_component(event_type);
1670 self.http
1671 .send_json(
1672 Method::PUT,
1673 &format!(
1674 "/_matrix/client/v3/user/{user_id}/rooms/{room_id}/account_data/{event_type}"
1675 ),
1676 Some(&self.token().await),
1677 Some(content),
1678 )
1679 .await?;
1680 Ok(())
1681 }
1682
1683 pub async fn get_room_account_data(
1684 &self,
1685 room_id: &str,
1686 event_type: &str,
1687 ) -> anyhow::Result<Value> {
1688 let user_id = self.get_user_id().await?;
1689 let user_id = encode_path_component(&user_id);
1690 let room_id = encode_path_component(room_id);
1691 let event_type = encode_path_component(event_type);
1692 self.http
1693 .send_json(
1694 Method::GET,
1695 &format!(
1696 "/_matrix/client/v3/user/{user_id}/rooms/{room_id}/account_data/{event_type}"
1697 ),
1698 Some(&self.token().await),
1699 None,
1700 )
1701 .await
1702 }
1703
1704 pub async fn get_openid_token(&self) -> anyhow::Result<Value> {
1705 let user_id = self.get_user_id().await?;
1706 let user_id = encode_path_component(&user_id);
1707 self.http
1708 .send_json(
1709 Method::POST,
1710 &format!("/_matrix/client/v3/user/{user_id}/openid/request_token"),
1711 Some(&self.token().await),
1712 Some(&json!({})),
1713 )
1714 .await
1715 }
1716
1717 pub async fn get_openid_connect_token(&self) -> anyhow::Result<Value> {
1718 self.get_openid_token().await
1719 }
1720}
1721
1722impl MatrixClient {
1723 pub async fn get_own_devices(&self) -> anyhow::Result<Vec<Value>> {
1724 let response = self
1725 .http
1726 .send_json(
1727 Method::GET,
1728 "/_matrix/client/v3/devices",
1729 Some(&self.token().await),
1730 None,
1731 )
1732 .await?;
1733 response
1734 .get("devices")
1735 .and_then(Value::as_array)
1736 .cloned()
1737 .ok_or_else(|| anyhow::anyhow!("missing devices in response"))
1738 }
1739
1740 pub async fn get_user_devices(&self, user_ids: &[&str]) -> anyhow::Result<Value> {
1741 let user_ids_str = user_ids.join(",");
1742 self.http
1743 .send_json(
1744 Method::POST,
1745 &format!("/_matrix/client/v3/keys/query?user_ids={}", user_ids_str),
1746 Some(&self.token().await),
1747 Some(&json!({})),
1748 )
1749 .await
1750 }
1751
1752 pub async fn upload_device_one_time_keys(&self, keys: &Value) -> anyhow::Result<Value> {
1753 self.http
1754 .send_json(
1755 Method::POST,
1756 "/_matrix/client/v3/keys/upload",
1757 Some(&self.token().await),
1758 Some(&json!({ "one_time_keys": keys })),
1759 )
1760 .await
1761 }
1762
1763 pub async fn claim_one_time_keys(
1764 &self,
1765 devices: &Value,
1766 key_algorithm: Option<&str>,
1767 ) -> anyhow::Result<Value> {
1768 let mut req_body = json!({
1769 "one_time_keys": devices
1770 });
1771 if let Some(algo) = key_algorithm {
1772 req_body["key_algorithm"] = json!(algo);
1773 }
1774 self.http
1775 .send_json(
1776 Method::POST,
1777 "/_matrix/client/v3/keys/claim",
1778 Some(&self.token().await),
1779 Some(&req_body),
1780 )
1781 .await
1782 }
1783
1784 pub async fn check_one_time_key_counts(&self) -> anyhow::Result<Value> {
1785 let response = self
1786 .http
1787 .send_json(
1788 Method::GET,
1789 "/_matrix/client/v3/sync?timeout=0",
1790 Some(&self.token().await),
1791 None,
1792 )
1793 .await?;
1794
1795 response
1796 .get("device_one_time_keys_count")
1797 .cloned()
1798 .ok_or_else(|| anyhow::anyhow!("missing device_one_time_keys_count in sync response"))
1799 }
1800
1801 pub async fn upload_fallback_key(&self, key: &Value) -> anyhow::Result<Value> {
1802 self.http
1803 .send_json(
1804 Method::POST,
1805 "/_matrix/client/v3/keys/upload",
1806 Some(&self.token().await),
1807 Some(&json!({ "fallback_keys": key })),
1808 )
1809 .await
1810 }
1811
1812 pub async fn send_to_devices(&self, event_type: &str, messages: &Value) -> anyhow::Result<()> {
1813 let txn_id = Uuid::new_v4().to_string();
1814 self.http
1815 .send_json(
1816 Method::PUT,
1817 &format!("/_matrix/client/v3/sendToDevice/{event_type}/{txn_id}"),
1818 Some(&self.token().await),
1819 Some(messages),
1820 )
1821 .await?;
1822 Ok(())
1823 }
1824
1825 pub async fn get_key_backup_version(&self) -> anyhow::Result<Option<Value>> {
1826 let response = self
1827 .http
1828 .send_json(
1829 Method::GET,
1830 "/_matrix/client/v3/room_keys/version",
1831 Some(&self.token().await),
1832 None,
1833 )
1834 .await?;
1835 if response
1836 .get("errcode")
1837 .and_then(Value::as_str)
1838 .is_some_and(|code| code == "M_NOT_FOUND")
1839 {
1840 return Ok(None);
1841 }
1842 Ok(Some(response))
1843 }
1844
1845 pub async fn sign_and_create_key_backup_version(&self, info: &Value) -> anyhow::Result<Value> {
1846 self.http
1847 .send_json(
1848 Method::POST,
1849 "/_matrix/client/v3/room_keys/version",
1850 Some(&self.token().await),
1851 Some(info),
1852 )
1853 .await
1854 }
1855
1856 pub async fn update_key_backup_version(
1857 &self,
1858 version: &str,
1859 info: &Value,
1860 ) -> anyhow::Result<()> {
1861 let version = encode_path_component(version);
1862 self.http
1863 .send_json(
1864 Method::PUT,
1865 &format!("/_matrix/client/v3/room_keys/version/{version}"),
1866 Some(&self.token().await),
1867 Some(info),
1868 )
1869 .await?;
1870 Ok(())
1871 }
1872
1873 pub async fn enable_key_backup(&self, _info: &Value) -> anyhow::Result<()> {
1874 anyhow::bail!("key backup enable requires full crypto integration")
1875 }
1876
1877 pub async fn disable_key_backup(&self) -> anyhow::Result<()> {
1878 Ok(())
1879 }
1880
1881 pub async fn export_room_keys_for_session(
1882 &self,
1883 _room_id: &str,
1884 _session_id: &str,
1885 ) -> anyhow::Result<Vec<Value>> {
1886 anyhow::bail!("room key export requires full crypto integration")
1887 }
1888}
1889
1890impl MatrixClient {
1891 pub async fn get_server_versions(&self) -> anyhow::Result<Value> {
1892 self.http
1893 .send_json(Method::GET, "/_matrix/client/versions", None, None)
1894 .await
1895 }
1896
1897 pub async fn get_capabilities(&self) -> anyhow::Result<Value> {
1898 self.http
1899 .send_json(
1900 Method::GET,
1901 "/_matrix/client/v3/capabilities",
1902 Some(&self.token().await),
1903 None,
1904 )
1905 .await
1906 }
1907
1908 pub async fn does_server_support_version(&self, version: &str) -> anyhow::Result<bool> {
1909 let versions = self.get_server_versions().await?;
1910 let supported = versions
1911 .get("versions")
1912 .and_then(Value::as_array)
1913 .map(|arr| arr.iter().filter_map(Value::as_str).any(|v| v == version))
1914 .unwrap_or(false);
1915 Ok(supported)
1916 }
1917
1918 pub async fn does_server_support_any_one_version(
1919 &self,
1920 versions: &[&str],
1921 ) -> anyhow::Result<bool> {
1922 for version in versions {
1923 if self.does_server_support_version(version).await? {
1924 return Ok(true);
1925 }
1926 }
1927 Ok(false)
1928 }
1929
1930 pub async fn does_server_support_unstable_feature(
1931 &self,
1932 feature: &str,
1933 ) -> anyhow::Result<bool> {
1934 let versions = self.get_server_versions().await?;
1935 let supported = versions
1936 .get("unstable_features")
1937 .and_then(|m| m.get(feature))
1938 .and_then(Value::as_bool)
1939 .unwrap_or(false);
1940 Ok(supported)
1941 }
1942
1943 pub async fn get_raw_event(&self, room_id: &str, event_id: &str) -> anyhow::Result<Value> {
1944 let room_id = encode_path_component(room_id);
1945 let event_id = encode_path_component(event_id);
1946 self.http
1947 .send_json(
1948 Method::GET,
1949 &format!("/_matrix/client/v3/rooms/{room_id}/event/{event_id}"),
1950 Some(&self.token().await),
1951 None,
1952 )
1953 .await
1954 }
1955
1956 pub async fn get_event_context(&self, room_id: &str, event_id: &str) -> anyhow::Result<Value> {
1957 let room_id = encode_path_component(room_id);
1958 let event_id = encode_path_component(event_id);
1959 self.http
1960 .send_json(
1961 Method::GET,
1962 &format!("/_matrix/client/v3/rooms/{room_id}/context/{event_id}"),
1963 Some(&self.token().await),
1964 None,
1965 )
1966 .await
1967 }
1968
1969 pub async fn get_event_nearest_to_timestamp(
1970 &self,
1971 room_id: &str,
1972 ts: u64,
1973 dir: &str,
1974 ) -> anyhow::Result<Value> {
1975 let room_id = encode_path_component(room_id);
1976 let endpoint =
1977 format!("/_matrix/client/v1/rooms/{room_id}/timestamp_to_event?ts={ts}&dir={dir}");
1978 self.http
1979 .send_json(Method::GET, &endpoint, Some(&self.token().await), None)
1980 .await
1981 }
1982
1983 pub async fn get_relations_for_event(
1984 &self,
1985 room_id: &str,
1986 event_id: &str,
1987 rel_type: Option<&str>,
1988 event_type: Option<&str>,
1989 ) -> anyhow::Result<Value> {
1990 let room_id_enc = encode_path_component(room_id);
1991 let event_id_enc = encode_path_component(event_id);
1992
1993 let mut endpoint =
1995 format!("/_matrix/client/v3/rooms/{room_id_enc}/relations/{event_id_enc}");
1996 if let Some(rt) = rel_type {
1997 endpoint.push('/');
1998 endpoint.push_str(&encode_path_component(rt));
1999 if let Some(et) = event_type {
2000 endpoint.push('/');
2001 endpoint.push_str(&encode_path_component(et));
2002 }
2003 }
2004
2005 self.http
2006 .send_json(Method::GET, &endpoint, Some(&self.token().await), None)
2007 .await
2008 }
2009}
2010
2011impl MatrixClient {
2012 pub async fn upload_media(
2013 &self,
2014 bytes: Vec<u8>,
2015 content_type: &str,
2016 filename: Option<&str>,
2017 ) -> anyhow::Result<MXCUrl> {
2018 let endpoint = match filename {
2019 Some(name) => format!("/_matrix/media/v3/upload?filename={name}"),
2020 None => "/_matrix/media/v3/upload".to_owned(),
2021 };
2022
2023 let url = self.homeserver().join(endpoint.trim_start_matches('/'))?;
2024 let response = reqwest::Client::new()
2025 .post(url)
2026 .bearer_auth(self.token().await)
2027 .header(reqwest::header::CONTENT_TYPE, content_type)
2028 .body(bytes)
2029 .send()
2030 .await?;
2031 let payload: Value = response.json().await?;
2032 let mxc = payload
2033 .get("content_uri")
2034 .and_then(Value::as_str)
2035 .context("missing content_uri in upload response")?;
2036 std::str::FromStr::from_str(mxc)
2037 }
2038
2039 pub async fn upload_content(
2040 &self,
2041 bytes: Vec<u8>,
2042 content_type: Option<&str>,
2043 filename: Option<&str>,
2044 ) -> anyhow::Result<String> {
2045 let mxc = self
2046 .upload_media(
2047 bytes,
2048 content_type.unwrap_or("application/octet-stream"),
2049 filename,
2050 )
2051 .await?;
2052 Ok(mxc.to_string())
2053 }
2054
2055 pub async fn upload_content_from_url(&self, url: &str) -> anyhow::Result<String> {
2056 let response = reqwest::Client::new().get(url).send().await?;
2057 let content_type = response
2058 .headers()
2059 .get(reqwest::header::CONTENT_TYPE)
2060 .and_then(|v| v.to_str().ok())
2061 .unwrap_or("application/octet-stream")
2062 .to_owned();
2063 let bytes = response.bytes().await?.to_vec();
2064 self.upload_content(bytes, Some(&content_type), None).await
2065 }
2066
2067 pub async fn download_media(&self, mxc: &MXCUrl) -> anyhow::Result<Vec<u8>> {
2068 let Some(stripped) = mxc.as_str().strip_prefix("mxc://") else {
2069 anyhow::bail!("invalid mxc uri");
2070 };
2071 let mut parts = stripped.splitn(2, '/');
2072 let server = parts.next().context("missing mxc server name")?;
2073 let media_id = parts.next().context("missing mxc media id")?;
2074
2075 let endpoint = format!(
2076 "/_matrix/media/v3/download/{}/{}",
2077 encode_path_component(server),
2078 encode_path_component(media_id)
2079 );
2080 let url = self.homeserver().join(endpoint.trim_start_matches('/'))?;
2081 let bytes = reqwest::Client::new()
2082 .get(url)
2083 .bearer_auth(self.token().await)
2084 .send()
2085 .await?
2086 .bytes()
2087 .await?;
2088 Ok(bytes.to_vec())
2089 }
2090
2091 pub async fn download_content(
2092 &self,
2093 mxc_url: &str,
2094 allow_remote: bool,
2095 ) -> anyhow::Result<DownloadedContent> {
2096 let mxc = mxc_url
2097 .parse::<MXCUrl>()
2098 .map_err(|_| anyhow::anyhow!("invalid mxc uri"))?;
2099 let stripped = mxc
2100 .as_str()
2101 .strip_prefix("mxc://")
2102 .ok_or_else(|| anyhow::anyhow!("invalid mxc uri"))?;
2103 let (server, media_id) = stripped
2104 .split_once('/')
2105 .ok_or_else(|| anyhow::anyhow!("missing mxc server or media id"))?;
2106
2107 let endpoint = format!(
2108 "/_matrix/media/v3/download/{}/{}?allow_remote={allow_remote}",
2109 encode_path_component(server),
2110 encode_path_component(media_id),
2111 );
2112 let url = self.homeserver().join(endpoint.trim_start_matches('/'))?;
2113 let response = reqwest::Client::new()
2114 .get(url)
2115 .bearer_auth(self.token().await)
2116 .send()
2117 .await?;
2118 let content_type = response
2119 .headers()
2120 .get(reqwest::header::CONTENT_TYPE)
2121 .and_then(|v| v.to_str().ok())
2122 .map(ToOwned::to_owned);
2123 let bytes = response.bytes().await?;
2124 Ok(DownloadedContent {
2125 data: bytes.to_vec(),
2126 content_type,
2127 })
2128 }
2129
2130 pub fn mxc_to_http(
2131 &self,
2132 mxc: &str,
2133 width: Option<u32>,
2134 height: Option<u32>,
2135 ) -> Option<String> {
2136 let stripped = mxc.strip_prefix("mxc://")?;
2137 let (server, media_id) = stripped.split_once('/')?;
2138
2139 let base = self.homeserver().as_str().trim_end_matches('/');
2140 if let (Some(w), Some(h)) = (width, height) {
2141 Some(format!(
2142 "{}/_matrix/media/v3/thumbnail/{}/{}/?width={}&height={}",
2143 base, server, media_id, w, h
2144 ))
2145 } else {
2146 Some(format!(
2147 "{}/_matrix/media/v3/download/{}/{}",
2148 base, server, media_id
2149 ))
2150 }
2151 }
2152
2153 pub fn mxc_to_http_thumbnail(
2154 &self,
2155 mxc: &str,
2156 width: u32,
2157 height: u32,
2158 method: &str,
2159 ) -> Option<String> {
2160 let stripped = mxc.strip_prefix("mxc://")?;
2161 let (server, media_id) = stripped.split_once('/')?;
2162 let method = match method {
2163 "crop" | "scale" => method,
2164 _ => "scale",
2165 };
2166 let base = self.homeserver().as_str().trim_end_matches('/');
2167 Some(format!(
2168 "{}/_matrix/media/v3/thumbnail/{}/{}?width={}&height={}&method={}",
2169 base, server, media_id, width, height, method
2170 ))
2171 }
2172}
2173
2174pub struct SynchronousMatrixClient {
2175 runtime: Runtime,
2176 inner: MatrixClient,
2177}
2178
2179pub trait SyncEventHandler: Send + Sync {
2180 fn on_account_data(&self, _event: &Event) {}
2181 fn on_room_account_data(&self, _room_id: &str, _event: &Event) {}
2182 fn on_room_leave(&self, _room_id: &str, _event: &Event) {}
2183 fn on_room_invite(&self, _room_id: &str, _event: &Event) {}
2184 fn on_room_join(&self, _room_id: &str, _event: &Event) {}
2185 fn on_room_archived(&self, _room_id: &str, _event: &Event) {}
2186 fn on_room_upgraded(&self, _room_id: &str, _event: &Event) {}
2187 fn on_room_message(&self, _room_id: &str, _event: &Event) {}
2188 fn on_room_event(&self, _room_id: &str, _event: &Event) {}
2189}
2190
2191impl SynchronousMatrixClient {
2192 pub fn new(inner: MatrixClient) -> anyhow::Result<Self> {
2193 let runtime = Runtime::new()?;
2194 Ok(Self { runtime, inner })
2195 }
2196
2197 pub fn sync_once(
2198 &self,
2199 since: Option<&str>,
2200 ) -> anyhow::Result<Vec<crate::models::events::RoomEvent>> {
2201 self.runtime.block_on(self.inner.sync_once(since))
2202 }
2203
2204 pub fn start_sync(&self, handler: Arc<dyn SyncEventHandler>) -> anyhow::Result<()> {
2205 let since: Option<String> = None;
2206 loop {
2207 let events = self.sync_once(since.as_deref())?;
2208 for room_event in &events {
2209 let room_id = &room_event.room_id;
2210 let event = &room_event.event;
2211
2212 handler.on_room_event(room_id, event);
2213
2214 let kind_str = match &event.kind {
2215 crate::models::events::EventKind::RoomMessage => "m.room.message",
2216 crate::models::events::EventKind::RoomMember => "m.room.member",
2217 crate::models::events::EventKind::RoomCreate => "m.room.create",
2218 crate::models::events::EventKind::Custom(s) => s.as_str(),
2219 _ => "",
2220 };
2221
2222 match kind_str {
2223 "m.room.message" => handler.on_room_message(room_id, event),
2224 "m.room.member" => {
2225 if let Some(content) = event.content.as_object()
2226 && let Some(membership) =
2227 content.get("membership").and_then(|v| v.as_str())
2228 {
2229 match membership {
2230 "join" => handler.on_room_join(room_id, event),
2231 "leave" => handler.on_room_leave(room_id, event),
2232 "invite" => handler.on_room_invite(room_id, event),
2233 _ => {}
2234 }
2235 }
2236 }
2237 "m.room.create" => handler.on_room_upgraded(room_id, event),
2238 _ => {}
2239 }
2240 }
2241 if !events.is_empty() {
2242 }
2244 }
2245 }
2246
2247 pub fn send_message(&self, room_id: &str, body: &str) -> anyhow::Result<String> {
2248 self.runtime
2249 .block_on(self.inner.send_message(room_id, body))
2250 }
2251
2252 pub fn send_text(&self, room_id: &str, body: &str) -> anyhow::Result<String> {
2253 self.runtime.block_on(self.inner.send_text(room_id, body))
2254 }
2255
2256 pub fn send_notice(&self, room_id: &str, body: &str) -> anyhow::Result<String> {
2257 self.runtime.block_on(self.inner.send_notice(room_id, body))
2258 }
2259
2260 pub fn send_event(
2261 &self,
2262 room_id: &str,
2263 event_type: &str,
2264 content: &Value,
2265 ) -> anyhow::Result<String> {
2266 self.runtime
2267 .block_on(self.inner.send_event(room_id, event_type, content))
2268 }
2269
2270 pub fn send_state_event(
2271 &self,
2272 room_id: &str,
2273 event_type: &str,
2274 state_key: &str,
2275 content: &Value,
2276 ) -> anyhow::Result<String> {
2277 self.runtime.block_on(
2278 self.inner
2279 .send_state_event(room_id, event_type, state_key, content),
2280 )
2281 }
2282
2283 pub fn redact_event(
2284 &self,
2285 room_id: &str,
2286 event_id: &str,
2287 reason: Option<&str>,
2288 ) -> anyhow::Result<String> {
2289 self.runtime
2290 .block_on(self.inner.redact_event(room_id, event_id, reason))
2291 }
2292
2293 pub fn join_room(&self, room_id_or_alias: &str) -> anyhow::Result<String> {
2294 self.runtime
2295 .block_on(self.inner.join_room(room_id_or_alias))
2296 }
2297
2298 pub fn leave_room(&self, room_id: &str, reason: Option<&str>) -> anyhow::Result<()> {
2299 self.runtime
2300 .block_on(self.inner.leave_room(room_id, reason))
2301 }
2302
2303 pub fn invite_user(&self, user_id: &str, room_id: &str) -> anyhow::Result<()> {
2304 self.runtime
2305 .block_on(self.inner.invite_user(user_id, room_id))
2306 }
2307
2308 pub fn kick_user(
2309 &self,
2310 user_id: &str,
2311 room_id: &str,
2312 reason: Option<&str>,
2313 ) -> anyhow::Result<()> {
2314 self.runtime
2315 .block_on(self.inner.kick_user(user_id, room_id, reason))
2316 }
2317
2318 pub fn ban_user(
2319 &self,
2320 user_id: &str,
2321 room_id: &str,
2322 reason: Option<&str>,
2323 ) -> anyhow::Result<()> {
2324 self.runtime
2325 .block_on(self.inner.ban_user(user_id, room_id, reason))
2326 }
2327
2328 pub fn unban_user(&self, user_id: &str, room_id: &str) -> anyhow::Result<()> {
2329 self.runtime
2330 .block_on(self.inner.unban_user(user_id, room_id))
2331 }
2332
2333 pub fn get_joined_rooms(&self) -> anyhow::Result<Vec<String>> {
2334 self.runtime.block_on(self.inner.get_joined_rooms())
2335 }
2336
2337 pub fn get_joined_room_members(&self, room_id: &str) -> anyhow::Result<Vec<String>> {
2338 self.runtime
2339 .block_on(self.inner.get_joined_room_members(room_id))
2340 }
2341
2342 pub fn get_room_state(&self, room_id: &str) -> anyhow::Result<Vec<Value>> {
2343 self.runtime.block_on(self.inner.get_room_state(room_id))
2344 }
2345
2346 pub fn get_room_state_event(
2347 &self,
2348 room_id: &str,
2349 event_type: &str,
2350 state_key: &str,
2351 ) -> anyhow::Result<Value> {
2352 self.runtime.block_on(
2353 self.inner
2354 .get_room_state_event(room_id, event_type, state_key),
2355 )
2356 }
2357
2358 pub fn get_user_id(&self) -> anyhow::Result<String> {
2359 self.runtime.block_on(self.inner.get_user_id())
2360 }
2361
2362 pub fn profile(&self, user_id: &str) -> anyhow::Result<MatrixProfile> {
2363 self.runtime.block_on(self.inner.profile(user_id))
2364 }
2365
2366 pub fn set_display_name(&self, display_name: &str) -> anyhow::Result<()> {
2367 self.runtime
2368 .block_on(self.inner.set_display_name(display_name))
2369 }
2370
2371 pub fn set_avatar_url(&self, avatar_url: &str) -> anyhow::Result<()> {
2372 self.runtime.block_on(self.inner.set_avatar_url(avatar_url))
2373 }
2374
2375 pub fn get_presence_status(&self, user_id: &str) -> anyhow::Result<Presence> {
2376 self.runtime
2377 .block_on(self.inner.get_presence_status(user_id))
2378 }
2379
2380 pub fn set_presence_status(
2381 &self,
2382 status: Presence,
2383 status_msg: Option<&str>,
2384 ) -> anyhow::Result<()> {
2385 self.runtime
2386 .block_on(self.inner.set_presence_status(status, status_msg))
2387 }
2388
2389 pub fn create_room(&self, options: &CreateRoom) -> anyhow::Result<String> {
2390 self.runtime.block_on(self.inner.create_room(options))
2391 }
2392
2393 pub fn create_room_alias(&self, alias: &str, room_id: &str) -> anyhow::Result<()> {
2394 self.runtime
2395 .block_on(self.inner.create_room_alias(alias, room_id))
2396 }
2397
2398 pub fn delete_room_alias(&self, alias: &str) -> anyhow::Result<()> {
2399 self.runtime.block_on(self.inner.delete_room_alias(alias))
2400 }
2401
2402 pub fn lookup_room_alias(&self, alias: &str) -> anyhow::Result<RoomDirectoryLookupResponse> {
2403 self.runtime.block_on(self.inner.lookup_room_alias(alias))
2404 }
2405
2406 pub fn set_account_data(&self, event_type: &str, content: &Value) -> anyhow::Result<()> {
2407 self.runtime
2408 .block_on(self.inner.set_account_data(event_type, content))
2409 }
2410
2411 pub fn get_account_data(&self, event_type: &str) -> anyhow::Result<Value> {
2412 self.runtime
2413 .block_on(self.inner.get_account_data(event_type))
2414 }
2415
2416 pub fn upload_media(
2417 &self,
2418 bytes: Vec<u8>,
2419 content_type: &str,
2420 filename: Option<&str>,
2421 ) -> anyhow::Result<MXCUrl> {
2422 self.runtime
2423 .block_on(self.inner.upload_media(bytes, content_type, filename))
2424 }
2425
2426 pub fn download_media(&self, mxc: &MXCUrl) -> anyhow::Result<Vec<u8>> {
2427 self.runtime.block_on(self.inner.download_media(mxc))
2428 }
2429
2430 pub fn mxc_to_http(
2431 &self,
2432 mxc: &str,
2433 width: Option<u32>,
2434 height: Option<u32>,
2435 ) -> Option<String> {
2436 self.inner.mxc_to_http(mxc, width, height)
2437 }
2438
2439 pub fn get_event(&self, room_id: &str, event_id: &str) -> anyhow::Result<Value> {
2440 self.runtime
2441 .block_on(self.inner.get_event(room_id, event_id))
2442 }
2443
2444 pub fn get_server_versions(&self) -> anyhow::Result<Value> {
2445 self.runtime.block_on(self.inner.get_server_versions())
2446 }
2447
2448 pub fn get_capabilities(&self) -> anyhow::Result<Value> {
2449 self.runtime.block_on(self.inner.get_capabilities())
2450 }
2451}
2452
2453#[derive(Clone)]
2454pub struct DMs {
2455 client: MatrixClient,
2456 cached: Arc<RwLock<BTreeMap<String, Vec<String>>>>,
2457}
2458
2459impl DMs {
2460 pub fn new(client: MatrixClient) -> Self {
2461 Self {
2462 client,
2463 cached: Arc::new(RwLock::new(BTreeMap::new())),
2464 }
2465 }
2466
2467 pub async fn update(&self) -> anyhow::Result<()> {
2468 let map = match self.client.get_account_data("m.direct").await {
2469 Ok(v) => v,
2470 Err(_) => json!({}),
2471 };
2472
2473 let mut cached = self.cached.write().await;
2474 cached.clear();
2475
2476 if let Some(obj) = map.as_object() {
2477 for (user_id, room_ids) in obj {
2478 if let Some(rooms) = room_ids.as_array() {
2479 let rooms: Vec<String> = rooms
2480 .iter()
2481 .filter_map(|r| r.as_str().map(ToOwned::to_owned))
2482 .collect();
2483 if !rooms.is_empty() {
2484 cached.insert(user_id.clone(), rooms);
2485 }
2486 }
2487 }
2488 }
2489 Ok(())
2490 }
2491
2492 pub async fn get_or_create_dm(&self, user_id: &str) -> anyhow::Result<String> {
2493 let cached = self.cached.read().await;
2494 if let Some(rooms) = cached.get(user_id)
2495 && let Some(room_id) = rooms.first()
2496 {
2497 return Ok(room_id.clone());
2498 }
2499 drop(cached);
2500
2501 let create = CreateRoom {
2502 invite: vec![user_id.to_owned()],
2503 is_direct: true,
2504 ..CreateRoom::default()
2505 };
2506 let room_id = self.client.create_room(&create).await?;
2507
2508 let mut cached = self.cached.write().await;
2509 cached.insert(user_id.to_owned(), vec![room_id.clone()]);
2510
2511 let mut obj = serde_json::Map::new();
2512 for (uid, rooms) in cached.iter() {
2513 obj.insert(
2514 uid.clone(),
2515 Value::Array(rooms.iter().map(|r| json!(r)).collect()),
2516 );
2517 }
2518 self.client
2519 .set_account_data("m.direct", &Value::Object(obj))
2520 .await?;
2521
2522 Ok(room_id)
2523 }
2524
2525 pub async fn is_dm(&self, room_id: &str) -> bool {
2526 let cached = self.cached.read().await;
2527 cached
2528 .values()
2529 .any(|rooms| rooms.contains(&room_id.to_owned()))
2530 }
2531
2532 pub async fn persist_cache(&self) -> anyhow::Result<()> {
2533 let cached = self.cached.read().await;
2534 let mut obj = serde_json::Map::new();
2535 for (uid, rooms) in cached.iter() {
2536 obj.insert(
2537 uid.clone(),
2538 Value::Array(rooms.iter().map(|r| json!(r)).collect()),
2539 );
2540 }
2541 self.client
2542 .set_account_data("m.direct", &Value::Object(obj))
2543 .await
2544 }
2545
2546 pub async fn handle_invite(&self, room_id: &str, event: &Event) -> anyhow::Result<()> {
2547 let sender = &event.sender;
2548
2549 let is_direct = event
2550 .content
2551 .get("is_direct")
2552 .and_then(Value::as_bool)
2553 .unwrap_or(false);
2554
2555 if is_direct {
2556 let mut cached = self.cached.write().await;
2557 if let Some(rooms) = cached.get_mut(sender) {
2558 if !rooms.contains(&room_id.to_owned()) {
2559 rooms.push(room_id.to_owned());
2560 }
2561 } else {
2562 cached.insert(sender.clone(), vec![room_id.to_owned()]);
2563 }
2564 }
2565 Ok(())
2566 }
2567}
2568
2569fn membership_to_query_value(membership: Membership) -> &'static str {
2570 match membership {
2571 Membership::Invite => "invite",
2572 Membership::Join => "join",
2573 Membership::Knock => "knock",
2574 Membership::Leave => "leave",
2575 Membership::Ban => "ban",
2576 }
2577}
2578
2579fn plain_text_from_html(html: &str) -> String {
2580 let mut output = String::with_capacity(html.len());
2581 let mut in_tag = false;
2582 for ch in html.chars() {
2583 match ch {
2584 '<' => in_tag = true,
2585 '>' => in_tag = false,
2586 _ if !in_tag => output.push(ch),
2587 _ => {}
2588 }
2589 }
2590 output
2591}
2592
2593pub fn encode_path_component(value: &str) -> String {
2594 utf8_percent_encode(value, NON_ALPHANUMERIC).to_string()
2595}