atagen

macltok

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

Blame: src/accounts.rs

cb0668d30c14atagen2026-10-08 11:34 UTC1
use std::sync::Arc;
cb0668d30c14atagen2026-10-08 11:34 UTC2
#[cfg(feature = "sqlite")]
cb0668d30c14atagen2026-10-08 11:34 UTC3
use std::{collections::BTreeSet, path::PathBuf};
cb0668d30c14atagen2026-10-08 11:34 UTC4
cb0668d30c14atagen2026-10-08 11:34 UTC5
use ruma::TransactionId;
cb0668d30c14atagen2026-10-08 11:34 UTC6
cb0668d30c14atagen2026-10-08 11:34 UTC7
use crate::{
cb0668d30c14atagen2026-10-08 11:34 UTC8
    AccountError, EncryptedSessionStore, OAuthLogin, PasswordLogin, PendingOAuthLogin, Restore,
cb0668d30c14atagen2026-10-08 11:34 UTC9
    Session, SessionRecord, SessionStore, SoftLogout, StoreConfig, account::homeserver_input,
cb0668d30c14atagen2026-10-08 11:34 UTC10
    engine::StoreLock,
cb0668d30c14atagen2026-10-08 11:34 UTC11
};
cb0668d30c14atagen2026-10-08 11:34 UTC12
cb0668d30c14atagen2026-10-08 11:34 UTC13
/// every account one owner has, a store each under one root and one passphrase. whoever holds
cb0668d30c14atagen2026-10-08 11:34 UTC14
/// the set can resume any soft-logged-out device in it with just its password, and the device's
cb0668d30c14atagen2026-10-08 11:34 UTC15
/// store has its e2ee keys, so a set belongs to one person and never to a whole machine
cb0668d30c14atagen2026-10-08 11:34 UTC16
pub struct Accounts {
cb0668d30c14atagen2026-10-08 11:34 UTC17
    root: Root,
cb0668d30c14atagen2026-10-08 11:34 UTC18
    passphrase: String,
cb0668d30c14atagen2026-10-08 11:34 UTC19
}
cb0668d30c14atagen2026-10-08 11:34 UTC20
cb0668d30c14atagen2026-10-08 11:34 UTC21
enum Root {
cb0668d30c14atagen2026-10-08 11:34 UTC22
    #[cfg(feature = "sqlite")]
cb0668d30c14atagen2026-10-08 11:34 UTC23
    Directory(PathBuf),
cb0668d30c14atagen2026-10-08 11:34 UTC24
    #[cfg(feature = "indexeddb")]
cb0668d30c14atagen2026-10-08 11:34 UTC25
    Prefix(String),
cb0668d30c14atagen2026-10-08 11:34 UTC26
}
cb0668d30c14atagen2026-10-08 11:34 UTC27
cb0668d30c14atagen2026-10-08 11:34 UTC28
/// one account's record and store
cb0668d30c14atagen2026-10-08 11:34 UTC29
#[derive(Clone)]
cb0668d30c14atagen2026-10-08 11:34 UTC30
pub struct Account {
cb0668d30c14atagen2026-10-08 11:34 UTC31
    id: String,
cb0668d30c14atagen2026-10-08 11:34 UTC32
    sessions: Arc<EncryptedSessionStore>,
cb0668d30c14atagen2026-10-08 11:34 UTC33
}
cb0668d30c14atagen2026-10-08 11:34 UTC34
cb0668d30c14atagen2026-10-08 11:34 UTC35
impl Account {
cb0668d30c14atagen2026-10-08 11:34 UTC36
    /// stable for the account's life, and safe in a cookie or a url
cb0668d30c14atagen2026-10-08 11:34 UTC37
    #[must_use]
cb0668d30c14atagen2026-10-08 11:34 UTC38
    pub fn id(&self) -> &str {
cb0668d30c14atagen2026-10-08 11:34 UTC39
        &self.id
cb0668d30c14atagen2026-10-08 11:34 UTC40
    }
cb0668d30c14atagen2026-10-08 11:34 UTC41
cb0668d30c14atagen2026-10-08 11:34 UTC42
    /// for `Termination::retain`. a discarded account drops out of `Accounts::list`
cb0668d30c14atagen2026-10-08 11:34 UTC43
    #[must_use]
cb0668d30c14atagen2026-10-08 11:34 UTC44
    pub fn sessions(&self) -> Arc<dyn SessionStore> {
cb0668d30c14atagen2026-10-08 11:34 UTC45
        self.sessions.clone()
cb0668d30c14atagen2026-10-08 11:34 UTC46
    }
cb0668d30c14atagen2026-10-08 11:34 UTC47
cb0668d30c14atagen2026-10-08 11:34 UTC48
    pub async fn restore(&self) -> Result<Option<Restore>, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC49
        Session::restore(self.sessions()).await
cb0668d30c14atagen2026-10-08 11:34 UTC50
    }
cb0668d30c14atagen2026-10-08 11:34 UTC51
}
cb0668d30c14atagen2026-10-08 11:34 UTC52
cb0668d30c14atagen2026-10-08 11:34 UTC53
impl std::fmt::Debug for Account {
cb0668d30c14atagen2026-10-08 11:34 UTC54
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
cb0668d30c14atagen2026-10-08 11:34 UTC55
        formatter
cb0668d30c14atagen2026-10-08 11:34 UTC56
            .debug_struct("Account")
cb0668d30c14atagen2026-10-08 11:34 UTC57
            .field("id", &self.id)
cb0668d30c14atagen2026-10-08 11:34 UTC58
            .finish_non_exhaustive()
cb0668d30c14atagen2026-10-08 11:34 UTC59
    }
cb0668d30c14atagen2026-10-08 11:34 UTC60
}
cb0668d30c14atagen2026-10-08 11:34 UTC61
cb0668d30c14atagen2026-10-08 11:34 UTC62
enum Found {
cb0668d30c14atagen2026-10-08 11:34 UTC63
    Nobody,
cb0668d30c14atagen2026-10-08 11:34 UTC64
    SoftLoggedOut(Account, SoftLogout),
cb0668d30c14atagen2026-10-08 11:34 UTC65
}
cb0668d30c14atagen2026-10-08 11:34 UTC66
cb0668d30c14atagen2026-10-08 11:34 UTC67
impl Accounts {
cb0668d30c14atagen2026-10-08 11:34 UTC68
    /// each account is a store directory under `root` with its record beside it
cb0668d30c14atagen2026-10-08 11:34 UTC69
    #[cfg(feature = "sqlite")]
cb0668d30c14atagen2026-10-08 11:34 UTC70
    #[must_use]
cb0668d30c14atagen2026-10-08 11:34 UTC71
    pub fn sqlite(root: impl Into<PathBuf>, passphrase: impl Into<String>) -> Self {
cb0668d30c14atagen2026-10-08 11:34 UTC72
        Self {
cb0668d30c14atagen2026-10-08 11:34 UTC73
            root: Root::Directory(root.into()),
cb0668d30c14atagen2026-10-08 11:34 UTC74
            passphrase: passphrase.into(),
cb0668d30c14atagen2026-10-08 11:34 UTC75
        }
cb0668d30c14atagen2026-10-08 11:34 UTC76
    }
cb0668d30c14atagen2026-10-08 11:34 UTC77
cb0668d30c14atagen2026-10-08 11:34 UTC78
    /// each account is a store named `<prefix>::<id>`, listed in `<prefix>::macltok-accounts`
cb0668d30c14atagen2026-10-08 11:34 UTC79
    #[cfg(feature = "indexeddb")]
cb0668d30c14atagen2026-10-08 11:34 UTC80
    #[must_use]
cb0668d30c14atagen2026-10-08 11:34 UTC81
    pub fn indexeddb(prefix: impl Into<String>, passphrase: impl Into<String>) -> Self {
cb0668d30c14atagen2026-10-08 11:34 UTC82
        Self {
cb0668d30c14atagen2026-10-08 11:34 UTC83
            root: Root::Prefix(prefix.into()),
cb0668d30c14atagen2026-10-08 11:34 UTC84
            passphrase: passphrase.into(),
cb0668d30c14atagen2026-10-08 11:34 UTC85
        }
cb0668d30c14atagen2026-10-08 11:34 UTC86
    }
cb0668d30c14atagen2026-10-08 11:34 UTC87
cb0668d30c14atagen2026-10-08 11:34 UTC88
    /// `None` for anything `Account::id` couldn't have been
cb0668d30c14atagen2026-10-08 11:34 UTC89
    #[must_use]
cb0668d30c14atagen2026-10-08 11:34 UTC90
    pub fn account(&self, id: &str) -> Option<Account> {
cb0668d30c14atagen2026-10-08 11:34 UTC91
        let valid =
cb0668d30c14atagen2026-10-08 11:34 UTC92
            !id.is_empty() && id.len() <= 64 && id.bytes().all(|b| b.is_ascii_alphanumeric());
cb0668d30c14atagen2026-10-08 11:34 UTC93
        valid.then(|| self.open(id.to_owned()))
cb0668d30c14atagen2026-10-08 11:34 UTC94
    }
cb0668d30c14atagen2026-10-08 11:34 UTC95
cb0668d30c14atagen2026-10-08 11:34 UTC96
    /// every account with a record. a store left without one, by a login that died halfway or a
cb0668d30c14atagen2026-10-08 11:34 UTC97
    /// discard that couldn't finish, is deleted on the way past
cb0668d30c14atagen2026-10-08 11:34 UTC98
    pub async fn list(&self) -> Result<Vec<Account>, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC99
        let mut accounts = Vec::new();
cb0668d30c14atagen2026-10-08 11:34 UTC100
        for account in self.slots().await? {
cb0668d30c14atagen2026-10-08 11:34 UTC101
            match account.sessions.load().await {
cb0668d30c14atagen2026-10-08 11:34 UTC102
                Ok(None) => self.sweep(&account).await,
cb0668d30c14atagen2026-10-08 11:34 UTC103
                Ok(Some(_)) | Err(_) => accounts.push(account),
cb0668d30c14atagen2026-10-08 11:34 UTC104
            }
cb0668d30c14atagen2026-10-08 11:34 UTC105
        }
cb0668d30c14atagen2026-10-08 11:34 UTC106
        Ok(accounts)
cb0668d30c14atagen2026-10-08 11:34 UTC107
    }
cb0668d30c14atagen2026-10-08 11:34 UTC108
cb0668d30c14atagen2026-10-08 11:34 UTC109
    /// a soft-logged-out account the login names is resumed as its own device, keeping its keys.
cb0668d30c14atagen2026-10-08 11:34 UTC110
    /// anyone else gets a new account
cb0668d30c14atagen2026-10-08 11:34 UTC111
    pub async fn login_password(
cb0668d30c14atagen2026-10-08 11:34 UTC112
        &self,
cb0668d30c14atagen2026-10-08 11:34 UTC113
        login: PasswordLogin,
cb0668d30c14atagen2026-10-08 11:34 UTC114
    ) -> Result<(Account, Session), AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC115
        homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
cb0668d30c14atagen2026-10-08 11:34 UTC116
        let found = self
cb0668d30c14atagen2026-10-08 11:34 UTC117
            .find(|record| record_names(record, &login.homeserver, &login.username))
cb0668d30c14atagen2026-10-08 11:34 UTC118
            .await?;
cb0668d30c14atagen2026-10-08 11:34 UTC119
        match found {
cb0668d30c14atagen2026-10-08 11:34 UTC120
            Found::SoftLoggedOut(account, soft) => {
cb0668d30c14atagen2026-10-08 11:34 UTC121
                let store_lock = account.sessions.store().lock().await?;
cb0668d30c14atagen2026-10-08 11:34 UTC122
                let login = PasswordLogin {
cb0668d30c14atagen2026-10-08 11:34 UTC123
                    device_display_name: login.device_display_name,
cb0668d30c14atagen2026-10-08 11:34 UTC124
                    ..soft.password_login(login.password)
cb0668d30c14atagen2026-10-08 11:34 UTC125
                };
cb0668d30c14atagen2026-10-08 11:34 UTC126
                let session =
cb0668d30c14atagen2026-10-08 11:34 UTC127
                    Session::login_password_locked(login, account.sessions(), store_lock).await?;
cb0668d30c14atagen2026-10-08 11:34 UTC128
                Ok((account, session))
cb0668d30c14atagen2026-10-08 11:34 UTC129
            }
cb0668d30c14atagen2026-10-08 11:34 UTC130
            Found::Nobody => {
cb0668d30c14atagen2026-10-08 11:34 UTC131
                let (account, store_lock) = self.fresh().await?;
cb0668d30c14atagen2026-10-08 11:34 UTC132
                let login = PasswordLogin {
cb0668d30c14atagen2026-10-08 11:34 UTC133
                    device_id: None,
cb0668d30c14atagen2026-10-08 11:34 UTC134
                    ..login
cb0668d30c14atagen2026-10-08 11:34 UTC135
                };
cb0668d30c14atagen2026-10-08 11:34 UTC136
                match Session::login_password_locked(login, account.sessions(), store_lock).await {
cb0668d30c14atagen2026-10-08 11:34 UTC137
                    Ok(session) => Ok((account, session)),
cb0668d30c14atagen2026-10-08 11:34 UTC138
                    Err(error) => Err(self.abandon(&account, error).await),
cb0668d30c14atagen2026-10-08 11:34 UTC139
                }
cb0668d30c14atagen2026-10-08 11:34 UTC140
            }
cb0668d30c14atagen2026-10-08 11:34 UTC141
        }
cb0668d30c14atagen2026-10-08 11:34 UTC142
    }
cb0668d30c14atagen2026-10-08 11:34 UTC143
cb0668d30c14atagen2026-10-08 11:34 UTC144
    /// resumes the soft-logged-out account whose device `login` names, as
cb0668d30c14atagen2026-10-08 11:34 UTC145
    /// `SoftLogout::oauth_login` sets it. without one it's a new account, since which account an
cb0668d30c14atagen2026-10-08 11:34 UTC146
    /// OAuth login is for isn't known until it finishes
cb0668d30c14atagen2026-10-08 11:34 UTC147
    pub async fn begin_oauth(
cb0668d30c14atagen2026-10-08 11:34 UTC148
        &self,
cb0668d30c14atagen2026-10-08 11:34 UTC149
        login: OAuthLogin,
cb0668d30c14atagen2026-10-08 11:34 UTC150
    ) -> Result<(Account, PendingOAuthLogin), AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC151
        homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
cb0668d30c14atagen2026-10-08 11:34 UTC152
        let device_id = login.device_id.clone();
cb0668d30c14atagen2026-10-08 11:34 UTC153
        let found = self
cb0668d30c14atagen2026-10-08 11:34 UTC154
            .find(|record| {
cb0668d30c14atagen2026-10-08 11:34 UTC155
                record
cb0668d30c14atagen2026-10-08 11:34 UTC156
                    .soft_logout()
cb0668d30c14atagen2026-10-08 11:34 UTC157
                    .is_some_and(|soft| device_id.as_ref() == Some(&soft.device_id))
cb0668d30c14atagen2026-10-08 11:34 UTC158
            })
cb0668d30c14atagen2026-10-08 11:34 UTC159
            .await?;
cb0668d30c14atagen2026-10-08 11:34 UTC160
        match found {
cb0668d30c14atagen2026-10-08 11:34 UTC161
            Found::SoftLoggedOut(account, soft) => {
cb0668d30c14atagen2026-10-08 11:34 UTC162
                let store_lock = account.sessions.store().lock().await?;
cb0668d30c14atagen2026-10-08 11:34 UTC163
                let login = OAuthLogin {
cb0668d30c14atagen2026-10-08 11:34 UTC164
                    homeserver: soft.homeserver,
cb0668d30c14atagen2026-10-08 11:34 UTC165
                    client_id: login.client_id.or(soft.client_id),
cb0668d30c14atagen2026-10-08 11:34 UTC166
                    allow_insecure_loopback: true,
cb0668d30c14atagen2026-10-08 11:34 UTC167
                    ..login
cb0668d30c14atagen2026-10-08 11:34 UTC168
                };
cb0668d30c14atagen2026-10-08 11:34 UTC169
                let pending =
cb0668d30c14atagen2026-10-08 11:34 UTC170
                    Session::begin_oauth_locked(login, account.sessions(), store_lock).await?;
cb0668d30c14atagen2026-10-08 11:34 UTC171
                Ok((account, pending))
cb0668d30c14atagen2026-10-08 11:34 UTC172
            }
cb0668d30c14atagen2026-10-08 11:34 UTC173
            Found::Nobody => {
cb0668d30c14atagen2026-10-08 11:34 UTC174
                let (account, store_lock) = self.fresh().await?;
cb0668d30c14atagen2026-10-08 11:34 UTC175
                let login = OAuthLogin {
cb0668d30c14atagen2026-10-08 11:34 UTC176
                    device_id: None,
cb0668d30c14atagen2026-10-08 11:34 UTC177
                    ..login
cb0668d30c14atagen2026-10-08 11:34 UTC178
                };
cb0668d30c14atagen2026-10-08 11:34 UTC179
                match Session::begin_oauth_locked(login, account.sessions(), store_lock).await {
cb0668d30c14atagen2026-10-08 11:34 UTC180
                    Ok(pending) => Ok((account, pending)),
cb0668d30c14atagen2026-10-08 11:34 UTC181
                    Err(error) => Err(self.abandon(&account, error).await),
cb0668d30c14atagen2026-10-08 11:34 UTC182
                }
cb0668d30c14atagen2026-10-08 11:34 UTC183
            }
cb0668d30c14atagen2026-10-08 11:34 UTC184
        }
cb0668d30c14atagen2026-10-08 11:34 UTC185
    }
cb0668d30c14atagen2026-10-08 11:34 UTC186
cb0668d30c14atagen2026-10-08 11:34 UTC187
    /// a live login for the same person is refused rather than given a second device
cb0668d30c14atagen2026-10-08 11:34 UTC188
    async fn find(&self, resumes: impl Fn(&SessionRecord) -> bool) -> Result<Found, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC189
        for account in self.slots().await? {
cb0668d30c14atagen2026-10-08 11:34 UTC190
            let Ok(Some(record)) = account.sessions.load().await else {
cb0668d30c14atagen2026-10-08 11:34 UTC191
                continue;
cb0668d30c14atagen2026-10-08 11:34 UTC192
            };
cb0668d30c14atagen2026-10-08 11:34 UTC193
            if !resumes(&record) {
cb0668d30c14atagen2026-10-08 11:34 UTC194
                continue;
cb0668d30c14atagen2026-10-08 11:34 UTC195
            }
cb0668d30c14atagen2026-10-08 11:34 UTC196
            return match record.soft_logout() {
cb0668d30c14atagen2026-10-08 11:34 UTC197
                Some(soft) => Ok(Found::SoftLoggedOut(account, soft)),
cb0668d30c14atagen2026-10-08 11:34 UTC198
                None => Err(AccountError::AlreadyLoggedIn(record.user_id().clone())),
cb0668d30c14atagen2026-10-08 11:34 UTC199
            };
cb0668d30c14atagen2026-10-08 11:34 UTC200
        }
cb0668d30c14atagen2026-10-08 11:34 UTC201
        Ok(Found::Nobody)
cb0668d30c14atagen2026-10-08 11:34 UTC202
    }
cb0668d30c14atagen2026-10-08 11:34 UTC203
cb0668d30c14atagen2026-10-08 11:34 UTC204
    /// locked before anything else can see it, so `list` never sweeps a login still under way
cb0668d30c14atagen2026-10-08 11:34 UTC205
    async fn fresh(&self) -> Result<(Account, StoreLock), AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC206
        let account = self.open(TransactionId::new().to_string());
cb0668d30c14atagen2026-10-08 11:34 UTC207
        let store_lock = account.sessions.store().lock().await?;
cb0668d30c14atagen2026-10-08 11:34 UTC208
        #[cfg(feature = "indexeddb")]
cb0668d30c14atagen2026-10-08 11:34 UTC209
        {
cb0668d30c14atagen2026-10-08 11:34 UTC210
            let Root::Prefix(prefix) = &self.root;
cb0668d30c14atagen2026-10-08 11:34 UTC211
            index::add(prefix, &account.id).await?;
cb0668d30c14atagen2026-10-08 11:34 UTC212
        }
cb0668d30c14atagen2026-10-08 11:34 UTC213
        Ok((account, store_lock))
cb0668d30c14atagen2026-10-08 11:34 UTC214
    }
cb0668d30c14atagen2026-10-08 11:34 UTC215
cb0668d30c14atagen2026-10-08 11:34 UTC216
    /// a failed first login leaves nothing behind. one whose record didn't save has a live device
cb0668d30c14atagen2026-10-08 11:34 UTC217
    /// on the server, so that stays for `UnsavedSession::save`
cb0668d30c14atagen2026-10-08 11:34 UTC218
    async fn abandon(&self, account: &Account, error: AccountError) -> AccountError {
cb0668d30c14atagen2026-10-08 11:34 UTC219
        if !matches!(error, AccountError::Unsaved { .. }) {
cb0668d30c14atagen2026-10-08 11:34 UTC220
            self.sweep(account).await;
cb0668d30c14atagen2026-10-08 11:34 UTC221
        }
cb0668d30c14atagen2026-10-08 11:34 UTC222
        error
cb0668d30c14atagen2026-10-08 11:34 UTC223
    }
cb0668d30c14atagen2026-10-08 11:34 UTC224
cb0668d30c14atagen2026-10-08 11:34 UTC225
    /// best effort, since whatever it leaves is found and swept again by the next `list`
cb0668d30c14atagen2026-10-08 11:34 UTC226
    async fn sweep(&self, account: &Account) {
cb0668d30c14atagen2026-10-08 11:34 UTC227
        let Ok(_store_lock) = account.sessions.store().lock().await else {
cb0668d30c14atagen2026-10-08 11:34 UTC228
            return;
cb0668d30c14atagen2026-10-08 11:34 UTC229
        };
cb0668d30c14atagen2026-10-08 11:34 UTC230
        if !matches!(account.sessions.load().await, Ok(None)) {
cb0668d30c14atagen2026-10-08 11:34 UTC231
            return;
cb0668d30c14atagen2026-10-08 11:34 UTC232
        }
cb0668d30c14atagen2026-10-08 11:34 UTC233
        if account.sessions.discard().await.is_err() {
cb0668d30c14atagen2026-10-08 11:34 UTC234
            return;
cb0668d30c14atagen2026-10-08 11:34 UTC235
        }
cb0668d30c14atagen2026-10-08 11:34 UTC236
        #[cfg(feature = "indexeddb")]
cb0668d30c14atagen2026-10-08 11:34 UTC237
        {
cb0668d30c14atagen2026-10-08 11:34 UTC238
            let Root::Prefix(prefix) = &self.root;
cb0668d30c14atagen2026-10-08 11:34 UTC239
            let _ = index::remove(prefix, &account.id).await;
cb0668d30c14atagen2026-10-08 11:34 UTC240
        }
cb0668d30c14atagen2026-10-08 11:34 UTC241
    }
cb0668d30c14atagen2026-10-08 11:34 UTC242
cb0668d30c14atagen2026-10-08 11:34 UTC243
    fn open(&self, id: String) -> Account {
cb0668d30c14atagen2026-10-08 11:34 UTC244
        let sessions = match &self.root {
cb0668d30c14atagen2026-10-08 11:34 UTC245
            #[cfg(feature = "sqlite")]
cb0668d30c14atagen2026-10-08 11:34 UTC246
            Root::Directory(root) => EncryptedSessionStore::file(
cb0668d30c14atagen2026-10-08 11:34 UTC247
                root.join(format!("{id}.session")),
cb0668d30c14atagen2026-10-08 11:34 UTC248
                StoreConfig::sqlite(root.join(&id), self.passphrase.clone()),
cb0668d30c14atagen2026-10-08 11:34 UTC249
            ),
cb0668d30c14atagen2026-10-08 11:34 UTC250
            #[cfg(feature = "indexeddb")]
cb0668d30c14atagen2026-10-08 11:34 UTC251
            Root::Prefix(prefix) => EncryptedSessionStore::indexeddb(StoreConfig::indexeddb(
cb0668d30c14atagen2026-10-08 11:34 UTC252
                format!("{prefix}::{id}"),
cb0668d30c14atagen2026-10-08 11:34 UTC253
                self.passphrase.clone(),
cb0668d30c14atagen2026-10-08 11:34 UTC254
            )),
cb0668d30c14atagen2026-10-08 11:34 UTC255
        };
cb0668d30c14atagen2026-10-08 11:34 UTC256
        Account {
cb0668d30c14atagen2026-10-08 11:34 UTC257
            id,
cb0668d30c14atagen2026-10-08 11:34 UTC258
            sessions: Arc::new(sessions),
cb0668d30c14atagen2026-10-08 11:34 UTC259
        }
cb0668d30c14atagen2026-10-08 11:34 UTC260
    }
cb0668d30c14atagen2026-10-08 11:34 UTC261
cb0668d30c14atagen2026-10-08 11:34 UTC262
    async fn slots(&self) -> Result<Vec<Account>, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC263
        let ids = match &self.root {
cb0668d30c14atagen2026-10-08 11:34 UTC264
            #[cfg(feature = "sqlite")]
cb0668d30c14atagen2026-10-08 11:34 UTC265
            Root::Directory(root) => directory_slots(root)?,
cb0668d30c14atagen2026-10-08 11:34 UTC266
            #[cfg(feature = "indexeddb")]
cb0668d30c14atagen2026-10-08 11:34 UTC267
            Root::Prefix(prefix) => index::list(prefix).await?,
cb0668d30c14atagen2026-10-08 11:34 UTC268
        };
cb0668d30c14atagen2026-10-08 11:34 UTC269
        Ok(ids.iter().filter_map(|id| self.account(id)).collect())
cb0668d30c14atagen2026-10-08 11:34 UTC270
    }
cb0668d30c14atagen2026-10-08 11:34 UTC271
}
cb0668d30c14atagen2026-10-08 11:34 UTC272
cb0668d30c14atagen2026-10-08 11:34 UTC273
/// a localpart or a full user id names the same account, but only on the same server. the server
cb0668d30c14atagen2026-10-08 11:34 UTC274
/// is compared by name as well as by url, so a delegated `example.org` still finds the account
cb0668d30c14atagen2026-10-08 11:34 UTC275
/// discovery took to `matrix.example.org`
cb0668d30c14atagen2026-10-08 11:34 UTC276
fn record_names(record: &SessionRecord, homeserver: &str, username: &str) -> bool {
cb0668d30c14atagen2026-10-08 11:34 UTC277
    let user_id = record.user_id();
cb0668d30c14atagen2026-10-08 11:34 UTC278
    let username = username.trim();
cb0668d30c14atagen2026-10-08 11:34 UTC279
    let user = if username.starts_with('@') {
cb0668d30c14atagen2026-10-08 11:34 UTC280
        username.eq_ignore_ascii_case(user_id.as_str())
cb0668d30c14atagen2026-10-08 11:34 UTC281
    } else {
cb0668d30c14atagen2026-10-08 11:34 UTC282
        username.eq_ignore_ascii_case(user_id.localpart())
cb0668d30c14atagen2026-10-08 11:34 UTC283
    };
cb0668d30c14atagen2026-10-08 11:34 UTC284
    let typed = authority(homeserver);
cb0668d30c14atagen2026-10-08 11:34 UTC285
    user && (typed.eq_ignore_ascii_case(user_id.server_name().as_str())
cb0668d30c14atagen2026-10-08 11:34 UTC286
        || typed.eq_ignore_ascii_case(authority(record.homeserver())))
cb0668d30c14atagen2026-10-08 11:34 UTC287
}
cb0668d30c14atagen2026-10-08 11:34 UTC288
cb0668d30c14atagen2026-10-08 11:34 UTC289
fn authority(homeserver: &str) -> &str {
cb0668d30c14atagen2026-10-08 11:34 UTC290
    let homeserver = homeserver.trim();
cb0668d30c14atagen2026-10-08 11:34 UTC291
    let rest = homeserver
cb0668d30c14atagen2026-10-08 11:34 UTC292
        .split_once("://")
cb0668d30c14atagen2026-10-08 11:34 UTC293
        .map_or(homeserver, |(_, rest)| rest);
cb0668d30c14atagen2026-10-08 11:34 UTC294
    rest.split('/').next().unwrap_or(rest)
cb0668d30c14atagen2026-10-08 11:34 UTC295
}
cb0668d30c14atagen2026-10-08 11:34 UTC296
cb0668d30c14atagen2026-10-08 11:34 UTC297
#[cfg(feature = "sqlite")]
cb0668d30c14atagen2026-10-08 11:34 UTC298
fn directory_slots(root: &std::path::Path) -> Result<BTreeSet<String>, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC299
    use std::io::ErrorKind;
cb0668d30c14atagen2026-10-08 11:34 UTC300
cb0668d30c14atagen2026-10-08 11:34 UTC301
    let entries = match std::fs::read_dir(root) {
cb0668d30c14atagen2026-10-08 11:34 UTC302
        Ok(entries) => entries,
cb0668d30c14atagen2026-10-08 11:34 UTC303
        Err(error) if error.kind() == ErrorKind::NotFound => return Ok(BTreeSet::new()),
cb0668d30c14atagen2026-10-08 11:34 UTC304
        Err(error) => return Err(crate::account::account_error("listing the accounts", error)),
cb0668d30c14atagen2026-10-08 11:34 UTC305
    };
cb0668d30c14atagen2026-10-08 11:34 UTC306
    let mut ids = BTreeSet::new();
cb0668d30c14atagen2026-10-08 11:34 UTC307
    for entry in entries {
cb0668d30c14atagen2026-10-08 11:34 UTC308
        let entry =
cb0668d30c14atagen2026-10-08 11:34 UTC309
            entry.map_err(|error| crate::account::account_error("listing the accounts", error))?;
cb0668d30c14atagen2026-10-08 11:34 UTC310
        let name = entry.file_name();
cb0668d30c14atagen2026-10-08 11:34 UTC311
        let Some(name) = name.to_str() else {
cb0668d30c14atagen2026-10-08 11:34 UTC312
            continue;
cb0668d30c14atagen2026-10-08 11:34 UTC313
        };
cb0668d30c14atagen2026-10-08 11:34 UTC314
        if entry.path().is_dir() {
cb0668d30c14atagen2026-10-08 11:34 UTC315
            ids.insert(name.to_owned());
cb0668d30c14atagen2026-10-08 11:34 UTC316
        } else if let Some(id) = name.strip_suffix(".session") {
cb0668d30c14atagen2026-10-08 11:34 UTC317
            ids.insert(id.to_owned());
cb0668d30c14atagen2026-10-08 11:34 UTC318
        }
cb0668d30c14atagen2026-10-08 11:34 UTC319
    }
cb0668d30c14atagen2026-10-08 11:34 UTC320
    Ok(ids)
cb0668d30c14atagen2026-10-08 11:34 UTC321
}
cb0668d30c14atagen2026-10-08 11:34 UTC322
cb0668d30c14atagen2026-10-08 11:34 UTC323
/// a key per account, so two tabs adding at once can't lose one to the other
cb0668d30c14atagen2026-10-08 11:34 UTC324
#[cfg(feature = "indexeddb")]
cb0668d30c14atagen2026-10-08 11:34 UTC325
mod index {
cb0668d30c14atagen2026-10-08 11:34 UTC326
    use std::collections::BTreeSet;
cb0668d30c14atagen2026-10-08 11:34 UTC327
cb0668d30c14atagen2026-10-08 11:34 UTC328
    use indexed_db_futures::{
cb0668d30c14atagen2026-10-08 11:34 UTC329
        Build,
cb0668d30c14atagen2026-10-08 11:34 UTC330
        database::Database,
cb0668d30c14atagen2026-10-08 11:34 UTC331
        error::{Error, OpenDbError},
cb0668d30c14atagen2026-10-08 11:34 UTC332
        prelude::*,
cb0668d30c14atagen2026-10-08 11:34 UTC333
        transaction::{Transaction, TransactionMode},
cb0668d30c14atagen2026-10-08 11:34 UTC334
    };
cb0668d30c14atagen2026-10-08 11:34 UTC335
cb0668d30c14atagen2026-10-08 11:34 UTC336
    use crate::{AccountError, account::account_error};
cb0668d30c14atagen2026-10-08 11:34 UTC337
cb0668d30c14atagen2026-10-08 11:34 UTC338
    const ACCOUNTS: &str = "accounts";
cb0668d30c14atagen2026-10-08 11:34 UTC339
cb0668d30c14atagen2026-10-08 11:34 UTC340
    fn name(prefix: &str) -> String {
cb0668d30c14atagen2026-10-08 11:34 UTC341
        format!("{prefix}::macltok-accounts")
cb0668d30c14atagen2026-10-08 11:34 UTC342
    }
cb0668d30c14atagen2026-10-08 11:34 UTC343
cb0668d30c14atagen2026-10-08 11:34 UTC344
    async fn open(prefix: &str) -> Result<Database, OpenDbError> {
cb0668d30c14atagen2026-10-08 11:34 UTC345
        Database::open(&name(prefix))
cb0668d30c14atagen2026-10-08 11:34 UTC346
            .with_version(1u32)
cb0668d30c14atagen2026-10-08 11:34 UTC347
            .with_on_upgrade_needed(|_, transaction: &Transaction<'_>| -> Result<(), Error> {
cb0668d30c14atagen2026-10-08 11:34 UTC348
                transaction.db().create_object_store(ACCOUNTS).build()?;
cb0668d30c14atagen2026-10-08 11:34 UTC349
                Ok(())
cb0668d30c14atagen2026-10-08 11:34 UTC350
            })
cb0668d30c14atagen2026-10-08 11:34 UTC351
            .build()?
cb0668d30c14atagen2026-10-08 11:34 UTC352
            .await
cb0668d30c14atagen2026-10-08 11:34 UTC353
    }
cb0668d30c14atagen2026-10-08 11:34 UTC354
cb0668d30c14atagen2026-10-08 11:34 UTC355
    pub(super) async fn list(prefix: &str) -> Result<BTreeSet<String>, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC356
        let failed = |error| account_error("listing the accounts", error);
cb0668d30c14atagen2026-10-08 11:34 UTC357
        let database = open(prefix).await.map_err(failed)?;
cb0668d30c14atagen2026-10-08 11:34 UTC358
        let ids = keys(&database).await;
cb0668d30c14atagen2026-10-08 11:34 UTC359
        database.close();
cb0668d30c14atagen2026-10-08 11:34 UTC360
        ids.map_err(|error| account_error("listing the accounts", error))
cb0668d30c14atagen2026-10-08 11:34 UTC361
    }
cb0668d30c14atagen2026-10-08 11:34 UTC362
cb0668d30c14atagen2026-10-08 11:34 UTC363
    pub(super) async fn add(prefix: &str, id: &str) -> Result<(), AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC364
        let failed = |error| account_error("adding the account", error);
cb0668d30c14atagen2026-10-08 11:34 UTC365
        let database = open(prefix).await.map_err(failed)?;
cb0668d30c14atagen2026-10-08 11:34 UTC366
        let added = write(&database, |store| {
cb0668d30c14atagen2026-10-08 11:34 UTC367
            store.put(true).with_key(id).build()?;
cb0668d30c14atagen2026-10-08 11:34 UTC368
            Ok(())
cb0668d30c14atagen2026-10-08 11:34 UTC369
        })
cb0668d30c14atagen2026-10-08 11:34 UTC370
        .await;
cb0668d30c14atagen2026-10-08 11:34 UTC371
        database.close();
cb0668d30c14atagen2026-10-08 11:34 UTC372
        added.map_err(|error| account_error("adding the account", error))
cb0668d30c14atagen2026-10-08 11:34 UTC373
    }
cb0668d30c14atagen2026-10-08 11:34 UTC374
cb0668d30c14atagen2026-10-08 11:34 UTC375
    pub(super) async fn remove(prefix: &str, id: &str) -> Result<(), AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC376
        let failed = |error| account_error("removing the account", error);
cb0668d30c14atagen2026-10-08 11:34 UTC377
        let database = open(prefix).await.map_err(failed)?;
cb0668d30c14atagen2026-10-08 11:34 UTC378
        let removed = write(&database, |store| {
cb0668d30c14atagen2026-10-08 11:34 UTC379
            store.delete(id).build()?;
cb0668d30c14atagen2026-10-08 11:34 UTC380
            Ok(())
cb0668d30c14atagen2026-10-08 11:34 UTC381
        })
cb0668d30c14atagen2026-10-08 11:34 UTC382
        .await;
cb0668d30c14atagen2026-10-08 11:34 UTC383
        database.close();
cb0668d30c14atagen2026-10-08 11:34 UTC384
        removed.map_err(|error| account_error("removing the account", error))
cb0668d30c14atagen2026-10-08 11:34 UTC385
    }
cb0668d30c14atagen2026-10-08 11:34 UTC386
cb0668d30c14atagen2026-10-08 11:34 UTC387
    async fn keys(database: &Database) -> Result<BTreeSet<String>, Error> {
cb0668d30c14atagen2026-10-08 11:34 UTC388
        let transaction = database
cb0668d30c14atagen2026-10-08 11:34 UTC389
            .transaction(ACCOUNTS)
cb0668d30c14atagen2026-10-08 11:34 UTC390
            .with_mode(TransactionMode::Readonly)
cb0668d30c14atagen2026-10-08 11:34 UTC391
            .build()?;
cb0668d30c14atagen2026-10-08 11:34 UTC392
        let store = transaction.object_store(ACCOUNTS)?;
cb0668d30c14atagen2026-10-08 11:34 UTC393
        store.get_all_keys::<String>().await?.collect()
cb0668d30c14atagen2026-10-08 11:34 UTC394
    }
cb0668d30c14atagen2026-10-08 11:34 UTC395
cb0668d30c14atagen2026-10-08 11:34 UTC396
    async fn write(
cb0668d30c14atagen2026-10-08 11:34 UTC397
        database: &Database,
cb0668d30c14atagen2026-10-08 11:34 UTC398
        change: impl FnOnce(&indexed_db_futures::object_store::ObjectStore<'_>) -> Result<(), Error>,
cb0668d30c14atagen2026-10-08 11:34 UTC399
    ) -> Result<(), Error> {
cb0668d30c14atagen2026-10-08 11:34 UTC400
        let transaction = database
cb0668d30c14atagen2026-10-08 11:34 UTC401
            .transaction(ACCOUNTS)
cb0668d30c14atagen2026-10-08 11:34 UTC402
            .with_mode(TransactionMode::Readwrite)
cb0668d30c14atagen2026-10-08 11:34 UTC403
            .build()?;
cb0668d30c14atagen2026-10-08 11:34 UTC404
        change(&transaction.object_store(ACCOUNTS)?)?;
cb0668d30c14atagen2026-10-08 11:34 UTC405
        transaction.commit().await
cb0668d30c14atagen2026-10-08 11:34 UTC406
    }
cb0668d30c14atagen2026-10-08 11:34 UTC407
}