Skip to content

Commit

Permalink
Merge pull request #149 from kevinaboos/persistent_state
Browse files Browse the repository at this point in the history
Persist and restore user client session
  • Loading branch information
kevinaboos authored Sep 25, 2024
2 parents fde5d29 + 806e677 commit f85d6bb
Show file tree
Hide file tree
Showing 7 changed files with 322 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

## Temp hack to support login on mobile platforms.
login.toml
.login.toml
/login.toml
56 changes: 54 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ metadata.makepad-auto-version = "zqpv-Yj-K7WNVK2I8h5Okhho46Q="

[dependencies]
makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "rik" }
directories = "5.0.1"
robius-open = "0.1.0"

anyhow = "1.0"
Expand All @@ -34,7 +35,10 @@ linkify = "0.10.0"
matrix-sdk = { git = "https://github.com/project-robius/matrix-rust-sdk", branch = "re-export-reactions-type", default-features = false, features = [ "experimental-sliding-sync", "e2e-encryption", "automatic-room-key-forwarding", "markdown", "sqlite", "rustls-tls", "bundled-sqlite" ] }
# matrix-sdk-ui = { git = "https://github.com/matrix-org/matrix-rust-sdk", default-features = false, features = [ "e2e-encryption", "rustls-tls" ] }
matrix-sdk-ui = { git = "https://github.com/project-robius/matrix-rust-sdk", branch = "re-export-reactions-type", default-features = false, features = [ "e2e-encryption", "rustls-tls" ] }
rand = "0.8.5"
rangemap = "1.5.0"
serde = "1.0"
serde_json = "1.0"
tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] }
tracing-subscriber = "0.3.17"
unicode-segmentation = "1.11.0"
Expand Down
23 changes: 23 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
use std::{path::Path, sync::OnceLock};

use directories::ProjectDirs;

pub use makepad_widgets;
pub mod app;
pub mod persistent_state;

/// Core UI content: the main home screen (rooms list), room screen.
pub mod home;
Expand All @@ -18,3 +23,21 @@ pub mod media_cache;

pub mod utils;
pub mod temp_storage;


pub const APP_QUALIFIER: &str = "org";
pub const APP_ORGANIZATION: &str = "robius";
pub const APP_NAME: &str = "robrix";

pub fn project_dir() -> &'static ProjectDirs {
static ROBRIX_PROJECT_DIRS: OnceLock<ProjectDirs> = OnceLock::new();

ROBRIX_PROJECT_DIRS.get_or_init(|| {
ProjectDirs::from(APP_QUALIFIER, APP_ORGANIZATION, APP_NAME)
.expect("Failed to obtain Robrix project directory")
})
}

pub fn app_data_dir() -> &'static Path {
project_dir().data_dir()
}
167 changes: 167 additions & 0 deletions src/persistent_state.rs
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(())
}
Loading

0 comments on commit f85d6bb

Please sign in to comment.