-
Notifications
You must be signed in to change notification settings - Fork 57
Expand file tree
/
Copy pathmatrix_state.rs
More file actions
241 lines (213 loc) · 7.85 KB
/
matrix_state.rs
File metadata and controls
241 lines (213 loc) · 7.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
//! Handles app persistence by saving and restoring client session data to/from the filesystem.
use std::path::PathBuf;
use anyhow::{anyhow, bail};
use makepad_widgets::{log, Cx};
use matrix_sdk::{
authentication::matrix::MatrixSession,
ruma::{OwnedUserId, UserId},
sliding_sync,
Client,
};
use serde::{Deserialize, Serialize};
use crate::{
app_data_dir,
login::login_screen::LoginAction,
persistence::utils::write_file_securely,
};
/// The data needed to re-build a client.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientSessionPersisted {
/// The URL of the homeserver of the user.
pub homeserver: String,
/// The path of the database.
pub db_path: PathBuf,
/// The passphrase of the database.
pub passphrase: String,
}
/// The full session to persist.
#[derive(Debug, Serialize, Deserialize)]
pub struct FullSessionPersisted {
/// The data to re-build the client.
pub client_session: ClientSessionPersisted,
/// The Matrix user session.
pub user_session: MatrixSession,
/// The latest sync token.
///
/// It is only needed to persist it when using `Client::sync_once()` and we
/// want to make our syncs faster by not receiving all the initial sync
/// again.
#[serde(skip_serializing_if = "Option::is_none")]
pub sync_token: Option<String>,
/// The sliding sync version to use for this client session.
///
/// This determines the sync protocol used by the Matrix client:
/// - `Native`: Uses the server's native sliding sync implementation for efficient syncing
/// - `None`: Falls back to standard Matrix sync (without sliding sync optimizations)
///
/// The value is restored and applied to the client via `client.set_sliding_sync_version()`
/// when rebuilding the session from persistent storage.
#[serde(default)]
pub sliding_sync_version: SlidingSyncVersion,
}
/// A serializable duplicate of [`sliding_sync::Version`].
#[derive(Debug, Default, Serialize, Deserialize)]
pub enum SlidingSyncVersion {
#[default]
Native,
None,
}
impl From<SlidingSyncVersion> for sliding_sync::Version {
fn from(version: SlidingSyncVersion) -> Self {
match version {
SlidingSyncVersion::None => sliding_sync::Version::None,
SlidingSyncVersion::Native => sliding_sync::Version::Native,
}
}
}
impl From<sliding_sync::Version> for SlidingSyncVersion {
fn from(version: sliding_sync::Version) -> Self {
match version {
sliding_sync::Version::None => SlidingSyncVersion::None,
sliding_sync::Version::Native => SlidingSyncVersion::Native,
}
}
}
fn user_id_to_file_name(user_id: &UserId) -> String {
user_id.as_str()
.replace(":", "_")
.replace("@", "")
}
/// Returns the path to the persistent state directory for the given user.
pub fn persistent_state_dir(user_id: &UserId) -> PathBuf {
app_data_dir()
.join(user_id_to_file_name(user_id))
.join("persistent_state")
}
/// Returns the path to the session file for the given user.
pub fn session_file_path(user_id: &UserId) -> PathBuf {
persistent_state_dir(user_id).join("session")
}
const LATEST_USER_ID_FILE_NAME: &str = "latest_user_id.txt";
/// Returns the user ID of the most recently-logged in user session.
pub async fn most_recent_user_id() -> Option<OwnedUserId> {
tokio::fs::read_to_string(
app_data_dir().join(LATEST_USER_ID_FILE_NAME)
)
.await
.ok()?
.trim()
.try_into()
.ok()
}
/// Save which user was the most recently logged in.
async fn save_latest_user_id(user_id: &UserId) -> anyhow::Result<()> {
write_file_securely(
app_data_dir().join(LATEST_USER_ID_FILE_NAME),
user_id.as_str(),
).await?;
Ok(())
}
/// Restores the given user's previous session from the filesystem.
///
/// If no User ID is specified, the ID of the most recently-logged in user
/// is retrieved from the filesystem.
pub async fn restore_session(
user_id: Option<OwnedUserId>
) -> anyhow::Result<(Client, Option<String>)> {
let user_id = if let Some(user_id) = user_id {
Some(user_id)
} else {
most_recent_user_id().await
};
let Some(user_id) = user_id else {
log!("Could not find previous latest User ID");
bail!("Could not find previous latest User ID");
};
let session_file = session_file_path(&user_id);
if !session_file.exists() {
log!("Could not find previous session file for user {user_id}");
bail!("Could not find previous session file");
}
let status_str = format!("Loading previous session file for {user_id}...");
log!("{status_str}: '{}'", session_file.display());
Cx::post_action(LoginAction::Status {
title: "Restoring session".into(),
status: status_str,
});
// The session was serialized as JSON in a file.
let serialized_session = tokio::fs::read_to_string(session_file).await?;
let FullSessionPersisted { client_session, user_session, sync_token, sliding_sync_version } =
serde_json::from_str(&serialized_session)?;
let status_str = format!(
"Loaded session file for {user_id}. Trying to connect to homeserver ({})...",
client_session.homeserver,
);
log!("{status_str}");
Cx::post_action(LoginAction::Status {
title: "Connecting to homeserver".into(),
status: status_str,
});
// Build the client with the previous settings from the session.
let client = Client::builder()
.homeserver_url(client_session.homeserver)
.sqlite_store(client_session.db_path, Some(&client_session.passphrase))
.handle_refresh_tokens()
.build()
.await?;
let sliding_sync_version = sliding_sync_version.into();
client.set_sliding_sync_version(sliding_sync_version);
let status_str = format!("Authenticating previous login session for {}...", user_session.meta.user_id);
log!("{status_str}");
Cx::post_action(LoginAction::Status {
title: "Authenticating session".into(),
status: status_str,
});
// Restore the Matrix user session.
client.restore_session(user_session).await?;
save_latest_user_id(&user_id).await?;
Ok((client, sync_token))
}
/// Persist a logged-in client session to the filesystem for later use.
///
/// TODO: This is not very secure, for simplicity. We should use robius-keychain
/// or `keyring-rs` to storing secrets securely.
///
/// Note that we could also build the user session from the login response.
pub async fn save_session(
client: &Client,
client_session: ClientSessionPersisted,
) -> anyhow::Result<()> {
let user_session = client
.matrix_auth()
.session()
.ok_or_else(|| anyhow!("A logged-in client should have a session"))?;
save_latest_user_id(&user_session.meta.user_id).await?;
let sliding_sync_version = client.sliding_sync_version().into();
// Save that user's session.
let session_file = session_file_path(&user_session.meta.user_id);
let serialized_session = serde_json::to_string(&FullSessionPersisted {
client_session,
user_session,
sync_token: None,
sliding_sync_version
})?;
write_file_securely(&session_file, serialized_session).await?;
log!("Session persisted to: {}", session_file.display());
Ok(())
}
/// Remove the LATEST_USER_ID_FILE_NAME file if it exists
///
/// Returns:
/// - Ok(true) if file was found and deleted
/// - Ok(false) if file didn't exist
/// - Err if deletion failed
pub async fn delete_latest_user_id() -> anyhow::Result<bool> {
let last_login_path = app_data_dir().join(LATEST_USER_ID_FILE_NAME);
if last_login_path.exists() {
tokio::fs::remove_file(&last_login_path).await
.map_err(|e| anyhow::anyhow!("Failed to remove latest user file: {e}"))
.map(|_| true)
} else {
Ok(false)
}
}