matrix_sdk/account.rs
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
17use matrix_sdk_base::{
18 media::{MediaFormat, MediaRequestParameters},
19 store::StateStoreExt,
20 StateStoreDataKey, StateStoreDataValue,
21};
22use mime::Mime;
23use ruma::{
24 api::client::{
25 account::{
26 add_3pid, change_password, deactivate, delete_3pid, get_3pids,
27 request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
28 },
29 config::{get_global_account_data, set_global_account_data},
30 error::ErrorKind,
31 profile::{
32 get_avatar_url, get_display_name, get_profile, set_avatar_url, set_display_name,
33 },
34 uiaa::AuthData,
35 },
36 assign,
37 events::{
38 ignored_user_list::{IgnoredUser, IgnoredUserListEventContent},
39 push_rules::PushRulesEventContent,
40 room::MediaSource,
41 AnyGlobalAccountDataEventContent, GlobalAccountDataEventContent,
42 GlobalAccountDataEventType, StaticEventContent,
43 },
44 push::Ruleset,
45 serde::Raw,
46 thirdparty::Medium,
47 ClientSecret, MxcUri, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, SessionId, UInt, UserId,
48};
49use serde::Deserialize;
50use tracing::error;
51
52use crate::{config::RequestConfig, Client, Error, Result};
53
54/// A high-level API to manage the client owner's account.
55///
56/// All the methods on this struct send a request to the homeserver.
57#[derive(Debug, Clone)]
58pub struct Account {
59 /// The underlying HTTP client.
60 client: Client,
61}
62
63impl Account {
64 /// The maximum number of visited room identifiers to keep in the state
65 /// store.
66 const VISITED_ROOMS_LIMIT: usize = 20;
67
68 pub(crate) fn new(client: Client) -> Self {
69 Self { client }
70 }
71
72 /// Get the display name of the account.
73 ///
74 /// # Examples
75 ///
76 /// ```no_run
77 /// # use matrix_sdk::Client;
78 /// # use url::Url;
79 /// # async {
80 /// # let homeserver = Url::parse("http://example.com")?;
81 /// let user = "example";
82 /// let client = Client::new(homeserver).await?;
83 /// client.matrix_auth().login_username(user, "password").send().await?;
84 ///
85 /// if let Some(name) = client.account().get_display_name().await? {
86 /// println!("Logged in as user '{user}' with display name '{name}'");
87 /// }
88 /// # anyhow::Ok(()) };
89 /// ```
90 pub async fn get_display_name(&self) -> Result<Option<String>> {
91 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
92 let request = get_display_name::v3::Request::new(user_id.to_owned());
93 let request_config = self.client.request_config().force_auth();
94 let response = self.client.send(request).with_request_config(request_config).await?;
95 Ok(response.displayname)
96 }
97
98 /// Set the display name of the account.
99 ///
100 /// # Examples
101 ///
102 /// ```no_run
103 /// # use matrix_sdk::Client;
104 /// # use url::Url;
105 /// # async {
106 /// # let homeserver = Url::parse("http://example.com")?;
107 /// let user = "example";
108 /// let client = Client::new(homeserver).await?;
109 /// client.matrix_auth().login_username(user, "password").send().await?;
110 ///
111 /// client.account().set_display_name(Some("Alice")).await?;
112 /// # anyhow::Ok(()) };
113 /// ```
114 pub async fn set_display_name(&self, name: Option<&str>) -> Result<()> {
115 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
116 let request =
117 set_display_name::v3::Request::new(user_id.to_owned(), name.map(ToOwned::to_owned));
118 self.client.send(request).await?;
119 Ok(())
120 }
121
122 /// Get the MXC URI of the account's avatar, if set.
123 ///
124 /// This always sends a request to the server to retrieve this information.
125 /// If successful, this fills the cache, and makes it so that
126 /// [`Self::get_cached_avatar_url`] will always return something.
127 ///
128 /// # Examples
129 ///
130 /// ```no_run
131 /// # use matrix_sdk::Client;
132 /// # use url::Url;
133 /// # async {
134 /// # let homeserver = Url::parse("http://example.com")?;
135 /// # let user = "example";
136 /// let client = Client::new(homeserver).await?;
137 /// client.matrix_auth().login_username(user, "password").send().await?;
138 ///
139 /// if let Some(url) = client.account().get_avatar_url().await? {
140 /// println!("Your avatar's mxc url is {url}");
141 /// }
142 /// # anyhow::Ok(()) };
143 /// ```
144 pub async fn get_avatar_url(&self) -> Result<Option<OwnedMxcUri>> {
145 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
146 let request = get_avatar_url::v3::Request::new(user_id.to_owned());
147
148 let config = Some(RequestConfig::new().force_auth());
149
150 let response = self.client.send(request).with_request_config(config).await?;
151 if let Some(url) = response.avatar_url.clone() {
152 // If an avatar is found cache it.
153 let _ = self
154 .client
155 .state_store()
156 .set_kv_data(
157 StateStoreDataKey::UserAvatarUrl(user_id),
158 StateStoreDataValue::UserAvatarUrl(url),
159 )
160 .await;
161 } else {
162 // If there is no avatar the user has removed it and we uncache it.
163 let _ = self
164 .client
165 .state_store()
166 .remove_kv_data(StateStoreDataKey::UserAvatarUrl(user_id))
167 .await;
168 }
169 Ok(response.avatar_url)
170 }
171
172 /// Get the URL of the account's avatar, if is stored in cache.
173 pub async fn get_cached_avatar_url(&self) -> Result<Option<OwnedMxcUri>> {
174 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
175 let data = self
176 .client
177 .state_store()
178 .get_kv_data(StateStoreDataKey::UserAvatarUrl(user_id))
179 .await?;
180 Ok(data.map(|v| v.into_user_avatar_url().expect("Session data is not a user avatar url")))
181 }
182
183 /// Set the MXC URI of the account's avatar.
184 ///
185 /// The avatar is unset if `url` is `None`.
186 pub async fn set_avatar_url(&self, url: Option<&MxcUri>) -> Result<()> {
187 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
188 let request =
189 set_avatar_url::v3::Request::new(user_id.to_owned(), url.map(ToOwned::to_owned));
190 self.client.send(request).await?;
191 Ok(())
192 }
193
194 /// Get the account's avatar, if set.
195 ///
196 /// Returns the avatar.
197 ///
198 /// If a thumbnail is requested no guarantee on the size of the image is
199 /// given.
200 ///
201 /// # Arguments
202 ///
203 /// * `format` - The desired format of the avatar.
204 ///
205 /// # Examples
206 ///
207 /// ```no_run
208 /// # use matrix_sdk::Client;
209 /// # use matrix_sdk::ruma::room_id;
210 /// # use matrix_sdk::media::MediaFormat;
211 /// # use url::Url;
212 /// # async {
213 /// # let homeserver = Url::parse("http://example.com")?;
214 /// # let user = "example";
215 /// let client = Client::new(homeserver).await?;
216 /// client.matrix_auth().login_username(user, "password").send().await?;
217 ///
218 /// if let Some(avatar) = client.account().get_avatar(MediaFormat::File).await?
219 /// {
220 /// std::fs::write("avatar.png", avatar);
221 /// }
222 /// # anyhow::Ok(()) };
223 /// ```
224 pub async fn get_avatar(&self, format: MediaFormat) -> Result<Option<Vec<u8>>> {
225 if let Some(url) = self.get_avatar_url().await? {
226 let request = MediaRequestParameters { source: MediaSource::Plain(url), format };
227 Ok(Some(self.client.media().get_media_content(&request, true).await?))
228 } else {
229 Ok(None)
230 }
231 }
232
233 /// Upload and set the account's avatar.
234 ///
235 /// This will upload the data produced by the reader to the homeserver's
236 /// content repository, and set the user's avatar to the MXC URI for the
237 /// uploaded file.
238 ///
239 /// This is a convenience method for calling [`Media::upload()`],
240 /// followed by [`Account::set_avatar_url()`].
241 ///
242 /// Returns the MXC URI of the uploaded avatar.
243 ///
244 /// # Examples
245 ///
246 /// ```no_run
247 /// # use std::fs;
248 /// # use matrix_sdk::Client;
249 /// # use url::Url;
250 /// # async {
251 /// # let homeserver = Url::parse("http://localhost:8080")?;
252 /// # let client = Client::new(homeserver).await?;
253 /// let image = fs::read("/home/example/selfie.jpg")?;
254 ///
255 /// client.account().upload_avatar(&mime::IMAGE_JPEG, image).await?;
256 /// # anyhow::Ok(()) };
257 /// ```
258 ///
259 /// [`Media::upload()`]: crate::Media::upload
260 pub async fn upload_avatar(&self, content_type: &Mime, data: Vec<u8>) -> Result<OwnedMxcUri> {
261 let upload_response = self.client.media().upload(content_type, data, None).await?;
262 self.set_avatar_url(Some(&upload_response.content_uri)).await?;
263 Ok(upload_response.content_uri)
264 }
265
266 /// Get the profile of the account.
267 ///
268 /// Allows to get both the display name and avatar URL in a single call.
269 ///
270 /// # Examples
271 ///
272 /// ```no_run
273 /// # use matrix_sdk::Client;
274 /// # use url::Url;
275 /// # async {
276 /// # let homeserver = Url::parse("http://localhost:8080")?;
277 /// # let client = Client::new(homeserver).await?;
278 /// let profile = client.account().fetch_user_profile().await?;
279 /// println!(
280 /// "You are '{:?}' with avatar '{:?}'",
281 /// profile.displayname, profile.avatar_url
282 /// );
283 /// # anyhow::Ok(()) };
284 /// ```
285 pub async fn fetch_user_profile(&self) -> Result<get_profile::v3::Response> {
286 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
287 self.fetch_user_profile_of(user_id).await
288 }
289
290 /// Get the profile for a given user id
291 ///
292 /// # Arguments
293 ///
294 /// * `user_id` the matrix id this function downloads the profile for
295 pub async fn fetch_user_profile_of(
296 &self,
297 user_id: &UserId,
298 ) -> Result<get_profile::v3::Response> {
299 let request = get_profile::v3::Request::new(user_id.to_owned());
300 Ok(self
301 .client
302 .send(request)
303 .with_request_config(RequestConfig::short_retry().force_auth())
304 .await?)
305 }
306
307 /// Change the password of the account.
308 ///
309 /// # Arguments
310 ///
311 /// * `new_password` - The new password to set.
312 ///
313 /// * `auth_data` - This request uses the [User-Interactive Authentication
314 /// API][uiaa]. The first request needs to set this to `None` and will
315 /// always fail with an [`UiaaResponse`]. The response will contain
316 /// information for the interactive auth and the same request needs to be
317 /// made but this time with some `auth_data` provided.
318 ///
319 /// # Returns
320 ///
321 /// This method might return an [`ErrorKind::WeakPassword`] error if the new
322 /// password is considered insecure by the homeserver, with details about
323 /// the strength requirements in the error's message.
324 ///
325 /// # Examples
326 ///
327 /// ```no_run
328 /// # use matrix_sdk::Client;
329 /// # use matrix_sdk::ruma::{
330 /// # api::client::{
331 /// # account::change_password::v3::{Request as ChangePasswordRequest},
332 /// # uiaa::{AuthData, Dummy},
333 /// # },
334 /// # assign,
335 /// # };
336 /// # use url::Url;
337 /// # async {
338 /// # let homeserver = Url::parse("http://localhost:8080")?;
339 /// # let client = Client::new(homeserver).await?;
340 /// client.account().change_password(
341 /// "myverysecretpassword",
342 /// Some(AuthData::Dummy(Dummy::new())),
343 /// ).await?;
344 /// # anyhow::Ok(()) };
345 /// ```
346 /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api
347 /// [`UiaaResponse`]: ruma::api::client::uiaa::UiaaResponse
348 /// [`ErrorKind::WeakPassword`]: ruma::api::client::error::ErrorKind::WeakPassword
349 pub async fn change_password(
350 &self,
351 new_password: &str,
352 auth_data: Option<AuthData>,
353 ) -> Result<change_password::v3::Response> {
354 let request = assign!(change_password::v3::Request::new(new_password.to_owned()), {
355 auth: auth_data,
356 });
357 Ok(self.client.send(request).await?)
358 }
359
360 /// Deactivate this account definitively.
361 ///
362 /// # Arguments
363 ///
364 /// * `id_server` - The identity server from which to unbind the user’s
365 /// [Third Party Identifiers][3pid].
366 ///
367 /// * `auth_data` - This request uses the [User-Interactive Authentication
368 /// API][uiaa]. The first request needs to set this to `None` and will
369 /// always fail with an [`UiaaResponse`]. The response will contain
370 /// information for the interactive auth and the same request needs to be
371 /// made but this time with some `auth_data` provided.
372 ///
373 /// * `erase` - Whether the user would like their content to be erased as
374 /// much as possible from the server.
375 ///
376 /// # Examples
377 ///
378 /// ```no_run
379 /// # use matrix_sdk::Client;
380 /// # use matrix_sdk::ruma::{
381 /// # api::client::{
382 /// # account::change_password::v3::{Request as ChangePasswordRequest},
383 /// # uiaa::{AuthData, Dummy},
384 /// # },
385 /// # assign,
386 /// # };
387 /// # use url::Url;
388 /// # async {
389 /// # let homeserver = Url::parse("http://localhost:8080")?;
390 /// # let client = Client::new(homeserver).await?;
391 /// # let account = client.account();
392 /// let response = account.deactivate(None, None, false).await;
393 ///
394 /// // Proceed with UIAA.
395 /// # anyhow::Ok(()) };
396 /// ```
397 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
398 /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api
399 /// [`UiaaResponse`]: ruma::api::client::uiaa::UiaaResponse
400 pub async fn deactivate(
401 &self,
402 id_server: Option<&str>,
403 auth_data: Option<AuthData>,
404 erase_data: bool,
405 ) -> Result<deactivate::v3::Response> {
406 let request = assign!(deactivate::v3::Request::new(), {
407 id_server: id_server.map(ToOwned::to_owned),
408 auth: auth_data,
409 erase: erase_data,
410 });
411 Ok(self.client.send(request).await?)
412 }
413
414 /// Get the registered [Third Party Identifiers][3pid] on the homeserver of
415 /// the account.
416 ///
417 /// These 3PIDs may be used by the homeserver to authenticate the user
418 /// during sensitive operations.
419 ///
420 /// # Examples
421 ///
422 /// ```no_run
423 /// # use matrix_sdk::Client;
424 /// # use url::Url;
425 /// # async {
426 /// # let homeserver = Url::parse("http://localhost:8080")?;
427 /// # let client = Client::new(homeserver).await?;
428 /// let threepids = client.account().get_3pids().await?.threepids;
429 ///
430 /// for threepid in threepids {
431 /// println!(
432 /// "Found 3PID '{}' of type '{}'",
433 /// threepid.address, threepid.medium
434 /// );
435 /// }
436 /// # anyhow::Ok(()) };
437 /// ```
438 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
439 pub async fn get_3pids(&self) -> Result<get_3pids::v3::Response> {
440 let request = get_3pids::v3::Request::new();
441 Ok(self.client.send(request).await?)
442 }
443
444 /// Request a token to validate an email address as a [Third Party
445 /// Identifier][3pid].
446 ///
447 /// This is the first step in registering an email address as 3PID. Next,
448 /// call [`Account::add_3pid()`] with the same `client_secret` and the
449 /// returned `sid`.
450 ///
451 /// # Arguments
452 ///
453 /// * `client_secret` - A client-generated secret string used to protect
454 /// this session.
455 ///
456 /// * `email` - The email address to validate.
457 ///
458 /// * `send_attempt` - The attempt number. This number needs to be
459 /// incremented if you want to request another token for the same
460 /// validation.
461 ///
462 /// # Returns
463 ///
464 /// * `sid` - The session ID to be used in following requests for this 3PID.
465 ///
466 /// * `submit_url` - If present, the user will submit the token to the
467 /// client, that must send it to this URL. If not, the client will not be
468 /// involved in the token submission.
469 ///
470 /// This method might return an [`ErrorKind::ThreepidInUse`] error if the
471 /// email address is already registered for this account or another, or an
472 /// [`ErrorKind::ThreepidDenied`] error if it is denied.
473 ///
474 /// # Examples
475 ///
476 /// ```no_run
477 /// # use matrix_sdk::Client;
478 /// # use matrix_sdk::ruma::{ClientSecret, uint};
479 /// # use url::Url;
480 /// # async {
481 /// # let homeserver = Url::parse("http://localhost:8080")?;
482 /// # let client = Client::new(homeserver).await?;
483 /// # let account = client.account();
484 /// # let secret = ClientSecret::parse("secret")?;
485 /// let token_response = account
486 /// .request_3pid_email_token(&secret, "john@matrix.org", uint!(0))
487 /// .await?;
488 ///
489 /// // Wait for the user to confirm that the token was submitted or prompt
490 /// // the user for the token and send it to submit_url.
491 ///
492 /// let uiaa_response =
493 /// account.add_3pid(&secret, &token_response.sid, None).await;
494 ///
495 /// // Proceed with UIAA.
496 /// # anyhow::Ok(()) };
497 /// ```
498 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
499 /// [`ErrorKind::ThreepidInUse`]: ruma::api::client::error::ErrorKind::ThreepidInUse
500 /// [`ErrorKind::ThreepidDenied`]: ruma::api::client::error::ErrorKind::ThreepidDenied
501 pub async fn request_3pid_email_token(
502 &self,
503 client_secret: &ClientSecret,
504 email: &str,
505 send_attempt: UInt,
506 ) -> Result<request_3pid_management_token_via_email::v3::Response> {
507 let request = request_3pid_management_token_via_email::v3::Request::new(
508 client_secret.to_owned(),
509 email.to_owned(),
510 send_attempt,
511 );
512 Ok(self.client.send(request).await?)
513 }
514
515 /// Request a token to validate a phone number as a [Third Party
516 /// Identifier][3pid].
517 ///
518 /// This is the first step in registering a phone number as 3PID. Next,
519 /// call [`Account::add_3pid()`] with the same `client_secret` and the
520 /// returned `sid`.
521 ///
522 /// # Arguments
523 ///
524 /// * `client_secret` - A client-generated secret string used to protect
525 /// this session.
526 ///
527 /// * `country` - The two-letter uppercase ISO-3166-1 alpha-2 country code
528 /// that the number in phone_number should be parsed as if it were dialled
529 /// from.
530 ///
531 /// * `phone_number` - The phone number to validate.
532 ///
533 /// * `send_attempt` - The attempt number. This number needs to be
534 /// incremented if you want to request another token for the same
535 /// validation.
536 ///
537 /// # Returns
538 ///
539 /// * `sid` - The session ID to be used in following requests for this 3PID.
540 ///
541 /// * `submit_url` - If present, the user will submit the token to the
542 /// client, that must send it to this URL. If not, the client will not be
543 /// involved in the token submission.
544 ///
545 /// This method might return an [`ErrorKind::ThreepidInUse`] error if the
546 /// phone number is already registered for this account or another, or an
547 /// [`ErrorKind::ThreepidDenied`] error if it is denied.
548 ///
549 /// # Examples
550 ///
551 /// ```no_run
552 /// # use matrix_sdk::Client;
553 /// # use matrix_sdk::ruma::{ClientSecret, uint};
554 /// # use url::Url;
555 /// # async {
556 /// # let homeserver = Url::parse("http://localhost:8080")?;
557 /// # let client = Client::new(homeserver).await?;
558 /// # let account = client.account();
559 /// # let secret = ClientSecret::parse("secret")?;
560 /// let token_response = account
561 /// .request_3pid_msisdn_token(&secret, "FR", "0123456789", uint!(0))
562 /// .await?;
563 ///
564 /// // Wait for the user to confirm that the token was submitted or prompt
565 /// // the user for the token and send it to submit_url.
566 ///
567 /// let uiaa_response =
568 /// account.add_3pid(&secret, &token_response.sid, None).await;
569 ///
570 /// // Proceed with UIAA.
571 /// # anyhow::Ok(()) };
572 /// ```
573 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
574 /// [`ErrorKind::ThreepidInUse`]: ruma::api::client::error::ErrorKind::ThreepidInUse
575 /// [`ErrorKind::ThreepidDenied`]: ruma::api::client::error::ErrorKind::ThreepidDenied
576 pub async fn request_3pid_msisdn_token(
577 &self,
578 client_secret: &ClientSecret,
579 country: &str,
580 phone_number: &str,
581 send_attempt: UInt,
582 ) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
583 let request = request_3pid_management_token_via_msisdn::v3::Request::new(
584 client_secret.to_owned(),
585 country.to_owned(),
586 phone_number.to_owned(),
587 send_attempt,
588 );
589 Ok(self.client.send(request).await?)
590 }
591
592 /// Add a [Third Party Identifier][3pid] on the homeserver for this
593 /// account.
594 ///
595 /// This 3PID may be used by the homeserver to authenticate the user
596 /// during sensitive operations.
597 ///
598 /// This method should be called after
599 /// [`Account::request_3pid_email_token()`] or
600 /// [`Account::request_3pid_msisdn_token()`] to complete the 3PID
601 ///
602 /// # Arguments
603 ///
604 /// * `client_secret` - The same client secret used in
605 /// [`Account::request_3pid_email_token()`] or
606 /// [`Account::request_3pid_msisdn_token()`].
607 ///
608 /// * `sid` - The session ID returned in
609 /// [`Account::request_3pid_email_token()`] or
610 /// [`Account::request_3pid_msisdn_token()`].
611 ///
612 /// * `auth_data` - This request uses the [User-Interactive Authentication
613 /// API][uiaa]. The first request needs to set this to `None` and will
614 /// always fail with an [`UiaaResponse`]. The response will contain
615 /// information for the interactive auth and the same request needs to be
616 /// made but this time with some `auth_data` provided.
617 ///
618 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
619 /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api
620 /// [`UiaaResponse`]: ruma::api::client::uiaa::UiaaResponse
621 pub async fn add_3pid(
622 &self,
623 client_secret: &ClientSecret,
624 sid: &SessionId,
625 auth_data: Option<AuthData>,
626 ) -> Result<add_3pid::v3::Response> {
627 #[rustfmt::skip] // rustfmt wants to merge the next two lines
628 let request =
629 assign!(add_3pid::v3::Request::new(client_secret.to_owned(), sid.to_owned()), {
630 auth: auth_data
631 });
632 Ok(self.client.send(request).await?)
633 }
634
635 /// Delete a [Third Party Identifier][3pid] from the homeserver for this
636 /// account.
637 ///
638 /// # Arguments
639 ///
640 /// * `address` - The 3PID being removed.
641 ///
642 /// * `medium` - The type of the 3PID.
643 ///
644 /// * `id_server` - The identity server to unbind from. If not provided, the
645 /// homeserver should unbind the 3PID from the identity server it was
646 /// bound to previously.
647 ///
648 /// # Returns
649 ///
650 /// * [`ThirdPartyIdRemovalStatus::Success`] if the 3PID was also unbound
651 /// from the identity server.
652 ///
653 /// * [`ThirdPartyIdRemovalStatus::NoSupport`] if the 3PID was not unbound
654 /// from the identity server. This can also mean that the 3PID was not
655 /// bound to an identity server in the first place.
656 ///
657 /// # Examples
658 ///
659 /// ```no_run
660 /// # use matrix_sdk::Client;
661 /// # use matrix_sdk::ruma::thirdparty::Medium;
662 /// # use matrix_sdk::ruma::api::client::account::ThirdPartyIdRemovalStatus;
663 /// # use url::Url;
664 /// # async {
665 /// # let homeserver = Url::parse("http://localhost:8080")?;
666 /// # let client = Client::new(homeserver).await?;
667 /// # let account = client.account();
668 /// match account
669 /// .delete_3pid("paul@matrix.org", Medium::Email, None)
670 /// .await?
671 /// .id_server_unbind_result
672 /// {
673 /// ThirdPartyIdRemovalStatus::Success => {
674 /// println!("3PID unbound from the Identity Server");
675 /// }
676 /// _ => println!("Could not unbind 3PID from the Identity Server"),
677 /// }
678 /// # anyhow::Ok(()) };
679 /// ```
680 /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types
681 /// [`ThirdPartyIdRemovalStatus::Success`]: ruma::api::client::account::ThirdPartyIdRemovalStatus::Success
682 /// [`ThirdPartyIdRemovalStatus::NoSupport`]: ruma::api::client::account::ThirdPartyIdRemovalStatus::NoSupport
683 pub async fn delete_3pid(
684 &self,
685 address: &str,
686 medium: Medium,
687 id_server: Option<&str>,
688 ) -> Result<delete_3pid::v3::Response> {
689 let request = assign!(delete_3pid::v3::Request::new(medium, address.to_owned()), {
690 id_server: id_server.map(ToOwned::to_owned),
691 });
692 Ok(self.client.send(request).await?)
693 }
694
695 /// Get the content of an account data event of statically-known type.
696 ///
697 /// # Examples
698 ///
699 /// ```no_run
700 /// # use matrix_sdk::Client;
701 /// # async {
702 /// # let client = Client::new("http://localhost:8080".parse()?).await?;
703 /// # let account = client.account();
704 /// use matrix_sdk::ruma::events::ignored_user_list::IgnoredUserListEventContent;
705 ///
706 /// let maybe_content = account.account_data::<IgnoredUserListEventContent>().await?;
707 /// if let Some(raw_content) = maybe_content {
708 /// let content = raw_content.deserialize()?;
709 /// println!("Ignored users:");
710 /// for user_id in content.ignored_users.keys() {
711 /// println!("- {user_id}");
712 /// }
713 /// }
714 /// # anyhow::Ok(()) };
715 /// ```
716 pub async fn account_data<C>(&self) -> Result<Option<Raw<C>>>
717 where
718 C: GlobalAccountDataEventContent + StaticEventContent,
719 {
720 get_raw_content(self.client.state_store().get_account_data_event_static::<C>().await?)
721 }
722
723 /// Get the content of an account data event of a given type.
724 pub async fn account_data_raw(
725 &self,
726 event_type: GlobalAccountDataEventType,
727 ) -> Result<Option<Raw<AnyGlobalAccountDataEventContent>>> {
728 get_raw_content(self.client.state_store().get_account_data_event(event_type).await?)
729 }
730
731 /// Fetch a global account data event from the server.
732 ///
733 /// The content from the response will not be persisted in the store.
734 ///
735 /// Examples
736 ///
737 /// ```no_run
738 /// # use matrix_sdk::Client;
739 /// # async {
740 /// # let client = Client::new("http://localhost:8080".parse()?).await?;
741 /// # let account = client.account();
742 /// use matrix_sdk::ruma::events::{ignored_user_list::IgnoredUserListEventContent, GlobalAccountDataEventType};
743 ///
744 /// if let Some(raw_content) = account.fetch_account_data(GlobalAccountDataEventType::IgnoredUserList).await? {
745 /// let content = raw_content.deserialize_as::<IgnoredUserListEventContent>()?;
746 ///
747 /// println!("Ignored users:");
748 ///
749 /// for user_id in content.ignored_users.keys() {
750 /// println!("- {user_id}");
751 /// }
752 /// }
753 /// # anyhow::Ok(()) };
754 pub async fn fetch_account_data(
755 &self,
756 event_type: GlobalAccountDataEventType,
757 ) -> Result<Option<Raw<AnyGlobalAccountDataEventContent>>> {
758 let own_user = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
759
760 let request = get_global_account_data::v3::Request::new(own_user.to_owned(), event_type);
761
762 match self.client.send(request).await {
763 Ok(r) => Ok(Some(r.account_data)),
764 Err(e) => {
765 if let Some(kind) = e.client_api_error_kind() {
766 if kind == &ErrorKind::NotFound {
767 Ok(None)
768 } else {
769 Err(e.into())
770 }
771 } else {
772 Err(e.into())
773 }
774 }
775 }
776 }
777
778 /// Set the given account data event.
779 ///
780 /// # Examples
781 ///
782 /// ```no_run
783 /// # use matrix_sdk::Client;
784 /// # async {
785 /// # let client = Client::new("http://localhost:8080".parse()?).await?;
786 /// # let account = client.account();
787 /// use matrix_sdk::ruma::{
788 /// events::ignored_user_list::{IgnoredUser, IgnoredUserListEventContent},
789 /// user_id,
790 /// };
791 ///
792 /// let mut content = account
793 /// .account_data::<IgnoredUserListEventContent>()
794 /// .await?
795 /// .map(|c| c.deserialize())
796 /// .transpose()?
797 /// .unwrap_or_default();
798 /// content
799 /// .ignored_users
800 /// .insert(user_id!("@foo:bar.com").to_owned(), IgnoredUser::new());
801 /// account.set_account_data(content).await?;
802 /// # anyhow::Ok(()) };
803 /// ```
804 pub async fn set_account_data<T>(
805 &self,
806 content: T,
807 ) -> Result<set_global_account_data::v3::Response>
808 where
809 T: GlobalAccountDataEventContent,
810 {
811 let own_user = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
812
813 let request = set_global_account_data::v3::Request::new(own_user.to_owned(), &content)?;
814
815 Ok(self.client.send(request).await?)
816 }
817
818 /// Set the given raw account data event.
819 pub async fn set_account_data_raw(
820 &self,
821 event_type: GlobalAccountDataEventType,
822 content: Raw<AnyGlobalAccountDataEventContent>,
823 ) -> Result<set_global_account_data::v3::Response> {
824 let own_user = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
825
826 let request =
827 set_global_account_data::v3::Request::new_raw(own_user.to_owned(), event_type, content);
828
829 Ok(self.client.send(request).await?)
830 }
831
832 /// Marks the room identified by `room_id` as a "direct chat" with each
833 /// user in `user_ids`.
834 ///
835 /// # Arguments
836 ///
837 /// * `room_id` - The room ID of the direct message room.
838 /// * `user_ids` - The user IDs to be associated with this direct message
839 /// room.
840 pub async fn mark_as_dm(&self, room_id: &RoomId, user_ids: &[OwnedUserId]) -> Result<()> {
841 use ruma::events::direct::DirectEventContent;
842
843 // This function does a read/update/store of an account data event stored on the
844 // homeserver. We first fetch the existing account data event, the event
845 // contains a map which gets updated by this method, finally we upload the
846 // modified event.
847 //
848 // To prevent multiple calls to this method trying to update the map of DMs same
849 // time, and thus trampling on each other we introduce a lock which acts
850 // as a semaphore.
851 let _guard = self.client.locks().mark_as_dm_lock.lock().await;
852
853 // Now we need to mark the room as a DM for ourselves, we fetch the
854 // existing `m.direct` event and append the room to the list of DMs we
855 // have with this user.
856
857 // We are fetching the content from the server because we currently can't rely
858 // on `/sync` giving us the correct data in a timely manner.
859 let raw_content = self.fetch_account_data(GlobalAccountDataEventType::Direct).await?;
860
861 let mut content = if let Some(raw_content) = raw_content {
862 // Log the error and pass it upwards if we fail to deserialize the m.direct
863 // event.
864 raw_content.deserialize_as::<DirectEventContent>().map_err(|err| {
865 error!("unable to deserialize m.direct event content; aborting request to mark {room_id} as dm: {err}");
866 err
867 })?
868 } else {
869 // If there was no m.direct event server-side, create a default one.
870 Default::default()
871 };
872
873 for user_id in user_ids {
874 content.entry(user_id.into()).or_default().push(room_id.to_owned());
875 }
876
877 // TODO: We should probably save the fact that we need to send this out
878 // because otherwise we might end up in a state where we have a DM that
879 // isn't marked as one.
880 self.set_account_data(content).await?;
881
882 Ok(())
883 }
884
885 /// Adds the given user ID to the account's ignore list.
886 pub async fn ignore_user(&self, user_id: &UserId) -> Result<()> {
887 let mut ignored_user_list = self.get_ignored_user_list_event_content().await?;
888 ignored_user_list.ignored_users.insert(user_id.to_owned(), IgnoredUser::new());
889
890 // Updating the account data
891 self.set_account_data(ignored_user_list).await?;
892 // TODO: I think I should reset all the storage and perform a new local sync
893 // here but I don't know how
894 Ok(())
895 }
896
897 /// Removes the given user ID from the account's ignore list.
898 pub async fn unignore_user(&self, user_id: &UserId) -> Result<()> {
899 let mut ignored_user_list = self.get_ignored_user_list_event_content().await?;
900 ignored_user_list.ignored_users.remove(user_id);
901
902 // Updating the account data
903 self.set_account_data(ignored_user_list).await?;
904 // TODO: I think I should reset all the storage and perform a new local sync
905 // here but I don't know how
906 Ok(())
907 }
908
909 async fn get_ignored_user_list_event_content(&self) -> Result<IgnoredUserListEventContent> {
910 let ignored_user_list = self
911 .account_data::<IgnoredUserListEventContent>()
912 .await?
913 .map(|c| c.deserialize())
914 .transpose()?
915 .unwrap_or_default();
916 Ok(ignored_user_list)
917 }
918
919 /// Get the current push rules from storage.
920 ///
921 /// If no push rules event was found, or it fails to deserialize, a ruleset
922 /// with the server-default push rules is returned.
923 ///
924 /// Panics if called when the client is not logged in.
925 pub async fn push_rules(&self) -> Result<Ruleset> {
926 Ok(self
927 .account_data::<PushRulesEventContent>()
928 .await?
929 .and_then(|r| match r.deserialize() {
930 Ok(r) => Some(r.global),
931 Err(e) => {
932 error!("Push rules event failed to deserialize: {e}");
933 None
934 }
935 })
936 .unwrap_or_else(|| {
937 Ruleset::server_default(
938 self.client.user_id().expect("The client should be logged in"),
939 )
940 }))
941 }
942
943 /// Retrieves the user's recently visited room list
944 pub async fn get_recently_visited_rooms(&self) -> Result<Vec<OwnedRoomId>> {
945 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
946 let data = self
947 .client
948 .state_store()
949 .get_kv_data(StateStoreDataKey::RecentlyVisitedRooms(user_id))
950 .await?;
951
952 Ok(data
953 .map(|v| {
954 v.into_recently_visited_rooms()
955 .expect("Session data is not a list of recently visited rooms")
956 })
957 .unwrap_or_default())
958 }
959
960 /// Moves/inserts the given room to the front of the recently visited list
961 pub async fn track_recently_visited_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {
962 let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?;
963
964 // Get the previously stored recently visited rooms
965 let mut recently_visited_rooms = self.get_recently_visited_rooms().await?;
966
967 // Remove all other occurrences of the new room_id
968 recently_visited_rooms.retain(|r| r != &room_id);
969
970 // And insert it as the most recent
971 recently_visited_rooms.insert(0, room_id);
972
973 // Cap the whole list to the VISITED_ROOMS_LIMIT
974 recently_visited_rooms.truncate(Self::VISITED_ROOMS_LIMIT);
975
976 let data = StateStoreDataValue::RecentlyVisitedRooms(recently_visited_rooms);
977 self.client
978 .state_store()
979 .set_kv_data(StateStoreDataKey::RecentlyVisitedRooms(user_id), data)
980 .await?;
981 Ok(())
982 }
983}
984
985fn get_raw_content<Ev, C>(raw: Option<Raw<Ev>>) -> Result<Option<Raw<C>>> {
986 #[derive(Deserialize)]
987 #[serde(bound = "C: Sized")] // Replace default Deserialize bound
988 struct GetRawContent<C> {
989 content: Raw<C>,
990 }
991
992 Ok(raw
993 .map(|event| event.deserialize_as::<GetRawContent<C>>())
994 .transpose()?
995 .map(|get_raw| get_raw.content))
996}