example_persist_session/
main.rs1#![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#[derive(Debug, Serialize, Deserialize)]
23struct ClientSession {
24 homeserver: String,
26
27 db_path: PathBuf,
29
30 passphrase: String,
32}
33
34#[derive(Debug, Serialize, Deserialize)]
36struct FullSession {
37 client_session: ClientSession,
39
40 user_session: MatrixSession,
42
43 #[serde(skip_serializing_if = "Option::is_none")]
49 sync_token: Option<String>,
50}
51
52#[tokio::main]
68async fn main() -> anyhow::Result<()> {
69 tracing_subscriber::fmt::init();
70
71 let data_dir = dirs::data_dir().expect("no data_dir directory found").join("persist_session");
73 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
85async fn restore_session(session_file: &Path) -> anyhow::Result<(Client, Option<String>)> {
87 println!("Previous session found in '{}'", session_file.to_string_lossy());
88
89 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 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 client.restore_session(user_session).await?;
105
106 Ok((client, sync_token))
107}
108
109async 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 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 Ok(client)
163}
164
165async fn build_client(data_dir: &Path) -> anyhow::Result<(Client, ClientSession)> {
167 let mut rng = rng();
168
169 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 let passphrase: String =
179 (&mut rng).sample_iter(Alphanumeric).take(32).map(char::from).collect();
180
181 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 .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 return Err(error.into());
212 }
213 },
214 }
215 }
216}
217
218async 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 let filter = FilterDefinition::with_lazy_loading();
230
231 let mut sync_settings = SyncSettings::default().filter(filter.into());
232
233 if let Some(sync_token) = initial_sync_token {
237 sync_settings = sync_settings.token(sync_token);
238 }
239
240 loop {
245 match client.sync_once(sync_settings.clone()).await {
246 Ok(response) => {
247 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 client.add_event_handler(on_room_message);
264
265 client
267 .sync_with_result_callback(sync_settings, |sync_result| async move {
268 let response = sync_result?;
269
270 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
282async 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
296async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) {
298 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 room.room_id().to_string()
310 }
311 };
312
313 println!("[{room_name}] {}: {}", event.sender, text_content.body)
314}