atagen

macltok

public
Star0
Watch0
0d763e4284ef82eb13418fd683b8c8866f099c92
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: src/account.rs

62fe32759529atagen2026-09-26 08:50 UTC1
#[cfg(feature = "sqlite")]
62fe32759529atagen2026-09-26 08:50 UTC2
use std::path::PathBuf;
f12377bce9b9atagen2026-09-26 12:32 UTC3
#[cfg(not(target_family = "wasm"))]
f12377bce9b9atagen2026-09-26 12:32 UTC4
use std::sync::atomic::{AtomicBool, Ordering};
713205a95af2atagen2026-09-26 09:24 UTC5
use std::{collections::HashMap, sync::Arc};
96fdf90aacd6atagen2026-09-08 08:24 UTC6
96fdf90aacd6atagen2026-09-08 08:24 UTC7
use matrix_sdk::{
f12377bce9b9atagen2026-09-26 12:32 UTC8
    Client, ClientBuilder, SendOutsideWasm, SessionChange, SessionMeta, SyncOutsideWasm,
f12377bce9b9atagen2026-09-26 12:32 UTC9
    ThreadingSupport,
96fdf90aacd6atagen2026-09-08 08:24 UTC10
    authentication::{
96fdf90aacd6atagen2026-09-08 08:24 UTC11
        AuthSession,
96fdf90aacd6atagen2026-09-08 08:24 UTC12
        matrix::MatrixSession,
96fdf90aacd6atagen2026-09-08 08:24 UTC13
        oauth::{
d8d8a9fe770aatagen2026-10-22 07:44 UTC14
            ClientId, ClientRegistrationData, CsrfToken, OAuthSession, PendingAuthorization,
d8d8a9fe770aatagen2026-10-22 07:44 UTC15
            UserSession,
96fdf90aacd6atagen2026-09-08 08:24 UTC16
            registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
96fdf90aacd6atagen2026-09-08 08:24 UTC17
        },
96fdf90aacd6atagen2026-09-08 08:24 UTC18
    },
62fe32759529atagen2026-09-26 08:50 UTC19
    cross_process_lock::CrossProcessLockConfig,
96fdf90aacd6atagen2026-09-08 08:24 UTC20
    encryption::{BackupDownloadStrategy, EncryptionSettings},
d8d8a9fe770aatagen2026-10-22 07:44 UTC21
    ruma::{MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, serde::Raw},
96fdf90aacd6atagen2026-09-08 08:24 UTC22
};
96fdf90aacd6atagen2026-09-08 08:24 UTC23
use serde::{Deserialize, Serialize};
96fdf90aacd6atagen2026-09-08 08:24 UTC24
use thiserror::Error;
f12377bce9b9atagen2026-09-26 12:32 UTC25
use tokio::sync::{broadcast, mpsc};
96fdf90aacd6atagen2026-09-08 08:24 UTC26
use url::Url;
96fdf90aacd6atagen2026-09-08 08:24 UTC27
713205a95af2atagen2026-09-26 09:24 UTC28
use crate::{
9a25b6ce66f4atagen2026-10-08 11:15 UTC29
    ClientHandle, Engine, EngineConfig, EngineError, ErrorKind, EventStream, ServerError,
9a25b6ce66f4atagen2026-10-08 11:15 UTC30
    SessionStore,
a250c7fa43fbatagen2026-09-26 13:35 UTC31
    engine::{Persistence, StoreLock},
a250c7fa43fbatagen2026-09-26 13:35 UTC32
    error::SourceError,
713205a95af2atagen2026-09-26 09:24 UTC33
};
96fdf90aacd6atagen2026-09-08 08:24 UTC34
f12377bce9b9atagen2026-09-26 12:32 UTC35
/// a logged in client and the `SessionStore` its record lives in. the engine it builds
f12377bce9b9atagen2026-09-26 12:32 UTC36
/// saves every token rotation there, unless told `without_persistence`
96fdf90aacd6atagen2026-09-08 08:24 UTC37
pub struct Session {
96fdf90aacd6atagen2026-09-08 08:24 UTC38
    client: Client,
f12377bce9b9atagen2026-09-26 12:32 UTC39
    tracking: Tracking,
a250c7fa43fbatagen2026-09-26 13:35 UTC40
    store_lock: StoreLock,
f12377bce9b9atagen2026-09-26 12:32 UTC41
}
f12377bce9b9atagen2026-09-26 12:32 UTC42
f12377bce9b9atagen2026-09-26 12:32 UTC43
/// taken as soon as the client exists, since the sdk can refresh before there's an engine
f12377bce9b9atagen2026-09-26 12:32 UTC44
struct Tracking {
cbb715e11963atagen2026-10-24 23:24 UTC45
    store: Arc<dyn SessionStore>,
f12377bce9b9atagen2026-09-26 12:32 UTC46
    session_changes: broadcast::Receiver<SessionChange>,
f12377bce9b9atagen2026-09-26 12:32 UTC47
    persistence: Option<Persistence>,
f12377bce9b9atagen2026-09-26 12:32 UTC48
    #[cfg(not(target_family = "wasm"))]
f12377bce9b9atagen2026-09-26 12:32 UTC49
    inline: Arc<AtomicBool>,
96fdf90aacd6atagen2026-09-08 08:24 UTC50
}
96fdf90aacd6atagen2026-09-08 08:24 UTC51
31ce2453c82fatagen2026-09-24 13:07 UTC52
#[non_exhaustive]
96fdf90aacd6atagen2026-09-08 08:24 UTC53
pub struct PasswordLogin {
96fdf90aacd6atagen2026-09-08 08:24 UTC54
    pub homeserver: String,
96fdf90aacd6atagen2026-09-08 08:24 UTC55
    pub username: String,
96fdf90aacd6atagen2026-09-08 08:24 UTC56
    pub password: String,
96fdf90aacd6atagen2026-09-08 08:24 UTC57
    pub device_display_name: Option<String>,
fa9114fcf75fatagen2026-09-24 12:51 UTC58
    /// set after a soft logout so the store's keys still belong to us
fa9114fcf75fatagen2026-09-24 12:51 UTC59
    pub device_id: Option<OwnedDeviceId>,
9c05b1dd327aatagen2026-10-08 11:27 UTC60
    /// let plain http through, for loopback hosts only
9c05b1dd327aatagen2026-10-08 11:27 UTC61
    pub allow_insecure_loopback: bool,
96fdf90aacd6atagen2026-09-08 08:24 UTC62
}
96fdf90aacd6atagen2026-09-08 08:24 UTC63
31ce2453c82fatagen2026-09-24 13:07 UTC64
impl PasswordLogin {
31ce2453c82fatagen2026-09-24 13:07 UTC65
    #[must_use]
31ce2453c82fatagen2026-09-24 13:07 UTC66
    pub fn new(
31ce2453c82fatagen2026-09-24 13:07 UTC67
        homeserver: impl Into<String>,
31ce2453c82fatagen2026-09-24 13:07 UTC68
        username: impl Into<String>,
31ce2453c82fatagen2026-09-24 13:07 UTC69
        password: impl Into<String>,
31ce2453c82fatagen2026-09-24 13:07 UTC70
    ) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC71
        Self {
31ce2453c82fatagen2026-09-24 13:07 UTC72
            homeserver: homeserver.into(),
31ce2453c82fatagen2026-09-24 13:07 UTC73
            username: username.into(),
31ce2453c82fatagen2026-09-24 13:07 UTC74
            password: password.into(),
31ce2453c82fatagen2026-09-24 13:07 UTC75
            device_display_name: None,
31ce2453c82fatagen2026-09-24 13:07 UTC76
            device_id: None,
9c05b1dd327aatagen2026-10-08 11:27 UTC77
            allow_insecure_loopback: false,
31ce2453c82fatagen2026-09-24 13:07 UTC78
        }
31ce2453c82fatagen2026-09-24 13:07 UTC79
    }
31ce2453c82fatagen2026-09-24 13:07 UTC80
31ce2453c82fatagen2026-09-24 13:07 UTC81
    #[must_use]
31ce2453c82fatagen2026-09-24 13:07 UTC82
    pub fn with_device_display_name(mut self, name: impl Into<String>) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC83
        self.device_display_name = Some(name.into());
31ce2453c82fatagen2026-09-24 13:07 UTC84
        self
31ce2453c82fatagen2026-09-24 13:07 UTC85
    }
31ce2453c82fatagen2026-09-24 13:07 UTC86
31ce2453c82fatagen2026-09-24 13:07 UTC87
    #[must_use]
31ce2453c82fatagen2026-09-24 13:07 UTC88
    pub fn with_device_id(mut self, device_id: OwnedDeviceId) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC89
        self.device_id = Some(device_id);
31ce2453c82fatagen2026-09-24 13:07 UTC90
        self
31ce2453c82fatagen2026-09-24 13:07 UTC91
    }
9c05b1dd327aatagen2026-10-08 11:27 UTC92
9c05b1dd327aatagen2026-10-08 11:27 UTC93
    #[must_use]
9c05b1dd327aatagen2026-10-08 11:27 UTC94
    pub fn allow_insecure_loopback(mut self, allow: bool) -> Self {
9c05b1dd327aatagen2026-10-08 11:27 UTC95
        self.allow_insecure_loopback = allow;
9c05b1dd327aatagen2026-10-08 11:27 UTC96
        self
9c05b1dd327aatagen2026-10-08 11:27 UTC97
    }
31ce2453c82fatagen2026-09-24 13:07 UTC98
}
31ce2453c82fatagen2026-09-24 13:07 UTC99
31ce2453c82fatagen2026-09-24 13:07 UTC100
#[non_exhaustive]
96fdf90aacd6atagen2026-09-08 08:24 UTC101
pub struct OAuthLogin {
96fdf90aacd6atagen2026-09-08 08:24 UTC102
    pub homeserver: String,
96fdf90aacd6atagen2026-09-08 08:24 UTC103
    pub redirect_uri: String,
96fdf90aacd6atagen2026-09-08 08:24 UTC104
    pub client_uri: String,
96fdf90aacd6atagen2026-09-08 08:24 UTC105
    pub client_name: Option<String>,
96fdf90aacd6atagen2026-09-08 08:24 UTC106
    pub client_id: Option<String>,
96fdf90aacd6atagen2026-09-08 08:24 UTC107
    pub device_id: Option<OwnedDeviceId>,
96fdf90aacd6atagen2026-09-08 08:24 UTC108
    pub application_type: OAuthApplicationType,
9c05b1dd327aatagen2026-10-08 11:27 UTC109
    /// let plain http through, for loopback hosts only
9c05b1dd327aatagen2026-10-08 11:27 UTC110
    pub allow_insecure_loopback: bool,
96fdf90aacd6atagen2026-09-08 08:24 UTC111
}
96fdf90aacd6atagen2026-09-08 08:24 UTC112
31ce2453c82fatagen2026-09-24 13:07 UTC113
impl OAuthLogin {
31ce2453c82fatagen2026-09-24 13:07 UTC114
    #[must_use]
31ce2453c82fatagen2026-09-24 13:07 UTC115
    pub fn new(
31ce2453c82fatagen2026-09-24 13:07 UTC116
        homeserver: impl Into<String>,
31ce2453c82fatagen2026-09-24 13:07 UTC117
        redirect_uri: impl Into<String>,
31ce2453c82fatagen2026-09-24 13:07 UTC118
        client_uri: impl Into<String>,
31ce2453c82fatagen2026-09-24 13:07 UTC119
        application_type: OAuthApplicationType,
31ce2453c82fatagen2026-09-24 13:07 UTC120
    ) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC121
        Self {
31ce2453c82fatagen2026-09-24 13:07 UTC122
            homeserver: homeserver.into(),
31ce2453c82fatagen2026-09-24 13:07 UTC123
            redirect_uri: redirect_uri.into(),
31ce2453c82fatagen2026-09-24 13:07 UTC124
            client_uri: client_uri.into(),
31ce2453c82fatagen2026-09-24 13:07 UTC125
            client_name: None,
31ce2453c82fatagen2026-09-24 13:07 UTC126
            client_id: None,
31ce2453c82fatagen2026-09-24 13:07 UTC127
            device_id: None,
31ce2453c82fatagen2026-09-24 13:07 UTC128
            application_type,
9c05b1dd327aatagen2026-10-08 11:27 UTC129
            allow_insecure_loopback: false,
31ce2453c82fatagen2026-09-24 13:07 UTC130
        }
31ce2453c82fatagen2026-09-24 13:07 UTC131
    }
31ce2453c82fatagen2026-09-24 13:07 UTC132
31ce2453c82fatagen2026-09-24 13:07 UTC133
    #[must_use]
31ce2453c82fatagen2026-09-24 13:07 UTC134
    pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC135
        self.client_name = Some(name.into());
31ce2453c82fatagen2026-09-24 13:07 UTC136
        self
31ce2453c82fatagen2026-09-24 13:07 UTC137
    }
31ce2453c82fatagen2026-09-24 13:07 UTC138
31ce2453c82fatagen2026-09-24 13:07 UTC139
    /// a statically registered client id, skipping dynamic registration
31ce2453c82fatagen2026-09-24 13:07 UTC140
    #[must_use]
31ce2453c82fatagen2026-09-24 13:07 UTC141
    pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC142
        self.client_id = Some(client_id.into());
31ce2453c82fatagen2026-09-24 13:07 UTC143
        self
31ce2453c82fatagen2026-09-24 13:07 UTC144
    }
31ce2453c82fatagen2026-09-24 13:07 UTC145
31ce2453c82fatagen2026-09-24 13:07 UTC146
    #[must_use]
31ce2453c82fatagen2026-09-24 13:07 UTC147
    pub fn with_device_id(mut self, device_id: OwnedDeviceId) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC148
        self.device_id = Some(device_id);
31ce2453c82fatagen2026-09-24 13:07 UTC149
        self
31ce2453c82fatagen2026-09-24 13:07 UTC150
    }
9c05b1dd327aatagen2026-10-08 11:27 UTC151
9c05b1dd327aatagen2026-10-08 11:27 UTC152
    #[must_use]
9c05b1dd327aatagen2026-10-08 11:27 UTC153
    pub fn allow_insecure_loopback(mut self, allow: bool) -> Self {
9c05b1dd327aatagen2026-10-08 11:27 UTC154
        self.allow_insecure_loopback = allow;
9c05b1dd327aatagen2026-10-08 11:27 UTC155
        self
9c05b1dd327aatagen2026-10-08 11:27 UTC156
    }
31ce2453c82fatagen2026-09-24 13:07 UTC157
}
31ce2453c82fatagen2026-09-24 13:07 UTC158
96fdf90aacd6atagen2026-09-08 08:24 UTC159
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31ce2453c82fatagen2026-09-24 13:07 UTC160
#[non_exhaustive]
96fdf90aacd6atagen2026-09-08 08:24 UTC161
pub enum OAuthApplicationType {
96fdf90aacd6atagen2026-09-08 08:24 UTC162
    Native,
96fdf90aacd6atagen2026-09-08 08:24 UTC163
    Web,
96fdf90aacd6atagen2026-09-08 08:24 UTC164
}
96fdf90aacd6atagen2026-09-08 08:24 UTC165
96fdf90aacd6atagen2026-09-08 08:24 UTC166
pub struct PendingOAuthLogin {
96fdf90aacd6atagen2026-09-08 08:24 UTC167
    client: Client,
713205a95af2atagen2026-09-26 09:24 UTC168
    sessions: Arc<dyn SessionStore>,
a250c7fa43fbatagen2026-09-26 13:35 UTC169
    tracking: Option<(Tracking, StoreLock)>,
96fdf90aacd6atagen2026-09-08 08:24 UTC170
    authorization_url: String,
96fdf90aacd6atagen2026-09-08 08:24 UTC171
    state: Option<CsrfToken>,
d8d8a9fe770aatagen2026-10-22 07:44 UTC172
    previous: Option<SessionRecord>,
96fdf90aacd6atagen2026-09-08 08:24 UTC173
}
96fdf90aacd6atagen2026-09-08 08:24 UTC174
96fdf90aacd6atagen2026-09-08 08:24 UTC175
impl PendingOAuthLogin {
96fdf90aacd6atagen2026-09-08 08:24 UTC176
    #[must_use]
96fdf90aacd6atagen2026-09-08 08:24 UTC177
    pub fn authorization_url(&self) -> &str {
96fdf90aacd6atagen2026-09-08 08:24 UTC178
        &self.authorization_url
96fdf90aacd6atagen2026-09-08 08:24 UTC179
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC180
96fdf90aacd6atagen2026-09-08 08:24 UTC181
    #[must_use]
96fdf90aacd6atagen2026-09-08 08:24 UTC182
    pub fn registered_client_id(&self) -> String {
96fdf90aacd6atagen2026-09-08 08:24 UTC183
        self.client
96fdf90aacd6atagen2026-09-08 08:24 UTC184
            .oauth()
96fdf90aacd6atagen2026-09-08 08:24 UTC185
            .client_id()
96fdf90aacd6atagen2026-09-08 08:24 UTC186
            .expect("OAuth client was registered before authorization")
96fdf90aacd6atagen2026-09-08 08:24 UTC187
            .as_str()
96fdf90aacd6atagen2026-09-08 08:24 UTC188
            .to_owned()
96fdf90aacd6atagen2026-09-08 08:24 UTC189
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC190
96fdf90aacd6atagen2026-09-08 08:24 UTC191
    #[must_use]
96fdf90aacd6atagen2026-09-08 08:24 UTC192
    pub const fn is_pending(&self) -> bool {
96fdf90aacd6atagen2026-09-08 08:24 UTC193
        self.state.is_some()
96fdf90aacd6atagen2026-09-08 08:24 UTC194
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC195
96fdf90aacd6atagen2026-09-08 08:24 UTC196
    pub async fn finish(&mut self, callback_url: &str) -> Result<Session, AccountError> {
96fdf90aacd6atagen2026-09-08 08:24 UTC197
        let callback_url = Url::parse(callback_url)
96fdf90aacd6atagen2026-09-08 08:24 UTC198
            .map_err(|error| account_error("parsing the OAuth callback URL", error))?;
96fdf90aacd6atagen2026-09-08 08:24 UTC199
        let state = self
96fdf90aacd6atagen2026-09-08 08:24 UTC200
            .state
96fdf90aacd6atagen2026-09-08 08:24 UTC201
            .take()
96fdf90aacd6atagen2026-09-08 08:24 UTC202
            .ok_or(AccountError::OAuthCallbackAlreadySubmitted)?;
96fdf90aacd6atagen2026-09-08 08:24 UTC203
        let oauth = self.client.oauth();
96fdf90aacd6atagen2026-09-08 08:24 UTC204
        if let Err(error) = oauth.finish_login(callback_url.into()).await {
96fdf90aacd6atagen2026-09-08 08:24 UTC205
            oauth.abort_login(&state).await;
d8d8a9fe770aatagen2026-10-22 07:44 UTC206
            if let Some(previous) = self.previous.take() {
d8d8a9fe770aatagen2026-10-22 07:44 UTC207
                put_back(self.sessions.as_ref(), previous).await;
d8d8a9fe770aatagen2026-10-22 07:44 UTC208
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC209
            return Err(account_error("OAuth login", error));
96fdf90aacd6atagen2026-09-08 08:24 UTC210
        }
a250c7fa43fbatagen2026-09-26 13:35 UTC211
        let (tracking, store_lock) = self
f12377bce9b9atagen2026-09-26 12:32 UTC212
            .tracking
f12377bce9b9atagen2026-09-26 12:32 UTC213
            .take()
f12377bce9b9atagen2026-09-26 12:32 UTC214
            .ok_or(AccountError::OAuthCallbackAlreadySubmitted)?;
a250c7fa43fbatagen2026-09-26 13:35 UTC215
        Session::saved(
a250c7fa43fbatagen2026-09-26 13:35 UTC216
            self.client.clone(),
a250c7fa43fbatagen2026-09-26 13:35 UTC217
            self.sessions.clone(),
a250c7fa43fbatagen2026-09-26 13:35 UTC218
            tracking,
a250c7fa43fbatagen2026-09-26 13:35 UTC219
            store_lock,
a250c7fa43fbatagen2026-09-26 13:35 UTC220
        )
a250c7fa43fbatagen2026-09-26 13:35 UTC221
        .await
96fdf90aacd6atagen2026-09-08 08:24 UTC222
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC223
d8d8a9fe770aatagen2026-10-22 07:44 UTC224
    /// puts back what the store held before, or deletes the store if it held nothing
d8d8a9fe770aatagen2026-10-22 07:44 UTC225
    pub async fn abort(self) {
d8d8a9fe770aatagen2026-10-22 07:44 UTC226
        let Self {
d8d8a9fe770aatagen2026-10-22 07:44 UTC227
            client,
d8d8a9fe770aatagen2026-10-22 07:44 UTC228
            sessions,
d8d8a9fe770aatagen2026-10-22 07:44 UTC229
            tracking,
d8d8a9fe770aatagen2026-10-22 07:44 UTC230
            state,
d8d8a9fe770aatagen2026-10-22 07:44 UTC231
            previous,
d8d8a9fe770aatagen2026-10-22 07:44 UTC232
            ..
d8d8a9fe770aatagen2026-10-22 07:44 UTC233
        } = self;
d8d8a9fe770aatagen2026-10-22 07:44 UTC234
        let Some(state) = state else {
d8d8a9fe770aatagen2026-10-22 07:44 UTC235
            return;
d8d8a9fe770aatagen2026-10-22 07:44 UTC236
        };
d8d8a9fe770aatagen2026-10-22 07:44 UTC237
        client.oauth().abort_login(&state).await;
d8d8a9fe770aatagen2026-10-22 07:44 UTC238
        drop(client);
d8d8a9fe770aatagen2026-10-22 07:44 UTC239
        match previous {
d8d8a9fe770aatagen2026-10-22 07:44 UTC240
            Some(previous) => put_back(sessions.as_ref(), previous).await,
d8d8a9fe770aatagen2026-10-22 07:44 UTC241
            None => {
d8d8a9fe770aatagen2026-10-22 07:44 UTC242
                if let Err(error) = sessions.discard().await {
d8d8a9fe770aatagen2026-10-22 07:44 UTC243
                    tracing::warn!(%error, "deleting an abandoned OAuth login's store failed");
d8d8a9fe770aatagen2026-10-22 07:44 UTC244
                }
d8d8a9fe770aatagen2026-10-22 07:44 UTC245
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC246
        }
d8d8a9fe770aatagen2026-10-22 07:44 UTC247
        drop(tracking);
d8d8a9fe770aatagen2026-10-22 07:44 UTC248
    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC249
}
d8d8a9fe770aatagen2026-10-22 07:44 UTC250
d8d8a9fe770aatagen2026-10-22 07:44 UTC251
async fn put_back(sessions: &dyn SessionStore, previous: SessionRecord) {
d8d8a9fe770aatagen2026-10-22 07:44 UTC252
    if let Err(error) = sessions.save(&previous).await {
d8d8a9fe770aatagen2026-10-22 07:44 UTC253
        tracing::warn!(%error, "putting back the record an OAuth login replaced failed");
96fdf90aacd6atagen2026-09-08 08:24 UTC254
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC255
}
96fdf90aacd6atagen2026-09-08 08:24 UTC256
96fdf90aacd6atagen2026-09-08 08:24 UTC257
pub struct StoreConfig {
62fe32759529atagen2026-09-26 08:50 UTC258
    location: StoreLocation,
96fdf90aacd6atagen2026-09-08 08:24 UTC259
    passphrase: String,
62fe32759529atagen2026-09-26 08:50 UTC260
}
62fe32759529atagen2026-09-26 08:50 UTC261
62fe32759529atagen2026-09-26 08:50 UTC262
enum StoreLocation {
62fe32759529atagen2026-09-26 08:50 UTC263
    #[cfg(feature = "sqlite")]
62fe32759529atagen2026-09-26 08:50 UTC264
    Sqlite(PathBuf),
62fe32759529atagen2026-09-26 08:50 UTC265
    #[cfg(feature = "indexeddb")]
62fe32759529atagen2026-09-26 08:50 UTC266
    IndexedDb(String),
96fdf90aacd6atagen2026-09-08 08:24 UTC267
}
96fdf90aacd6atagen2026-09-08 08:24 UTC268
96fdf90aacd6atagen2026-09-08 08:24 UTC269
impl StoreConfig {
62fe32759529atagen2026-09-26 08:50 UTC270
    #[cfg(feature = "sqlite")]
62fe32759529atagen2026-09-26 08:50 UTC271
    #[must_use]
62fe32759529atagen2026-09-26 08:50 UTC272
    pub fn sqlite(path: impl Into<PathBuf>, passphrase: impl Into<String>) -> Self {
62fe32759529atagen2026-09-26 08:50 UTC273
        Self {
62fe32759529atagen2026-09-26 08:50 UTC274
            location: StoreLocation::Sqlite(path.into()),
62fe32759529atagen2026-09-26 08:50 UTC275
            passphrase: passphrase.into(),
62fe32759529atagen2026-09-26 08:50 UTC276
        }
62fe32759529atagen2026-09-26 08:50 UTC277
    }
62fe32759529atagen2026-09-26 08:50 UTC278
62fe32759529atagen2026-09-26 08:50 UTC279
    #[cfg(feature = "indexeddb")]
96fdf90aacd6atagen2026-09-08 08:24 UTC280
    #[must_use]
62fe32759529atagen2026-09-26 08:50 UTC281
    pub fn indexeddb(name: impl Into<String>, passphrase: impl Into<String>) -> Self {
96fdf90aacd6atagen2026-09-08 08:24 UTC282
        Self {
62fe32759529atagen2026-09-26 08:50 UTC283
            location: StoreLocation::IndexedDb(name.into()),
96fdf90aacd6atagen2026-09-08 08:24 UTC284
            passphrase: passphrase.into(),
96fdf90aacd6atagen2026-09-08 08:24 UTC285
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC286
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC287
713205a95af2atagen2026-09-26 09:24 UTC288
    /// delete the store's databases, once nothing has them open
713205a95af2atagen2026-09-26 09:24 UTC289
    pub async fn delete(&self) -> Result<(), AccountError> {
713205a95af2atagen2026-09-26 09:24 UTC290
        match &self.location {
713205a95af2atagen2026-09-26 09:24 UTC291
            #[cfg(feature = "sqlite")]
713205a95af2atagen2026-09-26 09:24 UTC292
            StoreLocation::Sqlite(path) => crate::session_store::delete_sqlite_store(path)
713205a95af2atagen2026-09-26 09:24 UTC293
                .map_err(|error| account_error("deleting the Matrix store", error)),
713205a95af2atagen2026-09-26 09:24 UTC294
            #[cfg(feature = "indexeddb")]
713205a95af2atagen2026-09-26 09:24 UTC295
            StoreLocation::IndexedDb(name) => crate::session_store::delete_indexeddb_store(name)
713205a95af2atagen2026-09-26 09:24 UTC296
                .await
6db49c0ecf71atagen2026-09-26 12:41 UTC297
                .map_err(|error| error.into_account_error("deleting the Matrix store")),
713205a95af2atagen2026-09-26 09:24 UTC298
        }
713205a95af2atagen2026-09-26 09:24 UTC299
    }
713205a95af2atagen2026-09-26 09:24 UTC300
a250c7fa43fbatagen2026-09-26 13:35 UTC301
    /// taken before the client opens the store, and held until the engine and every
a250c7fa43fbatagen2026-09-26 13:35 UTC302
    /// `ClientHandle` are gone
a250c7fa43fbatagen2026-09-26 13:35 UTC303
    pub(crate) async fn lock(&self) -> Result<StoreLock, AccountError> {
a250c7fa43fbatagen2026-09-26 13:35 UTC304
        match &self.location {
a250c7fa43fbatagen2026-09-26 13:35 UTC305
            #[cfg(feature = "sqlite")]
a250c7fa43fbatagen2026-09-26 13:35 UTC306
            StoreLocation::Sqlite(path) => StoreLock::file(path),
a250c7fa43fbatagen2026-09-26 13:35 UTC307
            #[cfg(feature = "indexeddb")]
a250c7fa43fbatagen2026-09-26 13:35 UTC308
            StoreLocation::IndexedDb(name) => StoreLock::web(name).await,
a250c7fa43fbatagen2026-09-26 13:35 UTC309
        }
a250c7fa43fbatagen2026-09-26 13:35 UTC310
    }
a250c7fa43fbatagen2026-09-26 13:35 UTC311
713205a95af2atagen2026-09-26 09:24 UTC312
    pub(crate) fn passphrase(&self) -> &str {
713205a95af2atagen2026-09-26 09:24 UTC313
        &self.passphrase
713205a95af2atagen2026-09-26 09:24 UTC314
    }
713205a95af2atagen2026-09-26 09:24 UTC315
cbb715e11963atagen2026-10-24 23:24 UTC316
    #[cfg(feature = "sqlite")]
cbb715e11963atagen2026-10-24 23:24 UTC317
    pub(crate) fn mentions_location(&self) -> PathBuf {
cbb715e11963atagen2026-10-24 23:24 UTC318
        let StoreLocation::Sqlite(path) = &self.location;
cbb715e11963atagen2026-10-24 23:24 UTC319
        path.join("macltok-mentions.sqlite3")
cbb715e11963atagen2026-10-24 23:24 UTC320
    }
cbb715e11963atagen2026-10-24 23:24 UTC321
cbb715e11963atagen2026-10-24 23:24 UTC322
    #[cfg(feature = "indexeddb")]
cbb715e11963atagen2026-10-24 23:24 UTC323
    pub(crate) fn mentions_location(&self) -> String {
cbb715e11963atagen2026-10-24 23:24 UTC324
        format!("{}::macltok-mentions", self.indexeddb_name())
cbb715e11963atagen2026-10-24 23:24 UTC325
    }
cbb715e11963atagen2026-10-24 23:24 UTC326
713205a95af2atagen2026-09-26 09:24 UTC327
    #[cfg(feature = "indexeddb")]
713205a95af2atagen2026-09-26 09:24 UTC328
    pub(crate) fn indexeddb_name(&self) -> &str {
713205a95af2atagen2026-09-26 09:24 UTC329
        let StoreLocation::IndexedDb(name) = &self.location;
713205a95af2atagen2026-09-26 09:24 UTC330
        name
713205a95af2atagen2026-09-26 09:24 UTC331
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC332
}
96fdf90aacd6atagen2026-09-08 08:24 UTC333
f12377bce9b9atagen2026-09-26 12:32 UTC334
/// Serializable authentication secrets. A `SessionStore` keeps it, and the engine
f12377bce9b9atagen2026-09-26 12:32 UTC335
/// replaces it there on every token rotation.
96fdf90aacd6atagen2026-09-08 08:24 UTC336
#[derive(Clone, Deserialize, Serialize)]
96fdf90aacd6atagen2026-09-08 08:24 UTC337
pub struct SessionRecord {
96fdf90aacd6atagen2026-09-08 08:24 UTC338
    homeserver: String,
96fdf90aacd6atagen2026-09-08 08:24 UTC339
    authentication: AuthenticationRecord,
96fdf90aacd6atagen2026-09-08 08:24 UTC340
}
96fdf90aacd6atagen2026-09-08 08:24 UTC341
96fdf90aacd6atagen2026-09-08 08:24 UTC342
#[derive(Clone, Deserialize, Serialize)]
96fdf90aacd6atagen2026-09-08 08:24 UTC343
#[serde(tag = "type", content = "session", rename_all = "snake_case")]
96fdf90aacd6atagen2026-09-08 08:24 UTC344
enum AuthenticationRecord {
96fdf90aacd6atagen2026-09-08 08:24 UTC345
    Matrix(MatrixSession),
96fdf90aacd6atagen2026-09-08 08:24 UTC346
    OAuth {
96fdf90aacd6atagen2026-09-08 08:24 UTC347
        client_id: ClientId,
96fdf90aacd6atagen2026-09-08 08:24 UTC348
        user: UserSession,
96fdf90aacd6atagen2026-09-08 08:24 UTC349
    },
6f1d6591c10datagen2026-09-26 09:52 UTC350
    SoftLoggedOut {
6f1d6591c10datagen2026-09-26 09:52 UTC351
        meta: SessionMeta,
6f1d6591c10datagen2026-09-26 09:52 UTC352
        client_id: Option<ClientId>,
6f1d6591c10datagen2026-09-26 09:52 UTC353
    },
d8d8a9fe770aatagen2026-10-22 07:44 UTC354
    /// an oauth login waiting on its redirect. `authorization` holds the pkce verifier
d8d8a9fe770aatagen2026-10-22 07:44 UTC355
    PendingOAuth {
d8d8a9fe770aatagen2026-10-22 07:44 UTC356
        client_id: ClientId,
d8d8a9fe770aatagen2026-10-22 07:44 UTC357
        state: CsrfToken,
d8d8a9fe770aatagen2026-10-22 07:44 UTC358
        authorization: Box<PendingAuthorization>,
d8d8a9fe770aatagen2026-10-22 07:44 UTC359
        started_ms: u64,
d8d8a9fe770aatagen2026-10-22 07:44 UTC360
        /// what the store held before, put back if the login never finishes
d8d8a9fe770aatagen2026-10-22 07:44 UTC361
        previous: Option<Box<SessionRecord>>,
d8d8a9fe770aatagen2026-10-22 07:44 UTC362
    },
d8d8a9fe770aatagen2026-10-22 07:44 UTC363
}
d8d8a9fe770aatagen2026-10-22 07:44 UTC364
d8d8a9fe770aatagen2026-10-22 07:44 UTC365
/// authorization codes live for minutes, so a login this old was abandoned
d8d8a9fe770aatagen2026-10-22 07:44 UTC366
const OAUTH_PENDING_FOR_MS: u64 = 60 * 60 * 1000;
d8d8a9fe770aatagen2026-10-22 07:44 UTC367
d8d8a9fe770aatagen2026-10-22 07:44 UTC368
/// the authorization server answers in the query, or the fragment when asked to
d8d8a9fe770aatagen2026-10-22 07:44 UTC369
pub(crate) fn callback_state(callback: &Url) -> Option<String> {
d8d8a9fe770aatagen2026-10-22 07:44 UTC370
    let query = callback.query_pairs();
d8d8a9fe770aatagen2026-10-22 07:44 UTC371
    let fragment = url::form_urlencoded::parse(callback.fragment().unwrap_or_default().as_bytes());
d8d8a9fe770aatagen2026-10-22 07:44 UTC372
    query
d8d8a9fe770aatagen2026-10-22 07:44 UTC373
        .chain(fragment)
d8d8a9fe770aatagen2026-10-22 07:44 UTC374
        .find(|(key, _)| key == "state")
d8d8a9fe770aatagen2026-10-22 07:44 UTC375
        .map(|(_, state)| state.into_owned())
d8d8a9fe770aatagen2026-10-22 07:44 UTC376
}
d8d8a9fe770aatagen2026-10-22 07:44 UTC377
d8d8a9fe770aatagen2026-10-22 07:44 UTC378
fn now_ms() -> u64 {
d8d8a9fe770aatagen2026-10-22 07:44 UTC379
    MilliSecondsSinceUnixEpoch::now().get().into()
96fdf90aacd6atagen2026-09-08 08:24 UTC380
}
96fdf90aacd6atagen2026-09-08 08:24 UTC381
96fdf90aacd6atagen2026-09-08 08:24 UTC382
impl SessionRecord {
96fdf90aacd6atagen2026-09-08 08:24 UTC383
    #[must_use]
96fdf90aacd6atagen2026-09-08 08:24 UTC384
    pub fn homeserver(&self) -> &str {
96fdf90aacd6atagen2026-09-08 08:24 UTC385
        &self.homeserver
96fdf90aacd6atagen2026-09-08 08:24 UTC386
    }
6f1d6591c10datagen2026-09-26 09:52 UTC387
d8d8a9fe770aatagen2026-10-22 07:44 UTC388
    /// a pending oauth login is nobody yet, unless it resumes someone
d8d8a9fe770aatagen2026-10-22 07:44 UTC389
    pub(crate) fn user_id(&self) -> Option<&OwnedUserId> {
d8d8a9fe770aatagen2026-10-22 07:44 UTC390
        match &self.authentication {
d8d8a9fe770aatagen2026-10-22 07:44 UTC391
            AuthenticationRecord::Matrix(session) => Some(&session.meta.user_id),
d8d8a9fe770aatagen2026-10-22 07:44 UTC392
            AuthenticationRecord::OAuth { user, .. } => Some(&user.meta.user_id),
d8d8a9fe770aatagen2026-10-22 07:44 UTC393
            AuthenticationRecord::SoftLoggedOut { meta, .. } => Some(&meta.user_id),
d8d8a9fe770aatagen2026-10-22 07:44 UTC394
            AuthenticationRecord::PendingOAuth { previous, .. } => previous.as_ref()?.user_id(),
d8d8a9fe770aatagen2026-10-22 07:44 UTC395
        }
d8d8a9fe770aatagen2026-10-22 07:44 UTC396
    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC397
d8d8a9fe770aatagen2026-10-22 07:44 UTC398
    /// what a login should resume. a pending oauth login stands in for the record before it
d8d8a9fe770aatagen2026-10-22 07:44 UTC399
    pub(crate) fn resumable(self) -> Option<Self> {
d8d8a9fe770aatagen2026-10-22 07:44 UTC400
        match self.authentication {
d8d8a9fe770aatagen2026-10-22 07:44 UTC401
            AuthenticationRecord::PendingOAuth { previous, .. } => {
d8d8a9fe770aatagen2026-10-22 07:44 UTC402
                previous.and_then(|previous| previous.resumable())
d8d8a9fe770aatagen2026-10-22 07:44 UTC403
            }
d8d8a9fe770aatagen2026-10-22 07:44 UTC404
            _ => Some(self),
d8d8a9fe770aatagen2026-10-22 07:44 UTC405
        }
d8d8a9fe770aatagen2026-10-22 07:44 UTC406
    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC407
d8d8a9fe770aatagen2026-10-22 07:44 UTC408
    pub(crate) fn pending_oauth_state(&self) -> Option<&str> {
cb0668d30c14atagen2026-10-08 11:34 UTC409
        match &self.authentication {
d8d8a9fe770aatagen2026-10-22 07:44 UTC410
            AuthenticationRecord::PendingOAuth { state, .. } => Some(state.secret()),
d8d8a9fe770aatagen2026-10-22 07:44 UTC411
            _ => None,
cb0668d30c14atagen2026-10-08 11:34 UTC412
        }
cb0668d30c14atagen2026-10-08 11:34 UTC413
    }
cb0668d30c14atagen2026-10-08 11:34 UTC414
d8d8a9fe770aatagen2026-10-22 07:44 UTC415
    pub(crate) fn oauth_expired(&self) -> bool {
d8d8a9fe770aatagen2026-10-22 07:44 UTC416
        let AuthenticationRecord::PendingOAuth { started_ms, .. } = &self.authentication else {
d8d8a9fe770aatagen2026-10-22 07:44 UTC417
            return false;
d8d8a9fe770aatagen2026-10-22 07:44 UTC418
        };
d8d8a9fe770aatagen2026-10-22 07:44 UTC419
        now_ms().saturating_sub(*started_ms) > OAUTH_PENDING_FOR_MS
d8d8a9fe770aatagen2026-10-22 07:44 UTC420
    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC421
cb0668d30c14atagen2026-10-08 11:34 UTC422
    pub(crate) fn soft_logout(&self) -> Option<SoftLogout> {
d8d8a9fe770aatagen2026-10-22 07:44 UTC423
        let (meta, client_id) = match &self.authentication {
d8d8a9fe770aatagen2026-10-22 07:44 UTC424
            AuthenticationRecord::SoftLoggedOut { meta, client_id } => (meta, client_id),
d8d8a9fe770aatagen2026-10-22 07:44 UTC425
            AuthenticationRecord::PendingOAuth { previous, .. } => {
d8d8a9fe770aatagen2026-10-22 07:44 UTC426
                return previous.as_ref()?.soft_logout();
d8d8a9fe770aatagen2026-10-22 07:44 UTC427
            }
d8d8a9fe770aatagen2026-10-22 07:44 UTC428
            _ => return None,
cb0668d30c14atagen2026-10-08 11:34 UTC429
        };
cb0668d30c14atagen2026-10-08 11:34 UTC430
        Some(SoftLogout {
cb0668d30c14atagen2026-10-08 11:34 UTC431
            homeserver: self.homeserver.clone(),
cb0668d30c14atagen2026-10-08 11:34 UTC432
            user_id: meta.user_id.clone(),
cb0668d30c14atagen2026-10-08 11:34 UTC433
            device_id: meta.device_id.clone(),
cb0668d30c14atagen2026-10-08 11:34 UTC434
            client_id: client_id
cb0668d30c14atagen2026-10-08 11:34 UTC435
                .as_ref()
cb0668d30c14atagen2026-10-08 11:34 UTC436
                .map(|client_id| client_id.as_str().to_owned()),
cb0668d30c14atagen2026-10-08 11:34 UTC437
        })
cb0668d30c14atagen2026-10-08 11:34 UTC438
    }
cb0668d30c14atagen2026-10-08 11:34 UTC439
6f1d6591c10datagen2026-09-26 09:52 UTC440
    pub(crate) fn soft_logged_out(self) -> Self {
6f1d6591c10datagen2026-09-26 09:52 UTC441
        let authentication = match self.authentication {
6f1d6591c10datagen2026-09-26 09:52 UTC442
            AuthenticationRecord::Matrix(session) => AuthenticationRecord::SoftLoggedOut {
6f1d6591c10datagen2026-09-26 09:52 UTC443
                meta: session.meta,
6f1d6591c10datagen2026-09-26 09:52 UTC444
                client_id: None,
6f1d6591c10datagen2026-09-26 09:52 UTC445
            },
6f1d6591c10datagen2026-09-26 09:52 UTC446
            AuthenticationRecord::OAuth { client_id, user } => {
6f1d6591c10datagen2026-09-26 09:52 UTC447
                AuthenticationRecord::SoftLoggedOut {
6f1d6591c10datagen2026-09-26 09:52 UTC448
                    meta: user.meta,
6f1d6591c10datagen2026-09-26 09:52 UTC449
                    client_id: Some(client_id),
6f1d6591c10datagen2026-09-26 09:52 UTC450
                }
6f1d6591c10datagen2026-09-26 09:52 UTC451
            }
6f1d6591c10datagen2026-09-26 09:52 UTC452
            soft @ AuthenticationRecord::SoftLoggedOut { .. } => soft,
d8d8a9fe770aatagen2026-10-22 07:44 UTC453
            pending @ AuthenticationRecord::PendingOAuth { .. } => pending,
6f1d6591c10datagen2026-09-26 09:52 UTC454
        };
6f1d6591c10datagen2026-09-26 09:52 UTC455
        Self {
6f1d6591c10datagen2026-09-26 09:52 UTC456
            homeserver: self.homeserver,
6f1d6591c10datagen2026-09-26 09:52 UTC457
            authentication,
6f1d6591c10datagen2026-09-26 09:52 UTC458
        }
6f1d6591c10datagen2026-09-26 09:52 UTC459
    }
6f1d6591c10datagen2026-09-26 09:52 UTC460
}
6f1d6591c10datagen2026-09-26 09:52 UTC461
6f1d6591c10datagen2026-09-26 09:52 UTC462
pub enum Restore {
6f1d6591c10datagen2026-09-26 09:52 UTC463
    Resumed(Session),
6f1d6591c10datagen2026-09-26 09:52 UTC464
    SoftLoggedOut(SoftLogout),
d8d8a9fe770aatagen2026-10-22 07:44 UTC465
    /// an oauth login is waiting on its redirect. `Session::finish_oauth` takes the callback
d8d8a9fe770aatagen2026-10-22 07:44 UTC466
    OAuthPending,
6f1d6591c10datagen2026-09-26 09:52 UTC467
}
6f1d6591c10datagen2026-09-26 09:52 UTC468
6f1d6591c10datagen2026-09-26 09:52 UTC469
/// who to log in again as. the store keeps this device's e2ee keys, so a login
6f1d6591c10datagen2026-09-26 09:52 UTC470
/// as any other device would orphan them
6f1d6591c10datagen2026-09-26 09:52 UTC471
#[derive(Clone, Debug)]
6f1d6591c10datagen2026-09-26 09:52 UTC472
#[non_exhaustive]
6f1d6591c10datagen2026-09-26 09:52 UTC473
pub struct SoftLogout {
6f1d6591c10datagen2026-09-26 09:52 UTC474
    pub homeserver: String,
6f1d6591c10datagen2026-09-26 09:52 UTC475
    pub user_id: OwnedUserId,
6f1d6591c10datagen2026-09-26 09:52 UTC476
    pub device_id: OwnedDeviceId,
6f1d6591c10datagen2026-09-26 09:52 UTC477
    /// set when the login was OAuth, for `OAuthLogin::with_client_id`
6f1d6591c10datagen2026-09-26 09:52 UTC478
    pub client_id: Option<String>,
6f1d6591c10datagen2026-09-26 09:52 UTC479
}
6f1d6591c10datagen2026-09-26 09:52 UTC480
6f1d6591c10datagen2026-09-26 09:52 UTC481
impl SoftLogout {
6f1d6591c10datagen2026-09-26 09:52 UTC482
    #[must_use]
6f1d6591c10datagen2026-09-26 09:52 UTC483
    pub fn password_login(&self, password: impl Into<String>) -> PasswordLogin {
6f1d6591c10datagen2026-09-26 09:52 UTC484
        PasswordLogin::new(self.homeserver.clone(), self.user_id.as_str(), password)
6f1d6591c10datagen2026-09-26 09:52 UTC485
            .with_device_id(self.device_id.clone())
9c05b1dd327aatagen2026-10-08 11:27 UTC486
            .allow_insecure_loopback(true)
6f1d6591c10datagen2026-09-26 09:52 UTC487
    }
cb0668d30c14atagen2026-10-08 11:34 UTC488
cb0668d30c14atagen2026-10-08 11:34 UTC489
    #[must_use]
cb0668d30c14atagen2026-10-08 11:34 UTC490
    pub fn oauth_login(
cb0668d30c14atagen2026-10-08 11:34 UTC491
        &self,
cb0668d30c14atagen2026-10-08 11:34 UTC492
        redirect_uri: impl Into<String>,
cb0668d30c14atagen2026-10-08 11:34 UTC493
        client_uri: impl Into<String>,
cb0668d30c14atagen2026-10-08 11:34 UTC494
        application_type: OAuthApplicationType,
cb0668d30c14atagen2026-10-08 11:34 UTC495
    ) -> OAuthLogin {
cb0668d30c14atagen2026-10-08 11:34 UTC496
        let login = OAuthLogin::new(
cb0668d30c14atagen2026-10-08 11:34 UTC497
            self.homeserver.clone(),
cb0668d30c14atagen2026-10-08 11:34 UTC498
            redirect_uri,
cb0668d30c14atagen2026-10-08 11:34 UTC499
            client_uri,
cb0668d30c14atagen2026-10-08 11:34 UTC500
            application_type,
cb0668d30c14atagen2026-10-08 11:34 UTC501
        )
cb0668d30c14atagen2026-10-08 11:34 UTC502
        .with_device_id(self.device_id.clone())
cb0668d30c14atagen2026-10-08 11:34 UTC503
        .allow_insecure_loopback(true);
cb0668d30c14atagen2026-10-08 11:34 UTC504
        match &self.client_id {
cb0668d30c14atagen2026-10-08 11:34 UTC505
            Some(client_id) => login.with_client_id(client_id.clone()),
cb0668d30c14atagen2026-10-08 11:34 UTC506
            None => login,
cb0668d30c14atagen2026-10-08 11:34 UTC507
        }
cb0668d30c14atagen2026-10-08 11:34 UTC508
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC509
}
96fdf90aacd6atagen2026-09-08 08:24 UTC510
96fdf90aacd6atagen2026-09-08 08:24 UTC511
#[derive(Debug, Error)]
96fdf90aacd6atagen2026-09-08 08:24 UTC512
pub enum AccountError {
96fdf90aacd6atagen2026-09-08 08:24 UTC513
    #[error("{context} failed")]
96fdf90aacd6atagen2026-09-08 08:24 UTC514
    Operation {
96fdf90aacd6atagen2026-09-08 08:24 UTC515
        context: &'static str,
9a25b6ce66f4atagen2026-10-08 11:15 UTC516
        kind: ErrorKind,
9a25b6ce66f4atagen2026-10-08 11:15 UTC517
        server: Option<ServerError>,
96fdf90aacd6atagen2026-09-08 08:24 UTC518
        #[source]
62fe32759529atagen2026-09-26 08:50 UTC519
        source: Box<SourceError>,
96fdf90aacd6atagen2026-09-08 08:24 UTC520
    },
96fdf90aacd6atagen2026-09-08 08:24 UTC521
    #[error("the homeserver returned no authenticated session")]
96fdf90aacd6atagen2026-09-08 08:24 UTC522
    MissingSession,
96fdf90aacd6atagen2026-09-08 08:24 UTC523
    #[error("the SDK returned an unsupported authentication session")]
96fdf90aacd6atagen2026-09-08 08:24 UTC524
    UnsupportedAuthentication,
96fdf90aacd6atagen2026-09-08 08:24 UTC525
    #[error("the OAuth callback was already submitted")]
96fdf90aacd6atagen2026-09-08 08:24 UTC526
    OAuthCallbackAlreadySubmitted,
d8d8a9fe770aatagen2026-10-22 07:44 UTC527
    /// nothing is waiting for this callback. it was finished, abandoned, expired, or begun
d8d8a9fe770aatagen2026-10-22 07:44 UTC528
    /// somewhere else
d8d8a9fe770aatagen2026-10-22 07:44 UTC529
    #[error("no OAuth login is waiting for this callback")]
d8d8a9fe770aatagen2026-10-22 07:44 UTC530
    NoPendingOAuth,
9c05b1dd327aatagen2026-10-08 11:27 UTC531
    /// what was typed can't name a homeserver, so nothing was sent anywhere
9c05b1dd327aatagen2026-10-08 11:27 UTC532
    #[error("that isn't a usable homeserver, {0}")]
9c05b1dd327aatagen2026-10-08 11:27 UTC533
    InvalidHomeserver(&'static str),
cb0668d30c14atagen2026-10-08 11:34 UTC534
    /// `Accounts` already has a live login for them. a second would be another device
cb0668d30c14atagen2026-10-08 11:34 UTC535
    #[error("{0} is already logged in")]
cb0668d30c14atagen2026-10-08 11:34 UTC536
    AlreadyLoggedIn(OwnedUserId),
6db49c0ecf71atagen2026-09-26 12:41 UTC537
    /// an IndexedDB delete is waiting on the store's other connections. it goes through
6db49c0ecf71atagen2026-09-26 12:41 UTC538
    /// once they close, and opening the store again waits behind it
6db49c0ecf71atagen2026-09-26 12:41 UTC539
    #[error("the store is still open elsewhere, it will be deleted once that closes")]
6db49c0ecf71atagen2026-09-26 12:41 UTC540
    StoreDeletePending,
a250c7fa43fbatagen2026-09-26 13:35 UTC541
    /// another engine, in this process or another, has the store. only one runs per store
a250c7fa43fbatagen2026-09-26 13:35 UTC542
    #[error("another engine is using this store")]
a250c7fa43fbatagen2026-09-26 13:35 UTC543
    StoreInUse,
a250c7fa43fbatagen2026-09-26 13:35 UTC544
    /// the login worked but its first record didn't save. the server already has the
a250c7fa43fbatagen2026-09-26 13:35 UTC545
    /// device, so retry the save rather than log in again
a250c7fa43fbatagen2026-09-26 13:35 UTC546
    #[error("saving the new login's record failed")]
a250c7fa43fbatagen2026-09-26 13:35 UTC547
    Unsaved {
a250c7fa43fbatagen2026-09-26 13:35 UTC548
        session: Box<UnsavedSession>,
a250c7fa43fbatagen2026-09-26 13:35 UTC549
        #[source]
a250c7fa43fbatagen2026-09-26 13:35 UTC550
        source: Box<AccountError>,
a250c7fa43fbatagen2026-09-26 13:35 UTC551
    },
96fdf90aacd6atagen2026-09-08 08:24 UTC552
    #[error(transparent)]
96fdf90aacd6atagen2026-09-08 08:24 UTC553
    Engine(#[from] EngineError),
96fdf90aacd6atagen2026-09-08 08:24 UTC554
}
96fdf90aacd6atagen2026-09-08 08:24 UTC555
9a25b6ce66f4atagen2026-10-08 11:15 UTC556
impl AccountError {
9a25b6ce66f4atagen2026-10-08 11:15 UTC557
    /// what the homeserver said, when it refused
9a25b6ce66f4atagen2026-10-08 11:15 UTC558
    #[must_use]
9a25b6ce66f4atagen2026-10-08 11:15 UTC559
    pub fn server(&self) -> Option<&ServerError> {
9a25b6ce66f4atagen2026-10-08 11:15 UTC560
        match self {
9a25b6ce66f4atagen2026-10-08 11:15 UTC561
            Self::Operation { server, .. } => server.as_ref(),
9a25b6ce66f4atagen2026-10-08 11:15 UTC562
            Self::Unsaved { source, .. } => source.server(),
9a25b6ce66f4atagen2026-10-08 11:15 UTC563
            _ => None,
9a25b6ce66f4atagen2026-10-08 11:15 UTC564
        }
9a25b6ce66f4atagen2026-10-08 11:15 UTC565
    }
9a25b6ce66f4atagen2026-10-08 11:15 UTC566
}
9a25b6ce66f4atagen2026-10-08 11:15 UTC567
a250c7fa43fbatagen2026-09-26 13:35 UTC568
/// a live login whose record never reached its `SessionStore`
a250c7fa43fbatagen2026-09-26 13:35 UTC569
pub struct UnsavedSession {
a250c7fa43fbatagen2026-09-26 13:35 UTC570
    session: Session,
a250c7fa43fbatagen2026-09-26 13:35 UTC571
    sessions: Arc<dyn SessionStore>,
a250c7fa43fbatagen2026-09-26 13:35 UTC572
}
a250c7fa43fbatagen2026-09-26 13:35 UTC573
a250c7fa43fbatagen2026-09-26 13:35 UTC574
impl UnsavedSession {
a250c7fa43fbatagen2026-09-26 13:35 UTC575
    pub async fn save(self) -> Result<Session, AccountError> {
a250c7fa43fbatagen2026-09-26 13:35 UTC576
        let saved = match session_record(&self.session.client) {
a250c7fa43fbatagen2026-09-26 13:35 UTC577
            Ok(record) => self.sessions.save(&record).await,
a250c7fa43fbatagen2026-09-26 13:35 UTC578
            Err(error) => Err(error),
a250c7fa43fbatagen2026-09-26 13:35 UTC579
        };
a250c7fa43fbatagen2026-09-26 13:35 UTC580
        match saved {
a250c7fa43fbatagen2026-09-26 13:35 UTC581
            Ok(()) => Ok(self.session),
a250c7fa43fbatagen2026-09-26 13:35 UTC582
            Err(source) => Err(AccountError::Unsaved {
a250c7fa43fbatagen2026-09-26 13:35 UTC583
                session: Box::new(self),
a250c7fa43fbatagen2026-09-26 13:35 UTC584
                source: Box::new(source),
a250c7fa43fbatagen2026-09-26 13:35 UTC585
            }),
a250c7fa43fbatagen2026-09-26 13:35 UTC586
        }
a250c7fa43fbatagen2026-09-26 13:35 UTC587
    }
a250c7fa43fbatagen2026-09-26 13:35 UTC588
a250c7fa43fbatagen2026-09-26 13:35 UTC589
    /// the record to keep somewhere else, if the store won't take it
a250c7fa43fbatagen2026-09-26 13:35 UTC590
    pub fn record(&self) -> Result<SessionRecord, AccountError> {
a250c7fa43fbatagen2026-09-26 13:35 UTC591
        session_record(&self.session.client)
a250c7fa43fbatagen2026-09-26 13:35 UTC592
    }
a250c7fa43fbatagen2026-09-26 13:35 UTC593
}
a250c7fa43fbatagen2026-09-26 13:35 UTC594
a250c7fa43fbatagen2026-09-26 13:35 UTC595
impl std::fmt::Debug for UnsavedSession {
a250c7fa43fbatagen2026-09-26 13:35 UTC596
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
a250c7fa43fbatagen2026-09-26 13:35 UTC597
        formatter
a250c7fa43fbatagen2026-09-26 13:35 UTC598
            .debug_struct("UnsavedSession")
a250c7fa43fbatagen2026-09-26 13:35 UTC599
            .finish_non_exhaustive()
a250c7fa43fbatagen2026-09-26 13:35 UTC600
    }
a250c7fa43fbatagen2026-09-26 13:35 UTC601
}
a250c7fa43fbatagen2026-09-26 13:35 UTC602
96fdf90aacd6atagen2026-09-08 08:24 UTC603
impl Session {
96fdf90aacd6atagen2026-09-08 08:24 UTC604
    pub async fn login_password(
96fdf90aacd6atagen2026-09-08 08:24 UTC605
        login: PasswordLogin,
f12377bce9b9atagen2026-09-26 12:32 UTC606
        sessions: Arc<dyn SessionStore>,
96fdf90aacd6atagen2026-09-08 08:24 UTC607
    ) -> Result<Self, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC608
        homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
a250c7fa43fbatagen2026-09-26 13:35 UTC609
        let store_lock = sessions.store().lock().await?;
cb0668d30c14atagen2026-10-08 11:34 UTC610
        Self::login_password_locked(login, sessions, store_lock).await
cb0668d30c14atagen2026-10-08 11:34 UTC611
    }
cb0668d30c14atagen2026-10-08 11:34 UTC612
cb0668d30c14atagen2026-10-08 11:34 UTC613
    pub(crate) async fn login_password_locked(
cb0668d30c14atagen2026-10-08 11:34 UTC614
        login: PasswordLogin,
cb0668d30c14atagen2026-10-08 11:34 UTC615
        sessions: Arc<dyn SessionStore>,
cb0668d30c14atagen2026-10-08 11:34 UTC616
        store_lock: StoreLock,
cb0668d30c14atagen2026-10-08 11:34 UTC617
    ) -> Result<Self, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC618
        let homeserver = homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
713205a95af2atagen2026-09-26 09:24 UTC619
        let client = matrix_client_builder(sessions.store())
9c05b1dd327aatagen2026-10-08 11:27 UTC620
            .server_name_or_homeserver_url(homeserver)
96fdf90aacd6atagen2026-09-08 08:24 UTC621
            .build()
96fdf90aacd6atagen2026-09-08 08:24 UTC622
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC623
            .map_err(|error| account_error("building the Matrix client", error))?;
f12377bce9b9atagen2026-09-26 12:32 UTC624
        let tracking = Tracking::start(&client, sessions.clone())?;
96fdf90aacd6atagen2026-09-08 08:24 UTC625
        let mut request = client
96fdf90aacd6atagen2026-09-08 08:24 UTC626
            .matrix_auth()
96fdf90aacd6atagen2026-09-08 08:24 UTC627
            .login_username(login.username.trim(), &login.password)
96fdf90aacd6atagen2026-09-08 08:24 UTC628
            .request_refresh_token();
96fdf90aacd6atagen2026-09-08 08:24 UTC629
        if let Some(name) = login.device_display_name.as_deref() {
96fdf90aacd6atagen2026-09-08 08:24 UTC630
            request = request.initial_device_display_name(name);
96fdf90aacd6atagen2026-09-08 08:24 UTC631
        }
fa9114fcf75fatagen2026-09-24 12:51 UTC632
        if let Some(device_id) = login.device_id.as_deref() {
fa9114fcf75fatagen2026-09-24 12:51 UTC633
            request = request.device_id(device_id.as_str());
fa9114fcf75fatagen2026-09-24 12:51 UTC634
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC635
        request
96fdf90aacd6atagen2026-09-08 08:24 UTC636
            .send()
96fdf90aacd6atagen2026-09-08 08:24 UTC637
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC638
            .map_err(|error| account_error("password login", error))?;
a250c7fa43fbatagen2026-09-26 13:35 UTC639
        Self::saved(client, sessions, tracking, store_lock).await
96fdf90aacd6atagen2026-09-08 08:24 UTC640
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC641
a250c7fa43fbatagen2026-09-26 13:35 UTC642
    /// the pending login lives only in this process, so the one that begins it has to finish it
96fdf90aacd6atagen2026-09-08 08:24 UTC643
    pub async fn begin_oauth(
96fdf90aacd6atagen2026-09-08 08:24 UTC644
        login: OAuthLogin,
713205a95af2atagen2026-09-26 09:24 UTC645
        sessions: Arc<dyn SessionStore>,
96fdf90aacd6atagen2026-09-08 08:24 UTC646
    ) -> Result<PendingOAuthLogin, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC647
        homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
cb0668d30c14atagen2026-10-08 11:34 UTC648
        let store_lock = sessions.store().lock().await?;
cb0668d30c14atagen2026-10-08 11:34 UTC649
        Self::begin_oauth_locked(login, sessions, store_lock).await
cb0668d30c14atagen2026-10-08 11:34 UTC650
    }
cb0668d30c14atagen2026-10-08 11:34 UTC651
cb0668d30c14atagen2026-10-08 11:34 UTC652
    pub(crate) async fn begin_oauth_locked(
cb0668d30c14atagen2026-10-08 11:34 UTC653
        login: OAuthLogin,
cb0668d30c14atagen2026-10-08 11:34 UTC654
        sessions: Arc<dyn SessionStore>,
cb0668d30c14atagen2026-10-08 11:34 UTC655
        store_lock: StoreLock,
cb0668d30c14atagen2026-10-08 11:34 UTC656
    ) -> Result<PendingOAuthLogin, AccountError> {
96fdf90aacd6atagen2026-09-08 08:24 UTC657
        let redirect_uri = Url::parse(login.redirect_uri.trim())
96fdf90aacd6atagen2026-09-08 08:24 UTC658
            .map_err(|error| account_error("parsing the OAuth redirect URI", error))?;
96fdf90aacd6atagen2026-09-08 08:24 UTC659
        let client_uri = Url::parse(login.client_uri.trim())
96fdf90aacd6atagen2026-09-08 08:24 UTC660
            .map_err(|error| account_error("parsing the OAuth client URI", error))?;
9c05b1dd327aatagen2026-10-08 11:27 UTC661
        let homeserver = homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
713205a95af2atagen2026-09-26 09:24 UTC662
        let client = matrix_client_builder(sessions.store())
9c05b1dd327aatagen2026-10-08 11:27 UTC663
            .server_name_or_homeserver_url(homeserver)
96fdf90aacd6atagen2026-09-08 08:24 UTC664
            .build()
96fdf90aacd6atagen2026-09-08 08:24 UTC665
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC666
            .map_err(|error| account_error("building the Matrix client", error))?;
f12377bce9b9atagen2026-09-26 12:32 UTC667
        let tracking = Tracking::start(&client, sessions.clone())?;
96fdf90aacd6atagen2026-09-08 08:24 UTC668
        let application_type = match login.application_type {
96fdf90aacd6atagen2026-09-08 08:24 UTC669
            OAuthApplicationType::Native => ApplicationType::Native,
96fdf90aacd6atagen2026-09-08 08:24 UTC670
            OAuthApplicationType::Web => ApplicationType::Web,
96fdf90aacd6atagen2026-09-08 08:24 UTC671
        };
96fdf90aacd6atagen2026-09-08 08:24 UTC672
        let mut metadata = ClientMetadata::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC673
            application_type,
96fdf90aacd6atagen2026-09-08 08:24 UTC674
            vec![OAuthGrantType::AuthorizationCode {
96fdf90aacd6atagen2026-09-08 08:24 UTC675
                redirect_uris: vec![redirect_uri.clone()],
96fdf90aacd6atagen2026-09-08 08:24 UTC676
            }],
96fdf90aacd6atagen2026-09-08 08:24 UTC677
            Localized::new(client_uri, []),
96fdf90aacd6atagen2026-09-08 08:24 UTC678
        );
96fdf90aacd6atagen2026-09-08 08:24 UTC679
        metadata.client_name = login.client_name.map(|name| Localized::new(name, []));
96fdf90aacd6atagen2026-09-08 08:24 UTC680
        let metadata = Raw::new(&metadata)
96fdf90aacd6atagen2026-09-08 08:24 UTC681
            .map_err(|error| account_error("serializing OAuth client metadata", error))?;
96fdf90aacd6atagen2026-09-08 08:24 UTC682
        let mut registration = ClientRegistrationData::new(metadata);
96fdf90aacd6atagen2026-09-08 08:24 UTC683
        if let Some(client_id) = login.client_id {
96fdf90aacd6atagen2026-09-08 08:24 UTC684
            registration.static_registrations = Some(HashMap::from([(
96fdf90aacd6atagen2026-09-08 08:24 UTC685
                client.homeserver(),
96fdf90aacd6atagen2026-09-08 08:24 UTC686
                ClientId::new(client_id),
96fdf90aacd6atagen2026-09-08 08:24 UTC687
            )]));
96fdf90aacd6atagen2026-09-08 08:24 UTC688
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC689
        let authorization = client
96fdf90aacd6atagen2026-09-08 08:24 UTC690
            .oauth()
96fdf90aacd6atagen2026-09-08 08:24 UTC691
            .login(redirect_uri, login.device_id, Some(registration), None)
96fdf90aacd6atagen2026-09-08 08:24 UTC692
            .build()
96fdf90aacd6atagen2026-09-08 08:24 UTC693
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC694
            .map_err(|error| account_error("starting OAuth login", error))?;
d8d8a9fe770aatagen2026-10-22 07:44 UTC695
        let previous = sessions.load().await?.and_then(SessionRecord::resumable);
d8d8a9fe770aatagen2026-10-22 07:44 UTC696
        let pending = SessionRecord {
d8d8a9fe770aatagen2026-10-22 07:44 UTC697
            homeserver: client.homeserver().to_string(),
d8d8a9fe770aatagen2026-10-22 07:44 UTC698
            authentication: AuthenticationRecord::PendingOAuth {
d8d8a9fe770aatagen2026-10-22 07:44 UTC699
                client_id: client
d8d8a9fe770aatagen2026-10-22 07:44 UTC700
                    .oauth()
d8d8a9fe770aatagen2026-10-22 07:44 UTC701
                    .client_id()
d8d8a9fe770aatagen2026-10-22 07:44 UTC702
                    .ok_or(AccountError::MissingSession)?
d8d8a9fe770aatagen2026-10-22 07:44 UTC703
                    .clone(),
d8d8a9fe770aatagen2026-10-22 07:44 UTC704
                state: authorization.state.clone(),
d8d8a9fe770aatagen2026-10-22 07:44 UTC705
                authorization: Box::new(
d8d8a9fe770aatagen2026-10-22 07:44 UTC706
                    client
d8d8a9fe770aatagen2026-10-22 07:44 UTC707
                        .oauth()
d8d8a9fe770aatagen2026-10-22 07:44 UTC708
                        .pending_authorization(&authorization.state)
d8d8a9fe770aatagen2026-10-22 07:44 UTC709
                        .await
d8d8a9fe770aatagen2026-10-22 07:44 UTC710
                        .ok_or(AccountError::MissingSession)?,
d8d8a9fe770aatagen2026-10-22 07:44 UTC711
                ),
d8d8a9fe770aatagen2026-10-22 07:44 UTC712
                started_ms: now_ms(),
d8d8a9fe770aatagen2026-10-22 07:44 UTC713
                previous: previous.clone().map(Box::new),
d8d8a9fe770aatagen2026-10-22 07:44 UTC714
            },
d8d8a9fe770aatagen2026-10-22 07:44 UTC715
        };
d8d8a9fe770aatagen2026-10-22 07:44 UTC716
        sessions.save(&pending).await?;
96fdf90aacd6atagen2026-09-08 08:24 UTC717
        Ok(PendingOAuthLogin {
96fdf90aacd6atagen2026-09-08 08:24 UTC718
            client,
713205a95af2atagen2026-09-26 09:24 UTC719
            sessions,
a250c7fa43fbatagen2026-09-26 13:35 UTC720
            tracking: Some((tracking, store_lock)),
96fdf90aacd6atagen2026-09-08 08:24 UTC721
            authorization_url: authorization.url.to_string(),
96fdf90aacd6atagen2026-09-08 08:24 UTC722
            state: Some(authorization.state),
d8d8a9fe770aatagen2026-10-22 07:44 UTC723
            previous,
96fdf90aacd6atagen2026-09-08 08:24 UTC724
        })
96fdf90aacd6atagen2026-09-08 08:24 UTC725
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC726
d8d8a9fe770aatagen2026-10-22 07:44 UTC727
    /// finishes a login `begin_oauth` started in another process, from the callback the
d8d8a9fe770aatagen2026-10-22 07:44 UTC728
    /// redirect brought back. the pending login is in `sessions`, so nothing else is needed
d8d8a9fe770aatagen2026-10-22 07:44 UTC729
    pub async fn finish_oauth(
d8d8a9fe770aatagen2026-10-22 07:44 UTC730
        sessions: Arc<dyn SessionStore>,
d8d8a9fe770aatagen2026-10-22 07:44 UTC731
        callback_url: &str,
d8d8a9fe770aatagen2026-10-22 07:44 UTC732
    ) -> Result<Self, AccountError> {
d8d8a9fe770aatagen2026-10-22 07:44 UTC733
        let store_lock = sessions.store().lock().await?;
d8d8a9fe770aatagen2026-10-22 07:44 UTC734
        Self::finish_oauth_locked(sessions, store_lock, callback_url).await
d8d8a9fe770aatagen2026-10-22 07:44 UTC735
    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC736
d8d8a9fe770aatagen2026-10-22 07:44 UTC737
    pub(crate) async fn finish_oauth_locked(
d8d8a9fe770aatagen2026-10-22 07:44 UTC738
        sessions: Arc<dyn SessionStore>,
d8d8a9fe770aatagen2026-10-22 07:44 UTC739
        store_lock: StoreLock,
d8d8a9fe770aatagen2026-10-22 07:44 UTC740
        callback_url: &str,
d8d8a9fe770aatagen2026-10-22 07:44 UTC741
    ) -> Result<Self, AccountError> {
d8d8a9fe770aatagen2026-10-22 07:44 UTC742
        let callback = Url::parse(callback_url)
d8d8a9fe770aatagen2026-10-22 07:44 UTC743
            .map_err(|error| account_error("parsing the OAuth callback URL", error))?;
d8d8a9fe770aatagen2026-10-22 07:44 UTC744
        let record = sessions.load().await?.ok_or(AccountError::NoPendingOAuth)?;
d8d8a9fe770aatagen2026-10-22 07:44 UTC745
        if record.pending_oauth_state() != callback_state(&callback).as_deref() {
d8d8a9fe770aatagen2026-10-22 07:44 UTC746
            return Err(AccountError::NoPendingOAuth);
d8d8a9fe770aatagen2026-10-22 07:44 UTC747
        }
d8d8a9fe770aatagen2026-10-22 07:44 UTC748
        let AuthenticationRecord::PendingOAuth {
d8d8a9fe770aatagen2026-10-22 07:44 UTC749
            client_id,
d8d8a9fe770aatagen2026-10-22 07:44 UTC750
            state,
d8d8a9fe770aatagen2026-10-22 07:44 UTC751
            authorization,
d8d8a9fe770aatagen2026-10-22 07:44 UTC752
            previous,
d8d8a9fe770aatagen2026-10-22 07:44 UTC753
            ..
d8d8a9fe770aatagen2026-10-22 07:44 UTC754
        } = record.authentication
d8d8a9fe770aatagen2026-10-22 07:44 UTC755
        else {
d8d8a9fe770aatagen2026-10-22 07:44 UTC756
            return Err(AccountError::NoPendingOAuth);
d8d8a9fe770aatagen2026-10-22 07:44 UTC757
        };
d8d8a9fe770aatagen2026-10-22 07:44 UTC758
        let client = matrix_client_builder(sessions.store())
d8d8a9fe770aatagen2026-10-22 07:44 UTC759
            .homeserver_url(&record.homeserver)
d8d8a9fe770aatagen2026-10-22 07:44 UTC760
            .build()
d8d8a9fe770aatagen2026-10-22 07:44 UTC761
            .await
d8d8a9fe770aatagen2026-10-22 07:44 UTC762
            .map_err(|error| account_error("reopening the Matrix store", error))?;
d8d8a9fe770aatagen2026-10-22 07:44 UTC763
        let tracking = Tracking::start(&client, sessions.clone())?;
d8d8a9fe770aatagen2026-10-22 07:44 UTC764
        let oauth = client.oauth();
d8d8a9fe770aatagen2026-10-22 07:44 UTC765
        oauth.restore_registered_client(client_id);
d8d8a9fe770aatagen2026-10-22 07:44 UTC766
        let finished = match oauth
d8d8a9fe770aatagen2026-10-22 07:44 UTC767
            .restore_pending_authorization(state, *authorization)
d8d8a9fe770aatagen2026-10-22 07:44 UTC768
            .await
d8d8a9fe770aatagen2026-10-22 07:44 UTC769
        {
d8d8a9fe770aatagen2026-10-22 07:44 UTC770
            Ok(()) => oauth.finish_login(callback.into()).await,
d8d8a9fe770aatagen2026-10-22 07:44 UTC771
            Err(error) => Err(error.into()),
d8d8a9fe770aatagen2026-10-22 07:44 UTC772
        };
d8d8a9fe770aatagen2026-10-22 07:44 UTC773
        if let Err(error) = finished {
d8d8a9fe770aatagen2026-10-22 07:44 UTC774
            match previous {
d8d8a9fe770aatagen2026-10-22 07:44 UTC775
                Some(previous) => put_back(sessions.as_ref(), *previous).await,
d8d8a9fe770aatagen2026-10-22 07:44 UTC776
                None => {
d8d8a9fe770aatagen2026-10-22 07:44 UTC777
                    drop((tracking, client));
d8d8a9fe770aatagen2026-10-22 07:44 UTC778
                    if let Err(error) = sessions.discard().await {
d8d8a9fe770aatagen2026-10-22 07:44 UTC779
                        tracing::warn!(%error, "deleting a failed OAuth login's store failed");
d8d8a9fe770aatagen2026-10-22 07:44 UTC780
                    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC781
                }
d8d8a9fe770aatagen2026-10-22 07:44 UTC782
            }
d8d8a9fe770aatagen2026-10-22 07:44 UTC783
            return Err(account_error("OAuth login", error));
d8d8a9fe770aatagen2026-10-22 07:44 UTC784
        }
d8d8a9fe770aatagen2026-10-22 07:44 UTC785
        Self::saved(client, sessions, tracking, store_lock).await
d8d8a9fe770aatagen2026-10-22 07:44 UTC786
    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC787
713205a95af2atagen2026-09-26 09:24 UTC788
    /// Restore the record `sessions` holds, or `None` when it holds none.
f12377bce9b9atagen2026-09-26 12:32 UTC789
    /// The caller must also handle rejection and explicit logout.
f12377bce9b9atagen2026-09-26 12:32 UTC790
    pub async fn restore(sessions: Arc<dyn SessionStore>) -> Result<Option<Restore>, AccountError> {
713205a95af2atagen2026-09-26 09:24 UTC791
        let Some(record) = sessions.load().await? else {
713205a95af2atagen2026-09-26 09:24 UTC792
            return Ok(None);
713205a95af2atagen2026-09-26 09:24 UTC793
        };
713205a95af2atagen2026-09-26 09:24 UTC794
        let authentication = match record.authentication {
96fdf90aacd6atagen2026-09-08 08:24 UTC795
            AuthenticationRecord::Matrix(session) => AuthSession::Matrix(session),
96fdf90aacd6atagen2026-09-08 08:24 UTC796
            AuthenticationRecord::OAuth { client_id, user } => {
96fdf90aacd6atagen2026-09-08 08:24 UTC797
                OAuthSession { client_id, user }.into()
96fdf90aacd6atagen2026-09-08 08:24 UTC798
            }
d8d8a9fe770aatagen2026-10-22 07:44 UTC799
            AuthenticationRecord::PendingOAuth { .. } if record.oauth_expired() => {
d8d8a9fe770aatagen2026-10-22 07:44 UTC800
                let previous = record.resumable();
d8d8a9fe770aatagen2026-10-22 07:44 UTC801
                match previous {
d8d8a9fe770aatagen2026-10-22 07:44 UTC802
                    Some(previous) => put_back(sessions.as_ref(), previous).await,
d8d8a9fe770aatagen2026-10-22 07:44 UTC803
                    None => sessions.discard().await?,
d8d8a9fe770aatagen2026-10-22 07:44 UTC804
                }
d8d8a9fe770aatagen2026-10-22 07:44 UTC805
                return Box::pin(Self::restore(sessions)).await;
d8d8a9fe770aatagen2026-10-22 07:44 UTC806
            }
d8d8a9fe770aatagen2026-10-22 07:44 UTC807
            AuthenticationRecord::PendingOAuth { .. } => return Ok(Some(Restore::OAuthPending)),
6f1d6591c10datagen2026-09-26 09:52 UTC808
            AuthenticationRecord::SoftLoggedOut { meta, client_id } => {
6f1d6591c10datagen2026-09-26 09:52 UTC809
                return Ok(Some(Restore::SoftLoggedOut(SoftLogout {
6f1d6591c10datagen2026-09-26 09:52 UTC810
                    homeserver: record.homeserver,
6f1d6591c10datagen2026-09-26 09:52 UTC811
                    user_id: meta.user_id,
6f1d6591c10datagen2026-09-26 09:52 UTC812
                    device_id: meta.device_id,
6f1d6591c10datagen2026-09-26 09:52 UTC813
                    client_id: client_id.map(|client_id| client_id.as_str().to_owned()),
6f1d6591c10datagen2026-09-26 09:52 UTC814
                })));
6f1d6591c10datagen2026-09-26 09:52 UTC815
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC816
        };
a250c7fa43fbatagen2026-09-26 13:35 UTC817
        let store_lock = sessions.store().lock().await?;
6f1d6591c10datagen2026-09-26 09:52 UTC818
        let client = matrix_client_builder(sessions.store())
6f1d6591c10datagen2026-09-26 09:52 UTC819
            .homeserver_url(&record.homeserver)
6f1d6591c10datagen2026-09-26 09:52 UTC820
            .build()
6f1d6591c10datagen2026-09-26 09:52 UTC821
            .await
6f1d6591c10datagen2026-09-26 09:52 UTC822
            .map_err(|error| account_error("reopening the Matrix store", error))?;
f12377bce9b9atagen2026-09-26 12:32 UTC823
        let tracking = Tracking::start(&client, sessions)?;
96fdf90aacd6atagen2026-09-08 08:24 UTC824
        client
96fdf90aacd6atagen2026-09-08 08:24 UTC825
            .restore_session(authentication)
96fdf90aacd6atagen2026-09-08 08:24 UTC826
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC827
            .map_err(|error| account_error("restoring the authenticated session", error))?;
a250c7fa43fbatagen2026-09-26 13:35 UTC828
        Ok(Some(Restore::Resumed(Self {
a250c7fa43fbatagen2026-09-26 13:35 UTC829
            client,
a250c7fa43fbatagen2026-09-26 13:35 UTC830
            tracking,
a250c7fa43fbatagen2026-09-26 13:35 UTC831
            store_lock,
a250c7fa43fbatagen2026-09-26 13:35 UTC832
        })))
f12377bce9b9atagen2026-09-26 12:32 UTC833
    }
f12377bce9b9atagen2026-09-26 12:32 UTC834
f12377bce9b9atagen2026-09-26 12:32 UTC835
    /// leave the record to the host, which then saves `ClientHandle::session_record()`
f12377bce9b9atagen2026-09-26 12:32 UTC836
    /// on every `SessionTokensChanged`
f12377bce9b9atagen2026-09-26 12:32 UTC837
    #[must_use]
f12377bce9b9atagen2026-09-26 12:32 UTC838
    pub fn without_persistence(mut self) -> Self {
f12377bce9b9atagen2026-09-26 12:32 UTC839
        self.tracking.persistence = None;
f12377bce9b9atagen2026-09-26 12:32 UTC840
        #[cfg(not(target_family = "wasm"))]
f12377bce9b9atagen2026-09-26 12:32 UTC841
        self.tracking.inline.store(false, Ordering::Release);
f12377bce9b9atagen2026-09-26 12:32 UTC842
        self
96fdf90aacd6atagen2026-09-08 08:24 UTC843
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC844
96fdf90aacd6atagen2026-09-08 08:24 UTC845
    pub async fn build(
96fdf90aacd6atagen2026-09-08 08:24 UTC846
        self,
96fdf90aacd6atagen2026-09-08 08:24 UTC847
        config: EngineConfig,
96fdf90aacd6atagen2026-09-08 08:24 UTC848
    ) -> Result<(Engine, ClientHandle, EventStream), AccountError> {
cbb715e11963atagen2026-10-24 23:24 UTC849
        let account = crate::mentions::store::Account {
cbb715e11963atagen2026-10-24 23:24 UTC850
            homeserver: self.client.homeserver().to_string(),
cbb715e11963atagen2026-10-24 23:24 UTC851
            user_id: self
cbb715e11963atagen2026-10-24 23:24 UTC852
                .client
cbb715e11963atagen2026-10-24 23:24 UTC853
                .user_id()
cbb715e11963atagen2026-10-24 23:24 UTC854
                .ok_or(AccountError::MissingSession)?
cbb715e11963atagen2026-10-24 23:24 UTC855
                .to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC856
        };
cbb715e11963atagen2026-10-24 23:24 UTC857
        let mentions = crate::mentions::store::MentionsStore::open(
cbb715e11963atagen2026-10-24 23:24 UTC858
            self.tracking.store.store(),
cbb715e11963atagen2026-10-24 23:24 UTC859
            account,
cbb715e11963atagen2026-10-24 23:24 UTC860
            config.max_mentions_bytes,
cbb715e11963atagen2026-10-24 23:24 UTC861
        )
7cdaa86b9489atagen2026-10-25 08:43 UTC862
        .await;
f12377bce9b9atagen2026-09-26 12:32 UTC863
        Engine::with_persistence(
f12377bce9b9atagen2026-09-26 12:32 UTC864
            self.client,
f12377bce9b9atagen2026-09-26 12:32 UTC865
            config,
f12377bce9b9atagen2026-09-26 12:32 UTC866
            self.tracking.session_changes,
f12377bce9b9atagen2026-09-26 12:32 UTC867
            self.tracking.persistence,
a250c7fa43fbatagen2026-09-26 13:35 UTC868
            Some(self.store_lock),
cbb715e11963atagen2026-10-24 23:24 UTC869
            Some(mentions),
f12377bce9b9atagen2026-09-26 12:32 UTC870
        )
f12377bce9b9atagen2026-09-26 12:32 UTC871
        .await
f12377bce9b9atagen2026-09-26 12:32 UTC872
        .map_err(AccountError::from)
96fdf90aacd6atagen2026-09-08 08:24 UTC873
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC874
f12377bce9b9atagen2026-09-26 12:32 UTC875
    async fn saved(
f12377bce9b9atagen2026-09-26 12:32 UTC876
        client: Client,
a250c7fa43fbatagen2026-09-26 13:35 UTC877
        sessions: Arc<dyn SessionStore>,
f12377bce9b9atagen2026-09-26 12:32 UTC878
        tracking: Tracking,
a250c7fa43fbatagen2026-09-26 13:35 UTC879
        store_lock: StoreLock,
f12377bce9b9atagen2026-09-26 12:32 UTC880
    ) -> Result<Self, AccountError> {
a250c7fa43fbatagen2026-09-26 13:35 UTC881
        UnsavedSession {
a250c7fa43fbatagen2026-09-26 13:35 UTC882
            session: Self {
a250c7fa43fbatagen2026-09-26 13:35 UTC883
                client,
a250c7fa43fbatagen2026-09-26 13:35 UTC884
                tracking,
a250c7fa43fbatagen2026-09-26 13:35 UTC885
                store_lock,
a250c7fa43fbatagen2026-09-26 13:35 UTC886
            },
a250c7fa43fbatagen2026-09-26 13:35 UTC887
            sessions,
a250c7fa43fbatagen2026-09-26 13:35 UTC888
        }
a250c7fa43fbatagen2026-09-26 13:35 UTC889
        .save()
a250c7fa43fbatagen2026-09-26 13:35 UTC890
        .await
f12377bce9b9atagen2026-09-26 12:32 UTC891
    }
f12377bce9b9atagen2026-09-26 12:32 UTC892
}
f12377bce9b9atagen2026-09-26 12:32 UTC893
f12377bce9b9atagen2026-09-26 12:32 UTC894
impl Tracking {
f12377bce9b9atagen2026-09-26 12:32 UTC895
    fn start(client: &Client, sessions: Arc<dyn SessionStore>) -> Result<Self, AccountError> {
f12377bce9b9atagen2026-09-26 12:32 UTC896
        let session_changes = client.subscribe_to_session_changes();
f12377bce9b9atagen2026-09-26 12:32 UTC897
        #[cfg_attr(target_family = "wasm", allow(unused_variables))]
f12377bce9b9atagen2026-09-26 12:32 UTC898
        let (results, inline_results) = mpsc::unbounded_channel();
f12377bce9b9atagen2026-09-26 12:32 UTC899
        #[cfg(not(target_family = "wasm"))]
f12377bce9b9atagen2026-09-26 12:32 UTC900
        let inline = save_inline(client, sessions.clone(), results)?;
f12377bce9b9atagen2026-09-26 12:32 UTC901
        Ok(Self {
cbb715e11963atagen2026-10-24 23:24 UTC902
            store: sessions.clone(),
f12377bce9b9atagen2026-09-26 12:32 UTC903
            session_changes,
f12377bce9b9atagen2026-09-26 12:32 UTC904
            persistence: Some(Persistence {
f12377bce9b9atagen2026-09-26 12:32 UTC905
                save: Box::new(move |client| {
f12377bce9b9atagen2026-09-26 12:32 UTC906
                    let sessions = sessions.clone();
f12377bce9b9atagen2026-09-26 12:32 UTC907
                    Box::pin(async move {
f12377bce9b9atagen2026-09-26 12:32 UTC908
                        let record = session_record(&client).map_err(save_failed)?;
f12377bce9b9atagen2026-09-26 12:32 UTC909
                        sessions.save(&record).await.map_err(save_failed)
f12377bce9b9atagen2026-09-26 12:32 UTC910
                    })
f12377bce9b9atagen2026-09-26 12:32 UTC911
                }),
f12377bce9b9atagen2026-09-26 12:32 UTC912
                inline: inline_results,
f12377bce9b9atagen2026-09-26 12:32 UTC913
            }),
f12377bce9b9atagen2026-09-26 12:32 UTC914
            #[cfg(not(target_family = "wasm"))]
f12377bce9b9atagen2026-09-26 12:32 UTC915
            inline,
f12377bce9b9atagen2026-09-26 12:32 UTC916
        })
96fdf90aacd6atagen2026-09-08 08:24 UTC917
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC918
}
96fdf90aacd6atagen2026-09-08 08:24 UTC919
f12377bce9b9atagen2026-09-26 12:32 UTC920
/// the sdk calls this inside its refresh, after it has the new tokens and before it uses
f12377bce9b9atagen2026-09-26 12:32 UTC921
/// them, so a store that writes there and then can't lose a refresh token to a crash
f12377bce9b9atagen2026-09-26 12:32 UTC922
#[cfg(not(target_family = "wasm"))]
f12377bce9b9atagen2026-09-26 12:32 UTC923
fn save_inline(
f12377bce9b9atagen2026-09-26 12:32 UTC924
    client: &Client,
f12377bce9b9atagen2026-09-26 12:32 UTC925
    sessions: Arc<dyn SessionStore>,
f12377bce9b9atagen2026-09-26 12:32 UTC926
    results: mpsc::UnboundedSender<Result<(), crate::Error>>,
f12377bce9b9atagen2026-09-26 12:32 UTC927
) -> Result<Arc<AtomicBool>, AccountError> {
f12377bce9b9atagen2026-09-26 12:32 UTC928
    let enabled = Arc::new(AtomicBool::new(true));
f12377bce9b9atagen2026-09-26 12:32 UTC929
    let saving = enabled.clone();
f12377bce9b9atagen2026-09-26 12:32 UTC930
    client
f12377bce9b9atagen2026-09-26 12:32 UTC931
        .set_session_callbacks(
f12377bce9b9atagen2026-09-26 12:32 UTC932
            // only asked for with the cross process refresh lock, which we never enable
f12377bce9b9atagen2026-09-26 12:32 UTC933
            Box::new(|client| {
f12377bce9b9atagen2026-09-26 12:32 UTC934
                client
f12377bce9b9atagen2026-09-26 12:32 UTC935
                    .session_tokens()
f12377bce9b9atagen2026-09-26 12:32 UTC936
                    .ok_or_else(|| AccountError::MissingSession.into())
f12377bce9b9atagen2026-09-26 12:32 UTC937
            }),
f12377bce9b9atagen2026-09-26 12:32 UTC938
            Box::new(move |client| {
f12377bce9b9atagen2026-09-26 12:32 UTC939
                if !saving.load(Ordering::Acquire) {
f12377bce9b9atagen2026-09-26 12:32 UTC940
                    return Ok(());
f12377bce9b9atagen2026-09-26 12:32 UTC941
                }
f12377bce9b9atagen2026-09-26 12:32 UTC942
                let record = session_record(&client)?;
f12377bce9b9atagen2026-09-26 12:32 UTC943
                if let Some(result) = sessions.save_blocking(&record) {
f12377bce9b9atagen2026-09-26 12:32 UTC944
                    let _ = results.send(result.map_err(save_failed));
f12377bce9b9atagen2026-09-26 12:32 UTC945
                }
f12377bce9b9atagen2026-09-26 12:32 UTC946
                Ok(())
f12377bce9b9atagen2026-09-26 12:32 UTC947
            }),
f12377bce9b9atagen2026-09-26 12:32 UTC948
        )
f12377bce9b9atagen2026-09-26 12:32 UTC949
        .map_err(|error| account_error("registering the session callbacks", error))?;
f12377bce9b9atagen2026-09-26 12:32 UTC950
    Ok(enabled)
f12377bce9b9atagen2026-09-26 12:32 UTC951
}
f12377bce9b9atagen2026-09-26 12:32 UTC952
f12377bce9b9atagen2026-09-26 12:32 UTC953
fn save_failed(error: AccountError) -> crate::Error {
f12377bce9b9atagen2026-09-26 12:32 UTC954
    let message = format!("saving the session record failed, {error}");
f12377bce9b9atagen2026-09-26 12:32 UTC955
    crate::Error::with_source(ErrorKind::Store, message, error)
f12377bce9b9atagen2026-09-26 12:32 UTC956
}
f12377bce9b9atagen2026-09-26 12:32 UTC957
96fdf90aacd6atagen2026-09-08 08:24 UTC958
pub(crate) fn session_record(client: &Client) -> Result<SessionRecord, AccountError> {
96fdf90aacd6atagen2026-09-08 08:24 UTC959
    let authentication = match client.session().ok_or(AccountError::MissingSession)? {
96fdf90aacd6atagen2026-09-08 08:24 UTC960
        AuthSession::Matrix(session) => AuthenticationRecord::Matrix(session),
96fdf90aacd6atagen2026-09-08 08:24 UTC961
        AuthSession::OAuth(session) => {
96fdf90aacd6atagen2026-09-08 08:24 UTC962
            let OAuthSession { client_id, user } = *session;
96fdf90aacd6atagen2026-09-08 08:24 UTC963
            AuthenticationRecord::OAuth { client_id, user }
96fdf90aacd6atagen2026-09-08 08:24 UTC964
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC965
        _ => return Err(AccountError::UnsupportedAuthentication),
96fdf90aacd6atagen2026-09-08 08:24 UTC966
    };
96fdf90aacd6atagen2026-09-08 08:24 UTC967
    Ok(SessionRecord {
96fdf90aacd6atagen2026-09-08 08:24 UTC968
        homeserver: client.homeserver().to_string(),
96fdf90aacd6atagen2026-09-08 08:24 UTC969
        authentication,
96fdf90aacd6atagen2026-09-08 08:24 UTC970
    })
96fdf90aacd6atagen2026-09-08 08:24 UTC971
}
96fdf90aacd6atagen2026-09-08 08:24 UTC972
96fdf90aacd6atagen2026-09-08 08:24 UTC973
fn encryption_settings() -> EncryptionSettings {
96fdf90aacd6atagen2026-09-08 08:24 UTC974
    EncryptionSettings {
96fdf90aacd6atagen2026-09-08 08:24 UTC975
        auto_enable_cross_signing: true,
96fdf90aacd6atagen2026-09-08 08:24 UTC976
        backup_download_strategy: BackupDownloadStrategy::OneShot,
96fdf90aacd6atagen2026-09-08 08:24 UTC977
        auto_enable_backups: true,
96fdf90aacd6atagen2026-09-08 08:24 UTC978
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC979
}
96fdf90aacd6atagen2026-09-08 08:24 UTC980
a250c7fa43fbatagen2026-09-26 13:35 UTC981
/// `StoreLock` already keeps every other client off the store, and the sdk's lease would cost
a250c7fa43fbatagen2026-09-26 13:35 UTC982
/// a store write per locked section and a task renewing it every 50ms
96fdf90aacd6atagen2026-09-08 08:24 UTC983
fn matrix_client_builder(store: &StoreConfig) -> ClientBuilder {
a250c7fa43fbatagen2026-09-26 13:35 UTC984
    let builder = Client::builder()
96fdf90aacd6atagen2026-09-08 08:24 UTC985
        .with_encryption_settings(encryption_settings())
96fdf90aacd6atagen2026-09-08 08:24 UTC986
        .with_threading_support(ThreadingSupport::Enabled {
96fdf90aacd6atagen2026-09-08 08:24 UTC987
            with_subscriptions: false,
96fdf90aacd6atagen2026-09-08 08:24 UTC988
        })
a250c7fa43fbatagen2026-09-26 13:35 UTC989
        .handle_refresh_tokens()
a250c7fa43fbatagen2026-09-26 13:35 UTC990
        .cross_process_store_config(CrossProcessLockConfig::SingleProcess);
62fe32759529atagen2026-09-26 08:50 UTC991
    match &store.location {
62fe32759529atagen2026-09-26 08:50 UTC992
        #[cfg(feature = "sqlite")]
62fe32759529atagen2026-09-26 08:50 UTC993
        StoreLocation::Sqlite(path) => builder.sqlite_store(path, Some(&store.passphrase)),
62fe32759529atagen2026-09-26 08:50 UTC994
        #[cfg(feature = "indexeddb")]
62fe32759529atagen2026-09-26 08:50 UTC995
        StoreLocation::IndexedDb(name) => builder.indexeddb_store(name, Some(&store.passphrase)),
62fe32759529atagen2026-09-26 08:50 UTC996
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC997
}
96fdf90aacd6atagen2026-09-08 08:24 UTC998
9c05b1dd327aatagen2026-10-08 11:27 UTC999
/// a bare server name goes through untouched, since that's what makes the sdk try `.well-known`
cb0668d30c14atagen2026-10-08 11:34 UTC1000
pub(crate) fn homeserver_input(
cb0668d30c14atagen2026-10-08 11:34 UTC1001
    input: &str,
cb0668d30c14atagen2026-10-08 11:34 UTC1002
    allow_insecure_loopback: bool,
cb0668d30c14atagen2026-10-08 11:34 UTC1003
) -> Result<&str, AccountError> {
9c05b1dd327aatagen2026-10-08 11:27 UTC1004
    let input = input.trim();
9c05b1dd327aatagen2026-10-08 11:27 UTC1005
    let invalid = |reason| Err(AccountError::InvalidHomeserver(reason));
9c05b1dd327aatagen2026-10-08 11:27 UTC1006
    if input.is_empty() {
9c05b1dd327aatagen2026-10-08 11:27 UTC1007
        return invalid("it's empty");
9c05b1dd327aatagen2026-10-08 11:27 UTC1008
    }
9c05b1dd327aatagen2026-10-08 11:27 UTC1009
    if input.contains(char::is_whitespace) {
9c05b1dd327aatagen2026-10-08 11:27 UTC1010
        return invalid("it has spaces in it");
9c05b1dd327aatagen2026-10-08 11:27 UTC1011
    }
9c05b1dd327aatagen2026-10-08 11:27 UTC1012
    if !input.contains("://") {
9c05b1dd327aatagen2026-10-08 11:27 UTC1013
        return match <&ruma::ServerName>::try_from(input) {
9c05b1dd327aatagen2026-10-08 11:27 UTC1014
            Ok(_) => Ok(input),
9c05b1dd327aatagen2026-10-08 11:27 UTC1015
            Err(_) => invalid("it's neither a server name nor a url"),
9c05b1dd327aatagen2026-10-08 11:27 UTC1016
        };
9c05b1dd327aatagen2026-10-08 11:27 UTC1017
    }
9c05b1dd327aatagen2026-10-08 11:27 UTC1018
    let Ok(url) = Url::parse(input) else {
9c05b1dd327aatagen2026-10-08 11:27 UTC1019
        return invalid("it isn't a valid url");
9c05b1dd327aatagen2026-10-08 11:27 UTC1020
    };
9c05b1dd327aatagen2026-10-08 11:27 UTC1021
    if !url.username().is_empty() || url.password().is_some() {
9c05b1dd327aatagen2026-10-08 11:27 UTC1022
        return invalid("it has a username in it");
9c05b1dd327aatagen2026-10-08 11:27 UTC1023
    }
9c05b1dd327aatagen2026-10-08 11:27 UTC1024
    let loopback = match url.host() {
9c05b1dd327aatagen2026-10-08 11:27 UTC1025
        None => return invalid("it has no host"),
9c05b1dd327aatagen2026-10-08 11:27 UTC1026
        Some(url::Host::Domain(domain)) => domain.eq_ignore_ascii_case("localhost"),
9c05b1dd327aatagen2026-10-08 11:27 UTC1027
        Some(url::Host::Ipv4(address)) => address.is_loopback(),
9c05b1dd327aatagen2026-10-08 11:27 UTC1028
        Some(url::Host::Ipv6(address)) => address.is_loopback(),
9c05b1dd327aatagen2026-10-08 11:27 UTC1029
    };
9c05b1dd327aatagen2026-10-08 11:27 UTC1030
    match url.scheme() {
9c05b1dd327aatagen2026-10-08 11:27 UTC1031
        "https" => Ok(input),
9c05b1dd327aatagen2026-10-08 11:27 UTC1032
        "http" if allow_insecure_loopback && loopback => Ok(input),
9c05b1dd327aatagen2026-10-08 11:27 UTC1033
        "http" if allow_insecure_loopback => invalid("plain http only reaches loopback hosts"),
9c05b1dd327aatagen2026-10-08 11:27 UTC1034
        "http" => invalid("it isn't https"),
9c05b1dd327aatagen2026-10-08 11:27 UTC1035
        _ => invalid("only http and https are spoken"),
9c05b1dd327aatagen2026-10-08 11:27 UTC1036
    }
9c05b1dd327aatagen2026-10-08 11:27 UTC1037
}
9c05b1dd327aatagen2026-10-08 11:27 UTC1038
713205a95af2atagen2026-09-26 09:24 UTC1039
pub(crate) fn account_error(
96fdf90aacd6atagen2026-09-08 08:24 UTC1040
    context: &'static str,
62fe32759529atagen2026-09-26 08:50 UTC1041
    source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
96fdf90aacd6atagen2026-09-08 08:24 UTC1042
) -> AccountError {
96fdf90aacd6atagen2026-09-08 08:24 UTC1043
    AccountError::Operation {
96fdf90aacd6atagen2026-09-08 08:24 UTC1044
        context,
9a25b6ce66f4atagen2026-10-08 11:15 UTC1045
        kind: crate::error::classify(&source),
9a25b6ce66f4atagen2026-10-08 11:15 UTC1046
        server: ServerError::find(&source),
96fdf90aacd6atagen2026-09-08 08:24 UTC1047
        source: Box::new(source),
96fdf90aacd6atagen2026-09-08 08:24 UTC1048
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC1049
}