example_persist_session/main.rs
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 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
use std::{
io::{self, Write},
path::{Path, PathBuf},
};
use matrix_sdk::{
config::SyncSettings,
matrix_auth::MatrixSession,
ruma::{
api::client::filter::FilterDefinition,
events::room::message::{MessageType, OriginalSyncRoomMessageEvent},
},
Client, Error, LoopCtrl, Room, RoomState,
};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::{Deserialize, Serialize};
use tokio::fs;
/// The data needed to re-build a client.
#[derive(Debug, Serialize, Deserialize)]
struct ClientSession {
/// The URL of the homeserver of the user.
homeserver: String,
/// The path of the database.
db_path: PathBuf,
/// The passphrase of the database.
passphrase: String,
}
/// The full session to persist.
#[derive(Debug, Serialize, Deserialize)]
struct FullSession {
/// The data to re-build the client.
client_session: ClientSession,
/// The Matrix user session.
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")]
sync_token: Option<String>,
}
/// A simple example to show how to persist a client's data to be able to
/// restore it.
///
/// Restoring a session with encryption without having a persisted store
/// will break the encryption setup and the client will not be able to send or
/// receive encrypted messages, hence the need to persist the session.
///
/// To use this, just run `cargo run -p example-persist-session`, and everything
/// is interactive after that. You might want to set the `RUST_LOG` environment
/// variable to `warn` to reduce the noise in the logs. The program exits
/// whenever an unexpected error occurs.
///
/// To reset the login, simply delete the folder containing the session
/// file, the location is shown in the logs. Note that the database must be
/// deleted too as it can't be reused.
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
// The folder containing this example's data.
let data_dir = dirs::data_dir().expect("no data_dir directory found").join("persist_session");
// The file where the session is persisted.
let session_file = data_dir.join("session");
let (client, sync_token) = if session_file.exists() {
restore_session(&session_file).await?
} else {
(login(&data_dir, &session_file).await?, None)
};
sync(client, sync_token, &session_file).await.map_err(Into::into)
}
/// Restore a previous session.
async fn restore_session(session_file: &Path) -> anyhow::Result<(Client, Option<String>)> {
println!("Previous session found in '{}'", session_file.to_string_lossy());
// The session was serialized as JSON in a file.
let serialized_session = fs::read_to_string(session_file).await?;
let FullSession { 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))
.build()
.await?;
println!("Restoring session for {}…", user_session.meta.user_id);
// Restore the Matrix user session.
client.restore_session(user_session).await?;
Ok((client, sync_token))
}
/// Login with a new device.
async fn login(data_dir: &Path, session_file: &Path) -> anyhow::Result<Client> {
println!("No previous session found, logging in…");
let (client, client_session) = build_client(data_dir).await?;
let matrix_auth = client.matrix_auth();
loop {
print!("\nUsername: ");
io::stdout().flush().expect("Unable to write to stdout");
let mut username = String::new();
io::stdin().read_line(&mut username).expect("Unable to read user input");
username = username.trim().to_owned();
print!("Password: ");
io::stdout().flush().expect("Unable to write to stdout");
let mut password = String::new();
io::stdin().read_line(&mut password).expect("Unable to read user input");
password = password.trim().to_owned();
match matrix_auth
.login_username(&username, &password)
.initial_device_display_name("persist-session client")
.await
{
Ok(_) => {
println!("Logged in as {username}");
break;
}
Err(error) => {
println!("Error logging in: {error}");
println!("Please try again\n");
}
}
}
// Persist the session to reuse it later.
// This is not very secure, for simplicity. If the system provides a way of
// storing secrets securely, it should be used instead.
// Note that we could also build the user session from the login response.
let user_session = matrix_auth.session().expect("A logged-in client should have a session");
let serialized_session =
serde_json::to_string(&FullSession { client_session, user_session, sync_token: None })?;
fs::write(session_file, serialized_session).await?;
println!("Session persisted in {}", session_file.to_string_lossy());
// 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(client)
}
/// Build a new client.
async fn build_client(data_dir: &Path) -> anyhow::Result<(Client, ClientSession)> {
let mut rng = thread_rng();
// Generating a subfolder for the database is not mandatory, but it is useful if
// you allow several clients to run at the same time. Each one must have a
// separate database, which is a different folder with the SQLite store.
let db_subfolder: String =
(&mut rng).sample_iter(Alphanumeric).take(7).map(char::from).collect();
let db_path = data_dir.join(db_subfolder);
// Generate a random passphrase.
let passphrase: String =
(&mut rng).sample_iter(Alphanumeric).take(32).map(char::from).collect();
// We create a loop here so the user can retry if an error happens.
loop {
let mut homeserver = String::new();
print!("Homeserver URL: ");
io::stdout().flush().expect("Unable to write to stdout");
io::stdin().read_line(&mut homeserver).expect("Unable to read user input");
println!("\nChecking homeserver…");
match Client::builder()
.homeserver_url(&homeserver)
// We use the SQLite store, which is enabled by default. This is the crucial part to
// persist the encryption setup.
// Note that other store backends are available and you can even implement your own.
.sqlite_store(&db_path, Some(&passphrase))
.build()
.await
{
Ok(client) => return Ok((client, ClientSession { homeserver, db_path, passphrase })),
Err(error) => match &error {
matrix_sdk::ClientBuildError::AutoDiscovery(_)
| matrix_sdk::ClientBuildError::Url(_)
| matrix_sdk::ClientBuildError::Http(_) => {
println!("Error checking the homeserver: {error}");
println!("Please try again\n");
}
_ => {
// Forward other errors, it's unlikely we can retry with a different outcome.
return Err(error.into());
}
},
}
}
}
/// Setup the client to listen to new messages.
async fn sync(
client: Client,
initial_sync_token: Option<String>,
session_file: &Path,
) -> anyhow::Result<()> {
println!("Launching a first sync to ignore past messages…");
// Enable room members lazy-loading, it will speed up the initial sync a lot
// with accounts in lots of rooms.
// See <https://spec.matrix.org/v1.6/client-server-api/#lazy-loading-room-members>.
let filter = FilterDefinition::with_lazy_loading();
let mut sync_settings = SyncSettings::default().filter(filter.into());
// We restore the sync where we left.
// This is not necessary when not using `sync_once`. The other sync methods get
// the sync token from the store.
if let Some(sync_token) = initial_sync_token {
sync_settings = sync_settings.token(sync_token);
}
// Let's ignore messages before the program was launched.
// This is a loop in case the initial sync is longer than our timeout. The
// server should cache the response and it will ultimately take less time to
// receive.
loop {
match client.sync_once(sync_settings.clone()).await {
Ok(response) => {
// This is the last time we need to provide this token, the sync method after
// will handle it on its own.
sync_settings = sync_settings.token(response.next_batch.clone());
persist_sync_token(session_file, response.next_batch).await?;
break;
}
Err(error) => {
println!("An error occurred during initial sync: {error}");
println!("Trying again…");
}
}
}
println!("The client is ready! Listening to new messages…");
// Now that we've synced, let's attach a handler for incoming room messages.
client.add_event_handler(on_room_message);
// This loops until we kill the program or an error happens.
client
.sync_with_result_callback(sync_settings, |sync_result| async move {
let response = sync_result?;
// We persist the token each time to be able to restore our session
persist_sync_token(session_file, response.next_batch)
.await
.map_err(|err| Error::UnknownError(err.into()))?;
Ok(LoopCtrl::Continue)
})
.await?;
Ok(())
}
/// Persist the sync token for a future session.
/// Note that this is needed only when using `sync_once`. Other sync methods get
/// the sync token from the store.
async fn persist_sync_token(session_file: &Path, sync_token: String) -> anyhow::Result<()> {
let serialized_session = fs::read_to_string(session_file).await?;
let mut full_session: FullSession = serde_json::from_str(&serialized_session)?;
full_session.sync_token = Some(sync_token);
let serialized_session = serde_json::to_string(&full_session)?;
fs::write(session_file, serialized_session).await?;
Ok(())
}
/// Handle room messages.
async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) {
// We only want to log text messages in joined rooms.
if room.state() != RoomState::Joined {
return;
}
let MessageType::Text(text_content) = &event.content.msgtype else { return };
let room_name = match room.compute_display_name().await {
Ok(room_name) => room_name.to_string(),
Err(error) => {
println!("Error getting room display name: {error}");
// Let's fallback to the room ID.
room.room_id().to_string()
}
};
println!("[{room_name}] {}: {}", event.sender, text_content.body)
}