Skip to main content

example_persist_session/
main.rs

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