example_persist_session/
main.rs1use std::{
2 io::{self, Write},
3 path::{Path, PathBuf},
4};
5
6use matrix_sdk::{
7 Client, Error, LoopCtrl, Room, RoomState,
8 authentication::matrix::MatrixSession,
9 config::SyncSettings,
10 ruma::{
11 api::client::filter::FilterDefinition,
12 events::room::message::{MessageType, OriginalSyncRoomMessageEvent},
13 },
14};
15use rand::{Rng, distributions::Alphanumeric, thread_rng};
16use serde::{Deserialize, Serialize};
17use tokio::fs;
18
19#[derive(Debug, Serialize, Deserialize)]
21struct ClientSession {
22 homeserver: String,
24
25 db_path: PathBuf,
27
28 passphrase: String,
30}
31
32#[derive(Debug, Serialize, Deserialize)]
34struct FullSession {
35 client_session: ClientSession,
37
38 user_session: MatrixSession,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
47 sync_token: Option<String>,
48}
49
50#[tokio::main]
66async fn main() -> anyhow::Result<()> {
67 tracing_subscriber::fmt::init();
68
69 let data_dir = dirs::data_dir().expect("no data_dir directory found").join("persist_session");
71 let session_file = data_dir.join("session");
73
74 let (client, sync_token) = if session_file.exists() {
75 restore_session(&session_file).await?
76 } else {
77 (login(&data_dir, &session_file).await?, None)
78 };
79
80 sync(client, sync_token, &session_file).await
81}
82
83async fn restore_session(session_file: &Path) -> anyhow::Result<(Client, Option<String>)> {
85 println!("Previous session found in '{}'", session_file.to_string_lossy());
86
87 let serialized_session = fs::read_to_string(session_file).await?;
89 let FullSession { client_session, user_session, sync_token } =
90 serde_json::from_str(&serialized_session)?;
91
92 let client = Client::builder()
94 .homeserver_url(client_session.homeserver)
95 .sqlite_store(client_session.db_path, Some(&client_session.passphrase))
96 .build()
97 .await?;
98
99 println!("Restoring session for {}…", user_session.meta.user_id);
100
101 client.restore_session(user_session).await?;
103
104 Ok((client, sync_token))
105}
106
107async fn login(data_dir: &Path, session_file: &Path) -> anyhow::Result<Client> {
109 println!("No previous session found, logging in…");
110
111 let (client, client_session) = build_client(data_dir).await?;
112 let matrix_auth = client.matrix_auth();
113
114 loop {
115 print!("\nUsername: ");
116 io::stdout().flush().expect("Unable to write to stdout");
117 let mut username = String::new();
118 io::stdin().read_line(&mut username).expect("Unable to read user input");
119 username = username.trim().to_owned();
120
121 print!("Password: ");
122 io::stdout().flush().expect("Unable to write to stdout");
123 let mut password = String::new();
124 io::stdin().read_line(&mut password).expect("Unable to read user input");
125 password = password.trim().to_owned();
126
127 match matrix_auth
128 .login_username(&username, &password)
129 .initial_device_display_name("persist-session client")
130 .await
131 {
132 Ok(_) => {
133 println!("Logged in as {username}");
134 break;
135 }
136 Err(error) => {
137 println!("Error logging in: {error}");
138 println!("Please try again\n");
139 }
140 }
141 }
142
143 let user_session = matrix_auth.session().expect("A logged-in client should have a session");
148 let serialized_session =
149 serde_json::to_string(&FullSession { client_session, user_session, sync_token: None })?;
150 fs::write(session_file, serialized_session).await?;
151
152 println!("Session persisted in {}", session_file.to_string_lossy());
153
154 Ok(client)
161}
162
163async fn build_client(data_dir: &Path) -> anyhow::Result<(Client, ClientSession)> {
165 let mut rng = thread_rng();
166
167 let db_subfolder: String =
171 (&mut rng).sample_iter(Alphanumeric).take(7).map(char::from).collect();
172 let db_path = data_dir.join(db_subfolder);
173
174 let passphrase: String =
176 (&mut rng).sample_iter(Alphanumeric).take(32).map(char::from).collect();
177
178 loop {
180 let mut homeserver = String::new();
181
182 print!("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");
185
186 println!("\nChecking homeserver…");
187
188 match Client::builder()
189 .homeserver_url(&homeserver)
190 .sqlite_store(&db_path, Some(&passphrase))
194 .build()
195 .await
196 {
197 Ok(client) => return Ok((client, ClientSession { homeserver, db_path, passphrase })),
198 Err(error) => match &error {
199 matrix_sdk::ClientBuildError::AutoDiscovery(_)
200 | matrix_sdk::ClientBuildError::Url(_)
201 | matrix_sdk::ClientBuildError::Http(_) => {
202 println!("Error checking the homeserver: {error}");
203 println!("Please try again\n");
204 }
205 _ => {
206 return Err(error.into());
208 }
209 },
210 }
211 }
212}
213
214async fn sync(
216 client: Client,
217 initial_sync_token: Option<String>,
218 session_file: &Path,
219) -> anyhow::Result<()> {
220 println!("Launching a first sync to ignore past messages…");
221
222 let filter = FilterDefinition::with_lazy_loading();
226
227 let mut sync_settings = SyncSettings::default().filter(filter.into());
228
229 if let Some(sync_token) = initial_sync_token {
233 sync_settings = sync_settings.token(sync_token);
234 }
235
236 loop {
241 match client.sync_once(sync_settings.clone()).await {
242 Ok(response) => {
243 sync_settings = sync_settings.token(response.next_batch.clone());
246 persist_sync_token(session_file, response.next_batch).await?;
247 break;
248 }
249 Err(error) => {
250 println!("An error occurred during initial sync: {error}");
251 println!("Trying again…");
252 }
253 }
254 }
255
256 println!("The client is ready! Listening to new messages…");
257
258 client.add_event_handler(on_room_message);
260
261 client
263 .sync_with_result_callback(sync_settings, |sync_result| async move {
264 let response = sync_result?;
265
266 persist_sync_token(session_file, response.next_batch)
268 .await
269 .map_err(|err| Error::UnknownError(err.into()))?;
270
271 Ok(LoopCtrl::Continue)
272 })
273 .await?;
274
275 Ok(())
276}
277
278async fn persist_sync_token(session_file: &Path, sync_token: String) -> anyhow::Result<()> {
282 let serialized_session = fs::read_to_string(session_file).await?;
283 let mut full_session: FullSession = serde_json::from_str(&serialized_session)?;
284
285 full_session.sync_token = Some(sync_token);
286 let serialized_session = serde_json::to_string(&full_session)?;
287 fs::write(session_file, serialized_session).await?;
288
289 Ok(())
290}
291
292async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) {
294 if room.state() != RoomState::Joined {
296 return;
297 }
298 let MessageType::Text(text_content) = &event.content.msgtype else { return };
299
300 let room_name = match room.display_name().await {
301 Ok(room_name) => room_name.to_string(),
302 Err(error) => {
303 println!("Error getting room display name: {error}");
304 room.room_id().to_string()
306 }
307 };
308
309 println!("[{room_name}] {}: {}", event.sender, text_content.body)
310}