1use std::sync::Arc;
11
12use m4a_seam::keyproof::LOGIN_TYPE;
13use serde_json::json;
14use zeroize::Zeroizing;
15
16use super::wire::{field, HttpWire, Wire};
17use super::{Backend, BackendKind, Capabilities, Session};
18use crate::error::{AgentError, Result};
19use crate::identity::{token_label, IdentityStore, SessionIdentity};
20use crate::keyauth;
21
22const LOGIN: &str = "/_matrix/client/v3/login";
23const WHOAMI: &str = "/_matrix/client/v3/account/whoami";
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum SyncMode {
28 Auto,
30 V3,
32}
33
34pub struct MatrixBackend {
35 base: String,
36 wire: Arc<dyn Wire>,
37 #[cfg(feature = "engine")]
38 live: Arc<super::live::Live>,
39 #[cfg(feature = "engine")]
40 sliding: super::sliding::Sliding,
41}
42
43#[cfg(feature = "engine")]
46pub fn discover_base(origin: &str) -> Option<String> {
47 let exec = crate::engine::HttpExec::new(origin).ok()?;
48 let (st, v) = exec.get_json("/.well-known/matrix/client").ok()?;
49 if st != 200 {
50 return None;
51 }
52 let found = v.get("m.homeserver")?.get("base_url")?.as_str()?.trim_end_matches('/').to_string();
53 crate::engine::http::parse_origin(&found).ok()?;
55 Some(found)
56}
57
58impl MatrixBackend {
59 pub fn new(base_url: &str) -> Result<Self> {
61 Self::with_wire(base_url, Box::new(HttpWire::new(base_url)?))
62 }
63
64 #[cfg(feature = "engine")]
66 pub fn discover(origin: &str) -> Result<Self> {
67 let real = discover_base(origin).unwrap_or_else(|| origin.trim_end_matches('/').to_string());
68 Self::new(&real)
69 }
70
71 pub fn with_wire(base_url: &str, wire: Box<dyn Wire>) -> Result<Self> {
72 Ok(Self {
73 base: base_url.trim_end_matches('/').to_string(),
74 wire: Arc::from(wire),
75 #[cfg(feature = "engine")]
76 live: Arc::new(super::live::Live::new(crate::engine::HttpExec::new(base_url)?)),
77 #[cfg(feature = "engine")]
78 sliding: super::sliding::Sliding::new(),
79 })
80 }
81
82 #[cfg(feature = "engine")]
83 pub fn with_sync_mode(self, mode: SyncMode) -> Self {
84 self.sliding.set_mode(mode);
85 self
86 }
87
88 #[cfg(feature = "engine")]
90 pub fn uses_sliding_sync(&self) -> bool {
91 self.sliding.active()
92 }
93
94 pub fn base(&self) -> &str {
96 &self.base
97 }
98
99 fn login_by_signature(wire: &dyn Wire, ids: &IdentityStore, id: &SessionIdentity) -> Result<(String, Option<String>, String)> {
101 let (st, v) = wire.post(LOGIN, &json!({ "type": LOGIN_TYPE, "key_id": id.key_id }), None)?;
102 if st != 401 {
103 return Err(keyauth::refused_or(st, "login challenge"));
104 }
105 let c = keyauth::parse_challenge(v.get(LOGIN_TYPE).ok_or_else(|| AgentError::Protocol("no challenge in the 401".into()))?)?;
106 let signature = id.sign_login(ids.vault(), &c)?;
107 let (st, v) = wire.post(LOGIN, &json!({ "type": LOGIN_TYPE, "key_id": id.key_id, "challenge_id": c.challenge_id, "signature": signature }), None)?;
108 if st != 200 {
109 return Err(keyauth::refused_or(st, "login"));
110 }
111 let token = field(&v, "access_token")?;
112 ids.vault().put(&token_label(&id.session_id), token.as_bytes())?;
113 Ok((token, v.get("device_id").and_then(|d| d.as_str()).map(str::to_string), field(&v, "user_id")?))
114 }
115
116 fn session_of(&self, id: &SessionIdentity, token: String, user_id: String, device_id: Option<String>) -> Session {
117 let nick = user_id.trim_start_matches('@').split(':').next().unwrap_or_default().to_string();
118 let _ = self;
119 Session { token: Zeroizing::new(token), nick, cred_ref: id.key_id.clone(), user_id: Some(user_id), device_id }
120 }
121
122 #[allow(unused_variables)]
123 fn established(&self, ids: &IdentityStore, id: &SessionIdentity, token: &str) {
124 #[cfg(feature = "engine")]
125 {
126 self.live.set_token(token);
127 self.live.with_exec(|e| e.probe_prefix());
129 let (ids, id, wire) = (ids.clone(), id.clone(), Arc::clone(&self.wire));
130 let live = Arc::clone(&self.live);
131 self.live.set_refresher(Arc::new(move || {
132 let (token, _, _) = Self::login_by_signature(&*wire, &ids, &id)?;
133 let _ = &live;
134 Ok(token)
135 }));
136 self.sliding.decide(&self.live);
137 }
138 }
139}
140
141impl Backend for MatrixBackend {
142 fn kind(&self) -> BackendKind {
143 BackendKind::Matrix
144 }
145 fn capabilities(&self) -> Capabilities {
146 Capabilities { rooms: true, federation: true }
147 }
148 fn server_ref(&self) -> &str {
149 &self.base
150 }
151
152 fn ensure_session(&self, ids: &IdentityStore, id: &mut SessionIdentity, invite: Option<&str>) -> Result<Session> {
153 if id.tier != BackendKind::Matrix || id.server_ref != self.base {
154 return Err(AgentError::Identity("identity belongs to another tier or server".into()));
155 }
156 if let Some(t) = keyauth::stored_token(ids, id)? {
157 let (st, v) = self.wire.get(WHOAMI, Some(&t))?;
158 if st == 200 {
159 let user_id = field(&v, "user_id")?;
160 self.established(ids, id, &t);
161 return Ok(self.session_of(id, t, user_id, v.get("device_id").and_then(|d| d.as_str()).map(str::to_string)));
162 }
163 }
164 if !id.enrolled {
165 keyauth::enroll(&*self.wire, ids, id, invite.ok_or(AgentError::NeedsInvite)?)?;
166 }
167 let (token, device_id, user_id) = Self::login_by_signature(&*self.wire, ids, id)?;
168 self.established(ids, id, &token);
169 Ok(self.session_of(id, token, user_id, device_id))
170 }
171
172 #[cfg(feature = "engine")]
173 fn execute(&self, request: &mail4agent_messenger::OutgoingRequest) -> Result<mail4agent_messenger::HttpResponseDescriptor> {
174 if request.kind == mail4agent_messenger::OutgoingRequestKind::Sync {
175 if let Some(answer) = self.sliding.sync(&self.live, request)? {
176 return Ok(answer);
177 }
178 }
179 self.live.execute(request)
180 }
181
182 #[cfg(feature = "engine")]
183 fn wait_for_news(&self, timeout: std::time::Duration) -> Result<crate::engine::News> {
184 self.live.wait_for_news(timeout)
185 }
186
187 #[cfg(feature = "engine")]
188 fn whoami(&self) -> Result<(String, String)> {
189 self.live.whoami()
190 }
191
192 #[cfg(feature = "engine")]
193 fn keep_prefix(&self) -> bool {
194 self.live.keep_prefix()
195 }
196}