1use std::{
2 io::{self, Write},
3 path::{Path, PathBuf},
4};
56use matrix_sdk::{
7 authentication::matrix::MatrixSession,
8 config::SyncSettings,
9 ruma::{
10 api::client::filter::FilterDefinition,
11 events::room::message::{MessageType, OriginalSyncRoomMessageEvent},
12 },
13 Client, Error, LoopCtrl, Room, RoomState,
14};
15use rand::{distributions::Alphanumeric, thread_rng, Rng};
16use serde::{Deserialize, Serialize};
17use tokio::fs;
1819/// The data needed to re-build a client.
20#[derive(Debug, Serialize, Deserialize)]
21struct ClientSession {
22/// The URL of the homeserver of the user.
23homeserver: String,
2425/// The path of the database.
26db_path: PathBuf,
2728/// The passphrase of the database.
29passphrase: String,
30}
3132/// The full session to persist.
33#[derive(Debug, Serialize, Deserialize)]
34struct FullSession {
35/// The data to re-build the client.
36client_session: ClientSession,
3738/// The Matrix user session.
39user_session: MatrixSession,
4041/// The latest sync token.
42 ///
43 /// It is only needed to persist it when using `Client::sync_once()` and we
44 /// want to make our syncs faster by not receiving all the initial sync
45 /// again.
46#[serde(skip_serializing_if = "Option::is_none")]
47sync_token: Option<String>,
48}
4950/// A simple example to show how to persist a client's data to be able to
51/// restore it.
52///
53/// Restoring a session with encryption without having a persisted store
54/// will break the encryption setup and the client will not be able to send or
55/// receive encrypted messages, hence the need to persist the session.
56///
57/// To use this, just run `cargo run -p example-persist-session`, and everything
58/// is interactive after that. You might want to set the `RUST_LOG` environment
59/// variable to `warn` to reduce the noise in the logs. The program exits
60/// whenever an unexpected error occurs.
61///
62/// To reset the login, simply delete the folder containing the session
63/// file, the location is shown in the logs. Note that the database must be
64/// deleted too as it can't be reused.
65#[tokio::main]
66async fn main() -> anyhow::Result<()> {
67 tracing_subscriber::fmt::init();
6869// The folder containing this example's data.
70let data_dir = dirs::data_dir().expect("no data_dir directory found").join("persist_session");
71// The file where the session is persisted.
72let session_file = data_dir.join("session");
7374let (client, sync_token) = if session_file.exists() {
75 restore_session(&session_file).await?
76} else {
77 (login(&data_dir, &session_file).await?, None)
78 };
7980 sync(client, sync_token, &session_file).await
81}
8283/// Restore a previous session.
84async fn restore_session(session_file: &Path) -> anyhow::Result<(Client, Option<String>)> {
85println!("Previous session found in '{}'", session_file.to_string_lossy());
8687// The session was serialized as JSON in a file.
88let serialized_session = fs::read_to_string(session_file).await?;
89let FullSession { client_session, user_session, sync_token } =
90 serde_json::from_str(&serialized_session)?;
9192// Build the client with the previous settings from the session.
93let client = Client::builder()
94 .homeserver_url(client_session.homeserver)
95 .sqlite_store(client_session.db_path, Some(&client_session.passphrase))
96 .build()
97 .await?;
9899println!("Restoring session for {}…", user_session.meta.user_id);
100101// Restore the Matrix user session.
102client.restore_session(user_session).await?;
103104Ok((client, sync_token))
105}
106107/// Login with a new device.
108async fn login(data_dir: &Path, session_file: &Path) -> anyhow::Result<Client> {
109println!("No previous session found, logging in…");
110111let (client, client_session) = build_client(data_dir).await?;
112let matrix_auth = client.matrix_auth();
113114loop {
115print!("\nUsername: ");
116 io::stdout().flush().expect("Unable to write to stdout");
117let mut username = String::new();
118 io::stdin().read_line(&mut username).expect("Unable to read user input");
119 username = username.trim().to_owned();
120121print!("Password: ");
122 io::stdout().flush().expect("Unable to write to stdout");
123let mut password = String::new();
124 io::stdin().read_line(&mut password).expect("Unable to read user input");
125 password = password.trim().to_owned();
126127match matrix_auth
128 .login_username(&username, &password)
129 .initial_device_display_name("persist-session client")
130 .await
131{
132Ok(_) => {
133println!("Logged in as {username}");
134break;
135 }
136Err(error) => {
137println!("Error logging in: {error}");
138println!("Please try again\n");
139 }
140 }
141 }
142143// Persist the session to reuse it later.
144 // This is not very secure, for simplicity. If the system provides a way of
145 // storing secrets securely, it should be used instead.
146 // Note that we could also build the user session from the login response.
147let user_session = matrix_auth.session().expect("A logged-in client should have a session");
148let serialized_session =
149 serde_json::to_string(&FullSession { client_session, user_session, sync_token: None })?;
150 fs::write(session_file, serialized_session).await?;
151152println!("Session persisted in {}", session_file.to_string_lossy());
153154// After logging in, you might want to verify this session with another one (see
155 // the `emoji_verification` example), or bootstrap cross-signing if this is your
156 // first session with encryption, or if you need to reset cross-signing because
157 // you don't have access to your old sessions (see the
158 // `cross_signing_bootstrap` example).
159160Ok(client)
161}
162163/// Build a new client.
164async fn build_client(data_dir: &Path) -> anyhow::Result<(Client, ClientSession)> {
165let mut rng = thread_rng();
166167// Generating a subfolder for the database is not mandatory, but it is useful if
168 // you allow several clients to run at the same time. Each one must have a
169 // separate database, which is a different folder with the SQLite store.
170let db_subfolder: String =
171 (&mut rng).sample_iter(Alphanumeric).take(7).map(char::from).collect();
172let db_path = data_dir.join(db_subfolder);
173174// Generate a random passphrase.
175let passphrase: String =
176 (&mut rng).sample_iter(Alphanumeric).take(32).map(char::from).collect();
177178// We create a loop here so the user can retry if an error happens.
179loop {
180let mut homeserver = String::new();
181182print!("Homeserver URL: ");
183 io::stdout().flush().expect("Unable to write to stdout");
184 io::stdin().read_line(&mut homeserver).expect("Unable to read user input");
185186println!("\nChecking homeserver…");
187188match Client::builder()
189 .homeserver_url(&homeserver)
190// We use the SQLite store, which is enabled by default. This is the crucial part to
191 // persist the encryption setup.
192 // Note that other store backends are available and you can even implement your own.
193.sqlite_store(&db_path, Some(&passphrase))
194 .build()
195 .await
196{
197Ok(client) => return Ok((client, ClientSession { homeserver, db_path, passphrase })),
198Err(error) => match &error {
199 matrix_sdk::ClientBuildError::AutoDiscovery(_)
200 | matrix_sdk::ClientBuildError::Url(_)
201 | matrix_sdk::ClientBuildError::Http(_) => {
202println!("Error checking the homeserver: {error}");
203println!("Please try again\n");
204 }
205_ => {
206// Forward other errors, it's unlikely we can retry with a different outcome.
207return Err(error.into());
208 }
209 },
210 }
211 }
212}
213214/// Setup the client to listen to new messages.
215async fn sync(
216 client: Client,
217 initial_sync_token: Option<String>,
218 session_file: &Path,
219) -> anyhow::Result<()> {
220println!("Launching a first sync to ignore past messages…");
221222// Enable room members lazy-loading, it will speed up the initial sync a lot
223 // with accounts in lots of rooms.
224 // See <https://spec.matrix.org/v1.6/client-server-api/#lazy-loading-room-members>.
225let filter = FilterDefinition::with_lazy_loading();
226227let mut sync_settings = SyncSettings::default().filter(filter.into());
228229// We restore the sync where we left.
230 // This is not necessary when not using `sync_once`. The other sync methods get
231 // the sync token from the store.
232if let Some(sync_token) = initial_sync_token {
233 sync_settings = sync_settings.token(sync_token);
234 }
235236// Let's ignore messages before the program was launched.
237 // This is a loop in case the initial sync is longer than our timeout. The
238 // server should cache the response and it will ultimately take less time to
239 // receive.
240loop {
241match client.sync_once(sync_settings.clone()).await {
242Ok(response) => {
243// This is the last time we need to provide this token, the sync method after
244 // will handle it on its own.
245sync_settings = sync_settings.token(response.next_batch.clone());
246 persist_sync_token(session_file, response.next_batch).await?;
247break;
248 }
249Err(error) => {
250println!("An error occurred during initial sync: {error}");
251println!("Trying again…");
252 }
253 }
254 }
255256println!("The client is ready! Listening to new messages…");
257258// Now that we've synced, let's attach a handler for incoming room messages.
259client.add_event_handler(on_room_message);
260261// This loops until we kill the program or an error happens.
262client
263 .sync_with_result_callback(sync_settings, |sync_result| async move {
264let response = sync_result?;
265266// We persist the token each time to be able to restore our session
267persist_sync_token(session_file, response.next_batch)
268 .await
269.map_err(|err| Error::UnknownError(err.into()))?;
270271Ok(LoopCtrl::Continue)
272 })
273 .await?;
274275Ok(())
276}
277278/// Persist the sync token for a future session.
279/// Note that this is needed only when using `sync_once`. Other sync methods get
280/// the sync token from the store.
281async fn persist_sync_token(session_file: &Path, sync_token: String) -> anyhow::Result<()> {
282let serialized_session = fs::read_to_string(session_file).await?;
283let mut full_session: FullSession = serde_json::from_str(&serialized_session)?;
284285 full_session.sync_token = Some(sync_token);
286let serialized_session = serde_json::to_string(&full_session)?;
287 fs::write(session_file, serialized_session).await?;
288289Ok(())
290}
291292/// Handle room messages.
293async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) {
294// We only want to log text messages in joined rooms.
295if room.state() != RoomState::Joined {
296return;
297 }
298let MessageType::Text(text_content) = &event.content.msgtype else { return };
299300let room_name = match room.display_name().await {
301Ok(room_name) => room_name.to_string(),
302Err(error) => {
303println!("Error getting room display name: {error}");
304// Let's fallback to the room ID.
305room.room_id().to_string()
306 }
307 };
308309println!("[{room_name}] {}: {}", event.sender, text_content.body)
310}