Skip to main content

matrix_bot_sdk/
client.rs

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", "", &current_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        // spaces generally don't have direct flag or same defaults
1027        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(&current_room).await {
1188                Ok(ev) => ev,
1189                Err(_) => break, // Can't go back further
1190            };
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        // Simplistic reply fallback logic. Real bots usually format full fallback HTML,
1269        // but adding simple m.in_reply_to block for basic reply relationship.
1270        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        // Build base endpoint
1994        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                // In reality we should be saving/updating the next_batch token
2243            }
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}