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