Skip to main content

matrix_ui_serializable/init/
session.rs

1use anyhow::anyhow;
2use serde::{Deserialize, Serialize};
3use tracing::{error, info};
4
5use matrix_sdk::{
6    AuthSession, Client,
7    authentication::{
8        matrix::MatrixSession,
9        oauth::{ClientId, OAuthSession, UserSession},
10    },
11};
12
13use std::sync::Arc;
14
15use crate::{
16    init::{
17        login::build_client,
18        singletons::{CLIENT, HAS_SESSION_STORED},
19    },
20    models::{
21        events::{ToastNotificationRequest, ToastNotificationVariant},
22        state_updater::StateUpdater,
23    },
24    room::notifications::enqueue_toast_notification,
25};
26
27/// The data needed to re-build a client.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ClientSession {
30    /// The URL of the homeserver of the user.
31    pub(crate) homeserver: String,
32
33    /// The random identifier of the DB (to avoid collision with old data).
34    /// We do not store the full path since it can change when updating on some devices (iOS for instance)
35    pub(crate) db_identifier: String,
36
37    /// The passphrase of the database.
38    pub(crate) passphrase: String,
39}
40
41impl ClientSession {
42    pub fn new(homeserver: String, db_identifier: String, passphrase: String) -> Self {
43        ClientSession {
44            homeserver,
45            db_identifier,
46            passphrase,
47        }
48    }
49}
50
51#[derive(Debug, Serialize, Deserialize)]
52pub struct FullMatrixSession {
53    pub client_session: ClientSession,
54    pub user_session: SerializableAuthSession,
55}
56
57/// A user session using one of the available authentication APIs.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub enum SerializableAuthSession {
60    /// A session using the native Matrix authentication API.
61    Matrix(MatrixSession),
62
63    /// A session using the OAuth 2.0 API.
64    OAuth(SerializableOAuthSession),
65}
66
67impl From<AuthSession> for SerializableAuthSession {
68    fn from(value: AuthSession) -> Self {
69        match value {
70            AuthSession::Matrix(m) => Self::Matrix(m),
71            AuthSession::OAuth(o) => Self::OAuth(o.into()),
72            _ => panic!("This type of auth is not yet supported"),
73        }
74    }
75}
76
77impl From<SerializableAuthSession> for AuthSession {
78    fn from(value: SerializableAuthSession) -> Self {
79        match value {
80            SerializableAuthSession::Matrix(m) => Self::Matrix(m),
81            SerializableAuthSession::OAuth(o) => Self::OAuth(Box::new(o.into())),
82        }
83    }
84}
85
86/// A full session for the OAuth 2.0 API, with the serialize trait.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct SerializableOAuthSession {
89    /// The client ID obtained after registration.
90    pub client_id: ClientId,
91
92    /// The user session.
93    pub user: UserSession,
94}
95
96impl From<Box<OAuthSession>> for SerializableOAuthSession {
97    fn from(value: Box<OAuthSession>) -> Self {
98        Self {
99            client_id: value.client_id,
100            user: value.user,
101        }
102    }
103}
104
105impl From<SerializableOAuthSession> for OAuthSession {
106    fn from(value: SerializableOAuthSession) -> Self {
107        Self {
108            client_id: value.client_id,
109            user: value.user,
110        }
111    }
112}
113
114impl FullMatrixSession {
115    pub fn new(client_session: ClientSession, user_session: AuthSession) -> Self {
116        FullMatrixSession {
117            client_session,
118            user_session: user_session.into(),
119        }
120    }
121}
122
123pub async fn restore_client_from_session(session: FullMatrixSession) -> anyhow::Result<Client> {
124    let FullMatrixSession {
125        client_session,
126        user_session,
127    } = session;
128
129    let (client, _) = build_client(None, Some(client_session)).await?;
130
131    client.restore_session(user_session).await?;
132
133    CLIENT
134        .set(client.clone())
135        .expect("BUG: CLIENT already set!");
136
137    Ok(client)
138}
139
140pub async fn try_restore_session_to_state(
141    session_option: Option<String>,
142) -> crate::Result<Option<Client>> {
143    match session_option {
144        None => {
145            HAS_SESSION_STORED
146                .set(false)
147                .map_err(|b| anyhow!("HAS_SESSION_STORED was already defined. {b}"))?;
148            Ok(None)
149        }
150        Some(session_string) => {
151            HAS_SESSION_STORED
152                .set(true)
153                .map_err(|b| anyhow!("HAS_SESSION_STORED was already defined. {b}"))?;
154            let session: FullMatrixSession =
155                serde_json::from_str(&session_string).map_err(|e| anyhow!(e))?;
156            let initial_client = restore_client_from_session(session).await?;
157            Ok(Some(initial_client))
158        }
159    }
160}
161
162/// Sets up this client so that it automatically saves the session into keychain
163/// whenever there are new tokens that have been received.
164///
165/// This should always be set up whenever automatic refresh is happening.
166pub(crate) fn setup_token_background_save(updater: Arc<Box<dyn StateUpdater>>) {
167    tokio::spawn(async move {
168        let client = CLIENT.wait();
169        while let Ok(update) = client.subscribe_to_session_changes().recv().await {
170            match update {
171                matrix_sdk::SessionChange::UnknownToken(s) => {
172                    enqueue_toast_notification(ToastNotificationRequest::new(
173                        format!(
174                            "This session is no longer valid. Soft logout: {}",
175                            s.soft_logout
176                        ),
177                        None,
178                        ToastNotificationVariant::Error,
179                    ));
180                    error!(
181                        "Received an unknown token error; soft logout? {}",
182                        s.soft_logout
183                    );
184                }
185                matrix_sdk::SessionChange::TokensRefreshed => {
186                    // The tokens have been refreshed, persist them to disk.
187                    if let Err(err) = update_stored_session(client, updater.clone()).await {
188                        enqueue_toast_notification(ToastNotificationRequest::new(
189                            format!("Failed to persist refreshed session. Error: {err}"),
190                            None,
191                            ToastNotificationVariant::Error,
192                        ));
193                        error!("Unable to store a session in the background: {err}");
194                    }
195                }
196            }
197        }
198    });
199}
200
201/// Update the session stored in the keychain.
202///
203/// This should be called everytime the access token (and possibly refresh
204/// token) has changed.
205async fn update_stored_session(
206    client: &Client,
207    updater: Arc<Box<dyn StateUpdater>>,
208) -> anyhow::Result<()> {
209    info!("Updating the stored session...");
210
211    let user_session = client
212        .session()
213        .ok_or(anyhow!("No auth session available to persist!"))?;
214
215    updater
216        .as_ref()
217        .persist_refreshed_session(user_session)
218        .await?;
219
220    info!("Updating the stored session: done!");
221    Ok(())
222}