atagen

matrix-rust-sdk

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk.git

Blame: testing/matrix-sdk-integration-testing/src/helpers.rs

7ff8adafe9e4atagen2026-10-22 12:30 UTC1
use std::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC2
    future::Future,
7ff8adafe9e4atagen2026-10-22 12:30 UTC3
    ops::Deref,
7ff8adafe9e4atagen2026-10-22 12:30 UTC4
    option_env,
7ff8adafe9e4atagen2026-10-22 12:30 UTC5
    path::{Path, PathBuf},
7ff8adafe9e4atagen2026-10-22 12:30 UTC6
    sync::{Arc, LazyLock, Mutex as StdMutex},
7ff8adafe9e4atagen2026-10-22 12:30 UTC7
    time::Duration,
7ff8adafe9e4atagen2026-10-22 12:30 UTC8
};
7ff8adafe9e4atagen2026-10-22 12:30 UTC9
7ff8adafe9e4atagen2026-10-22 12:30 UTC10
use anyhow::Result;
7ff8adafe9e4atagen2026-10-22 12:30 UTC11
use assign::assign;
7ff8adafe9e4atagen2026-10-22 12:30 UTC12
use matrix_sdk::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC13
    Client, ClientBuilder, Room, ThreadingSupport,
7ff8adafe9e4atagen2026-10-22 12:30 UTC14
    config::{RequestConfig, SyncSettings},
7ff8adafe9e4atagen2026-10-22 12:30 UTC15
    encryption::EncryptionSettings,
7ff8adafe9e4atagen2026-10-22 12:30 UTC16
    ruma::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC17
        RoomId,
7ff8adafe9e4atagen2026-10-22 12:30 UTC18
        api::client::{account::register::v3::Request as RegistrationRequest, uiaa},
7ff8adafe9e4atagen2026-10-22 12:30 UTC19
        time::Instant,
7ff8adafe9e4atagen2026-10-22 12:30 UTC20
    },
7ff8adafe9e4atagen2026-10-22 12:30 UTC21
    sliding_sync::VersionBuilder,
7ff8adafe9e4atagen2026-10-22 12:30 UTC22
    sync::SyncResponse,
7ff8adafe9e4atagen2026-10-22 12:30 UTC23
    timeout::ElapsedError,
7ff8adafe9e4atagen2026-10-22 12:30 UTC24
};
7ff8adafe9e4atagen2026-10-22 12:30 UTC25
use matrix_sdk_base::crypto::{CollectStrategy, DecryptionSettings, TrustRequirement};
7ff8adafe9e4atagen2026-10-22 12:30 UTC26
use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
7ff8adafe9e4atagen2026-10-22 12:30 UTC27
use rand::RngExt;
7ff8adafe9e4atagen2026-10-22 12:30 UTC28
use tempfile::{TempDir, tempdir};
7ff8adafe9e4atagen2026-10-22 12:30 UTC29
use tokio::{sync::Mutex, time::sleep};
7ff8adafe9e4atagen2026-10-22 12:30 UTC30
7ff8adafe9e4atagen2026-10-22 12:30 UTC31
/// This global maintains temp directories alive for the whole lifetime of the
7ff8adafe9e4atagen2026-10-22 12:30 UTC32
/// process.
7ff8adafe9e4atagen2026-10-22 12:30 UTC33
static TMP_DIRS: LazyLock<Mutex<Vec<TempDir>>> = LazyLock::new(Mutex::default);
7ff8adafe9e4atagen2026-10-22 12:30 UTC34
7ff8adafe9e4atagen2026-10-22 12:30 UTC35
enum SqlitePath {
7ff8adafe9e4atagen2026-10-22 12:30 UTC36
    Random,
7ff8adafe9e4atagen2026-10-22 12:30 UTC37
    Path(PathBuf),
7ff8adafe9e4atagen2026-10-22 12:30 UTC38
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC39
7ff8adafe9e4atagen2026-10-22 12:30 UTC40
pub struct TestClientBuilder {
7ff8adafe9e4atagen2026-10-22 12:30 UTC41
    username: String,
7ff8adafe9e4atagen2026-10-22 12:30 UTC42
    use_sqlite_dir: Option<SqlitePath>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC43
    decryption_settings: Option<DecryptionSettings>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC44
    room_key_recipient_strategy: CollectStrategy,
7ff8adafe9e4atagen2026-10-22 12:30 UTC45
    encryption_settings: EncryptionSettings,
7ff8adafe9e4atagen2026-10-22 12:30 UTC46
    enable_share_history_on_invite: bool,
7ff8adafe9e4atagen2026-10-22 12:30 UTC47
    threading_support: ThreadingSupport,
7ff8adafe9e4atagen2026-10-22 12:30 UTC48
    http_proxy: Option<String>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC49
    cross_process_lock_config: CrossProcessLockConfig,
7ff8adafe9e4atagen2026-10-22 12:30 UTC50
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC51
7ff8adafe9e4atagen2026-10-22 12:30 UTC52
impl TestClientBuilder {
7ff8adafe9e4atagen2026-10-22 12:30 UTC53
    pub fn new(username: impl AsRef<str>) -> Self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC54
        let suffix: u128 = rand::rng().random();
7ff8adafe9e4atagen2026-10-22 12:30 UTC55
        let randomized_username = format!("{}{}", username.as_ref(), suffix);
7ff8adafe9e4atagen2026-10-22 12:30 UTC56
        Self::with_exact_username(randomized_username)
7ff8adafe9e4atagen2026-10-22 12:30 UTC57
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC58
7ff8adafe9e4atagen2026-10-22 12:30 UTC59
    pub fn with_exact_username(username: String) -> Self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC60
        Self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC61
            username,
7ff8adafe9e4atagen2026-10-22 12:30 UTC62
            use_sqlite_dir: None,
7ff8adafe9e4atagen2026-10-22 12:30 UTC63
            decryption_settings: None,
7ff8adafe9e4atagen2026-10-22 12:30 UTC64
            encryption_settings: Default::default(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC65
            room_key_recipient_strategy: Default::default(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC66
            enable_share_history_on_invite: false,
7ff8adafe9e4atagen2026-10-22 12:30 UTC67
            threading_support: ThreadingSupport::Disabled,
7ff8adafe9e4atagen2026-10-22 12:30 UTC68
            http_proxy: None,
7ff8adafe9e4atagen2026-10-22 12:30 UTC69
            cross_process_lock_config: CrossProcessLockConfig::SingleProcess,
7ff8adafe9e4atagen2026-10-22 12:30 UTC70
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC71
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC72
7ff8adafe9e4atagen2026-10-22 12:30 UTC73
    pub fn use_sqlite(mut self) -> Self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC74
        self.use_sqlite_dir = Some(SqlitePath::Random);
7ff8adafe9e4atagen2026-10-22 12:30 UTC75
        self
7ff8adafe9e4atagen2026-10-22 12:30 UTC76
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC77
7ff8adafe9e4atagen2026-10-22 12:30 UTC78
    /// Create or re-use a Sqlite store (with no passphrase) in the supplied
7ff8adafe9e4atagen2026-10-22 12:30 UTC79
    /// directory. Note: this path must remain valid throughout the use of
7ff8adafe9e4atagen2026-10-22 12:30 UTC80
    /// the constructed Client, so if you created a TempDir you must hang on
7ff8adafe9e4atagen2026-10-22 12:30 UTC81
    /// to a reference to it throughout the test.
7ff8adafe9e4atagen2026-10-22 12:30 UTC82
    pub fn use_sqlite_dir(mut self, path: &Path) -> Self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC83
        self.use_sqlite_dir = Some(SqlitePath::Path(path.to_owned()));
7ff8adafe9e4atagen2026-10-22 12:30 UTC84
        self
7ff8adafe9e4atagen2026-10-22 12:30 UTC85
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC86
7ff8adafe9e4atagen2026-10-22 12:30 UTC87
    pub fn encryption_settings(mut self, encryption_settings: EncryptionSettings) -> Self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC88
        self.encryption_settings = encryption_settings;
7ff8adafe9e4atagen2026-10-22 12:30 UTC89
        self
7ff8adafe9e4atagen2026-10-22 12:30 UTC90
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC91
7ff8adafe9e4atagen2026-10-22 12:30 UTC92
    pub fn enable_share_history_on_invite(mut self, enable_share_history_on_invite: bool) -> Self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC93
        self.enable_share_history_on_invite = enable_share_history_on_invite;
7ff8adafe9e4atagen2026-10-22 12:30 UTC94
        self
7ff8adafe9e4atagen2026-10-22 12:30 UTC95
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC96
7ff8adafe9e4atagen2026-10-22 12:30 UTC97
    pub fn enable_threading_support(mut self, thread_support: ThreadingSupport) -> Self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC98
        self.threading_support = thread_support;
7ff8adafe9e4atagen2026-10-22 12:30 UTC99
        self
7ff8adafe9e4atagen2026-10-22 12:30 UTC100
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC101
7ff8adafe9e4atagen2026-10-22 12:30 UTC102
    /// Simulate the behaviour of the clients when the "exclude insecure
7ff8adafe9e4atagen2026-10-22 12:30 UTC103
    /// devices" (MSC4153) labs flag is enabled.
7ff8adafe9e4atagen2026-10-22 12:30 UTC104
    pub fn exclude_insecure_devices(mut self, exclude_insecure_devices: bool) -> Self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC105
        let (sender_device_trust_requirement, room_key_recipient_strategy) =
7ff8adafe9e4atagen2026-10-22 12:30 UTC106
            if exclude_insecure_devices {
7ff8adafe9e4atagen2026-10-22 12:30 UTC107
                (TrustRequirement::CrossSignedOrLegacy, CollectStrategy::IdentityBasedStrategy)
7ff8adafe9e4atagen2026-10-22 12:30 UTC108
            } else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC109
                (TrustRequirement::Untrusted, CollectStrategy::AllDevices)
7ff8adafe9e4atagen2026-10-22 12:30 UTC110
            };
7ff8adafe9e4atagen2026-10-22 12:30 UTC111
        self.decryption_settings = Some(DecryptionSettings { sender_device_trust_requirement });
7ff8adafe9e4atagen2026-10-22 12:30 UTC112
        self.room_key_recipient_strategy = room_key_recipient_strategy;
7ff8adafe9e4atagen2026-10-22 12:30 UTC113
7ff8adafe9e4atagen2026-10-22 12:30 UTC114
        self
7ff8adafe9e4atagen2026-10-22 12:30 UTC115
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC116
7ff8adafe9e4atagen2026-10-22 12:30 UTC117
    pub fn cross_process_lock_config(
7ff8adafe9e4atagen2026-10-22 12:30 UTC118
        mut self,
7ff8adafe9e4atagen2026-10-22 12:30 UTC119
        cross_process_lock_config: CrossProcessLockConfig,
7ff8adafe9e4atagen2026-10-22 12:30 UTC120
    ) -> Self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC121
        self.cross_process_lock_config = cross_process_lock_config;
7ff8adafe9e4atagen2026-10-22 12:30 UTC122
        self
7ff8adafe9e4atagen2026-10-22 12:30 UTC123
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC124
7ff8adafe9e4atagen2026-10-22 12:30 UTC125
    fn common_client_builder(&self) -> ClientBuilder {
7ff8adafe9e4atagen2026-10-22 12:30 UTC126
        let homeserver_url =
7ff8adafe9e4atagen2026-10-22 12:30 UTC127
            option_env!("HOMESERVER_URL").unwrap_or("http://localhost:8228").to_owned();
7ff8adafe9e4atagen2026-10-22 12:30 UTC128
7ff8adafe9e4atagen2026-10-22 12:30 UTC129
        let mut client_builder = Client::builder()
7ff8adafe9e4atagen2026-10-22 12:30 UTC130
            .user_agent("matrix-sdk-integration-tests")
7ff8adafe9e4atagen2026-10-22 12:30 UTC131
            .homeserver_url(homeserver_url)
7ff8adafe9e4atagen2026-10-22 12:30 UTC132
            .sliding_sync_version_builder(VersionBuilder::Native)
7ff8adafe9e4atagen2026-10-22 12:30 UTC133
            .with_encryption_settings(self.encryption_settings)
7ff8adafe9e4atagen2026-10-22 12:30 UTC134
            .with_room_key_recipient_strategy(self.room_key_recipient_strategy.clone())
7ff8adafe9e4atagen2026-10-22 12:30 UTC135
            .with_enable_share_history_on_invite(self.enable_share_history_on_invite)
7ff8adafe9e4atagen2026-10-22 12:30 UTC136
            .with_threading_support(self.threading_support)
7ff8adafe9e4atagen2026-10-22 12:30 UTC137
            .request_config(RequestConfig::short_retry())
7ff8adafe9e4atagen2026-10-22 12:30 UTC138
            .cross_process_store_config(self.cross_process_lock_config.clone());
7ff8adafe9e4atagen2026-10-22 12:30 UTC139
7ff8adafe9e4atagen2026-10-22 12:30 UTC140
        if let Some(decryption_settings) = &self.decryption_settings {
7ff8adafe9e4atagen2026-10-22 12:30 UTC141
            client_builder = client_builder.with_decryption_settings(decryption_settings.clone())
7ff8adafe9e4atagen2026-10-22 12:30 UTC142
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC143
7ff8adafe9e4atagen2026-10-22 12:30 UTC144
        if let Some(proxy) = &self.http_proxy {
7ff8adafe9e4atagen2026-10-22 12:30 UTC145
            client_builder = client_builder.proxy(proxy);
7ff8adafe9e4atagen2026-10-22 12:30 UTC146
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC147
7ff8adafe9e4atagen2026-10-22 12:30 UTC148
        client_builder
7ff8adafe9e4atagen2026-10-22 12:30 UTC149
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC150
7ff8adafe9e4atagen2026-10-22 12:30 UTC151
    /// Create a new Client that is a copy of the supplied one, created using
7ff8adafe9e4atagen2026-10-22 12:30 UTC152
    /// [`Client::restore_session`].
7ff8adafe9e4atagen2026-10-22 12:30 UTC153
    pub async fn duplicate(self, other: &Client) -> Result<Client> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC154
        let client_builder = self.common_client_builder();
7ff8adafe9e4atagen2026-10-22 12:30 UTC155
7ff8adafe9e4atagen2026-10-22 12:30 UTC156
        let client = match self.use_sqlite_dir {
7ff8adafe9e4atagen2026-10-22 12:30 UTC157
            Some(SqlitePath::Path(path_buf)) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC158
                client_builder.sqlite_store(&path_buf, None).build().await?
7ff8adafe9e4atagen2026-10-22 12:30 UTC159
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC160
            _ => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC161
                panic!("You must call use_sqlite_dir for a duplicate client!");
7ff8adafe9e4atagen2026-10-22 12:30 UTC162
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC163
        };
7ff8adafe9e4atagen2026-10-22 12:30 UTC164
7ff8adafe9e4atagen2026-10-22 12:30 UTC165
        client
7ff8adafe9e4atagen2026-10-22 12:30 UTC166
            .restore_session(
7ff8adafe9e4atagen2026-10-22 12:30 UTC167
                other.session().expect("Session must be logged in before we can duplicate it"),
7ff8adafe9e4atagen2026-10-22 12:30 UTC168
            )
7ff8adafe9e4atagen2026-10-22 12:30 UTC169
            .await
7ff8adafe9e4atagen2026-10-22 12:30 UTC170
            .expect("Failed to restore session");
7ff8adafe9e4atagen2026-10-22 12:30 UTC171
7ff8adafe9e4atagen2026-10-22 12:30 UTC172
        Ok(client)
7ff8adafe9e4atagen2026-10-22 12:30 UTC173
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC174
7ff8adafe9e4atagen2026-10-22 12:30 UTC175
    pub async fn build(self) -> Result<Client> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC176
        let client_builder = self.common_client_builder();
7ff8adafe9e4atagen2026-10-22 12:30 UTC177
        let client = match self.use_sqlite_dir {
7ff8adafe9e4atagen2026-10-22 12:30 UTC178
            None => client_builder.build().await?,
7ff8adafe9e4atagen2026-10-22 12:30 UTC179
            Some(SqlitePath::Random) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC180
                let tmp_dir = tempdir()?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC181
                let client = client_builder.sqlite_store(tmp_dir.path(), None).build().await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC182
                TMP_DIRS.lock().await.push(tmp_dir);
7ff8adafe9e4atagen2026-10-22 12:30 UTC183
                client
7ff8adafe9e4atagen2026-10-22 12:30 UTC184
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC185
            Some(SqlitePath::Path(path_buf)) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC186
                client_builder.sqlite_store(&path_buf, None).build().await?
7ff8adafe9e4atagen2026-10-22 12:30 UTC187
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC188
        };
7ff8adafe9e4atagen2026-10-22 12:30 UTC189
7ff8adafe9e4atagen2026-10-22 12:30 UTC190
        // safe to assume we have not registered this user yet, but ignore if we did
7ff8adafe9e4atagen2026-10-22 12:30 UTC191
7ff8adafe9e4atagen2026-10-22 12:30 UTC192
        let auth = client.matrix_auth();
7ff8adafe9e4atagen2026-10-22 12:30 UTC193
        let mut try_login = true;
7ff8adafe9e4atagen2026-10-22 12:30 UTC194
        if let Err(resp) = auth.register(RegistrationRequest::new()).await {
7ff8adafe9e4atagen2026-10-22 12:30 UTC195
            // FIXME: do actually check the registration types...
7ff8adafe9e4atagen2026-10-22 12:30 UTC196
            if let Some(_response) = resp.as_uiaa_response() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC197
                let request = assign!(RegistrationRequest::new(), {
7ff8adafe9e4atagen2026-10-22 12:30 UTC198
                    username: Some(self.username.clone()),
7ff8adafe9e4atagen2026-10-22 12:30 UTC199
                    password: Some(self.username.clone()),
7ff8adafe9e4atagen2026-10-22 12:30 UTC200
7ff8adafe9e4atagen2026-10-22 12:30 UTC201
                    auth: Some(uiaa::AuthData::Dummy(uiaa::Dummy::new())),
7ff8adafe9e4atagen2026-10-22 12:30 UTC202
                });
7ff8adafe9e4atagen2026-10-22 12:30 UTC203
                // if this failed, we will attempt to login after.
7ff8adafe9e4atagen2026-10-22 12:30 UTC204
                try_login = auth.register(request).await.is_err();
7ff8adafe9e4atagen2026-10-22 12:30 UTC205
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC206
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC207
        if try_login {
7ff8adafe9e4atagen2026-10-22 12:30 UTC208
            auth.login_username(&self.username, &self.username).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC209
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC210
7ff8adafe9e4atagen2026-10-22 12:30 UTC211
        Ok(client)
7ff8adafe9e4atagen2026-10-22 12:30 UTC212
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC213
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC214
7ff8adafe9e4atagen2026-10-22 12:30 UTC215
/// Client that correctly maintains and propagates sync token values.
7ff8adafe9e4atagen2026-10-22 12:30 UTC216
#[derive(Clone)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC217
pub struct SyncTokenAwareClient {
7ff8adafe9e4atagen2026-10-22 12:30 UTC218
    client: Client,
7ff8adafe9e4atagen2026-10-22 12:30 UTC219
    token: Arc<StdMutex<Option<String>>>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC220
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC221
7ff8adafe9e4atagen2026-10-22 12:30 UTC222
impl SyncTokenAwareClient {
7ff8adafe9e4atagen2026-10-22 12:30 UTC223
    pub fn new(client: Client) -> Self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC224
        Self { client, token: Arc::new(None.into()) }
7ff8adafe9e4atagen2026-10-22 12:30 UTC225
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC226
7ff8adafe9e4atagen2026-10-22 12:30 UTC227
    pub async fn sync_once(&self) -> Result<SyncResponse> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC228
        let mut settings = SyncSettings::default().timeout(Duration::from_secs(1));
7ff8adafe9e4atagen2026-10-22 12:30 UTC229
7ff8adafe9e4atagen2026-10-22 12:30 UTC230
        let token = { self.token.lock().unwrap().clone() };
7ff8adafe9e4atagen2026-10-22 12:30 UTC231
        if let Some(token) = token {
7ff8adafe9e4atagen2026-10-22 12:30 UTC232
            settings = settings.token(token);
7ff8adafe9e4atagen2026-10-22 12:30 UTC233
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC234
7ff8adafe9e4atagen2026-10-22 12:30 UTC235
        let response = self.client.sync_once(settings).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC236
7ff8adafe9e4atagen2026-10-22 12:30 UTC237
        let mut prev_token = self.token.lock().unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC238
        if prev_token.as_ref() != Some(&response.next_batch) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC239
            *prev_token = Some(response.next_batch.clone());
7ff8adafe9e4atagen2026-10-22 12:30 UTC240
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC241
        Ok(response)
7ff8adafe9e4atagen2026-10-22 12:30 UTC242
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC243
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC244
7ff8adafe9e4atagen2026-10-22 12:30 UTC245
impl Deref for SyncTokenAwareClient {
7ff8adafe9e4atagen2026-10-22 12:30 UTC246
    type Target = Client;
7ff8adafe9e4atagen2026-10-22 12:30 UTC247
7ff8adafe9e4atagen2026-10-22 12:30 UTC248
    fn deref(&self) -> &Self::Target {
7ff8adafe9e4atagen2026-10-22 12:30 UTC249
        &self.client
7ff8adafe9e4atagen2026-10-22 12:30 UTC250
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC251
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC252
7ff8adafe9e4atagen2026-10-22 12:30 UTC253
/// Repeatedly run the given callback, at increasing intervals, until it returns
7ff8adafe9e4atagen2026-10-22 12:30 UTC254
/// `Some`, or the given timeout has elapsed.
7ff8adafe9e4atagen2026-10-22 12:30 UTC255
///
7ff8adafe9e4atagen2026-10-22 12:30 UTC256
/// The callback `cb` is called with the attempt number as an argument.
7ff8adafe9e4atagen2026-10-22 12:30 UTC257
///
7ff8adafe9e4atagen2026-10-22 12:30 UTC258
/// # Returns
7ff8adafe9e4atagen2026-10-22 12:30 UTC259
///
7ff8adafe9e4atagen2026-10-22 12:30 UTC260
/// The result of `cb` if it returned `Some`. If the timeout elapses, an error
7ff8adafe9e4atagen2026-10-22 12:30 UTC261
/// of type [`ElapsedError`].
7ff8adafe9e4atagen2026-10-22 12:30 UTC262
pub async fn wait_until_some<F, Fut, R>(
7ff8adafe9e4atagen2026-10-22 12:30 UTC263
    mut cb: F,
7ff8adafe9e4atagen2026-10-22 12:30 UTC264
    timeout_duration: Duration,
7ff8adafe9e4atagen2026-10-22 12:30 UTC265
) -> Result<R, ElapsedError>
7ff8adafe9e4atagen2026-10-22 12:30 UTC266
where
7ff8adafe9e4atagen2026-10-22 12:30 UTC267
    F: FnMut(u32) -> Fut,
7ff8adafe9e4atagen2026-10-22 12:30 UTC268
    Fut: Future<Output = Option<R>>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC269
{
7ff8adafe9e4atagen2026-10-22 12:30 UTC270
    let start = Instant::now();
7ff8adafe9e4atagen2026-10-22 12:30 UTC271
    let mut attempt_count = 1;
7ff8adafe9e4atagen2026-10-22 12:30 UTC272
    loop {
7ff8adafe9e4atagen2026-10-22 12:30 UTC273
        if let Some(r) = cb(attempt_count).await {
7ff8adafe9e4atagen2026-10-22 12:30 UTC274
            return Ok(r);
7ff8adafe9e4atagen2026-10-22 12:30 UTC275
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC276
        if start - Instant::now() > timeout_duration {
7ff8adafe9e4atagen2026-10-22 12:30 UTC277
            return Err(ElapsedError());
7ff8adafe9e4atagen2026-10-22 12:30 UTC278
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC279
        sleep(Duration::from_millis(125) * attempt_count).await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC280
        attempt_count += 1;
7ff8adafe9e4atagen2026-10-22 12:30 UTC281
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC282
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC283
7ff8adafe9e4atagen2026-10-22 12:30 UTC284
/// Waits for a room to arrive from a sync, for ~2 seconds.
7ff8adafe9e4atagen2026-10-22 12:30 UTC285
///
7ff8adafe9e4atagen2026-10-22 12:30 UTC286
/// Note: this doesn't run any sync, it assumes a sync has been running in the
7ff8adafe9e4atagen2026-10-22 12:30 UTC287
/// background.
7ff8adafe9e4atagen2026-10-22 12:30 UTC288
pub async fn wait_for_room(client: &Client, room_id: &RoomId) -> Room {
7ff8adafe9e4atagen2026-10-22 12:30 UTC289
    let cb = async |i| {
7ff8adafe9e4atagen2026-10-22 12:30 UTC290
        client.get_room(room_id).or_else(|| {
7ff8adafe9e4atagen2026-10-22 12:30 UTC291
            tracing::warn!("attempt #{i}, alice couldn't see the room, retrying in a few…");
7ff8adafe9e4atagen2026-10-22 12:30 UTC292
            None
7ff8adafe9e4atagen2026-10-22 12:30 UTC293
        })
7ff8adafe9e4atagen2026-10-22 12:30 UTC294
    };
7ff8adafe9e4atagen2026-10-22 12:30 UTC295
7ff8adafe9e4atagen2026-10-22 12:30 UTC296
    wait_until_some(cb, Duration::from_secs(2))
7ff8adafe9e4atagen2026-10-22 12:30 UTC297
        .await
7ff8adafe9e4atagen2026-10-22 12:30 UTC298
        .expect("room couldn't be fetched after all attempts")
7ff8adafe9e4atagen2026-10-22 12:30 UTC299
}