atagen

matrix-rust-sdk

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk.git
crates/matrix-sdk/src/account.rsRust1605 lines59.8 KB
1
// Copyright 2020 Damir Jelić
2
// Copyright 2020 The Matrix.org Foundation C.I.C.
3
// Copyright 2022 Kévin Commaille
4
//
5
// Licensed under the Apache License, Version 2.0 (the "License");
6
// you may not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
//     http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing, software
12
// distributed under the License is distributed on an "AS IS" BASIS,
13
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
17
use futures_core::Stream;
18
use futures_util::{StreamExt, stream};
19
#[cfg(feature = "experimental-element-recent-emojis")]
20
use itertools::Itertools;
21
#[cfg(feature = "experimental-element-recent-emojis")]
22
use js_int::uint;
23
#[cfg(feature = "experimental-element-recent-emojis")]
24
use matrix_sdk_base::recent_emojis::RecentEmojisContent;
25
use matrix_sdk_base::{
26
    SendOutsideWasm, StateStoreDataKey, StateStoreDataValue, SyncOutsideWasm,
27
    media::{MediaFormat, MediaRequestParameters},
28
    store::StateStoreExt,
29
};
30
use mime::Mime;
31
#[cfg(feature = "experimental-element-recent-emojis")]
32
use ruma::api::client::config::set_global_account_data::v3::Request as UpdateGlobalAccountDataRequest;
33
use ruma::{
34
    ClientSecret, MxcUri, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, SessionId, UInt, UserId,
35
    api::{
36
        Metadata,
37
        client::{
38
            account::{
39
                add_3pid, change_password, deactivate, delete_3pid, get_3pids,
40
                request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
41
                request_openid_token,
42
            },
43
            config::{get_global_account_data, set_global_account_data},
44
            profile::{
45
                DisplayName, StaticProfileField, delete_profile_field, get_avatar_url, get_profile,
46
                get_profile_field, set_avatar_url, set_display_name, set_profile_field,
47
            },
48
            uiaa::AuthData,
49
        },
50
        error::ErrorKind,
51
    },
52
    assign,
53
    events::{
54
        AnyGlobalAccountDataEventContent, GlobalAccountDataEvent, GlobalAccountDataEventContent,
55
        GlobalAccountDataEventType, StaticEventContent,
56
        ignored_user_list::{IgnoredUser, IgnoredUserListEventContent},
57
        media_preview_config::{
58
            InviteAvatars, MediaPreviewConfigEventContent, MediaPreviews,
59
            UnstableMediaPreviewConfigEventContent,
60
        },
61
        push_rules::PushRulesEventContent,
62
        room::MediaSource,
63
    },
64
    profile::{ProfileFieldName, ProfileFieldValue},
65
    push::Ruleset,
66
    serde::Raw,
67
    thirdparty::Medium,
68
};
69
use serde::Deserialize;
70
use tracing::error;
71
72
use crate::{Client, Error, Result, config::RequestConfig};
73
74
/// The maximum number of recent emojis that should be stored and loaded.
75
#[cfg(feature = "experimental-element-recent-emojis")]
76
const MAX_RECENT_EMOJI_COUNT: usize = 100;
77
78
/// A high-level API to manage the client owner's account.
79
///
80
/// All the methods on this struct send a request to the homeserver.
81
#[derive(Debug, Clone)]
82
pub struct Account {
83
    /// The underlying HTTP client.
84
    client: Client,
85
}
86
87
impl Account {
88
    /// The maximum number of visited room identifiers to keep in the state
89
    /// store.
90
    const VISITED_ROOMS_LIMIT: usize = 20;
91
92
    pub(crate) fn new(client: Client) -> Self {
93
        Self { client }
94
    }
95
96
    /// Get the display name of the account.
97
    ///
98
    /// # Examples
99
    ///
100
    /// ```no_run
101
    /// # use matrix_sdk::Client;
102
    /// # use url::Url;
103
    /// # async {
104
    /// # let homeserver = Url::parse("http://example.com")?;
105
    /// let user = "example";
106
    /// let client = Client::new(homeserver).await?;
107
    /// client.matrix_auth().login_username(user, "password").send().await?;
108
    ///
109
    /// if let Some(name) = client.account().get_display_name().await? {
110
    ///     println!("Logged in as user '{user}' with display name '{name}'");
111
    /// }
112
    /// # anyhow::Ok(()) };
113
    /// ```
114
    pub async fn get_display_name(&self) -> Result<Option<String>> {
115
        let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
116
        self.fetch_profile_field_of_static::<DisplayName>(user_id.to_owned()).await
117
    }
118
119
    /// Set the display name of the account.
120
    ///
121
    /// # Examples
122
    ///
123
    /// ```no_run
124
    /// # use matrix_sdk::Client;
125
    /// # use url::Url;
126
    /// # async {
127
    /// # let homeserver = Url::parse("http://example.com")?;
128
    /// let user = "example";
129
    /// let client = Client::new(homeserver).await?;
130
    /// client.matrix_auth().login_username(user, "password").send().await?;
131
    ///
132
    /// client.account().set_display_name(Some("Alice")).await?;
133
    /// # anyhow::Ok(()) };
134
    /// ```
135
    pub async fn set_display_name(&self, name: Option<&str>) -> Result<()> {
136
        let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
137
138
        // Prefer the endpoint to delete profile fields, if it is supported.
139
        if name.is_none() {
140
            let versions = self.client.supported_versions().await?;
141
142
            if delete_profile_field::v3::Request::PATH_BUILDER.is_supported(&versions) {
143
                return self.delete_profile_field(ProfileFieldName::DisplayName).await;
144
            }
145
        }
146
147
        // If name is `Some(_)`, this endpoint is the same as `set_profile_field`, but
148
        // we still need to use it in case it is `None` and the server doesn't support
149
        // the delete endpoint yet.
150
        #[allow(deprecated)]
151
        let request =
152
            set_display_name::v3::Request::new(user_id.to_owned(), name.map(ToOwned::to_owned));
153
        self.client.send(request).await?;
154
155
        Ok(())
156
    }
157
158
    /// Request an OpenID token for the current account.
159
    pub async fn request_openid_token(&self) -> Result<request_openid_token::v3::Response> {
160
        let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
161
162
        self.client
163
            .send(request_openid_token::v3::Request::new(user_id.to_owned()))
164
            .await
165
            .map_err(|error| Error::Http(Box::new(error)))
166
    }
167
168
    /// Get the MXC URI of the account's avatar, if set.
169
    ///
170
    /// This always sends a request to the server to retrieve this information.
171
    /// If successful, this fills the cache, and makes it so that
172
    /// [`Self::get_cached_avatar_url`] will always return something.
173
    ///
174
    /// # Examples
175
    ///
176
    /// ```no_run
177
    /// # use matrix_sdk::Client;
178
    /// # use url::Url;
179
    /// # async {
180
    /// # let homeserver = Url::parse("http://example.com")?;
181
    /// # let user = "example";
182
    /// let client = Client::new(homeserver).await?;
183
    /// client.matrix_auth().login_username(user, "password").send().await?;
184
    ///
185
    /// if let Some(url) = client.account().get_avatar_url().await? {
186
    ///     println!("Your avatar's mxc url is {url}");
187
    /// }
188
    /// # anyhow::Ok(()) };
189
    /// ```
190
    pub async fn get_avatar_url(&self) -> Result<Option<OwnedMxcUri>> {
191
        let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
192
193
        #[allow(deprecated)] // get_profile_field fails when the response is {"avatar_url":null} 🤷‍♂️
194
        let request = get_avatar_url::v3::Request::new(user_id.to_owned());
195
        let avatar_url = self
196
            .client
197
            .send(request)
198
            .with_request_config(RequestConfig::short_retry().force_auth())
199
            .await?
200
            .avatar_url;
201
202
        if let Some(url) = avatar_url.clone() {
203
            // If an avatar is found cache it.
204
            let _ = self
205
                .client
206
                .state_store()
207
                .set_kv_data(
208
                    StateStoreDataKey::UserAvatarUrl(user_id),
209
                    StateStoreDataValue::UserAvatarUrl(url),
210
                )
211
                .await;
212
        } else {
213
            // If there is no avatar the user has removed it and we uncache it.
214
            let _ = self
215
                .client
216
                .state_store()
217
                .remove_kv_data(StateStoreDataKey::UserAvatarUrl(user_id))
218
                .await;
219
        }
220
        Ok(avatar_url)
221
    }
222
223
    /// Get the URL of the account's avatar, if is stored in cache.
224
    pub async fn get_cached_avatar_url(&self) -> Result<Option<OwnedMxcUri>> {
225
        let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
226
        let data = self
227
            .client
228
            .state_store()
229
            .get_kv_data(StateStoreDataKey::UserAvatarUrl(user_id))
230
            .await?;
231
        Ok(data.map(|v| v.into_user_avatar_url().expect("Session data is not a user avatar url")))
232
    }
233
234
    /// Set the MXC URI of the account's avatar.
235
    ///
236
    /// The avatar is unset if `url` is `None`.
237
    pub async fn set_avatar_url(&self, url: Option<&MxcUri>) -> Result<()> {
238
        let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
239
240
        // Prefer the endpoint to delete profile fields, if it is supported.
241
        if url.is_none() {
242
            let versions = self.client.supported_versions().await?;
243
244
            if delete_profile_field::v3::Request::PATH_BUILDER.is_supported(&versions) {
245
                return self.delete_profile_field(ProfileFieldName::AvatarUrl).await;
246
            }
247
        }
248
249
        // If url is `Some(_)`, this endpoint is the same as `set_profile_field`, but
250
        // we still need to use it in case it is `None` and the server doesn't support
251
        // the delete endpoint yet.
252
        #[allow(deprecated)]
253
        let request =
254
            set_avatar_url::v3::Request::new(user_id.to_owned(), url.map(ToOwned::to_owned));
255
        self.client.send(request).await?;
256
257
        Ok(())
258
    }
259
260
    /// Get the account's avatar, if set.
261
    ///
262
    /// Returns the avatar.
263
    ///
264
    /// If a thumbnail is requested no guarantee on the size of the image is
265
    /// given.
266
    ///
267
    /// # Arguments
268
    ///
269
    /// * `format` - The desired format of the avatar.
270
    ///
271
    /// # Examples
272
    ///
273
    /// ```no_run
274
    /// # use matrix_sdk::Client;
275
    /// # use matrix_sdk::ruma::room_id;
276
    /// # use matrix_sdk::media::MediaFormat;
277
    /// # use url::Url;
278
    /// # async {
279
    /// # let homeserver = Url::parse("http://example.com")?;
280
    /// # let user = "example";
281
    /// let client = Client::new(homeserver).await?;
282
    /// client.matrix_auth().login_username(user, "password").send().await?;
283
    ///
284
    /// if let Some(avatar) = client.account().get_avatar(MediaFormat::File).await?
285
    /// {
286
    ///     std::fs::write("avatar.png", avatar);
287
    /// }
288
    /// # anyhow::Ok(()) };
289
    /// ```
290
    pub async fn get_avatar(&self, format: MediaFormat) -> Result<Option<Vec<u8>>> {
291
        if let Some(url) = self.get_avatar_url().await? {
292
            let request = MediaRequestParameters { source: MediaSource::Plain(url), format };
293
            Ok(Some(self.client.media().get_media_content(&request, true).await?))
294
        } else {
295
            Ok(None)
296
        }
297
    }
298
299
    /// Upload and set the account's avatar.
300
    ///
301
    /// This will upload the data produced by the reader to the homeserver's
302
    /// content repository, and set the user's avatar to the MXC URI for the
303
    /// uploaded file.
304
    ///
305
    /// This is a convenience method for calling [`Media::upload()`],
306
    /// followed by [`Account::set_avatar_url()`].
307
    ///
308
    /// Returns the MXC URI of the uploaded avatar.
309
    ///
310
    /// # Examples
311
    ///
312
    /// ```no_run
313
    /// # use std::fs;
314
    /// # use matrix_sdk::Client;
315
    /// # use url::Url;
316
    /// # async {
317
    /// # let homeserver = Url::parse("http://localhost:8080")?;
318
    /// # let client = Client::new(homeserver).await?;
319
    /// let image = fs::read("/home/example/selfie.jpg")?;
320
    ///
321
    /// client.account().upload_avatar(&mime::IMAGE_JPEG, image).await?;
322
    /// # anyhow::Ok(()) };
323
    /// ```
324
    ///
325
    /// [`Media::upload()`]: crate::Media::upload
326
    pub async fn upload_avatar(&self, content_type: &Mime, data: Vec<u8>) -> Result<OwnedMxcUri> {
327
        let upload_response = self.client.media().upload(content_type, data, None).await?;
328
        self.set_avatar_url(Some(&upload_response.content_uri)).await?;
329
        Ok(upload_response.content_uri)
330
    }
331
332
    /// Get the profile of this account.
333
    ///
334
    /// Allows to get all the profile data in a single call.
335
    ///
336
    /// # Examples
337
    ///
338
    /// ```no_run
339
    /// # use matrix_sdk::Client;
340
    /// use ruma::api::client::profile::{AvatarUrl, DisplayName};
341
    /// # use url::Url;
342
    /// # async {
343
    /// # let homeserver = Url::parse("http://localhost:8080")?;
344
    /// # let client = Client::new(homeserver).await?;
345
    ///
346
    /// let profile = client.account().fetch_user_profile().await?;
347
    /// let display_name = profile.get_static::<DisplayName>()?;
348
    /// let avatar_url = profile.get_static::<AvatarUrl>()?;
349
    ///
350
    /// println!("You are '{display_name:?}' with avatar '{avatar_url:?}'");
351
    /// # anyhow::Ok(()) };
352
    /// ```
353
    pub async fn fetch_user_profile(&self) -> Result<get_profile::v3::Response> {
354
        let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
355
        self.fetch_user_profile_of(user_id).await
356
    }
357
358
    /// Get the profile for a given user id
359
    ///
360
    /// # Arguments
361
    ///
362
    /// * `user_id` the matrix id this function downloads the profile for
363
    pub async fn fetch_user_profile_of(
364
        &self,
365
        user_id: &UserId,
366
    ) -> Result<get_profile::v3::Response> {
367
        let request = get_profile::v3::Request::new(user_id.to_owned());
368
        Ok(self
369
            .client
370
            .send(request)
371
            .with_request_config(RequestConfig::short_retry().force_auth())
372
            .await?)
373
    }
374
375
    /// Get the given field from the given user's profile.
376
    ///
377
    /// # Arguments
378
    ///
379
    /// * `user_id` - The ID of the user to get the profile field of.
380
    ///
381
    /// * `field` - The name of the profile field to get.
382
    ///
383
    /// # Returns
384
    ///
385
    /// Returns an error if the request fails or if deserialization of the
386
    /// response fails.
387
    ///
388
    /// If the field is not set, the server should respond with an error with an
389
    /// [`ErrorCode::NotFound`], but it might also respond with an empty
390
    /// response, which would result in `Ok(None)`. Note that this error code
391
    /// might also mean that the given user ID doesn't exist.
392
    ///
393
    /// [`ErrorCode::NotFound`]: ruma::api::error::ErrorCode::NotFound
394
    pub async fn fetch_profile_field_of(
395
        &self,
396
        user_id: OwnedUserId,
397
        field: ProfileFieldName,
398
    ) -> Result<Option<ProfileFieldValue>> {
399
        let request = get_profile_field::v3::Request::new(user_id, field);
400
        let response = self
401
            .client
402
            .send(request)
403
            .with_request_config(RequestConfig::short_retry().force_auth())
404
            .await?;
405
406
        Ok(response.value)
407
    }
408
409
    /// Get the given statically-known field from the given user's profile.
410
    ///
411
    /// # Arguments
412
    ///
413
    /// * `user_id` - The ID of the user to get the profile field of.
414
    ///
415
    /// # Returns
416
    ///
417
    /// Returns an error if the request fails or if deserialization of the
418
    /// response fails.
419
    ///
420
    /// If the field is not set, the server should respond with an error with an
421
    /// [`ErrorCode::NotFound`], but it might also respond with an empty
422
    /// response, which would result in `Ok(None)`. Note that this error code
423
    /// might also mean that the given user ID doesn't exist.
424
    ///
425
    /// [`ErrorCode::NotFound`]: ruma::api::error::ErrorCode::NotFound
426
    pub async fn fetch_profile_field_of_static<F>(
427
        &self,
428
        user_id: OwnedUserId,
429
    ) -> Result<Option<F::Value>>
430
    where
431
        F: StaticProfileField
432
            + std::fmt::Debug
433
            + Clone
434
            + SendOutsideWasm
435
            + SyncOutsideWasm
436
            + 'static,
437
        F::Value: SendOutsideWasm + SyncOutsideWasm,
438
    {
439
        let request = get_profile_field::v3::Request::new_static::<F>(user_id);
440
        let response = self
441
            .client
442
            .send(request)
443
            .with_request_config(RequestConfig::short_retry().force_auth())
444
            .await?;
445
446
        Ok(response.value)
447
    }
448
449
    /// Set the given field of our own user's profile.
450
    ///
451
    /// [`Client::homeserver_capabilities()`] should be called first to check it
452
    /// the field can be set on the homeserver.
453
    ///
454
    /// # Arguments
455
    ///
456
    /// * `value` - The value of the profile field to set.
457
    ///
458
    /// # Returns
459
    ///
460
    /// Returns an error if the request fails.
461
    pub async fn set_profile_field(&self, value: ProfileFieldValue) -> Result<()> {
462
        let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
463
        let request = set_profile_field::v3::Request::new(user_id.to_owned(), value);
464
        self.client.send(request).await?;
465
466
        Ok(())
467
    }
468
469
    /// Delete the given field of our own user's profile.
470
    ///
471
    /// [`Client::homeserver_capabilities()`] should be called first to check it
472
    /// the field can be modified on the homeserver.
473
    ///
474
    /// # Arguments
475
    ///
476
    /// * `field` - The profile field to delete.
477
    ///
478
    /// # Returns
479
    ///
480
    /// Returns an error if the server doesn't support extended profile fields
481
    /// of if the request fails in some other way.
482
    pub async fn delete_profile_field(&self, field: ProfileFieldName) -> Result<()> {
483
        let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
484
        let request = delete_profile_field::v3::Request::new(user_id.to_owned(), field);
485
        self.client.send(request).await?;
486
487
        Ok(())
488
    }
489
490
    /// Change the password of the account.
491
    ///
492
    /// # Arguments
493
    ///
494
    /// * `new_password` - The new password to set.
495
    ///
496
    /// * `auth_data` - This request uses the [User-Interactive Authentication
497
    ///   API][uiaa]. The first request needs to set this to `None` and will
498
    ///   always fail with an [`UiaaResponse`]. The response will contain
499
    ///   information for the interactive auth and the same request needs to be
500
    ///   made but this time with some `auth_data` provided.
501
    ///
502
    /// # Returns
503
    ///
504
    /// This method might return an [`ErrorKind::WeakPassword`] error if the new
505
    /// password is considered insecure by the homeserver, with details about
506
    /// the strength requirements in the error's message.
507
    ///
508
    /// # Examples
509
    ///
510
    /// ```no_run
511
    /// # use matrix_sdk::Client;
512
    /// # use matrix_sdk::ruma::{
513
    /// #     api::client::{
514
    /// #         account::change_password::v3::{Request as ChangePasswordRequest},
515
    /// #         uiaa::{AuthData, Dummy},
516
    /// #     },
517
    /// #     assign,
518
    /// # };
519
    /// # use url::Url;
520
    /// # async {
521
    /// # let homeserver = Url::parse("http://localhost:8080")?;
522
    /// # let client = Client::new(homeserver).await?;
523
    /// client.account().change_password(
524
    ///     "myverysecretpassword",
525
    ///     Some(AuthData::Dummy(Dummy::new())),
526
    /// ).await?;
527
    /// # anyhow::Ok(()) };
528
    /// ```
529
    /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api
530
    /// [`UiaaResponse`]: ruma::api::client::uiaa::UiaaResponse
531
    /// [`ErrorKind::WeakPassword`]: ruma::api::error::ErrorKind::WeakPassword
532
    pub async fn change_password(
533
        &self,
534
        new_password: &str,
535
        auth_data: Option<AuthData>,
536
    ) -> Result<change_password::v3::Response> {
537
        let request = assign!(change_password::v3::Request::new(new_password.to_owned()), {
538
            auth: auth_data,
539
        });
540
        Ok(self.client.send(request).await?)
541
    }
542
543
    /// Deactivate this account definitively.
544
    ///
545
    /// # Arguments
546
    ///
547
    /// * `id_server` - The identity server from which to unbind the user’s
548
    ///   [Third Party Identifiers][3pid].
549
    ///
550
    /// * `auth_data` - This request uses the [User-Interactive Authentication
551
    ///   API][uiaa]. The first request needs to set this to `None` and will
552
    ///   always fail with an [`UiaaResponse`]. The response will contain
553
    ///   information for the interactive auth and the same request needs to be
554
    ///   made but this time with some `auth_data` provided.
555
    ///
556
    /// * `erase` - Whether the user would like their content to be erased as
557
    ///   much as possible from the server.
558
    ///
559
    /// # Examples
560
    ///
561
    /// ```no_run
562
    /// # use matrix_sdk::Client;
563
    /// # use matrix_sdk::ruma::{
564
    /// #     api::client::{
565
    /// #         account::change_password::v3::{Request as ChangePasswordRequest},
566
    /// #         uiaa::{AuthData, Dummy},
567
    /// #     },
568
    /// #     assign,
569
    /// # };
570
    /// # use url::Url;
571
    /// # async {
572
    /// # let homeserver = Url::parse("http://localhost:8080")?;
573
    /// # let client = Client::new(homeserver).await?;
574
    /// # let account = client.account();
575
    /// let response = account.deactivate(None, None, false).await;
576
    ///
577
    /// // Proceed with UIAA.
578
    /// # anyhow::Ok(()) };
579
    /// ```
580
    /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
581
    /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api
582
    /// [`UiaaResponse`]: ruma::api::client::uiaa::UiaaResponse
583
    pub async fn deactivate(
584
        &self,
585
        id_server: Option<&str>,
586
        auth_data: Option<AuthData>,
587
        erase_data: bool,
588
    ) -> Result<deactivate::v3::Response> {
589
        let request = assign!(deactivate::v3::Request::new(), {
590
            id_server: id_server.map(ToOwned::to_owned),
591
            auth: auth_data,
592
            erase: erase_data,
593
        });
594
        Ok(self.client.send(request).await?)
595
    }
596
597
    /// Get the registered [Third Party Identifiers][3pid] on the homeserver of
598
    /// the account.
599
    ///
600
    /// These 3PIDs may be used by the homeserver to authenticate the user
601
    /// during sensitive operations.
602
    ///
603
    /// # Examples
604
    ///
605
    /// ```no_run
606
    /// # use matrix_sdk::Client;
607
    /// # use url::Url;
608
    /// # async {
609
    /// # let homeserver = Url::parse("http://localhost:8080")?;
610
    /// # let client = Client::new(homeserver).await?;
611
    /// let threepids = client.account().get_3pids().await?.threepids;
612
    ///
613
    /// for threepid in threepids {
614
    ///     println!(
615
    ///         "Found 3PID '{}' of type '{}'",
616
    ///         threepid.address, threepid.medium
617
    ///     );
618
    /// }
619
    /// # anyhow::Ok(()) };
620
    /// ```
621
    /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
622
    pub async fn get_3pids(&self) -> Result<get_3pids::v3::Response> {
623
        let request = get_3pids::v3::Request::new();
624
        Ok(self.client.send(request).await?)
625
    }
626
627
    /// Request a token to validate an email address as a [Third Party
628
    /// Identifier][3pid].
629
    ///
630
    /// This is the first step in registering an email address as 3PID. Next,
631
    /// call [`Account::add_3pid()`] with the same `client_secret` and the
632
    /// returned `sid`.
633
    ///
634
    /// # Arguments
635
    ///
636
    /// * `client_secret` - A client-generated secret string used to protect
637
    ///   this session.
638
    ///
639
    /// * `email` - The email address to validate.
640
    ///
641
    /// * `send_attempt` - The attempt number. This number needs to be
642
    ///   incremented if you want to request another token for the same
643
    ///   validation.
644
    ///
645
    /// # Returns
646
    ///
647
    /// * `sid` - The session ID to be used in following requests for this 3PID.
648
    ///
649
    /// * `submit_url` - If present, the user will submit the token to the
650
    ///   client, that must send it to this URL. If not, the client will not be
651
    ///   involved in the token submission.
652
    ///
653
    /// This method might return an [`ErrorKind::ThreepidInUse`] error if the
654
    /// email address is already registered for this account or another, or an
655
    /// [`ErrorKind::ThreepidDenied`] error if it is denied.
656
    ///
657
    /// # Examples
658
    ///
659
    /// ```no_run
660
    /// # use matrix_sdk::Client;
661
    /// # use matrix_sdk::ruma::{ClientSecret, uint};
662
    /// # use url::Url;
663
    /// # async {
664
    /// # let homeserver = Url::parse("http://localhost:8080")?;
665
    /// # let client = Client::new(homeserver).await?;
666
    /// # let account = client.account();
667
    /// # let secret = ClientSecret::parse("secret")?;
668
    /// let token_response = account
669
    ///     .request_3pid_email_token(&secret, "john@matrix.org", uint!(0))
670
    ///     .await?;
671
    ///
672
    /// // Wait for the user to confirm that the token was submitted or prompt
673
    /// // the user for the token and send it to submit_url.
674
    ///
675
    /// let uiaa_response =
676
    ///     account.add_3pid(&secret, &token_response.sid, None).await;
677
    ///
678
    /// // Proceed with UIAA.
679
    /// # anyhow::Ok(()) };
680
    /// ```
681
    /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
682
    /// [`ErrorKind::ThreepidInUse`]: ruma::api::error::ErrorKind::ThreepidInUse
683
    /// [`ErrorKind::ThreepidDenied`]: ruma::api::error::ErrorKind::ThreepidDenied
684
    pub async fn request_3pid_email_token(
685
        &self,
686
        client_secret: &ClientSecret,
687
        email: &str,
688
        send_attempt: UInt,
689
    ) -> Result<request_3pid_management_token_via_email::v3::Response> {
690
        let request = request_3pid_management_token_via_email::v3::Request::new(
691
            client_secret.to_owned(),
692
            email.to_owned(),
693
            send_attempt,
694
        );
695
        Ok(self.client.send(request).await?)
696
    }
697
698
    /// Request a token to validate a phone number as a [Third Party
699
    /// Identifier][3pid].
700
    ///
701
    /// This is the first step in registering a phone number as 3PID. Next,
702
    /// call [`Account::add_3pid()`] with the same `client_secret` and the
703
    /// returned `sid`.
704
    ///
705
    /// # Arguments
706
    ///
707
    /// * `client_secret` - A client-generated secret string used to protect
708
    ///   this session.
709
    ///
710
    /// * `country` - The two-letter uppercase ISO-3166-1 alpha-2 country code
711
    ///   that the number in phone_number should be parsed as if it were dialled
712
    ///   from.
713
    ///
714
    /// * `phone_number` - The phone number to validate.
715
    ///
716
    /// * `send_attempt` - The attempt number. This number needs to be
717
    ///   incremented if you want to request another token for the same
718
    ///   validation.
719
    ///
720
    /// # Returns
721
    ///
722
    /// * `sid` - The session ID to be used in following requests for this 3PID.
723
    ///
724
    /// * `submit_url` - If present, the user will submit the token to the
725
    ///   client, that must send it to this URL. If not, the client will not be
726
    ///   involved in the token submission.
727
    ///
728
    /// This method might return an [`ErrorKind::ThreepidInUse`] error if the
729
    /// phone number is already registered for this account or another, or an
730
    /// [`ErrorKind::ThreepidDenied`] error if it is denied.
731
    ///
732
    /// # Examples
733
    ///
734
    /// ```no_run
735
    /// # use matrix_sdk::Client;
736
    /// # use matrix_sdk::ruma::{ClientSecret, uint};
737
    /// # use url::Url;
738
    /// # async {
739
    /// # let homeserver = Url::parse("http://localhost:8080")?;
740
    /// # let client = Client::new(homeserver).await?;
741
    /// # let account = client.account();
742
    /// # let secret = ClientSecret::parse("secret")?;
743
    /// let token_response = account
744
    ///     .request_3pid_msisdn_token(&secret, "FR", "0123456789", uint!(0))
745
    ///     .await?;
746
    ///
747
    /// // Wait for the user to confirm that the token was submitted or prompt
748
    /// // the user for the token and send it to submit_url.
749
    ///
750
    /// let uiaa_response =
751
    ///     account.add_3pid(&secret, &token_response.sid, None).await;
752
    ///
753
    /// // Proceed with UIAA.
754
    /// # anyhow::Ok(()) };
755
    /// ```
756
    /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
757
    /// [`ErrorKind::ThreepidInUse`]: ruma::api::error::ErrorKind::ThreepidInUse
758
    /// [`ErrorKind::ThreepidDenied`]: ruma::api::error::ErrorKind::ThreepidDenied
759
    pub async fn request_3pid_msisdn_token(
760
        &self,
761
        client_secret: &ClientSecret,
762
        country: &str,
763
        phone_number: &str,
764
        send_attempt: UInt,
765
    ) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
766
        let request = request_3pid_management_token_via_msisdn::v3::Request::new(
767
            client_secret.to_owned(),
768
            country.to_owned(),
769
            phone_number.to_owned(),
770
            send_attempt,
771
        );
772
        Ok(self.client.send(request).await?)
773
    }
774
775
    /// Add a [Third Party Identifier][3pid] on the homeserver for this
776
    /// account.
777
    ///
778
    /// This 3PID may be used by the homeserver to authenticate the user
779
    /// during sensitive operations.
780
    ///
781
    /// This method should be called after
782
    /// [`Account::request_3pid_email_token()`] or
783
    /// [`Account::request_3pid_msisdn_token()`] to complete the 3PID
784
    ///
785
    /// # Arguments
786
    ///
787
    /// * `client_secret` - The same client secret used in
788
    ///   [`Account::request_3pid_email_token()`] or
789
    ///   [`Account::request_3pid_msisdn_token()`].
790
    ///
791
    /// * `sid` - The session ID returned in
792
    ///   [`Account::request_3pid_email_token()`] or
793
    ///   [`Account::request_3pid_msisdn_token()`].
794
    ///
795
    /// * `auth_data` - This request uses the [User-Interactive Authentication
796
    ///   API][uiaa]. The first request needs to set this to `None` and will
797
    ///   always fail with an [`UiaaResponse`]. The response will contain
798
    ///   information for the interactive auth and the same request needs to be
799
    ///   made but this time with some `auth_data` provided.
800
    ///
801
    /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
802
    /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api
803
    /// [`UiaaResponse`]: ruma::api::client::uiaa::UiaaResponse
804
    pub async fn add_3pid(
805
        &self,
806
        client_secret: &ClientSecret,
807
        sid: &SessionId,
808
        auth_data: Option<AuthData>,
809
    ) -> Result<add_3pid::v3::Response> {
810
        #[rustfmt::skip] // rustfmt wants to merge the next two lines
811
        let request =
812
            assign!(add_3pid::v3::Request::new(client_secret.to_owned(), sid.to_owned()), {
813
                auth: auth_data
814
            });
815
        Ok(self.client.send(request).await?)
816
    }
817
818
    /// Delete a [Third Party Identifier][3pid] from the homeserver for this
819
    /// account.
820
    ///
821
    /// # Arguments
822
    ///
823
    /// * `address` - The 3PID being removed.
824
    ///
825
    /// * `medium` - The type of the 3PID.
826
    ///
827
    /// * `id_server` - The identity server to unbind from. If not provided, the
828
    ///   homeserver should unbind the 3PID from the identity server it was
829
    ///   bound to previously.
830
    ///
831
    /// # Returns
832
    ///
833
    /// * [`ThirdPartyIdRemovalStatus::Success`] if the 3PID was also unbound
834
    ///   from the identity server.
835
    ///
836
    /// * [`ThirdPartyIdRemovalStatus::NoSupport`] if the 3PID was not unbound
837
    ///   from the identity server. This can also mean that the 3PID was not
838
    ///   bound to an identity server in the first place.
839
    ///
840
    /// # Examples
841
    ///
842
    /// ```no_run
843
    /// # use matrix_sdk::Client;
844
    /// # use matrix_sdk::ruma::thirdparty::Medium;
845
    /// # use matrix_sdk::ruma::api::client::account::ThirdPartyIdRemovalStatus;
846
    /// # use url::Url;
847
    /// # async {
848
    /// # let homeserver = Url::parse("http://localhost:8080")?;
849
    /// # let client = Client::new(homeserver).await?;
850
    /// # let account = client.account();
851
    /// match account
852
    ///     .delete_3pid("paul@matrix.org", Medium::Email, None)
853
    ///     .await?
854
    ///     .id_server_unbind_result
855
    /// {
856
    ///     ThirdPartyIdRemovalStatus::Success => {
857
    ///         println!("3PID unbound from the Identity Server");
858
    ///     }
859
    ///     _ => println!("Could not unbind 3PID from the Identity Server"),
860
    /// }
861
    /// # anyhow::Ok(()) };
862
    /// ```
863
    /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
864
    /// [`ThirdPartyIdRemovalStatus::Success`]: ruma::api::client::account::ThirdPartyIdRemovalStatus::Success
865
    /// [`ThirdPartyIdRemovalStatus::NoSupport`]: ruma::api::client::account::ThirdPartyIdRemovalStatus::NoSupport
866
    pub async fn delete_3pid(
867
        &self,
868
        address: &str,
869
        medium: Medium,
870
        id_server: Option<&str>,
871
    ) -> Result<delete_3pid::v3::Response> {
872
        let request = assign!(delete_3pid::v3::Request::new(medium, address.to_owned()), {
873
            id_server: id_server.map(ToOwned::to_owned),
874
        });
875
        Ok(self.client.send(request).await?)
876
    }
877
878
    /// Get the content of an account data event of statically-known type, from
879
    /// storage.
880
    ///
881
    /// # Examples
882
    ///
883
    /// ```no_run
884
    /// # use matrix_sdk::Client;
885
    /// # async {
886
    /// # let client = Client::new("http://localhost:8080".parse()?).await?;
887
    /// # let account = client.account();
888
    /// use matrix_sdk::ruma::events::ignored_user_list::IgnoredUserListEventContent;
889
    ///
890
    /// let maybe_content = account.account_data::<IgnoredUserListEventContent>().await?;
891
    /// if let Some(raw_content) = maybe_content {
892
    ///     let content = raw_content.deserialize()?;
893
    ///     println!("Ignored users:");
894
    ///     for user_id in content.ignored_users.keys() {
895
    ///         println!("- {user_id}");
896
    ///     }
897
    /// }
898
    /// # anyhow::Ok(()) };
899
    /// ```
900
    pub async fn account_data<C>(&self) -> Result<Option<Raw<C>>>
901
    where
902
        C: GlobalAccountDataEventContent + StaticEventContent<IsPrefix = ruma::events::False>,
903
    {
904
        get_raw_content(self.client.state_store().get_account_data_event_static::<C>().await?)
905
    }
906
907
    /// Get the content of an account data event of a given type, from storage.
908
    pub async fn account_data_raw(
909
        &self,
910
        event_type: GlobalAccountDataEventType,
911
    ) -> Result<Option<Raw<AnyGlobalAccountDataEventContent>>> {
912
        get_raw_content(self.client.state_store().get_account_data_event(event_type).await?)
913
    }
914
915
    /// Fetch a global account data event from the server.
916
    ///
917
    /// The content from the response will not be persisted in the store.
918
    ///
919
    /// Examples
920
    ///
921
    /// ```no_run
922
    /// # use matrix_sdk::Client;
923
    /// # async {
924
    /// # let client = Client::new("http://localhost:8080".parse()?).await?;
925
    /// # let account = client.account();
926
    /// use matrix_sdk::ruma::events::{ignored_user_list::IgnoredUserListEventContent, GlobalAccountDataEventType};
927
    ///
928
    /// if let Some(raw_content) = account.fetch_account_data(GlobalAccountDataEventType::IgnoredUserList).await? {
929
    ///     let content = raw_content.deserialize_as_unchecked::<IgnoredUserListEventContent>()?;
930
    ///
931
    ///     println!("Ignored users:");
932
    ///
933
    ///     for user_id in content.ignored_users.keys() {
934
    ///         println!("- {user_id}");
935
    ///     }
936
    /// }
937
    /// # anyhow::Ok(()) };
938
    pub async fn fetch_account_data(
939
        &self,
940
        event_type: GlobalAccountDataEventType,
941
    ) -> Result<Option<Raw<AnyGlobalAccountDataEventContent>>> {
942
        let own_user = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
943
944
        let request = get_global_account_data::v3::Request::new(own_user.to_owned(), event_type);
945
946
        match self.client.send(request).await {
947
            Ok(r) => Ok(Some(r.account_data)),
948
            Err(e) => {
949
                if let Some(kind) = e.client_api_error_kind() {
950
                    if kind == &ErrorKind::NotFound { Ok(None) } else { Err(e.into()) }
951
                } else {
952
                    Err(e.into())
953
                }
954
            }
955
        }
956
    }
957
958
    /// Fetch an account data event of statically-known type from the server.
959
    pub async fn fetch_account_data_static<C>(&self) -> Result<Option<Raw<C>>>
960
    where
961
        C: GlobalAccountDataEventContent + StaticEventContent<IsPrefix = ruma::events::False>,
962
    {
963
        Ok(self.fetch_account_data(C::TYPE.into()).await?.map(Raw::cast_unchecked))
964
    }
965
966
    /// Set the given account data event.
967
    ///
968
    /// # Examples
969
    ///
970
    /// ```no_run
971
    /// # use matrix_sdk::Client;
972
    /// # async {
973
    /// # let client = Client::new("http://localhost:8080".parse()?).await?;
974
    /// # let account = client.account();
975
    /// use matrix_sdk::ruma::{
976
    ///     events::ignored_user_list::{IgnoredUser, IgnoredUserListEventContent},
977
    ///     user_id,
978
    /// };
979
    ///
980
    /// let mut content = account
981
    ///     .account_data::<IgnoredUserListEventContent>()
982
    ///     .await?
983
    ///     .map(|c| c.deserialize())
984
    ///     .transpose()?
985
    ///     .unwrap_or_default();
986
    /// content
987
    ///     .ignored_users
988
    ///     .insert(user_id!("@foo:bar.com").to_owned(), IgnoredUser::new());
989
    /// account.set_account_data(content).await?;
990
    /// # anyhow::Ok(()) };
991
    /// ```
992
    pub async fn set_account_data<T>(
993
        &self,
994
        content: T,
995
    ) -> Result<set_global_account_data::v3::Response>
996
    where
997
        T: GlobalAccountDataEventContent,
998
    {
999
        let own_user = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
1000
1001
        let request = set_global_account_data::v3::Request::new(own_user.to_owned(), &content)?;
1002
1003
        Ok(self.client.send(request).await?)
1004
    }
1005
1006
    /// Set the given raw account data event.
1007
    pub async fn set_account_data_raw(
1008
        &self,
1009
        event_type: GlobalAccountDataEventType,
1010
        content: Raw<AnyGlobalAccountDataEventContent>,
1011
    ) -> Result<set_global_account_data::v3::Response> {
1012
        let own_user = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
1013
1014
        let request =
1015
            set_global_account_data::v3::Request::new_raw(own_user.to_owned(), event_type, content);
1016
1017
        Ok(self.client.send(request).await?)
1018
    }
1019
1020
    /// Marks the room identified by `room_id` as a "direct chat" with each
1021
    /// user in `user_ids`.
1022
    ///
1023
    /// # Arguments
1024
    ///
1025
    /// * `room_id` - The room ID of the direct message room.
1026
    /// * `user_ids` - The user IDs to be associated with this direct message
1027
    ///   room.
1028
    pub async fn mark_as_dm(&self, room_id: &RoomId, user_ids: &[OwnedUserId]) -> Result<()> {
1029
        use ruma::events::direct::DirectEventContent;
1030
1031
        // This function does a read/update/store of an account data event stored on the
1032
        // homeserver. We first fetch the existing account data event, the event
1033
        // contains a map which gets updated by this method, finally we upload the
1034
        // modified event.
1035
        //
1036
        // To prevent multiple calls to this method trying to update the map of DMs same
1037
        // time, and thus trampling on each other we introduce a lock which acts
1038
        // as a semaphore.
1039
        let _guard = self.client.locks().mark_as_dm_lock.lock().await;
1040
1041
        // Now we need to mark the room as a DM for ourselves, we fetch the
1042
        // existing `m.direct` event and append the room to the list of DMs we
1043
        // have with this user.
1044
1045
        // We are fetching the content from the server because we currently can't rely
1046
        // on `/sync` giving us the correct data in a timely manner.
1047
        let raw_content = self.fetch_account_data_static::<DirectEventContent>().await?;
1048
1049
        let mut content = if let Some(raw_content) = raw_content {
1050
            // Log the error and pass it upwards if we fail to deserialize the m.direct
1051
            // event.
1052
            raw_content.deserialize().map_err(|err| {
1053
                error!("unable to deserialize m.direct event content; aborting request to mark {room_id} as dm: {err}");
1054
                err
1055
            })?
1056
        } else {
1057
            // If there was no m.direct event server-side, create a default one.
1058
            Default::default()
1059
        };
1060
1061
        for user_id in user_ids {
1062
            content.entry(user_id.into()).or_default().push(room_id.to_owned());
1063
        }
1064
1065
        // TODO: We should probably save the fact that we need to send this out
1066
        // because otherwise we might end up in a state where we have a DM that
1067
        // isn't marked as one.
1068
        self.set_account_data(content).await?;
1069
1070
        Ok(())
1071
    }
1072
1073
    /// Adds the given user ID to the account's ignore list.
1074
    pub async fn ignore_user(&self, user_id: &UserId) -> Result<()> {
1075
        let own_user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
1076
        if user_id == own_user_id {
1077
            return Err(Error::CantIgnoreLoggedInUser);
1078
        }
1079
1080
        let mut ignored_user_list = self.get_ignored_user_list_event_content().await?;
1081
        ignored_user_list.ignored_users.insert(user_id.to_owned(), IgnoredUser::new());
1082
1083
        self.set_account_data(ignored_user_list).await?;
1084
1085
        // In theory, we should also clear some caches here, because they may include
1086
        // events sent by the ignored user. In practice, we expect callers to
1087
        // take care of this, or subsystems to listen to user list changes and
1088
        // clear caches accordingly.
1089
1090
        Ok(())
1091
    }
1092
1093
    /// Removes the given user ID from the account's ignore list.
1094
    pub async fn unignore_user(&self, user_id: &UserId) -> Result<()> {
1095
        let mut ignored_user_list = self.get_ignored_user_list_event_content().await?;
1096
1097
        // Only update account data if the user was ignored in the first place.
1098
        if ignored_user_list.ignored_users.remove(user_id).is_some() {
1099
            self.set_account_data(ignored_user_list).await?;
1100
        }
1101
1102
        // See comment in `ignore_user`.
1103
        Ok(())
1104
    }
1105
1106
    async fn get_ignored_user_list_event_content(&self) -> Result<IgnoredUserListEventContent> {
1107
        let ignored_user_list = self
1108
            .account_data::<IgnoredUserListEventContent>()
1109
            .await?
1110
            .map(|c| c.deserialize())
1111
            .transpose()?
1112
            .unwrap_or_default();
1113
        Ok(ignored_user_list)
1114
    }
1115
1116
    /// Get the current push rules from storage.
1117
    ///
1118
    /// If no push rules event was found, or it fails to deserialize, a ruleset
1119
    /// with the server-default push rules is returned.
1120
    ///
1121
    /// Panics if called when the client is not logged in.
1122
    pub async fn push_rules(&self) -> Result<Ruleset> {
1123
        Ok(self
1124
            .account_data::<PushRulesEventContent>()
1125
            .await?
1126
            .and_then(|r| match r.deserialize() {
1127
                Ok(r) => Some(r.global),
1128
                Err(e) => {
1129
                    error!("Push rules event failed to deserialize: {e}");
1130
                    None
1131
                }
1132
            })
1133
            .unwrap_or_else(|| {
1134
                Ruleset::server_default(
1135
                    self.client.user_id().expect("The client should be logged in"),
1136
                )
1137
            }))
1138
    }
1139
1140
    /// Retrieves the user's recently visited room list
1141
    pub async fn get_recently_visited_rooms(&self) -> Result<Vec<OwnedRoomId>> {
1142
        let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
1143
        let data = self
1144
            .client
1145
            .state_store()
1146
            .get_kv_data(StateStoreDataKey::RecentlyVisitedRooms(user_id))
1147
            .await?;
1148
1149
        Ok(data
1150
            .map(|v| {
1151
                v.into_recently_visited_rooms()
1152
                    .expect("Session data is not a list of recently visited rooms")
1153
            })
1154
            .unwrap_or_default())
1155
    }
1156
1157
    /// Moves/inserts the given room to the front of the recently visited list
1158
    pub async fn track_recently_visited_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {
1159
        let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
1160
1161
        // Get the previously stored recently visited rooms
1162
        let mut recently_visited_rooms = self.get_recently_visited_rooms().await?;
1163
1164
        // Remove all other occurrences of the new room_id
1165
        recently_visited_rooms.retain(|r| r != &room_id);
1166
1167
        // And insert it as the most recent
1168
        recently_visited_rooms.insert(0, room_id);
1169
1170
        // Cap the whole list to the VISITED_ROOMS_LIMIT
1171
        recently_visited_rooms.truncate(Self::VISITED_ROOMS_LIMIT);
1172
1173
        let data = StateStoreDataValue::RecentlyVisitedRooms(recently_visited_rooms);
1174
        self.client
1175
            .state_store()
1176
            .set_kv_data(StateStoreDataKey::RecentlyVisitedRooms(user_id), data)
1177
            .await?;
1178
        Ok(())
1179
    }
1180
1181
    /// Observes the media preview configuration.
1182
    ///
1183
    /// This value is linked to the [MSC 4278](https://github.com/matrix-org/matrix-spec-proposals/pull/4278) which is still in an unstable state.
1184
    ///
1185
    /// This will return the initial value of the configuration and a stream
1186
    /// that will yield new values as they are received.
1187
    ///
1188
    /// The initial value is the one that was stored in the account data
1189
    /// when the client was started.
1190
    /// and the following code is using a temporary solution until we know which
1191
    /// Matrix version will support the stable type.
1192
    ///
1193
    /// # Examples
1194
    ///
1195
    /// ```no_run
1196
    /// # use futures_util::{pin_mut, StreamExt};
1197
    /// # use matrix_sdk::Client;
1198
    /// # use matrix_sdk::ruma::events::media_preview_config::MediaPreviews;
1199
    /// # use url::Url;
1200
    /// # async {
1201
    /// # let homeserver = Url::parse("http://localhost:8080")?;
1202
    /// # let client = Client::new(homeserver).await?;
1203
    /// let account = client.account();
1204
    ///
1205
    /// let (initial_config, config_stream) =
1206
    ///     account.observe_media_preview_config().await?;
1207
    ///
1208
    /// println!("Initial media preview config: {:?}", initial_config);
1209
    ///
1210
    /// pin_mut!(config_stream);
1211
    /// while let Some(new_config) = config_stream.next().await {
1212
    ///     println!("Updated media preview config: {:?}", new_config);
1213
    /// }
1214
    /// # anyhow::Ok(()) };
1215
    /// ```
1216
    pub async fn observe_media_preview_config(
1217
        &self,
1218
    ) -> Result<
1219
        (
1220
            Option<MediaPreviewConfigEventContent>,
1221
            impl Stream<Item = MediaPreviewConfigEventContent> + use<>,
1222
        ),
1223
        Error,
1224
    > {
1225
        // We need to create two observers, one for the stable event and one for the
1226
        // unstable and combine them into a single stream.
1227
        let first_observer = self
1228
            .client
1229
            .observe_events::<GlobalAccountDataEvent<MediaPreviewConfigEventContent>, ()>();
1230
1231
        let stream = first_observer.subscribe().map(|event| event.0.content);
1232
1233
        let second_observer = self
1234
            .client
1235
            .observe_events::<GlobalAccountDataEvent<UnstableMediaPreviewConfigEventContent>, ()>();
1236
1237
        let second_stream = second_observer.subscribe().map(|event| event.0.content.0);
1238
1239
        let mut combined_stream = stream::select(stream, second_stream);
1240
1241
        let result_stream = async_stream::stream! {
1242
            // The observers need to be alive for the individual streams to be alive, so let's now
1243
            // create a stream that takes ownership of them.
1244
            let _first_observer = first_observer;
1245
            let _second_observer = second_observer;
1246
1247
            while let Some(item) = combined_stream.next().await {
1248
                yield item
1249
            }
1250
        };
1251
1252
        // We need to get the initial value of the media preview config event
1253
        // we do this after creating the observers to make sure that we don't
1254
        // create a race condition
1255
        let initial_value = self.get_media_preview_config_event_content().await?;
1256
1257
        Ok((initial_value, result_stream))
1258
    }
1259
1260
    /// Fetch the media preview configuration event content from the server.
1261
    ///
1262
    /// Will check first for the stable event and then for the unstable one.
1263
    pub async fn fetch_media_preview_config_event_content(
1264
        &self,
1265
    ) -> Result<Option<MediaPreviewConfigEventContent>> {
1266
        // First we check if there is a value in the stable event
1267
        let media_preview_config =
1268
            self.fetch_account_data_static::<MediaPreviewConfigEventContent>().await?;
1269
1270
        let media_preview_config = if let Some(media_preview_config) = media_preview_config {
1271
            Some(media_preview_config)
1272
        } else {
1273
            // If there is no value in the stable event, we check the unstable
1274
            self.fetch_account_data_static::<UnstableMediaPreviewConfigEventContent>()
1275
                .await?
1276
                .map(Raw::cast)
1277
        };
1278
1279
        // We deserialize the content of the event, if is not found we return the
1280
        // default
1281
        let media_preview_config = media_preview_config.and_then(|value| value.deserialize().ok());
1282
1283
        Ok(media_preview_config)
1284
    }
1285
1286
    /// Get the media preview configuration event content stored in the cache.
1287
    ///
1288
    /// Will check first for the stable event and then for the unstable one.
1289
    pub async fn get_media_preview_config_event_content(
1290
        &self,
1291
    ) -> Result<Option<MediaPreviewConfigEventContent>> {
1292
        let media_preview_config = self
1293
            .account_data::<MediaPreviewConfigEventContent>()
1294
            .await?
1295
            .and_then(|r| r.deserialize().ok());
1296
1297
        if let Some(media_preview_config) = media_preview_config {
1298
            Ok(Some(media_preview_config))
1299
        } else {
1300
            Ok(self
1301
                .account_data::<UnstableMediaPreviewConfigEventContent>()
1302
                .await?
1303
                .and_then(|r| r.deserialize().ok())
1304
                .map(Into::into))
1305
        }
1306
    }
1307
1308
    /// Set the media previews display policy in the timeline.
1309
    ///
1310
    /// This will always use the unstable event until we know which Matrix
1311
    /// version will support it.
1312
    pub async fn set_media_previews_display_policy(&self, policy: MediaPreviews) -> Result<()> {
1313
        let mut media_preview_config =
1314
            self.fetch_media_preview_config_event_content().await?.unwrap_or_default();
1315
        media_preview_config.media_previews = Some(policy);
1316
1317
        // Updating the unstable account data
1318
        let unstable_media_preview_config =
1319
            UnstableMediaPreviewConfigEventContent::from(media_preview_config);
1320
        self.set_account_data(unstable_media_preview_config).await?;
1321
        Ok(())
1322
    }
1323
1324
    /// Set the display policy for avatars in invite requests.
1325
    ///
1326
    /// This will always use the unstable event until we know which matrix
1327
    /// version will support it.
1328
    pub async fn set_invite_avatars_display_policy(&self, policy: InviteAvatars) -> Result<()> {
1329
        let mut media_preview_config =
1330
            self.fetch_media_preview_config_event_content().await?.unwrap_or_default();
1331
        media_preview_config.invite_avatars = Some(policy);
1332
1333
        // Updating the unstable account data
1334
        let unstable_media_preview_config =
1335
            UnstableMediaPreviewConfigEventContent::from(media_preview_config);
1336
        self.set_account_data(unstable_media_preview_config).await?;
1337
        Ok(())
1338
    }
1339
1340
    /// Adds a recently used emoji to the list and uploads the updated
1341
    /// `io.element.recent_emoji` content to the global account data.
1342
    ///
1343
    /// Before updating the data, it'll fetch it from the homeserver, to make
1344
    /// sure the updated values are always used. However, note this could still
1345
    /// result in a race condition if it's used concurrently.
1346
    #[cfg(feature = "experimental-element-recent-emojis")]
1347
    pub async fn add_recent_emoji(&self, emoji: &str) -> Result<()> {
1348
        let Some(user_id) = self.client.user_id() else {
1349
            return Err(Error::AuthenticationRequired);
1350
        };
1351
        let mut recent_emojis = self.get_recent_emojis(true).await?;
1352
1353
        let index = recent_emojis.iter().position(|(unicode, _)| unicode == emoji);
1354
1355
        // Truncate to the max allowed size, which will remove any emojis that
1356
        // haven't been used in a very long time. This will also ease the pressure on
1357
        // `remove` and `insert` shifting lots of elements in the list
1358
        recent_emojis.truncate(MAX_RECENT_EMOJI_COUNT);
1359
1360
        // Remove the emoji from the list if it was present and get it's `count` value
1361
        let count = if let Some(index) = index { recent_emojis.remove(index).1 } else { uint!(0) };
1362
1363
        // Insert the emoji with the updated count at the start of the list, so it's
1364
        // considered the most recently used emoji
1365
        recent_emojis.insert(0, (emoji.to_owned(), count + uint!(1)));
1366
1367
        // If the item was a new one, the list will now be `MAX_RECENT_EMOJI_COUNT` + 1,
1368
        // so truncate it again (this is a no-op if it already has the right size)
1369
        recent_emojis.truncate(MAX_RECENT_EMOJI_COUNT);
1370
1371
        let request = UpdateGlobalAccountDataRequest::new(
1372
            user_id.to_owned(),
1373
            &RecentEmojisContent::new(recent_emojis),
1374
        )?;
1375
        let _ = self.client.send(request).await?;
1376
1377
        Ok(())
1378
    }
1379
1380
    /// Gets the list of recently used emojis from the `io.element.recent_emoji`
1381
    /// global account data.
1382
    ///
1383
    /// If the `refresh` param is `true`, the data will be fetched from the
1384
    /// homeserver instead of the local storage.
1385
    #[cfg(feature = "experimental-element-recent-emojis")]
1386
    pub async fn get_recent_emojis(&self, refresh: bool) -> Result<Vec<(String, UInt)>> {
1387
        let content = if refresh {
1388
            let Some(user_id) = self.client.user_id() else {
1389
                return Err(Error::AuthenticationRequired);
1390
            };
1391
            let event_type = RecentEmojisContent::default().event_type();
1392
            let response = self
1393
                .client
1394
                .send(get_global_account_data::v3::Request::new(
1395
                    user_id.to_owned(),
1396
                    event_type.clone(),
1397
                ))
1398
                .await?;
1399
            let content = response.account_data.cast_unchecked().deserialize()?;
1400
            Some(content)
1401
        } else {
1402
            self.client
1403
                .state_store()
1404
                .get_account_data_event_static::<RecentEmojisContent>()
1405
                .await?
1406
                .map(|raw| raw.deserialize().map(|event| event.content))
1407
                .transpose()?
1408
        };
1409
1410
        if let Some(content) = content {
1411
            // Sort by count, descending. For items with the same count, since they were
1412
            // previously ordered by recency in the list, more recent emojis will be
1413
            // returned first.
1414
            let sorted_emojis = content
1415
                .recent_emoji
1416
                .into_iter()
1417
                // Items with higher counts should be first
1418
                .sorted_by(|(_, count_a), (_, count_b)| count_b.cmp(count_a))
1419
                // Make sure we take only up to MAX_RECENT_EMOJI_COUNT
1420
                .take(MAX_RECENT_EMOJI_COUNT)
1421
                .collect();
1422
            Ok(sorted_emojis)
1423
        } else {
1424
            Ok(Vec::new())
1425
        }
1426
    }
1427
}
1428
1429
fn get_raw_content<Ev, C>(raw: Option<Raw<Ev>>) -> Result<Option<Raw<C>>> {
1430
    #[derive(Deserialize)]
1431
    #[serde(bound = "C: Sized")] // Replace default Deserialize bound
1432
    struct GetRawContent<C> {
1433
        content: Raw<C>,
1434
    }
1435
1436
    Ok(raw
1437
        .map(|event| event.deserialize_as_unchecked::<GetRawContent<C>>())
1438
        .transpose()?
1439
        .map(|get_raw| get_raw.content))
1440
}
1441
1442
#[cfg(test)]
1443
mod tests {
1444
    use assert_matches::assert_matches;
1445
    use matrix_sdk_test::async_test;
1446
1447
    use crate::{Error, test_utils::client::MockClientBuilder};
1448
1449
    #[async_test]
1450
    async fn test_dont_ignore_oneself() {
1451
        let client = MockClientBuilder::new(None).build().await;
1452
1453
        // It's forbidden to ignore the logged-in user.
1454
        assert_matches!(
1455
            client.account().ignore_user(client.user_id().unwrap()).await,
1456
            Err(Error::CantIgnoreLoggedInUser)
1457
        );
1458
    }
1459
}
1460
1461
#[cfg(test)]
1462
#[cfg(feature = "experimental-element-recent-emojis")]
1463
mod test_recent_emojis {
1464
    use js_int::{UInt, uint};
1465
    use matrix_sdk_base::recent_emojis::RecentEmojisContent;
1466
    use matrix_sdk_test::{async_test, event_factory::EventFactory};
1467
1468
    use crate::{
1469
        account::MAX_RECENT_EMOJI_COUNT, config::SyncSettings, test_utils::mocks::MatrixMockServer,
1470
    };
1471
1472
    #[async_test]
1473
    async fn test_recent_emojis() {
1474
        let server = MatrixMockServer::new().await;
1475
        let client = server.client_builder().build().await;
1476
        let user_id = client.user_id().expect("session_id");
1477
1478
        server
1479
            .mock_add_recent_emojis()
1480
            .ok(user_id)
1481
            .named("Update recent emojis global account data")
1482
            .mock_once()
1483
            .mount()
1484
            .await;
1485
1486
        let recent_emojis = client.account().get_recent_emojis(false).await.expect("recent emojis");
1487
        assert!(recent_emojis.is_empty());
1488
1489
        let emoji_list = vec![
1490
            (":/".to_owned(), uint!(1)),
1491
            (":)".to_owned(), uint!(12)),
1492
            (":D".to_owned(), uint!(12)),
1493
        ];
1494
1495
        server
1496
            .mock_get_recent_emojis()
1497
            .ok(user_id, emoji_list.clone())
1498
            .named("Fetch recent emojis")
1499
            .mock_once()
1500
            .mount()
1501
            .await;
1502
1503
        client.account().add_recent_emoji(":)").await.expect("adding emoji");
1504
1505
        server
1506
            .mock_sync()
1507
            .ok(|builder| {
1508
                let content = RecentEmojisContent::new(emoji_list);
1509
                let event_builder = EventFactory::new().global_account_data(content);
1510
                builder.add_global_account_data(event_builder);
1511
            })
1512
            .named("Sync")
1513
            .mount()
1514
            .await;
1515
1516
        client.sync_once(SyncSettings::default()).await.expect("sync failed");
1517
1518
        let recent_emojis = client.account().get_recent_emojis(false).await.expect("recent emojis");
1519
1520
        // Assert size
1521
        assert_eq!(recent_emojis.len(), 3);
1522
1523
        // Assert ordering: first by times used, then by recency
1524
        assert_eq!(recent_emojis[0].0, ":)");
1525
        assert_eq!(recent_emojis[1].0, ":D");
1526
        assert_eq!(recent_emojis[2].0, ":/");
1527
    }
1528
1529
    #[async_test]
1530
    async fn test_max_recent_emoji_count() {
1531
        let server = MatrixMockServer::new().await;
1532
        let client = server.client_builder().build().await;
1533
        let user_id = client.user_id().expect("session_id");
1534
1535
        // This list is > the MAX_RECENT_EMOJI_COUNT
1536
        let long_emoji_list = (0..MAX_RECENT_EMOJI_COUNT * 2)
1537
            .map(|i| (i.to_string(), uint!(1)))
1538
            .collect::<Vec<(String, UInt)>>();
1539
1540
        // Initially we locally don't have any emojis
1541
        let recent_emojis = client.account().get_recent_emojis(false).await.expect("recent emojis");
1542
        assert!(recent_emojis.is_empty());
1543
1544
        server
1545
            .mock_get_recent_emojis()
1546
            .ok(user_id, long_emoji_list.clone())
1547
            .named("Fetch recent emojis")
1548
            .expect(3)
1549
            .mount()
1550
            .await;
1551
1552
        // Now with a list of emojis longer than the max count, we fetch the emoji list
1553
        let recent_emojis = client.account().get_recent_emojis(true).await.expect("recent emojis");
1554
1555
        // It should only return until the max count
1556
        assert_eq!(recent_emojis.len(), MAX_RECENT_EMOJI_COUNT);
1557
        assert_eq!(recent_emojis, long_emoji_list[..MAX_RECENT_EMOJI_COUNT]);
1558
1559
        // Simulate the logic we expect when adding a new emoji:
1560
        // 1. Remove the existing emoji if present
1561
        // 2. Increase its count value and insert it at the front.
1562
        // 3. Truncate at MAX_RECENT_EMOJI_COUNT
1563
        let expected_updated_emoji_list = {
1564
            let mut list = long_emoji_list.clone();
1565
            let item = list.remove(50);
1566
            list.insert(0, (item.0, item.1 + uint!(1)));
1567
            list.truncate(MAX_RECENT_EMOJI_COUNT);
1568
            list
1569
        };
1570
1571
        // Now if we add a new emoji that was not in the list, the last one in the list
1572
        // should be gone
1573
        server
1574
            .mock_add_recent_emojis()
1575
            .match_emojis_in_request_body(expected_updated_emoji_list)
1576
            .ok(user_id)
1577
            .named("Update recent emojis global account data with existing emoji")
1578
            .mock_once()
1579
            .mount()
1580
            .await;
1581
1582
        client.account().add_recent_emoji("50").await.expect("adding emoji");
1583
1584
        // Do the same, but now with a new emoji that wasn't previously in the list
1585
        let expected_updated_emoji_list = {
1586
            let mut list = long_emoji_list.clone();
1587
            let item = (":D".to_owned(), uint!(1));
1588
            list.insert(0, item);
1589
            list.truncate(MAX_RECENT_EMOJI_COUNT);
1590
            list
1591
        };
1592
1593
        // We should still have `MAX_RECENT_EMOJI_COUNT` items
1594
        server
1595
            .mock_add_recent_emojis()
1596
            .match_emojis_in_request_body(expected_updated_emoji_list)
1597
            .ok(user_id)
1598
            .named("Update recent emojis global account data with new emoji")
1599
            .mock_once()
1600
            .mount()
1601
            .await;
1602
1603
        client.account().add_recent_emoji(":D").await.expect("adding emoji");
1604
    }
1605
}