-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #149 from kevinaboos/persistent_state
Persist and restore user client session
- Loading branch information
Showing
7 changed files
with
322 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,4 +5,4 @@ | |
|
||
## Temp hack to support login on mobile platforms. | ||
login.toml | ||
.login.toml | ||
/login.toml |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
//! Handles app persistence by saving and restoring client session data to/from the filesystem. | ||
use std::{ | ||
path::{Path, PathBuf}, | ||
}; | ||
use anyhow::{anyhow, bail}; | ||
use makepad_widgets::log; | ||
use matrix_sdk::{ | ||
matrix_auth::MatrixSession, | ||
ruma::{OwnedUserId, UserId}, | ||
Client, | ||
}; | ||
use serde::{Deserialize, Serialize}; | ||
use tokio::fs; | ||
|
||
use crate::app_data_dir; | ||
|
||
/// The data needed to re-build a client. | ||
#[derive(Debug, 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>, | ||
} | ||
|
||
fn user_id_to_file_name(user_id: &UserId) -> String { | ||
user_id.as_str() | ||
.replace(":", "_") | ||
.replace("@", "") | ||
} | ||
|
||
pub fn persistent_state_dir(user_id: &UserId) -> PathBuf { | ||
app_data_dir() | ||
.join(user_id_to_file_name(user_id)) | ||
.join("persistent_state") | ||
} | ||
|
||
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 fn most_recent_user_id() -> Option<OwnedUserId> { | ||
std::fs::read_to_string( | ||
app_data_dir().join(LATEST_USER_ID_FILE_NAME) | ||
) | ||
.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<()> { | ||
fs::write( | ||
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 Some(user_id) = user_id.or_else(|| most_recent_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"); | ||
} | ||
log!("Found existing session at '{}'", session_file.display()); | ||
|
||
// The session was serialized as JSON in a file. | ||
let serialized_session = fs::read_to_string(session_file).await?; | ||
let FullSessionPersisted { client_session, user_session, sync_token } = | ||
serde_json::from_str(&serialized_session)?; | ||
|
||
// 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)) | ||
.simplified_sliding_sync(false) | ||
.build() | ||
.await?; | ||
|
||
log!("Restoring previous session for {}", user_session.meta.user_id); | ||
|
||
// 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?; | ||
|
||
// 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, | ||
})?; | ||
if let Some(parent) = session_file.parent() { | ||
fs::create_dir_all(parent).await?; | ||
} | ||
fs::write(&session_file, serialized_session).await?; | ||
|
||
log!("Session persisted to: {}", session_file.display()); | ||
|
||
// After logging in, you might want to verify this session with another one (see | ||
// the `emoji_verification` example), or bootstrap cross-signing if this is your | ||
// first session with encryption, or if you need to reset cross-signing because | ||
// you don't have access to your old sessions (see the | ||
// `cross_signing_bootstrap` example). | ||
|
||
Ok(()) | ||
} |
Oops, something went wrong.