From 8e41bccf8bdb4bdcd4973133c94118c8e32a27f9 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 11 Feb 2022 20:45:22 +0100 Subject: [PATCH 01/22] Upgrade Ruma --- crates/matrix-qrcode/Cargo.toml | 4 ++-- crates/matrix-sdk-appservice/Cargo.toml | 4 ++-- .../matrix-sdk-appservice/src/webserver/warp.rs | 17 +++++++++-------- crates/matrix-sdk-base/Cargo.toml | 4 ++-- crates/matrix-sdk-base/src/rooms/mod.rs | 2 +- crates/matrix-sdk-common/Cargo.toml | 2 +- crates/matrix-sdk-crypto/Cargo.toml | 4 ++-- crates/matrix-sdk-test/Cargo.toml | 2 +- crates/matrix-sdk/Cargo.toml | 4 ++-- crates/matrix-sdk/src/room/common.rs | 4 ++-- 10 files changed, 24 insertions(+), 23 deletions(-) diff --git a/crates/matrix-qrcode/Cargo.toml b/crates/matrix-qrcode/Cargo.toml index 2fde5e913..b11e0075e 100644 --- a/crates/matrix-qrcode/Cargo.toml +++ b/crates/matrix-qrcode/Cargo.toml @@ -29,8 +29,8 @@ thiserror = "1.0.25" [dependencies.ruma-identifiers] git = "https://github.com/ruma/ruma/" -rev = "37095f88553b311e7a70adaaabe39976fb8ff71c" +rev = "b9f32bc6327542d382d4eb42ec43623495c50e66" [dependencies.ruma-serde] git = "https://github.com/ruma/ruma/" -rev = "37095f88553b311e7a70adaaabe39976fb8ff71c" +rev = "b9f32bc6327542d382d4eb42ec43623495c50e66" diff --git a/crates/matrix-sdk-appservice/Cargo.toml b/crates/matrix-sdk-appservice/Cargo.toml index d35f29941..b77cfffa0 100644 --- a/crates/matrix-sdk-appservice/Cargo.toml +++ b/crates/matrix-sdk-appservice/Cargo.toml @@ -38,8 +38,8 @@ warp = { version = "0.3.1", optional = true, default-features = false } [dependencies.ruma] git = "https://github.com/ruma/ruma/" -rev = "37095f88553b311e7a70adaaabe39976fb8ff71c" -features = ["client-api-c", "appservice-api-s", "unstable-pre-spec"] +rev = "b9f32bc6327542d382d4eb42ec43623495c50e66" +features = ["client-api-c", "appservice-api-s"] [dev-dependencies] matrix-sdk-test = { version = "0.4", path = "../matrix-sdk-test", features = ["appservice"] } diff --git a/crates/matrix-sdk-appservice/src/webserver/warp.rs b/crates/matrix-sdk-appservice/src/webserver/warp.rs index a7b57989e..6dfebbd27 100644 --- a/crates/matrix-sdk-appservice/src/webserver/warp.rs +++ b/crates/matrix-sdk-appservice/src/webserver/warp.rs @@ -165,13 +165,13 @@ mod handlers { use super::*; pub async fn user( - _user_id: String, + user_id: String, appservice: AppService, request: http::Request, ) -> Result { if let Some(user_exists) = appservice.event_handler.users.lock().await.as_mut() { - let request = - query_user::IncomingRequest::try_from_http_request(request).map_err(Error::from)?; + let request = query_user::IncomingRequest::try_from_http_request(request, &[user_id]) + .map_err(Error::from)?; return if user_exists(appservice.clone(), request).await { Ok(warp::reply::json(&String::from("{}"))) } else { @@ -182,13 +182,13 @@ mod handlers { } pub async fn room( - _room_id: String, + room_id: String, appservice: AppService, request: http::Request, ) -> Result { if let Some(room_exists) = appservice.event_handler.rooms.lock().await.as_mut() { - let request = - query_room::IncomingRequest::try_from_http_request(request).map_err(Error::from)?; + let request = query_room::IncomingRequest::try_from_http_request(request, &[room_id]) + .map_err(Error::from)?; return if room_exists(appservice.clone(), request).await { Ok(warp::reply::json(&String::from("{}"))) } else { @@ -199,12 +199,13 @@ mod handlers { } pub async fn transaction( - _txn_id: String, + txn_id: String, appservice: AppService, request: http::Request, ) -> Result { let incoming_transaction: ruma::api::appservice::event::push_events::v1::IncomingRequest = - ruma::api::IncomingRequest::try_from_http_request(request).map_err(Error::from)?; + ruma::api::IncomingRequest::try_from_http_request(request, &[txn_id]) + .map_err(Error::from)?; let client = appservice.get_cached_client(None)?; client.receive_transaction(incoming_transaction).await.map_err(Error::from)?; diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 35653b1bd..b3b32d1e0 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -61,8 +61,8 @@ wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional = [dependencies.ruma] git = "https://github.com/ruma/ruma/" -rev = "37095f88553b311e7a70adaaabe39976fb8ff71c" -features = ["client-api-c", "unstable-pre-spec"] +rev = "b9f32bc6327542d382d4eb42ec43623495c50e66" +features = ["client-api-c"] [dev-dependencies] futures = { version = "0.3.15", default-features = false, features = ["executor"] } diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs index 819aa93fd..f766f5801 100644 --- a/crates/matrix-sdk-base/src/rooms/mod.rs +++ b/crates/matrix-sdk-base/src/rooms/mod.rs @@ -79,7 +79,7 @@ impl BaseRoomInfo { true } AnyStateEventContent::RoomAvatar(a) => { - self.avatar_url = a.url.clone(); + self.avatar_url = Some(a.url.clone()); true } AnyStateEventContent::RoomName(n) => { diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index f0e9153f4..efe8cfceb 100644 --- a/crates/matrix-sdk-common/Cargo.toml +++ b/crates/matrix-sdk-common/Cargo.toml @@ -21,7 +21,7 @@ serde = "1.0.126" [dependencies.ruma] git = "https://github.com/ruma/ruma/" -rev = "37095f88553b311e7a70adaaabe39976fb8ff71c" +rev = "b9f32bc6327542d382d4eb42ec43623495c50e66" features = ["client-api-c"] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 13482f2bf..a01a88d3d 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -52,8 +52,8 @@ indexed_db_futures = { version = "0.2.0", optional = true } wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional = true } [dependencies.ruma] git = "https://github.com/ruma/ruma/" -rev = "37095f88553b311e7a70adaaabe39976fb8ff71c" -features = ["client-api-c", "rand", "unstable-pre-spec"] +rev = "b9f32bc6327542d382d4eb42ec43623495c50e66" +features = ["client-api-c", "rand", "unstable-msc2676", "unstable-msc2677"] [dev-dependencies] futures = { version = "0.3.15", default-features = false, features = ["executor"] } diff --git a/crates/matrix-sdk-test/Cargo.toml b/crates/matrix-sdk-test/Cargo.toml index cba254299..951182a99 100644 --- a/crates/matrix-sdk-test/Cargo.toml +++ b/crates/matrix-sdk-test/Cargo.toml @@ -23,5 +23,5 @@ serde_json = "1.0.64" [dependencies.ruma] git = "https://github.com/ruma/ruma/" -rev = "37095f88553b311e7a70adaaabe39976fb8ff71c" +rev = "b9f32bc6327542d382d4eb42ec43623495c50e66" features = ["client-api-c"] diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 32c416fe7..2adc756dc 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -77,8 +77,8 @@ default_features = false [dependencies.ruma] git = "https://github.com/ruma/ruma/" -rev = "37095f88553b311e7a70adaaabe39976fb8ff71c" -features = ["client-api-c", "compat", "rand", "unstable-pre-spec"] +rev = "b9f32bc6327542d382d4eb42ec43623495c50e66" +features = ["client-api-c", "compat", "rand"] [dependencies.tokio-stream] version = "0.1.6" diff --git a/crates/matrix-sdk/src/room/common.rs b/crates/matrix-sdk/src/room/common.rs index 76fafad99..f815ac183 100644 --- a/crates/matrix-sdk/src/room/common.rs +++ b/crates/matrix-sdk/src/room/common.rs @@ -523,7 +523,7 @@ pub struct MessagesOptions<'a> { pub limit: UInt, /// A [`RoomEventFilter`] to filter returned events with. - pub filter: Option>, + pub filter: RoomEventFilter<'a>, } impl<'a> MessagesOptions<'a> { @@ -531,7 +531,7 @@ impl<'a> MessagesOptions<'a> { /// /// All other parameters will be defaulted. pub fn new(from: &'a str, dir: Direction) -> Self { - Self { from, to: None, dir, limit: uint!(10), filter: None } + Self { from, to: None, dir, limit: uint!(10), filter: RoomEventFilter::default() } } /// Creates `MessagesOptions` with the given start token, and `dir` set to From 735d9e58946c395aa946555e59819e2c38bf8177 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sat, 12 Feb 2022 04:08:33 +0100 Subject: [PATCH 02/22] Use user_id! macro in more tests --- crates/matrix-sdk-base/src/client.rs | 6 ++-- crates/matrix-sdk-crypto/README.md | 4 +-- crates/matrix-sdk-crypto/src/machine.rs | 6 ++-- .../src/verification/sas/sas_state.rs | 4 +-- crates/matrix-sdk/README.md | 6 ++-- .../src/encryption/identities/devices.rs | 30 ++++++++-------- .../src/encryption/identities/mod.rs | 12 +++---- .../src/encryption/identities/users.rs | 36 +++++++++---------- crates/matrix-sdk/src/encryption/mod.rs | 18 +++++----- 9 files changed, 61 insertions(+), 61 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index c4ef02efc..02d1a2c5d 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -1286,12 +1286,12 @@ impl BaseClient { /// ```no_run /// # use std::convert::TryFrom; /// # use matrix_sdk_base::BaseClient; - /// # use ruma::UserId; + /// # use ruma::user_id; /// # use futures::executor::block_on; - /// # let alice = Box::::try_from("@alice:example.org").unwrap(); + /// # let alice = user_id!("@alice:example.org"); /// # block_on(async { /// # let client = BaseClient::new().await.unwrap(); - /// let devices = client.get_user_devices(&alice).await.unwrap(); + /// let devices = client.get_user_devices(alice).await.unwrap(); /// /// for device in devices.devices() { /// println!("{:?}", device); diff --git a/crates/matrix-sdk-crypto/README.md b/crates/matrix-sdk-crypto/README.md index d157d588b..8ce11a3e8 100644 --- a/crates/matrix-sdk-crypto/README.md +++ b/crates/matrix-sdk-crypto/README.md @@ -22,12 +22,12 @@ use std::{collections::BTreeMap, convert::TryFrom}; use matrix_sdk_crypto::{OlmMachine, OlmError}; use ruma::{ api::client::r0::sync::sync_events::{ToDevice, DeviceLists}, - device_id, UserId, + device_id, user_id, }; #[tokio::main] async fn main() -> Result<(), OlmError> { - let alice = Box::::try_from("@alice:example.org").unwrap(); + let alice = user_id!("@alice:example.org"); let machine = OlmMachine::new(&alice, device_id!("DEVICEID")); let to_device_events = ToDevice::default(); diff --git a/crates/matrix-sdk-crypto/src/machine.rs b/crates/matrix-sdk-crypto/src/machine.rs index 8c3b4bfca..5e895e3ef 100644 --- a/crates/matrix-sdk-crypto/src/machine.rs +++ b/crates/matrix-sdk-crypto/src/machine.rs @@ -449,10 +449,10 @@ impl OlmMachine { /// ``` /// # use std::convert::TryFrom; /// # use matrix_sdk_crypto::OlmMachine; - /// # use ruma::UserId; + /// # use ruma::user_id; /// # use futures::executor::block_on; - /// # let alice = Box::::try_from("@alice:example.org").unwrap(); - /// # let machine = OlmMachine::new(&alice, device_id!("DEVICEID")); + /// # let alice = user_id!("@alice:example.org"); + /// # let machine = OlmMachine::new(alice, device_id!("DEVICEID")); /// # block_on(async { /// if machine.should_upload_keys().await { /// let request = machine diff --git a/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs b/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs index 0f759321c..2e0acc6e0 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs @@ -1379,8 +1379,8 @@ mod test { let content = bob.as_content(); let content = AcceptContent::from(&content); - let sender = Box::::try_from("@malory:example.org").unwrap(); - alice.into_accepted(&sender, &content).expect_err("Didn't cancel on a invalid sender"); + let sender = user_id!("@malory:example.org"); + alice.into_accepted(sender, &content).expect_err("Didn't cancel on a invalid sender"); } #[async_test] diff --git a/crates/matrix-sdk/README.md b/crates/matrix-sdk/README.md index 6afa11495..0d2a98dc0 100644 --- a/crates/matrix-sdk/README.md +++ b/crates/matrix-sdk/README.md @@ -28,13 +28,13 @@ This is demonstrated in the example below. use std::convert::TryFrom; use matrix_sdk::{ Client, config::SyncSettings, Result, - ruma::{UserId, events::room::message::SyncRoomMessageEvent}, + ruma::{user_id, events::room::message::SyncRoomMessageEvent}, }; #[tokio::main] async fn main() -> Result<()> { - let alice = Box::::try_from("@alice:example.org")?; - let client = Client::new_from_user_id(&alice).await?; + let alice = user_id!("@alice:example.org"); + let client = Client::new_from_user_id(alice).await?; // First we need to log in. client.login(alice, "password", None, None).await?; diff --git a/crates/matrix-sdk/src/encryption/identities/devices.rs b/crates/matrix-sdk/src/encryption/identities/devices.rs index a37ee28e4..cf0f3fd02 100644 --- a/crates/matrix-sdk/src/encryption/identities/devices.rs +++ b/crates/matrix-sdk/src/encryption/identities/devices.rs @@ -82,14 +82,14 @@ impl Device { /// /// ```no_run /// # use std::convert::TryFrom; - /// # use matrix_sdk::{Client, ruma::{device_id, UserId}}; + /// # use matrix_sdk::{Client, ruma::{device_id, user_id}}; /// # use url::Url; /// # use futures::executor::block_on; /// # block_on(async { - /// # let alice = Box::::try_from("@alice:example.org")?; + /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com")?; /// # let client = Client::new(homeserver).await?; - /// let device = client.get_device(&alice, device_id!("DEVICEID")).await?; + /// let device = client.get_device(alice, device_id!("DEVICEID")).await?; /// /// if let Some(device) = device { /// let verification = device.request_verification().await?; @@ -127,17 +127,17 @@ impl Device { /// # use matrix_sdk::{ /// # Client, /// # ruma::{ - /// # device_id, UserId, + /// # device_id, user_id, /// # events::key::verification::VerificationMethod, /// # } /// # }; /// # use url::Url; /// # use futures::executor::block_on; /// # block_on(async { - /// # let alice = Box::::try_from("@alice:example.org")?; + /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com")?; /// # let client = Client::new(homeserver).await?; - /// let device = client.get_device(&alice, device_id!("DEVICEID")).await?; + /// let device = client.get_device(alice, device_id!("DEVICEID")).await?; /// /// // We don't want to support showing a QR code, we only support SAS /// // verification @@ -172,14 +172,14 @@ impl Device { /// /// ```no_run /// # use std::convert::TryFrom; - /// # use matrix_sdk::{Client, ruma::{device_id, UserId}}; + /// # use matrix_sdk::{Client, ruma::{device_id, user_id}}; /// # use url::Url; /// # use futures::executor::block_on; /// # block_on(async { - /// # let alice = Box::::try_from("@alice:example.org")?; + /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com")?; /// # let client = Client::new(homeserver).await?; - /// let device = client.get_device(&alice, device_id!("DEVICEID")).await?; + /// let device = client.get_device(alice, device_id!("DEVICEID")).await?; /// /// if let Some(device) = device { /// let verification = device.start_verification().await?; @@ -233,17 +233,17 @@ impl Device { /// # use matrix_sdk::{ /// # Client, /// # ruma::{ - /// # device_id, UserId, + /// # device_id, user_id, /// # events::key::verification::VerificationMethod, /// # } /// # }; /// # use url::Url; /// # use futures::executor::block_on; /// # block_on(async { - /// # let alice = Box::::try_from("@alice:example.org")?; + /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com")?; /// # let client = Client::new(homeserver).await?; - /// let device = client.get_device(&alice, device_id!("DEVICEID")).await?; + /// let device = client.get_device(alice, device_id!("DEVICEID")).await?; /// /// if let Some(device) = device { /// device.verify().await?; @@ -348,17 +348,17 @@ impl Device { /// # use matrix_sdk::{ /// # Client, /// # ruma::{ - /// # device_id, UserId, + /// # device_id, user_id, /// # events::key::verification::VerificationMethod, /// # } /// # }; /// # use url::Url; /// # use futures::executor::block_on; /// # block_on(async { - /// # let alice = Box::::try_from("@alice:example.org")?; + /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com")?; /// # let client = Client::new(homeserver).await?; - /// let device = client.get_device(&alice, device_id!("DEVICEID")).await?; + /// let device = client.get_device(alice, device_id!("DEVICEID")).await?; /// /// if let Some(device) = device { /// if device.verified() { diff --git a/crates/matrix-sdk/src/encryption/identities/mod.rs b/crates/matrix-sdk/src/encryption/identities/mod.rs index 0cb7b8a22..e0beea868 100644 --- a/crates/matrix-sdk/src/encryption/identities/mod.rs +++ b/crates/matrix-sdk/src/encryption/identities/mod.rs @@ -36,14 +36,14 @@ //! //! ```no_run //! # use std::convert::TryFrom; -//! # use matrix_sdk::{Client, ruma::{device_id, UserId}}; +//! # use matrix_sdk::{Client, ruma::{device_id, user_id}}; //! # use url::Url; //! # use futures::executor::block_on; -//! # let alice = Box::::try_from("@alice:example.org").unwrap(); +//! # let alice = user_id!("@alice:example.org"); //! # let homeserver = Url::parse("http://example.com").unwrap(); //! # block_on(async { //! # let client = Client::new(homeserver).await.unwrap(); -//! let device = client.get_device(&alice, device_id!("DEVICEID")).await?; +//! let device = client.get_device(alice, device_id!("DEVICEID")).await?; //! //! if let Some(device) = device { //! // Let's request the device to be verified. @@ -62,14 +62,14 @@ //! //! ```no_run //! # use std::convert::TryFrom; -//! # use matrix_sdk::{Client, ruma::UserId}; +//! # use matrix_sdk::{Client, ruma::user_id}; //! # use url::Url; //! # use futures::executor::block_on; -//! # let alice = Box::::try_from("@alice:example.org").unwrap(); +//! # let alice = user_id!("@alice:example.org"); //! # let homeserver = Url::parse("http://example.com").unwrap(); //! # block_on(async { //! # let client = Client::new(homeserver).await.unwrap(); -//! let user = client.get_user_identity(&alice).await?; +//! let user = client.get_user_identity(alice).await?; //! //! if let Some(user) = user { //! // Let's request the user to be verified. diff --git a/crates/matrix-sdk/src/encryption/identities/users.rs b/crates/matrix-sdk/src/encryption/identities/users.rs index 6bdc6fd33..b7424f6ca 100644 --- a/crates/matrix-sdk/src/encryption/identities/users.rs +++ b/crates/matrix-sdk/src/encryption/identities/users.rs @@ -91,13 +91,13 @@ impl UserIdentity { /// /// ```no_run /// # use std::convert::TryFrom; - /// # use matrix_sdk::{Client, ruma::UserId}; + /// # use matrix_sdk::{Client, ruma::user_id}; /// # use url::Url; - /// # let alice = Box::::try_from("@alice:example.org").unwrap(); + /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # futures::executor::block_on(async { /// # let client = Client::new(homeserver).await.unwrap(); - /// let user = client.get_user_identity(&alice).await?; + /// let user = client.get_user_identity(alice).await?; /// /// if let Some(user) = user { /// println!("This user identity belongs to {}", user.user_id().as_str()); @@ -143,13 +143,13 @@ impl UserIdentity { /// /// ```no_run /// # use std::convert::TryFrom; - /// # use matrix_sdk::{Client, ruma::UserId}; + /// # use matrix_sdk::{Client, ruma::user_id}; /// # use url::Url; - /// # let alice = Box::::try_from("@alice:example.org").unwrap(); + /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # futures::executor::block_on(async { /// # let client = Client::new(homeserver).await.unwrap(); - /// let user = client.get_user_identity(&alice).await?; + /// let user = client.get_user_identity(alice).await?; /// /// if let Some(user) = user { /// let verification = user.request_verification().await?; @@ -198,17 +198,17 @@ impl UserIdentity { /// # use matrix_sdk::{ /// # Client, /// # ruma::{ - /// # UserId, + /// # user_id, /// # events::key::verification::VerificationMethod, /// # } /// # }; /// # use url::Url; /// # use futures::executor::block_on; - /// # let alice = Box::::try_from("@alice:example.org").unwrap(); + /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # block_on(async { /// # let client = Client::new(homeserver).await.unwrap(); - /// let user = client.get_user_identity(&alice).await?; + /// let user = client.get_user_identity(alice).await?; /// /// // We don't want to support showing a QR code, we only support SAS /// // verification @@ -277,17 +277,17 @@ impl UserIdentity { /// # use matrix_sdk::{ /// # Client, /// # ruma::{ - /// # UserId, + /// # user_id, /// # events::key::verification::VerificationMethod, /// # } /// # }; /// # use url::Url; /// # use futures::executor::block_on; - /// # let alice = Box::::try_from("@alice:example.org").unwrap(); + /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # block_on(async { /// # let client = Client::new(homeserver).await.unwrap(); - /// let user = client.get_user_identity(&alice).await?; + /// let user = client.get_user_identity(alice).await?; /// /// if let Some(user) = user { /// user.verify().await?; @@ -320,17 +320,17 @@ impl UserIdentity { /// # use matrix_sdk::{ /// # Client, /// # ruma::{ - /// # UserId, + /// # user_id, /// # events::key::verification::VerificationMethod, /// # } /// # }; /// # use url::Url; /// # use futures::executor::block_on; - /// # let alice = Box::::try_from("@alice:example.org").unwrap(); + /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # block_on(async { /// # let client = Client::new(homeserver).await.unwrap(); - /// let user = client.get_user_identity(&alice).await?; + /// let user = client.get_user_identity(alice).await?; /// /// if let Some(user) = user { /// if user.verified() { @@ -360,17 +360,17 @@ impl UserIdentity { /// # use matrix_sdk::{ /// # Client, /// # ruma::{ - /// # UserId, + /// # user_id, /// # events::key::verification::VerificationMethod, /// # } /// # }; /// # use url::Url; /// # use futures::executor::block_on; - /// # let alice = Box::::try_from("@alice:example.org").unwrap(); + /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # block_on(async { /// # let client = Client::new(homeserver).await.unwrap(); - /// let user = client.get_user_identity(&alice).await?; + /// let user = client.get_user_identity(alice).await?; /// /// if let Some(user) = user { /// // Let's verify the user after we confirm that the master key diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index c08abea75..deafd5ef9 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -367,14 +367,14 @@ impl Client { /// /// ```no_run /// # use std::convert::TryFrom; - /// # use matrix_sdk::{Client, ruma::{device_id, UserId}}; + /// # use matrix_sdk::{Client, ruma::{device_id, user_id}}; /// # use url::Url; /// # use futures::executor::block_on; /// # block_on(async { - /// # let alice = Box::::try_from("@alice:example.org")?; + /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com")?; /// # let client = Client::new(homeserver).await?; - /// if let Some(device) = client.get_device(&alice, device_id!("DEVICEID")).await? { + /// if let Some(device) = client.get_device(alice, device_id!("DEVICEID")).await? { /// println!("{:?}", device.verified()); /// /// if !device.verified() { @@ -407,14 +407,14 @@ impl Client { /// /// ```no_run /// # use std::convert::TryFrom; - /// # use matrix_sdk::{Client, ruma::UserId}; + /// # use matrix_sdk::{Client, ruma::user_id}; /// # use url::Url; /// # use futures::executor::block_on; /// # block_on(async { - /// # let alice = Box::::try_from("@alice:example.org")?; + /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com")?; /// # let client = Client::new(homeserver).await?; - /// let devices = client.get_user_devices(&alice).await?; + /// let devices = client.get_user_devices(alice).await?; /// /// for device in devices.devices() { /// println!("{:?}", device); @@ -446,14 +446,14 @@ impl Client { /// /// ```no_run /// # use std::convert::TryFrom; - /// # use matrix_sdk::{Client, ruma::UserId}; + /// # use matrix_sdk::{Client, ruma::user_id}; /// # use url::Url; /// # use futures::executor::block_on; /// # block_on(async { - /// # let alice = Box::::try_from("@alice:example.org")?; + /// # let alice = user_id!("@alice:example.org"); /// # let homeserver = Url::parse("http://example.com")?; /// # let client = Client::new(homeserver).await?; - /// let user = client.get_user_identity(&alice).await?; + /// let user = client.get_user_identity(alice).await?; /// /// if let Some(user) = user { /// println!("{:?}", user.verified()); From a7dcd26588c35227e74a843c81e92cdbf37ec8c3 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sat, 12 Feb 2022 04:13:04 +0100 Subject: [PATCH 03/22] Remove unused variable from test --- crates/matrix-sdk/src/encryption/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index deafd5ef9..90c05e667 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -499,14 +499,13 @@ impl Client { /// ```no_run /// # use std::{convert::TryFrom, collections::BTreeMap}; /// # use matrix_sdk::{ - /// # ruma::{api::client::r0::uiaa, assign, UserId}, + /// # ruma::{api::client::r0::uiaa, assign}, /// # Client, /// # }; /// # use url::Url; /// # use futures::executor::block_on; /// # use serde_json::json; /// # block_on(async { - /// # let user_id = Box::::try_from("@alice:example.org")?; /// # let homeserver = Url::parse("http://example.com")?; /// # let client = Client::new(homeserver).await?; /// if let Err(e) = client.bootstrap_cross_signing(None).await { From c3d9c73d00a28464d337739cfe2473fbfc776228 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sat, 12 Feb 2022 04:20:37 +0100 Subject: [PATCH 04/22] Make identifier parsing easier to read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … by using `Id::parse` instead of `Box::::try_from`. --- .../examples/appservice_autojoin.rs | 4 ++-- .../examples/state_inspector.rs | 24 +++++++------------ crates/matrix-sdk-base/src/rooms/normal.rs | 7 ++---- .../src/store/indexeddb_store.rs | 4 ++-- .../matrix-sdk-base/src/store/sled_store.rs | 8 +++---- .../src/olm/group_sessions/inbound.rs | 2 +- .../matrix-sdk-crypto/src/store/indexeddb.rs | 2 +- crates/matrix-sdk-crypto/src/store/sled.rs | 2 +- crates/matrix-sdk/examples/get_profiles.rs | 4 ++-- crates/matrix-sdk/src/client.rs | 14 ++++------- 10 files changed, 27 insertions(+), 44 deletions(-) diff --git a/crates/matrix-sdk-appservice/examples/appservice_autojoin.rs b/crates/matrix-sdk-appservice/examples/appservice_autojoin.rs index 49946b1d3..110652cc6 100644 --- a/crates/matrix-sdk-appservice/examples/appservice_autojoin.rs +++ b/crates/matrix-sdk-appservice/examples/appservice_autojoin.rs @@ -1,4 +1,4 @@ -use std::{convert::TryFrom, env}; +use std::env; use matrix_sdk_appservice::{ matrix_sdk::{ @@ -21,7 +21,7 @@ pub async fn handle_room_member( if !appservice.user_id_is_in_namespace(&event.state_key)? { trace!("not an appservice user: {}", event.state_key); } else if let MembershipState::Invite = event.content.membership { - let user_id = Box::::try_from(event.state_key.as_str())?; + let user_id = UserId::parse(event.state_key.as_str())?; appservice.register_virtual_user(user_id.localpart()).await?; let client = appservice.virtual_user_client(user_id.localpart()).await?; diff --git a/crates/matrix-sdk-base/examples/state_inspector.rs b/crates/matrix-sdk-base/examples/state_inspector.rs index c695d61c1..c4c83d046 100644 --- a/crates/matrix-sdk-base/examples/state_inspector.rs +++ b/crates/matrix-sdk-base/examples/state_inspector.rs @@ -222,24 +222,24 @@ impl Inspector { async fn run(&self, matches: ArgMatches) { match matches.subcommand() { Some(("get-profiles", args)) => { - let room_id = Box::::try_from(args.value_of("room-id").unwrap()).unwrap(); + let room_id = RoomId::parse(args.value_of("room-id").unwrap()).unwrap(); self.get_profiles(room_id).await; } Some(("get-members", args)) => { - let room_id = Box::::try_from(args.value_of("room-id").unwrap()).unwrap(); + let room_id = RoomId::parse(args.value_of("room-id").unwrap()).unwrap(); self.get_members(room_id).await; } Some(("list-rooms", _)) => self.list_rooms().await, Some(("get-display-names", args)) => { - let room_id = Box::::try_from(args.value_of("room-id").unwrap()).unwrap(); + let room_id = RoomId::parse(args.value_of("room-id").unwrap()).unwrap(); let display_name = args.value_of("display-name").unwrap().to_string(); self.get_display_name_owners(room_id, display_name).await; } Some(("get-state", args)) => { - let room_id = Box::::try_from(args.value_of("room-id").unwrap()).unwrap(); + let room_id = RoomId::parse(args.value_of("room-id").unwrap()).unwrap(); let event_type = EventType::try_from(args.value_of("event-type").unwrap()).unwrap(); self.get_state(room_id, event_type).await; } @@ -285,27 +285,19 @@ impl Inspector { vec![ Argparse::new("list-rooms"), Argparse::new("get-members").arg(Arg::new("room-id").required(true).validator(|r| { - Box::::try_from(r) - .map(|_| ()) - .map_err(|_| "Invalid room id given".to_owned()) + RoomId::parse(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned()) })), Argparse::new("get-profiles").arg(Arg::new("room-id").required(true).validator(|r| { - Box::::try_from(r) - .map(|_| ()) - .map_err(|_| "Invalid room id given".to_owned()) + RoomId::parse(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned()) })), Argparse::new("get-display-names") .arg(Arg::new("room-id").required(true).validator(|r| { - Box::::try_from(r) - .map(|_| ()) - .map_err(|_| "Invalid room id given".to_owned()) + RoomId::parse(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned()) })) .arg(Arg::new("display-name").required(true)), Argparse::new("get-state") .arg(Arg::new("room-id").required(true).validator(|r| { - Box::::try_from(r) - .map(|_| ()) - .map_err(|_| "Invalid room id given".to_owned()) + RoomId::parse(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned()) })) .arg(Arg::new("event-type").required(true).validator(|e| { EventType::try_from(e).map(|_| ()).map_err(|_| "Invalid event type".to_string()) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 42f3a83a6..afb07cd89 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -12,10 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{ - convert::TryFrom, - sync::{Arc, RwLock as SyncRwLock}, -}; +use std::sync::{Arc, RwLock as SyncRwLock}; use futures_util::stream::{self, StreamExt}; use ruma::{ @@ -346,7 +343,7 @@ impl Room { let members: Vec<_> = stream::iter(summary.heroes.iter().filter(|u| !is_own_user_id(u))) .filter_map(|u| async move { - let user_id = Box::::try_from(u.as_str()).ok()?; + let user_id = UserId::parse(u.as_str()).ok()?; self.get_member(&user_id).await.transpose() }) .collect() diff --git a/crates/matrix-sdk-base/src/store/indexeddb_store.rs b/crates/matrix-sdk-base/src/store/indexeddb_store.rs index fe54a743b..07e52c766 100644 --- a/crates/matrix-sdk-base/src/store/indexeddb_store.rs +++ b/crates/matrix-sdk-base/src/store/indexeddb_store.rs @@ -556,7 +556,7 @@ impl IndexeddbStore { .await? .iter() .filter_map(|key| match key.as_string() { - Some(k) => Box::::try_from(&k[skip..]).ok(), + Some(k) => UserId::parse(&k[skip..]).ok(), _ => None, }) .collect::>()) @@ -701,7 +701,7 @@ impl IndexeddbStore { let res = store.get(&k)?.await?.ok_or(StoreError::Codec(format!("no data at {:?}", k)))?; let u = if let Some(k_str) = k.as_string() { - Box::::try_from(&k_str[prefix_len..]) + UserId::parse(&k_str[prefix_len..]) .map_err(|e| StoreError::Codec(format!("{:?}", e)))? } else { return Err(StoreError::Codec(format!("{:?}", k))); diff --git a/crates/matrix-sdk-base/src/store/sled_store.rs b/crates/matrix-sdk-base/src/store/sled_store.rs index f274d9484..96c570e45 100644 --- a/crates/matrix-sdk-base/src/store/sled_store.rs +++ b/crates/matrix-sdk-base/src/store/sled_store.rs @@ -14,7 +14,7 @@ use std::{ collections::BTreeSet, - convert::{TryFrom, TryInto}, + convert::TryInto, path::{Path, PathBuf}, sync::Arc, time::Instant, @@ -660,7 +660,7 @@ impl SledStore { let user_id = iter.next().expect("User ids weren't properly encoded"); - Ok(Box::::try_from(String::from_utf8_lossy(user_id).to_string())?) + Ok(UserId::parse(String::from_utf8_lossy(user_id).to_string())?) }; let members = self.members.clone(); @@ -679,7 +679,7 @@ impl SledStore { let key = room_id.encode(); spawn_blocking(move || { stream::iter(db.invited_user_ids.scan_prefix(key).map(|u| { - Box::::try_from(String::from_utf8_lossy(&u?.1).to_string()) + UserId::parse(String::from_utf8_lossy(&u?.1).to_string()) .map_err(StoreError::Identifier) })) }) @@ -695,7 +695,7 @@ impl SledStore { let key = room_id.encode(); spawn_blocking(move || { stream::iter(db.joined_user_ids.scan_prefix(key).map(|u| { - Box::::try_from(String::from_utf8_lossy(&u?.1).to_string()) + UserId::parse(String::from_utf8_lossy(&u?.1).to_string()) .map_err(StoreError::Identifier) })) }) diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs index 69542a937..80bb62874 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs @@ -366,7 +366,7 @@ impl InboundGroupSession { let room_id = decrypted_object .get("room_id") - .and_then(|r| r.as_str().and_then(|r| Box::::try_from(r).ok())); + .and_then(|r| r.as_str().and_then(|r| RoomId::parse(r).ok())); // Check that we have a room id and that the event wasn't forwarded from // another room. diff --git a/crates/matrix-sdk-crypto/src/store/indexeddb.rs b/crates/matrix-sdk-crypto/src/store/indexeddb.rs index b98d2cd3f..ef6333a60 100644 --- a/crates/matrix-sdk-crypto/src/store/indexeddb.rs +++ b/crates/matrix-sdk-crypto/src/store/indexeddb.rs @@ -386,7 +386,7 @@ impl IndexeddbStore { Some(Ok(false)) => false, _ => true, }; - let user = match user_id.as_string().map(|u| Box::::try_from(u)) { + let user = match user_id.as_string().map(|u| UserId::parse(u)) { Some(Ok(user)) => user, _ => continue, }; diff --git a/crates/matrix-sdk-crypto/src/store/sled.rs b/crates/matrix-sdk-crypto/src/store/sled.rs index d38295bbe..b7d3f2863 100644 --- a/crates/matrix-sdk-crypto/src/store/sled.rs +++ b/crates/matrix-sdk-crypto/src/store/sled.rs @@ -443,7 +443,7 @@ impl SledStore { async fn load_tracked_users(&self) -> Result<()> { for value in &self.tracked_users { let (user, dirty) = value?; - let user = Box::::try_from(String::from_utf8_lossy(&user).to_string())?; + let user = UserId::parse(String::from_utf8_lossy(&user).to_string())?; let dirty = dirty.get(0).map(|d| *d == 1).unwrap_or(true); self.tracked_users_cache.insert(user.to_owned()); diff --git a/crates/matrix-sdk/examples/get_profiles.rs b/crates/matrix-sdk/examples/get_profiles.rs index 4289acf37..030ff6444 100644 --- a/crates/matrix-sdk/examples/get_profiles.rs +++ b/crates/matrix-sdk/examples/get_profiles.rs @@ -1,4 +1,4 @@ -use std::{convert::TryFrom, env, process::exit}; +use std::{env, process::exit}; use matrix_sdk::{ ruma::{api::client::r0::profile, MxcUri, UserId}, @@ -62,7 +62,7 @@ async fn main() -> Result<(), matrix_sdk::Error> { let client = login(homeserver_url, &username, &password).await?; - let user_id = Box::::try_from(username).expect("Couldn't parse the MXID"); + let user_id = UserId::parse(username).expect("Couldn't parse the MXID"); let profile = get_profile(client, &user_id).await?; println!("{:#?}", profile); Ok(()) diff --git a/crates/matrix-sdk/src/client.rs b/crates/matrix-sdk/src/client.rs index c529dc002..5493e9e9a 100644 --- a/crates/matrix-sdk/src/client.rs +++ b/crates/matrix-sdk/src/client.rs @@ -231,7 +231,7 @@ impl Client { /// use matrix_sdk::{Client, ruma::UserId}; /// /// // First let's try to construct an user id, presumably from user input. - /// let alice = Box::::try_from("@alice:example.org")?; + /// let alice = UserId::parse("@alice:example.org")?; /// /// // Now let's try to discover the homeserver and create a client object. /// let client = Client::new_from_user_id(&alice).await?; @@ -2367,13 +2367,7 @@ pub(crate) mod test { #[cfg(target_arch = "wasm32")] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - use std::{ - collections::BTreeMap, - convert::{TryFrom, TryInto}, - io::Cursor, - str::FromStr, - time::Duration, - }; + use std::{collections::BTreeMap, convert::TryInto, io::Cursor, str::FromStr, time::Duration}; use matrix_sdk_base::media::{MediaFormat, MediaRequest, MediaThumbnailSize, MediaType}; use matrix_sdk_test::{test_json, EventBuilder, EventsJson}; @@ -2447,7 +2441,7 @@ pub(crate) mod test { async fn successful_discovery() { let server_url = mockito::server_url(); let domain = server_url.strip_prefix("http://").unwrap(); - let alice = Box::::try_from("@alice:".to_string() + domain).unwrap(); + let alice = UserId::parse("@alice:".to_string() + domain).unwrap(); let _m_well_known = mock("GET", "/.well-known/matrix/client") .with_status(200) @@ -2469,7 +2463,7 @@ pub(crate) mod test { async fn discovery_broken_server() { let server_url = mockito::server_url(); let domain = server_url.strip_prefix("http://").unwrap(); - let alice = Box::::try_from("@alice:".to_string() + domain).unwrap(); + let alice = UserId::parse("@alice:".to_string() + domain).unwrap(); let _m = mock("GET", "/.well-known/matrix/client") .with_status(200) From f220f2e743a9ecfdc053a6ee12bbf54bd3211a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 15 Feb 2022 12:32:53 +0100 Subject: [PATCH 05/22] fix(crypto): Remove an unused lifetime --- crates/matrix-sdk-crypto/src/olm/signing/pk_signing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-crypto/src/olm/signing/pk_signing.rs b/crates/matrix-sdk-crypto/src/olm/signing/pk_signing.rs index f66750b33..b3f108245 100644 --- a/crates/matrix-sdk-crypto/src/olm/signing/pk_signing.rs +++ b/crates/matrix-sdk-crypto/src/olm/signing/pk_signing.rs @@ -159,7 +159,7 @@ impl MasterSigning { self.inner.sign(message).await.0 } - pub async fn sign_subkey<'a>(&self, subkey: &mut CrossSigningKey) { + pub async fn sign_subkey(&self, subkey: &mut CrossSigningKey) { let subkey_without_signatures = json!({ "user_id": subkey.user_id.clone(), "keys": subkey.keys.clone(), From c67d5afaf48ccb8cd1fd414c8feb5649b729dd61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 15 Feb 2022 17:42:12 +0100 Subject: [PATCH 06/22] feat(sdk): Add method to get homeserver capabilities --- crates/matrix-sdk/src/client.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/matrix-sdk/src/client.rs b/crates/matrix-sdk/src/client.rs index c529dc002..07fa7891c 100644 --- a/crates/matrix-sdk/src/client.rs +++ b/crates/matrix-sdk/src/client.rs @@ -43,6 +43,7 @@ use ruma::{ client::{ r0::{ account::{register, whoami}, + capabilities::{get_capabilities, Capabilities}, device::{delete_devices, get_devices}, directory::{get_public_rooms, get_public_rooms_filtered}, filter::{create_filter::Request as FilterUploadRequest, FilterDefinition}, @@ -346,6 +347,33 @@ impl Client { .await } + /// Get the capabilities of the homeserver. + /// + /// This method should be used to check what features are supported by the + /// homeserver. + /// + /// # Example + /// ```no_run + /// # use futures::executor::block_on; + /// # use matrix_sdk::Client; + /// # use url::Url; + /// # block_on(async { + /// # let homeserver = Url::parse("http://example.com")?; + /// let client = Client::new(homeserver).await?; + /// + /// let capabilities = client.get_capabilities().await?; + /// + /// if capabilities.change_password.enabled { + /// // Change password + /// } + /// + /// # Result::<_, anyhow::Error>::Ok(()) }); + /// ``` + pub async fn get_capabilities(&self) -> HttpResult { + let res = self.send(get_capabilities::Request::new(), None).await?; + Ok(res.capabilities) + } + /// Process a [transaction] received from the homeserver /// /// # Arguments From 467005b603f2ee82a58fb1efd09d7212fa4a7329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 9 Feb 2022 18:03:40 +0100 Subject: [PATCH 07/22] feat(sdk): Move account-related methods to Account struct Methods moved from Client to Account: - display_name renamed to get_display_name - set_display_name - avatar_url renamed to get_avatar_url - set_avatar_url - avatar renamed to get_avatar - upload_avatar --- crates/matrix-sdk/src/account.rs | 181 +++++++++++++++++++++++++++++++ crates/matrix-sdk/src/client.rs | 156 +------------------------- crates/matrix-sdk/src/lib.rs | 2 + 3 files changed, 187 insertions(+), 152 deletions(-) create mode 100644 crates/matrix-sdk/src/account.rs diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs new file mode 100644 index 000000000..785bc8bdf --- /dev/null +++ b/crates/matrix-sdk/src/account.rs @@ -0,0 +1,181 @@ +use std::io::Read; + +use matrix_sdk_base::media::{MediaFormat, MediaRequest, MediaType}; +use mime::Mime; +use ruma::{ + api::client::r0::profile::{ + get_avatar_url, get_display_name, set_avatar_url, set_display_name, + }, + MxcUri, +}; + +use crate::{config::RequestConfig, Client, Error, Result}; + +/// A high-level API to manage the client owner's account. +/// +/// All the methods on this struct send a request to the homeserver. +#[derive(Debug, Clone)] +pub struct Account { + /// The underlying HTTP client. + client: Client, +} + +impl Account { + pub(crate) fn new(client: Client) -> Self { + Self { client } + } + + /// Get the display name of the account. + /// + /// # Example + /// ```no_run + /// # use futures::executor::block_on; + /// # use matrix_sdk::Client; + /// # use url::Url; + /// # let homeserver = Url::parse("http://example.com").unwrap(); + /// # block_on(async { + /// let user = "example"; + /// let client = Client::new(homeserver).await.unwrap(); + /// client.login(user, "password", None, None).await.unwrap(); + /// + /// if let Some(name) = client.account().get_display_name().await.unwrap() { + /// println!("Logged in as user '{}' with display name '{}'", user, name); + /// } + /// # }) + /// ``` + pub async fn get_display_name(&self) -> Result> { + let user_id = self.client.user_id().await.ok_or(Error::AuthenticationRequired)?; + let request = get_display_name::Request::new(&user_id); + let response = self.client.send(request, None).await?; + Ok(response.displayname) + } + + /// Set the display name of the account. + /// + /// # Example + /// ```no_run + /// # use futures::executor::block_on; + /// # use matrix_sdk::Client; + /// # use url::Url; + /// # let homeserver = Url::parse("http://example.com").unwrap(); + /// # block_on(async { + /// let user = "example"; + /// let client = Client::new(homeserver).await.unwrap(); + /// client.login(user, "password", None, None).await.unwrap(); + /// + /// client.account().set_display_name(Some("Alice")).await.expect("Failed setting display name"); + /// # }) + /// ``` + pub async fn set_display_name(&self, name: Option<&str>) -> Result<()> { + let user_id = self.client.user_id().await.ok_or(Error::AuthenticationRequired)?; + let request = set_display_name::Request::new(&user_id, name); + self.client.send(request, None).await?; + Ok(()) + } + + /// Get the MXC avatar url of the account, if set. + /// + /// # Example + /// ```no_run + /// # use futures::executor::block_on; + /// # use matrix_sdk::Client; + /// # use url::Url; + /// # let homeserver = Url::parse("http://example.com").unwrap(); + /// # block_on(async { + /// # let user = "example"; + /// let client = Client::new(homeserver).await.unwrap(); + /// client.login(user, "password", None, None).await.unwrap(); + /// + /// if let Some(url) = client.account().get_avatar_url().await.unwrap() { + /// println!("Your avatar's mxc url is {}", url); + /// } + /// # }) + /// ``` + pub async fn get_avatar_url(&self) -> Result>> { + let user_id = self.client.user_id().await.ok_or(Error::AuthenticationRequired)?; + let request = get_avatar_url::Request::new(&user_id); + + let config = Some(RequestConfig::new().force_auth()); + + let response = self.client.send(request, config).await?; + Ok(response.avatar_url) + } + + /// Set the MXC avatar url of the account. + /// + /// The avatar is unset if `url` is `None`. + pub async fn set_avatar_url(&self, url: Option<&MxcUri>) -> Result<()> { + let user_id = self.client.user_id().await.ok_or(Error::AuthenticationRequired)?; + let request = set_avatar_url::Request::new(&user_id, url); + self.client.send(request, None).await?; + Ok(()) + } + + /// Get the account's avatar, if set. + /// + /// Returns the avatar. + /// + /// If a thumbnail is requested no guarantee on the size of the image is + /// given. + /// + /// # Arguments + /// + /// * `format` - The desired format of the avatar. + /// + /// # Example + /// ```no_run + /// # use futures::executor::block_on; + /// # use matrix_sdk::Client; + /// # use matrix_sdk::ruma::room_id; + /// # use matrix_sdk::media::MediaFormat; + /// # use url::Url; + /// # let homeserver = Url::parse("http://example.com").unwrap(); + /// # block_on(async { + /// # let user = "example"; + /// let client = Client::new(homeserver).await.unwrap(); + /// client.login(user, "password", None, None).await.unwrap(); + /// + /// if let Some(avatar) = client.account().get_avatar(MediaFormat::File).await.unwrap() { + /// std::fs::write("avatar.png", avatar); + /// } + /// # }) + /// ``` + pub async fn get_avatar(&self, format: MediaFormat) -> Result>> { + if let Some(url) = self.get_avatar_url().await? { + let request = MediaRequest { media_type: MediaType::Uri(url), format }; + Ok(Some(self.client.get_media_content(&request, true).await?)) + } else { + Ok(None) + } + } + + /// Upload and set the account's avatar. + /// + /// This will upload the data produced by the reader to the homeserver's + /// content repository, and set the user's avatar to the MXC url for the + /// uploaded file. + /// + /// This is a convenience method for calling [`Client::upload()`], + /// followed by [`set_avatar_url()`](#method.set_avatar_url). + /// + /// # Example + /// ```no_run + /// # use std::{path::Path, fs::File, io::Read}; + /// # use futures::executor::block_on; + /// # use matrix_sdk::Client; + /// # use url::Url; + /// # block_on(async { + /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); + /// # let client = Client::new(homeserver).await.unwrap(); + /// let path = Path::new("/home/example/selfie.jpg"); + /// let mut image = File::open(&path).unwrap(); + /// + /// client.account().upload_avatar(&mime::IMAGE_JPEG, &mut image).await.expect("Can't set avatar"); + /// # }) + /// ``` + pub async fn upload_avatar(&self, content_type: &Mime, reader: &mut R) -> Result<()> { + let upload_response = self.client.upload(content_type, reader).await?; + self.set_avatar_url(Some(&upload_response.content_uri)).await?; + Ok(()) + } +} diff --git a/crates/matrix-sdk/src/client.rs b/crates/matrix-sdk/src/client.rs index 5493e9e9a..f7e6dcdb8 100644 --- a/crates/matrix-sdk/src/client.rs +++ b/crates/matrix-sdk/src/client.rs @@ -48,7 +48,6 @@ use ruma::{ filter::{create_filter::Request as FilterUploadRequest, FilterDefinition}, media::{create_content, get_content, get_content_thumbnail}, membership::{join_room_by_id, join_room_by_id_or_alias}, - profile::{get_avatar_url, get_display_name, set_avatar_url, set_display_name}, push::get_notifications::Notification, room::create_room, session::{get_login_types, login, sso_login}, @@ -73,7 +72,7 @@ use crate::{ error::{HttpError, HttpResult}, event_handler::{EventHandler, EventHandlerData, EventHandlerResult, EventKind, SyncEvent}, http_client::{client_with_config, HttpClient}, - room, Error, Result, + room, Account, Error, Result, }; /// A conservative upload speed of 1Mbps @@ -398,161 +397,14 @@ impl Client { self.inner.base_client.session().read().await.clone() } - /// Fetches the display name of the owner of the client. - /// - /// # Example - /// ```no_run - /// # use futures::executor::block_on; - /// # use matrix_sdk::Client; - /// # use url::Url; - /// # let homeserver = Url::parse("http://example.com").unwrap(); - /// # block_on(async { - /// let user = "example"; - /// let client = Client::new(homeserver).await.unwrap(); - /// client.login(user, "password", None, None).await.unwrap(); - /// - /// if let Some(name) = client.display_name().await.unwrap() { - /// println!("Logged in as user '{}' with display name '{}'", user, name); - /// } - /// # }) - /// ``` - pub async fn display_name(&self) -> Result> { - let user_id = self.user_id().await.ok_or(Error::AuthenticationRequired)?; - let request = get_display_name::Request::new(&user_id); - let response = self.send(request, None).await?; - Ok(response.displayname) - } - - /// Sets the display name of the owner of the client. - /// - /// # Example - /// ```no_run - /// # use futures::executor::block_on; - /// # use matrix_sdk::Client; - /// # use url::Url; - /// # let homeserver = Url::parse("http://example.com").unwrap(); - /// # block_on(async { - /// let user = "example"; - /// let client = Client::new(homeserver).await.unwrap(); - /// client.login(user, "password", None, None).await.unwrap(); - /// - /// client.set_display_name(Some("Alice")).await.expect("Failed setting display name"); - /// # }) - /// ``` - pub async fn set_display_name(&self, name: Option<&str>) -> Result<()> { - let user_id = self.user_id().await.ok_or(Error::AuthenticationRequired)?; - let request = set_display_name::Request::new(&user_id, name); - self.send(request, None).await?; - Ok(()) - } - - /// Gets the mxc avatar url of the owner of the client, if set. - /// - /// # Example - /// ```no_run - /// # use futures::executor::block_on; - /// # use matrix_sdk::Client; - /// # use url::Url; - /// # let homeserver = Url::parse("http://example.com").unwrap(); - /// # block_on(async { - /// # let user = "example"; - /// let client = Client::new(homeserver).await.unwrap(); - /// client.login(user, "password", None, None).await.unwrap(); - /// - /// if let Some(url) = client.avatar_url().await.unwrap() { - /// println!("Your avatar's mxc url is {}", url); - /// } - /// # }) - /// ``` - pub async fn avatar_url(&self) -> Result>> { - let user_id = self.user_id().await.ok_or(Error::AuthenticationRequired)?; - let request = get_avatar_url::Request::new(&user_id); - - let config = Some(RequestConfig::new().force_auth()); - - let response = self.send(request, config).await?; - Ok(response.avatar_url) - } - - /// Gets the avatar of the owner of the client, if set. - /// - /// Returns the avatar. - /// If a thumbnail is requested no guarantee on the size of the image is - /// given. - /// - /// # Arguments - /// - /// * `format` - The desired format of the avatar. - /// - /// # Example - /// ```no_run - /// # use futures::executor::block_on; - /// # use matrix_sdk::Client; - /// # use matrix_sdk::ruma::room_id; - /// # use matrix_sdk::media::MediaFormat; - /// # use url::Url; - /// # let homeserver = Url::parse("http://example.com").unwrap(); - /// # block_on(async { - /// # let user = "example"; - /// let client = Client::new(homeserver).await.unwrap(); - /// client.login(user, "password", None, None).await.unwrap(); - /// - /// if let Some(avatar) = client.avatar(MediaFormat::File).await.unwrap() { - /// std::fs::write("avatar.png", avatar); - /// } - /// # }) - /// ``` - pub async fn avatar(&self, format: MediaFormat) -> Result>> { - if let Some(url) = self.avatar_url().await? { - let request = MediaRequest { media_type: MediaType::Uri(url), format }; - Ok(Some(self.get_media_content(&request, true).await?)) - } else { - Ok(None) - } - } - /// Get a reference to the store. pub fn store(&self) -> &Store { self.inner.base_client.store() } - /// Sets the mxc avatar url of the client's owner. The avatar gets unset if - /// `url` is `None`. - pub async fn set_avatar_url(&self, url: Option<&MxcUri>) -> Result<()> { - let user_id = self.user_id().await.ok_or(Error::AuthenticationRequired)?; - let request = set_avatar_url::Request::new(&user_id, url); - self.send(request, None).await?; - Ok(()) - } - - /// Upload and set the owning client's avatar. - /// - /// The will upload the data produced by the reader to the homeserver's - /// content repository, and set the user's avatar to the mxc url for the - /// uploaded file. - /// - /// This is a convenience method for calling [`upload()`](#method.upload), - /// followed by [`set_avatar_url()`](#method.set_avatar_url). - /// - /// # Example - /// ```no_run - /// # use std::{path::Path, fs::File, io::Read}; - /// # use futures::executor::block_on; - /// # use matrix_sdk::Client; - /// # use url::Url; - /// # block_on(async { - /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); - /// # let client = Client::new(homeserver).await.unwrap(); - /// let path = Path::new("/home/example/selfie.jpg"); - /// let mut image = File::open(&path).unwrap(); - /// - /// client.upload_avatar(&mime::IMAGE_JPEG, &mut image).await.expect("Can't set avatar"); - /// # }) - /// ``` - pub async fn upload_avatar(&self, content_type: &Mime, reader: &mut R) -> Result<()> { - let upload_response = self.upload(content_type, reader).await?; - self.set_avatar_url(Some(&upload_response.content_uri)).await?; - Ok(()) + /// Get the account of the current owner of the client. + pub fn account(&self) -> Account { + Account::new(self.clone()) } /// Register a handler for a specific event type. diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 552bd11b2..669d01e8e 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -46,6 +46,7 @@ pub use reqwest; #[doc(no_inline)] pub use ruma; +mod account; mod client; pub mod config; mod error; @@ -59,6 +60,7 @@ mod sync; #[cfg(feature = "encryption")] pub mod encryption; +pub use account::Account; pub use client::{Client, LoopCtrl}; pub use error::{Error, HttpError, HttpResult, Result}; pub use http_client::HttpSend; From 30d3cafa0cf9022101f5cc7ef09a3f8e35125a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 9 Feb 2022 18:07:04 +0100 Subject: [PATCH 08/22] feat(sdk): Make upload_avatar return an MXC URI --- crates/matrix-sdk/src/account.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs index 785bc8bdf..18f646c17 100644 --- a/crates/matrix-sdk/src/account.rs +++ b/crates/matrix-sdk/src/account.rs @@ -158,6 +158,8 @@ impl Account { /// This is a convenience method for calling [`Client::upload()`], /// followed by [`set_avatar_url()`](#method.set_avatar_url). /// + /// Returns the MXC url of the uploaded avatar. + /// /// # Example /// ```no_run /// # use std::{path::Path, fs::File, io::Read}; @@ -173,9 +175,13 @@ impl Account { /// client.account().upload_avatar(&mime::IMAGE_JPEG, &mut image).await.expect("Can't set avatar"); /// # }) /// ``` - pub async fn upload_avatar(&self, content_type: &Mime, reader: &mut R) -> Result<()> { + pub async fn upload_avatar( + &self, + content_type: &Mime, + reader: &mut R, + ) -> Result> { let upload_response = self.client.upload(content_type, reader).await?; self.set_avatar_url(Some(&upload_response.content_uri)).await?; - Ok(()) + Ok(upload_response.content_uri) } } From 9449b3ef2314781e9c649138a809e02e76b7659f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Thu, 10 Feb 2022 03:45:41 +0100 Subject: [PATCH 09/22] feat(sdk): Add more methods to Account - get_profile - change_password - deactivate - get_3pids - request_3pid_email_token - request_3pid_msisdn_token - add_3pid - delete_3pid --- crates/matrix-sdk/src/account.rs | 447 ++++++++++++++++++++++++++++++- 1 file changed, 444 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs index 18f646c17..c4e9467b2 100644 --- a/crates/matrix-sdk/src/account.rs +++ b/crates/matrix-sdk/src/account.rs @@ -3,10 +3,20 @@ use std::io::Read; use matrix_sdk_base::media::{MediaFormat, MediaRequest, MediaType}; use mime::Mime; use ruma::{ - api::client::r0::profile::{ - get_avatar_url, get_display_name, set_avatar_url, set_display_name, + api::client::r0::{ + account::{ + add_3pid, change_password, deactivate, delete_3pid, get_3pids, + request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn, + ThirdPartyIdRemovalStatus, + }, + profile::{ + get_avatar_url, get_display_name, get_profile, set_avatar_url, set_display_name, + }, + uiaa::AuthData, }, - MxcUri, + assign, + thirdparty::{Medium, ThirdPartyIdentifier}, + ClientSecret, MxcUri, SessionId, UInt, }; use crate::{config::RequestConfig, Client, Error, Result}; @@ -184,4 +194,435 @@ impl Account { self.set_avatar_url(Some(&upload_response.content_uri)).await?; Ok(upload_response.content_uri) } + + /// Get the profile of the account. + /// + /// Allows to get both the display name and avatar url in a single call. + /// + /// Returns a `(display_name, avatar_url)` tuple. + /// + /// # Example + /// ```no_run + /// # use futures::executor::block_on; + /// # use matrix_sdk::Client; + /// # use url::Url; + /// # let homeserver = Url::parse("http://example.com").unwrap(); + /// # block_on(async { + /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); + /// # let client = Client::new(homeserver).await.unwrap(); + /// + /// if let (Some(name), Some(avatar_url)) = client.account().get_profile().await? { + /// println!("You are '{}' with avatar '{}'", name, avatar_url); + /// } + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); + /// ``` + pub async fn get_profile(&self) -> Result<(Option, Option>)> { + let user_id = self.client.user_id().await.ok_or(Error::AuthenticationRequired)?; + let request = get_profile::Request::new(&user_id); + let response = self.client.send(request, None).await?; + Ok((response.displayname, response.avatar_url)) + } + + /// Change the password of the account. + /// + /// # Arguments + /// + /// * `new_password` - The new password to set. + /// + /// * `auth_data` - This request uses the [User-Interactive Authentication + /// API][uiaa]. The first request needs to set this to `None` and will + /// always fail with an [`UiaaResponse`]. The response will contain + /// information for the interactive auth and the same request needs to be + /// made but this time with some `auth_data` provided. + /// + /// # Returns + /// + /// This method might return a [`WeakPassword`] error if the new password is + /// considered insecure by the homeserver, with details about the strength + /// requirements in the error's message. + /// + /// # Example + /// ```no_run + /// # use std::convert::TryFrom; + /// # use matrix_sdk::Client; + /// # use matrix_sdk::ruma::{ + /// # api::client::r0::{ + /// # account::change_password::{Request as ChangePasswordRequest}, + /// # uiaa::{AuthData, Dummy}, + /// # }, + /// # assign, + /// # }; + /// # use futures::executor::block_on; + /// # use url::Url; + /// # block_on(async { + /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); + /// # let client = Client::new(homeserver).await.unwrap(); + /// + /// client.account().change_password( + /// "myverysecretpassword", + /// Some(AuthData::Dummy(Dummy::new())), + /// ).await?; + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); + /// ``` + /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api + /// [`UiaaResponse`]: ruma::api::client::r0::uiaa::UiaaResponse + /// [`WeakPassword`]: ruma::api::client::error::ErrorKind::WeakPassword + pub async fn change_password( + &self, + new_password: &str, + auth_data: Option>, + ) -> Result<()> { + let request = assign!(change_password::Request::new(new_password), { + auth: auth_data, + }); + self.client.send(request, None).await?; + Ok(()) + } + + /// Deactivate this account definitively. + /// + /// # Arguments + /// + /// * `id_server` - The identity server from which to unbind the user’s + /// [Third Party Identifiers][3pid]. + /// + /// * `auth_data` - This request uses the [User-Interactive Authentication + /// API][uiaa]. The first request needs to set this to `None` and will + /// always fail with an [`UiaaResponse`]. The response will contain + /// information for the interactive auth and the same request needs to be + /// made but this time with some `auth_data` provided. + /// + /// # Returns + /// + /// * [`Success`] if the 3PIDs were also unbound from the identity server. + /// + /// * [`NoSupport`] if the 3PIDs were not unbound from the identity server. + /// This can also mean that no 3PIDs were bound to an identity server in + /// the first place. + /// + /// # Example + /// ```no_run + /// # use std::convert::TryFrom; + /// # use matrix_sdk::Client; + /// # use matrix_sdk::ruma::{ + /// # api::client::r0::{ + /// # account::change_password::{Request as ChangePasswordRequest}, + /// # uiaa::{AuthData, Dummy}, + /// # }, + /// # assign, + /// # }; + /// # use futures::executor::block_on; + /// # use url::Url; + /// # block_on(async { + /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); + /// # let client = Client::new(homeserver).await.unwrap(); + /// # let account = client.account(); + /// + /// let response = account.deactivate(None, None).await; + /// + /// // Proceed with UIAA. + /// + /// }); + /// ``` + /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types + /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api + /// [`UiaaResponse`]: ruma::api::client::r0::uiaa::UiaaResponse + /// [`Success`]: ruma::api::client::r0::account::ThirdPartyIdRemovalStatus::Success + /// [`NoSupport`]: ruma::api::client::r0::account::ThirdPartyIdRemovalStatus::NoSupport + pub async fn deactivate( + &self, + id_server: Option<&str>, + auth_data: Option>, + ) -> Result { + let request = assign!(deactivate::Request::new(), { + id_server, + auth: auth_data, + }); + let response = self.client.send(request, None).await?; + Ok(response.id_server_unbind_result) + } + + /// Get the registered [Third Party Identifiers][3pid] on the homeserver of + /// the account. + /// + /// These 3PIDs may be used by the homeserver to authenticate the user + /// during sensitive operations. + /// + /// # Example + /// ```no_run + /// # use futures::executor::block_on; + /// # use matrix_sdk::Client; + /// # use url::Url; + /// # block_on(async { + /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); + /// # let client = Client::new(homeserver).await.unwrap(); + /// + /// let threepids = client.account().get_3pids().await?; + /// + /// for threepid in threepids { + /// println!("Found 3PID '{}' of type '{}'", threepid.address, threepid.medium); + /// } + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); + /// ``` + /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types + pub async fn get_3pids(&self) -> Result> { + let request = get_3pids::Request::new(); + let response = self.client.send(request, None).await?; + Ok(response.threepids) + } + + /// Request a token to validate an email address as a [Third Party + /// Identifier][3pid]. + /// + /// This is the first step in registering an email address as 3PID. Next, + /// call [`add_3pid`] with the same `client_secret` and the returned `sid`. + /// + /// # Arguments + /// + /// * `client_secret` - A client-generated secret string used to protect + /// this session. + /// + /// * `email` - The email address to validate. + /// + /// * `send_attempt` - The attempt number. This number needs to be + /// incremented if you want to request another token for the same + /// validation. + /// + /// # Returns + /// + /// An `(sid, submit_url)` tuple. + /// + /// * `sid` - The session ID to be used in following requests for this 3PID. + /// + /// * `submit_url` - If present, the user will submit the token to the + /// client, that must send it to this URL. If not, the client will not be + /// involved in the token submission. + /// + /// This method might return a [`ThreepidInUse`] if the email address is + /// already registered for this account or another, or a [`ThreepidDenied`] + /// error if it is denied. + /// + /// # Example + /// ```no_run + /// # use futures::executor::block_on; + /// # use matrix_sdk::Client; + /// # use matrix_sdk::ruma::{ClientSecret, uint}; + /// # use url::Url; + /// # block_on(async { + /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); + /// # let client = Client::new(homeserver).await.unwrap(); + /// # let account = client.account(); + /// # let secret = ClientSecret::parse("secret").unwrap(); + /// + /// let (sid, submit_url) = account.request_3pid_email_token( + /// &secret, + /// "john@matrix.org", + /// uint!(0), + /// ).await?; + /// + /// // Wait for the user to confirm that the token was submitted or prompt + /// // the user for the token and send it to submit_url. + /// + /// let response = account.add_3pid(&secret, &sid, None).await; + /// + /// // Proceed with UIAA. + /// + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); + /// ``` + /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types + /// [`ThreepidInUse`]: ruma::api::client::error::ErrorKind::ThreepidInUse + /// [`ThreepidDenied`]: ruma::api::client::error::ErrorKind::ThreepidDenied + pub async fn request_3pid_email_token( + &self, + client_secret: &ClientSecret, + email: &str, + send_attempt: UInt, + ) -> Result<(Box, Option)> { + let request = request_3pid_management_token_via_email::Request::new( + client_secret, + email, + send_attempt, + ); + let response = self.client.send(request, None).await?; + Ok((response.sid, response.submit_url)) + } + + /// Request a token to validate a phone number as a [Third Party + /// Identifier][3pid]. + /// + /// This is the first step in registering a phone number as 3PID. Next, + /// call [`add_3pid`] with the same `client_secret` and the returned `sid`. + /// + /// # Arguments + /// + /// * `client_secret` - A client-generated secret string used to protect + /// this session. + /// + /// * `country` - The two-letter uppercase ISO-3166-1 alpha-2 country code + /// that the number in phone_number should be parsed as if it were dialled + /// from. + /// + /// * `phone_number` - The phone number to validate. + /// + /// * `send_attempt` - The attempt number. This number needs to be + /// incremented if you want to request another token for the same + /// validation. + /// + /// # Returns + /// + /// An `(sid, submit_url)` tuple. + /// + /// * `sid` - The session ID to be used in following requests for this 3PID. + /// + /// * `submit_url` - If present, the user will submit the token to the + /// client, that must send it to this URL. If not, the client will not be + /// involved in the token submission. + /// + /// This method might return a [`ThreepidInUse`] if the phone number is + /// already registered for this account or another, or a [`ThreepidDenied`] + /// error if it is denied. + /// + /// # Example + /// ```no_run + /// # use futures::executor::block_on; + /// # use matrix_sdk::Client; + /// # use matrix_sdk::ruma::{ClientSecret, uint}; + /// # use url::Url; + /// # block_on(async { + /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); + /// # let client = Client::new(homeserver).await.unwrap(); + /// # let account = client.account(); + /// # let secret = ClientSecret::parse("secret").unwrap(); + /// + /// let (sid, submit_url) = account.request_3pid_msisdn_token( + /// &secret, + /// "FR", + /// "0123456789", + /// uint!(0), + /// ).await?; + /// + /// // Wait for the user to confirm that the token was submitted or prompt + /// // the user for the token and send it to submit_url. + /// + /// let response = account.add_3pid(&secret, &sid, None).await; + /// + /// // Proceed with UIAA. + /// + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); + /// ``` + /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types + /// [`ThreepidInUse`]: ruma::api::client::error::ErrorKind::ThreepidInUse + /// [`ThreepidDenied`]: ruma::api::client::error::ErrorKind::ThreepidDenied + pub async fn request_3pid_msisdn_token( + &self, + client_secret: &ClientSecret, + country: &str, + phone_number: &str, + send_attempt: UInt, + ) -> Result<(Box, Option)> { + let request = request_3pid_management_token_via_msisdn::Request::new( + client_secret, + country, + phone_number, + send_attempt, + ); + let response = self.client.send(request, None).await?; + Ok((response.sid, response.submit_url)) + } + + /// Add a [Third Party Identifier][3pid] on the homeserver for this + /// account. + /// + /// This 3PID may be used by the homeserver to authenticate the user + /// during sensitive operations. To register it against an identity server, + /// use [`bind_3pid`]. + /// + /// This method should be called after [`request_3pid_email_token`] or + /// [`request_3pid_msisdn_token`] to complete the 3PID registration. + /// + /// # Arguments + /// + /// * `client_secret` - The same client secret used in + /// [`request_3pid_email_token`] or [`request_3pid_msisdn_token`]. + /// + /// * `sid` - The session ID returned in + /// [`request_3pid_email_token`] or [`request_3pid_msisdn_token`]. + /// + /// * `auth_data` - This request uses the [User-Interactive Authentication + /// API][uiaa]. The first request needs to set this to `None` and will + /// always fail with an [`UiaaResponse`]. The response will contain + /// information for the interactive auth and the same request needs to be + /// made but this time with some `auth_data` provided. + /// + /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types + /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api + /// [`UiaaResponse`]: ruma::api::client::r0::uiaa::UiaaResponse + pub async fn add_3pid( + &self, + client_secret: &ClientSecret, + sid: &SessionId, + auth_data: Option>, + ) -> Result<()> { + let request = assign!(add_3pid::Request::new(client_secret, sid), { + auth: auth_data, + }); + self.client.send(request, None).await?; + Ok(()) + } + + /// Delete a [Third Party Identifier][3pid] from the homeserver for this + /// account. + /// + /// # Arguments + /// + /// * `address` - The 3PID being removed. + /// + /// * `medium` - The type of the 3PID. + /// + /// * `id_server` - The identity server to unbind from. If not provided, the + /// homeserver should unbind the 3PID from the identity server it was bound + /// to with [`bind_3pid`]. + /// + /// # Returns + /// + /// * [`Success`] if the 3PID was also unbound from the identity server. + /// + /// * [`NoSupport`] if the 3PID was not unbound from the identity server. + /// This can also mean that the 3PID was not bound to an identity server in + /// the first place. + /// + /// # Example + /// ```no_run + /// # use futures::executor::block_on; + /// # use matrix_sdk::Client; + /// # use matrix_sdk::ruma::thirdparty::Medium; + /// # use matrix_sdk::ruma::api::client::r0::account::ThirdPartyIdRemovalStatus; + /// # use url::Url; + /// # block_on(async { + /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); + /// # let client = Client::new(homeserver).await.unwrap(); + /// # let account = client.account(); + /// + /// match account.delete_3pid("paul@matrix.org", Medium::Email, None).await? { + /// ThirdPartyIdRemovalStatus::Success => println!("3PID unbound from the Identity Server"), + /// _ => println!("Could not unbind 3PID from the Identity Server"), + /// } + /// + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); + /// ``` + /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types + /// [`Success`]: ruma::api::client::r0::account::ThirdPartyIdRemovalStatus::Success + /// [`NoSupport`]: ruma::api::client::r0::account::ThirdPartyIdRemovalStatus::NoSupport + pub async fn delete_3pid( + &self, + address: &str, + medium: Medium, + id_server: Option<&str>, + ) -> Result { + let request = assign!(delete_3pid::Request::new(medium, address), { + id_server: id_server, + }); + let response = self.client.send(request, None).await?; + Ok(response.id_server_unbind_result) + } } From abed1e2986061feee1165f81e3c560b198477372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 15 Feb 2022 17:30:52 +0100 Subject: [PATCH 10/22] fix(sdk): Fix Account docs Add a license and fix the dead links. --- crates/matrix-sdk/src/account.rs | 198 +++++++++++++++++-------------- 1 file changed, 106 insertions(+), 92 deletions(-) diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs index c4e9467b2..35780df49 100644 --- a/crates/matrix-sdk/src/account.rs +++ b/crates/matrix-sdk/src/account.rs @@ -1,3 +1,19 @@ +// Copyright 2020 Damir Jelić +// Copyright 2020 The Matrix.org Foundation C.I.C. +// Copyright 2022 Kévin Commaille +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + use std::io::Read; use matrix_sdk_base::media::{MediaFormat, MediaRequest, MediaType}; @@ -42,16 +58,16 @@ impl Account { /// # use futures::executor::block_on; /// # use matrix_sdk::Client; /// # use url::Url; - /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # block_on(async { + /// # let homeserver = Url::parse("http://example.com")?; /// let user = "example"; - /// let client = Client::new(homeserver).await.unwrap(); - /// client.login(user, "password", None, None).await.unwrap(); + /// let client = Client::new(homeserver).await?; + /// client.login(user, "password", None, None).await?; /// - /// if let Some(name) = client.account().get_display_name().await.unwrap() { + /// if let Some(name) = client.account().get_display_name().await? { /// println!("Logged in as user '{}' with display name '{}'", user, name); /// } - /// # }) + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` pub async fn get_display_name(&self) -> Result> { let user_id = self.client.user_id().await.ok_or(Error::AuthenticationRequired)?; @@ -67,14 +83,14 @@ impl Account { /// # use futures::executor::block_on; /// # use matrix_sdk::Client; /// # use url::Url; - /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # block_on(async { + /// # let homeserver = Url::parse("http://example.com")?; /// let user = "example"; - /// let client = Client::new(homeserver).await.unwrap(); - /// client.login(user, "password", None, None).await.unwrap(); + /// let client = Client::new(homeserver).await?; + /// client.login(user, "password", None, None).await?; /// - /// client.account().set_display_name(Some("Alice")).await.expect("Failed setting display name"); - /// # }) + /// client.account().set_display_name(Some("Alice")).await?; + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` pub async fn set_display_name(&self, name: Option<&str>) -> Result<()> { let user_id = self.client.user_id().await.ok_or(Error::AuthenticationRequired)?; @@ -83,23 +99,23 @@ impl Account { Ok(()) } - /// Get the MXC avatar url of the account, if set. + /// Get the MXC URI of the account's avatar, if set. /// /// # Example /// ```no_run /// # use futures::executor::block_on; /// # use matrix_sdk::Client; /// # use url::Url; - /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # block_on(async { + /// # let homeserver = Url::parse("http://example.com")?; /// # let user = "example"; - /// let client = Client::new(homeserver).await.unwrap(); - /// client.login(user, "password", None, None).await.unwrap(); + /// let client = Client::new(homeserver).await?; + /// client.login(user, "password", None, None).await?; /// - /// if let Some(url) = client.account().get_avatar_url().await.unwrap() { + /// if let Some(url) = client.account().get_avatar_url().await? { /// println!("Your avatar's mxc url is {}", url); /// } - /// # }) + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` pub async fn get_avatar_url(&self) -> Result>> { let user_id = self.client.user_id().await.ok_or(Error::AuthenticationRequired)?; @@ -111,7 +127,7 @@ impl Account { Ok(response.avatar_url) } - /// Set the MXC avatar url of the account. + /// Set the MXC URI of the account's avatar. /// /// The avatar is unset if `url` is `None`. pub async fn set_avatar_url(&self, url: Option<&MxcUri>) -> Result<()> { @@ -139,16 +155,16 @@ impl Account { /// # use matrix_sdk::ruma::room_id; /// # use matrix_sdk::media::MediaFormat; /// # use url::Url; - /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # block_on(async { + /// # let homeserver = Url::parse("http://example.com")?; /// # let user = "example"; - /// let client = Client::new(homeserver).await.unwrap(); - /// client.login(user, "password", None, None).await.unwrap(); + /// let client = Client::new(homeserver).await?; + /// client.login(user, "password", None, None).await?; /// - /// if let Some(avatar) = client.account().get_avatar(MediaFormat::File).await.unwrap() { + /// if let Some(avatar) = client.account().get_avatar(MediaFormat::File).await? { /// std::fs::write("avatar.png", avatar); /// } - /// # }) + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` pub async fn get_avatar(&self, format: MediaFormat) -> Result>> { if let Some(url) = self.get_avatar_url().await? { @@ -162,13 +178,13 @@ impl Account { /// Upload and set the account's avatar. /// /// This will upload the data produced by the reader to the homeserver's - /// content repository, and set the user's avatar to the MXC url for the + /// content repository, and set the user's avatar to the MXC URI for the /// uploaded file. /// /// This is a convenience method for calling [`Client::upload()`], - /// followed by [`set_avatar_url()`](#method.set_avatar_url). + /// followed by [`Account::set_avatar_url()`]. /// - /// Returns the MXC url of the uploaded avatar. + /// Returns the MXC URI of the uploaded avatar. /// /// # Example /// ```no_run @@ -177,13 +193,13 @@ impl Account { /// # use matrix_sdk::Client; /// # use url::Url; /// # block_on(async { - /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); - /// # let client = Client::new(homeserver).await.unwrap(); + /// # let homeserver = Url::parse("http://localhost:8080")?; + /// # let client = Client::new(homeserver).await?; /// let path = Path::new("/home/example/selfie.jpg"); - /// let mut image = File::open(&path).unwrap(); + /// let mut image = File::open(&path)?; /// - /// client.account().upload_avatar(&mime::IMAGE_JPEG, &mut image).await.expect("Can't set avatar"); - /// # }) + /// client.account().upload_avatar(&mime::IMAGE_JPEG, &mut image).await?; + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` pub async fn upload_avatar( &self, @@ -197,7 +213,7 @@ impl Account { /// Get the profile of the account. /// - /// Allows to get both the display name and avatar url in a single call. + /// Allows to get both the display name and avatar URL in a single call. /// /// Returns a `(display_name, avatar_url)` tuple. /// @@ -206,11 +222,9 @@ impl Account { /// # use futures::executor::block_on; /// # use matrix_sdk::Client; /// # use url::Url; - /// # let homeserver = Url::parse("http://example.com").unwrap(); /// # block_on(async { - /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); - /// # let client = Client::new(homeserver).await.unwrap(); - /// + /// # let homeserver = Url::parse("http://localhost:8080")?; + /// # let client = Client::new(homeserver).await?; /// if let (Some(name), Some(avatar_url)) = client.account().get_profile().await? { /// println!("You are '{}' with avatar '{}'", name, avatar_url); /// } @@ -237,9 +251,9 @@ impl Account { /// /// # Returns /// - /// This method might return a [`WeakPassword`] error if the new password is - /// considered insecure by the homeserver, with details about the strength - /// requirements in the error's message. + /// This method might return an [`ErrorKind::WeakPassword`] error if the new + /// password is considered insecure by the homeserver, with details about + /// the strength requirements in the error's message. /// /// # Example /// ```no_run @@ -255,9 +269,8 @@ impl Account { /// # use futures::executor::block_on; /// # use url::Url; /// # block_on(async { - /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); - /// # let client = Client::new(homeserver).await.unwrap(); - /// + /// # let homeserver = Url::parse("http://localhost:8080")?; + /// # let client = Client::new(homeserver).await?; /// client.account().change_password( /// "myverysecretpassword", /// Some(AuthData::Dummy(Dummy::new())), @@ -266,7 +279,7 @@ impl Account { /// ``` /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api /// [`UiaaResponse`]: ruma::api::client::r0::uiaa::UiaaResponse - /// [`WeakPassword`]: ruma::api::client::error::ErrorKind::WeakPassword + /// [`ErrorKind::WeakPassword`]: ruma::api::client::error::ErrorKind::WeakPassword pub async fn change_password( &self, new_password: &str, @@ -294,11 +307,12 @@ impl Account { /// /// # Returns /// - /// * [`Success`] if the 3PIDs were also unbound from the identity server. + /// * [`ThirdPartyIdRemovalStatus::Success`] if the 3PIDs were also unbound + /// from the identity server. /// - /// * [`NoSupport`] if the 3PIDs were not unbound from the identity server. - /// This can also mean that no 3PIDs were bound to an identity server in - /// the first place. + /// * [`ThirdPartyIdRemovalStatus::NoSupport`] if the 3PIDs were not unbound + /// from the identity server. This can also mean that no 3PIDs were bound to + /// an identity server in the first place. /// /// # Example /// ```no_run @@ -314,21 +328,18 @@ impl Account { /// # use futures::executor::block_on; /// # use url::Url; /// # block_on(async { - /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); - /// # let client = Client::new(homeserver).await.unwrap(); + /// # let homeserver = Url::parse("http://localhost:8080")?; + /// # let client = Client::new(homeserver).await?; /// # let account = client.account(); - /// /// let response = account.deactivate(None, None).await; /// /// // Proceed with UIAA. /// - /// }); + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types /// [uiaa]: https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api /// [`UiaaResponse`]: ruma::api::client::r0::uiaa::UiaaResponse - /// [`Success`]: ruma::api::client::r0::account::ThirdPartyIdRemovalStatus::Success - /// [`NoSupport`]: ruma::api::client::r0::account::ThirdPartyIdRemovalStatus::NoSupport pub async fn deactivate( &self, id_server: Option<&str>, @@ -354,9 +365,8 @@ impl Account { /// # use matrix_sdk::Client; /// # use url::Url; /// # block_on(async { - /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); - /// # let client = Client::new(homeserver).await.unwrap(); - /// + /// # let homeserver = Url::parse("http://localhost:8080")?; + /// # let client = Client::new(homeserver).await?; /// let threepids = client.account().get_3pids().await?; /// /// for threepid in threepids { @@ -375,7 +385,8 @@ impl Account { /// Identifier][3pid]. /// /// This is the first step in registering an email address as 3PID. Next, - /// call [`add_3pid`] with the same `client_secret` and the returned `sid`. + /// call [`Account::add_3pid()`] with the same `client_secret` and the + /// returned `sid`. /// /// # Arguments /// @@ -398,9 +409,9 @@ impl Account { /// client, that must send it to this URL. If not, the client will not be /// involved in the token submission. /// - /// This method might return a [`ThreepidInUse`] if the email address is - /// already registered for this account or another, or a [`ThreepidDenied`] - /// error if it is denied. + /// This method might return an [`ErrorKind::ThreepidInUse`] error if the + /// email address is already registered for this account or another, or an + /// [`ErrorKind::ThreepidDenied`] error if it is denied. /// /// # Example /// ```no_run @@ -409,11 +420,10 @@ impl Account { /// # use matrix_sdk::ruma::{ClientSecret, uint}; /// # use url::Url; /// # block_on(async { - /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); - /// # let client = Client::new(homeserver).await.unwrap(); + /// # let homeserver = Url::parse("http://localhost:8080")?; + /// # let client = Client::new(homeserver).await?; /// # let account = client.account(); - /// # let secret = ClientSecret::parse("secret").unwrap(); - /// + /// # let secret = ClientSecret::parse("secret")?; /// let (sid, submit_url) = account.request_3pid_email_token( /// &secret, /// "john@matrix.org", @@ -430,8 +440,8 @@ impl Account { /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types - /// [`ThreepidInUse`]: ruma::api::client::error::ErrorKind::ThreepidInUse - /// [`ThreepidDenied`]: ruma::api::client::error::ErrorKind::ThreepidDenied + /// [`ErrorKind::ThreepidInUse`]: ruma::api::client::error::ErrorKind::ThreepidInUse + /// [`ErrorKind::ThreepidDenied`]: ruma::api::client::error::ErrorKind::ThreepidDenied pub async fn request_3pid_email_token( &self, client_secret: &ClientSecret, @@ -451,7 +461,8 @@ impl Account { /// Identifier][3pid]. /// /// This is the first step in registering a phone number as 3PID. Next, - /// call [`add_3pid`] with the same `client_secret` and the returned `sid`. + /// call [`Account::add_3pid()`] with the same `client_secret` and the + /// returned `sid`. /// /// # Arguments /// @@ -478,9 +489,9 @@ impl Account { /// client, that must send it to this URL. If not, the client will not be /// involved in the token submission. /// - /// This method might return a [`ThreepidInUse`] if the phone number is - /// already registered for this account or another, or a [`ThreepidDenied`] - /// error if it is denied. + /// This method might return an [`ErrorKind::ThreepidInUse`] error if the + /// phone number is already registered for this account or another, or an + /// [`ErrorKind::ThreepidDenied`] error if it is denied. /// /// # Example /// ```no_run @@ -489,11 +500,10 @@ impl Account { /// # use matrix_sdk::ruma::{ClientSecret, uint}; /// # use url::Url; /// # block_on(async { - /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); - /// # let client = Client::new(homeserver).await.unwrap(); + /// # let homeserver = Url::parse("http://localhost:8080")?; + /// # let client = Client::new(homeserver).await?; /// # let account = client.account(); - /// # let secret = ClientSecret::parse("secret").unwrap(); - /// + /// # let secret = ClientSecret::parse("secret")?; /// let (sid, submit_url) = account.request_3pid_msisdn_token( /// &secret, /// "FR", @@ -511,8 +521,8 @@ impl Account { /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types - /// [`ThreepidInUse`]: ruma::api::client::error::ErrorKind::ThreepidInUse - /// [`ThreepidDenied`]: ruma::api::client::error::ErrorKind::ThreepidDenied + /// [`ErrorKind::ThreepidInUse`]: ruma::api::client::error::ErrorKind::ThreepidInUse + /// [`ErrorKind::ThreepidDenied`]: ruma::api::client::error::ErrorKind::ThreepidDenied pub async fn request_3pid_msisdn_token( &self, client_secret: &ClientSecret, @@ -534,19 +544,21 @@ impl Account { /// account. /// /// This 3PID may be used by the homeserver to authenticate the user - /// during sensitive operations. To register it against an identity server, - /// use [`bind_3pid`]. + /// during sensitive operations. /// - /// This method should be called after [`request_3pid_email_token`] or - /// [`request_3pid_msisdn_token`] to complete the 3PID registration. + /// This method should be called after + /// [`Account::request_3pid_email_token()`] or + /// [`Account::request_3pid_msisdn_token()`] to complete the 3PID /// /// # Arguments /// /// * `client_secret` - The same client secret used in - /// [`request_3pid_email_token`] or [`request_3pid_msisdn_token`]. + /// [`Account::request_3pid_email_token()`] or + /// [`Account::request_3pid_msisdn_token()`]. /// /// * `sid` - The session ID returned in - /// [`request_3pid_email_token`] or [`request_3pid_msisdn_token`]. + /// [`Account::request_3pid_email_token()`] or + /// [`Account::request_3pid_msisdn_token()`]. /// /// * `auth_data` - This request uses the [User-Interactive Authentication /// API][uiaa]. The first request needs to set this to `None` and will @@ -581,15 +593,16 @@ impl Account { /// /// * `id_server` - The identity server to unbind from. If not provided, the /// homeserver should unbind the 3PID from the identity server it was bound - /// to with [`bind_3pid`]. + /// to previously. /// /// # Returns /// - /// * [`Success`] if the 3PID was also unbound from the identity server. + /// * [`ThirdPartyIdRemovalStatus::Success`] if the 3PID was also unbound + /// from the identity server. /// - /// * [`NoSupport`] if the 3PID was not unbound from the identity server. - /// This can also mean that the 3PID was not bound to an identity server in - /// the first place. + /// * [`ThirdPartyIdRemovalStatus::NoSupport`] if the 3PID was not unbound + /// from the identity server. This can also mean that the 3PID was not bound + /// to an identity server in the first place. /// /// # Example /// ```no_run @@ -599,20 +612,21 @@ impl Account { /// # use matrix_sdk::ruma::api::client::r0::account::ThirdPartyIdRemovalStatus; /// # use url::Url; /// # block_on(async { - /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); - /// # let client = Client::new(homeserver).await.unwrap(); + /// # let homeserver = Url::parse("http://localhost:8080")?; + /// # let client = Client::new(homeserver).await?; /// # let account = client.account(); - /// /// match account.delete_3pid("paul@matrix.org", Medium::Email, None).await? { - /// ThirdPartyIdRemovalStatus::Success => println!("3PID unbound from the Identity Server"), + /// ThirdPartyIdRemovalStatus::Success => { + /// println!("3PID unbound from the Identity Server"); + /// } /// _ => println!("Could not unbind 3PID from the Identity Server"), /// } /// /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types - /// [`Success`]: ruma::api::client::r0::account::ThirdPartyIdRemovalStatus::Success - /// [`NoSupport`]: ruma::api::client::r0::account::ThirdPartyIdRemovalStatus::NoSupport + /// [`ThirdPartyIdRemovalStatus::Success`]: ruma::api::client::r0::account::ThirdPartyIdRemovalStatus::Success + /// [`ThirdPartyIdRemovalStatus::NoSupport`]: ruma::api::client::r0::account::ThirdPartyIdRemovalStatus::NoSupport pub async fn delete_3pid( &self, address: &str, From 6389749931d1724090a3338dac784ec0c0ce2950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 15 Feb 2022 17:34:45 +0100 Subject: [PATCH 11/22] fix(sdk): Return whole responses in Account --- crates/matrix-sdk/src/account.rs | 107 +++++++++++++++---------------- 1 file changed, 50 insertions(+), 57 deletions(-) diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs index 35780df49..65ecb8f42 100644 --- a/crates/matrix-sdk/src/account.rs +++ b/crates/matrix-sdk/src/account.rs @@ -23,7 +23,6 @@ use ruma::{ account::{ add_3pid, change_password, deactivate, delete_3pid, get_3pids, request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn, - ThirdPartyIdRemovalStatus, }, profile::{ get_avatar_url, get_display_name, get_profile, set_avatar_url, set_display_name, @@ -31,7 +30,7 @@ use ruma::{ uiaa::AuthData, }, assign, - thirdparty::{Medium, ThirdPartyIdentifier}, + thirdparty::Medium, ClientSecret, MxcUri, SessionId, UInt, }; @@ -215,8 +214,6 @@ impl Account { /// /// Allows to get both the display name and avatar URL in a single call. /// - /// Returns a `(display_name, avatar_url)` tuple. - /// /// # Example /// ```no_run /// # use futures::executor::block_on; @@ -225,16 +222,19 @@ impl Account { /// # block_on(async { /// # let homeserver = Url::parse("http://localhost:8080")?; /// # let client = Client::new(homeserver).await?; - /// if let (Some(name), Some(avatar_url)) = client.account().get_profile().await? { - /// println!("You are '{}' with avatar '{}'", name, avatar_url); + /// if let profile = client.account().get_profile().await? { + /// println!( + /// "You are '{:?}' with avatar '{:?}'", + /// profile.displayname, + /// profile.avatar_url + /// ); /// } /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` - pub async fn get_profile(&self) -> Result<(Option, Option>)> { + pub async fn get_profile(&self) -> Result { let user_id = self.client.user_id().await.ok_or(Error::AuthenticationRequired)?; let request = get_profile::Request::new(&user_id); - let response = self.client.send(request, None).await?; - Ok((response.displayname, response.avatar_url)) + Ok(self.client.send(request, None).await?) } /// Change the password of the account. @@ -284,12 +284,11 @@ impl Account { &self, new_password: &str, auth_data: Option>, - ) -> Result<()> { + ) -> Result { let request = assign!(change_password::Request::new(new_password), { auth: auth_data, }); - self.client.send(request, None).await?; - Ok(()) + Ok(self.client.send(request, None).await?) } /// Deactivate this account definitively. @@ -305,15 +304,6 @@ impl Account { /// information for the interactive auth and the same request needs to be /// made but this time with some `auth_data` provided. /// - /// # Returns - /// - /// * [`ThirdPartyIdRemovalStatus::Success`] if the 3PIDs were also unbound - /// from the identity server. - /// - /// * [`ThirdPartyIdRemovalStatus::NoSupport`] if the 3PIDs were not unbound - /// from the identity server. This can also mean that no 3PIDs were bound to - /// an identity server in the first place. - /// /// # Example /// ```no_run /// # use std::convert::TryFrom; @@ -344,13 +334,12 @@ impl Account { &self, id_server: Option<&str>, auth_data: Option>, - ) -> Result { + ) -> Result { let request = assign!(deactivate::Request::new(), { id_server, auth: auth_data, }); - let response = self.client.send(request, None).await?; - Ok(response.id_server_unbind_result) + Ok(self.client.send(request, None).await?) } /// Get the registered [Third Party Identifiers][3pid] on the homeserver of @@ -367,7 +356,7 @@ impl Account { /// # block_on(async { /// # let homeserver = Url::parse("http://localhost:8080")?; /// # let client = Client::new(homeserver).await?; - /// let threepids = client.account().get_3pids().await?; + /// let threepids = client.account().get_3pids().await?.threepids; /// /// for threepid in threepids { /// println!("Found 3PID '{}' of type '{}'", threepid.address, threepid.medium); @@ -375,10 +364,9 @@ impl Account { /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types - pub async fn get_3pids(&self) -> Result> { + pub async fn get_3pids(&self) -> Result { let request = get_3pids::Request::new(); - let response = self.client.send(request, None).await?; - Ok(response.threepids) + Ok(self.client.send(request, None).await?) } /// Request a token to validate an email address as a [Third Party @@ -401,13 +389,12 @@ impl Account { /// /// # Returns /// - /// An `(sid, submit_url)` tuple. + /// * `sid` - The session ID to be used in following requests for + /// this 3PID. /// - /// * `sid` - The session ID to be used in following requests for this 3PID. - /// - /// * `submit_url` - If present, the user will submit the token to the - /// client, that must send it to this URL. If not, the client will not be - /// involved in the token submission. + /// * `submit_url` - If present, the user will submit the token to + /// the client, that must send it to this URL. If not, the client will not + /// be involved in the token submission. /// /// This method might return an [`ErrorKind::ThreepidInUse`] error if the /// email address is already registered for this account or another, or an @@ -424,7 +411,7 @@ impl Account { /// # let client = Client::new(homeserver).await?; /// # let account = client.account(); /// # let secret = ClientSecret::parse("secret")?; - /// let (sid, submit_url) = account.request_3pid_email_token( + /// let token_response = account.request_3pid_email_token( /// &secret, /// "john@matrix.org", /// uint!(0), @@ -433,7 +420,11 @@ impl Account { /// // Wait for the user to confirm that the token was submitted or prompt /// // the user for the token and send it to submit_url. /// - /// let response = account.add_3pid(&secret, &sid, None).await; + /// let uiaa_response = account.add_3pid( + /// &secret, + /// &token_response.sid, + /// None + /// ).await; /// /// // Proceed with UIAA. /// @@ -447,14 +438,13 @@ impl Account { client_secret: &ClientSecret, email: &str, send_attempt: UInt, - ) -> Result<(Box, Option)> { + ) -> Result { let request = request_3pid_management_token_via_email::Request::new( client_secret, email, send_attempt, ); - let response = self.client.send(request, None).await?; - Ok((response.sid, response.submit_url)) + Ok(self.client.send(request, None).await?) } /// Request a token to validate a phone number as a [Third Party @@ -481,8 +471,6 @@ impl Account { /// /// # Returns /// - /// An `(sid, submit_url)` tuple. - /// /// * `sid` - The session ID to be used in following requests for this 3PID. /// /// * `submit_url` - If present, the user will submit the token to the @@ -504,7 +492,7 @@ impl Account { /// # let client = Client::new(homeserver).await?; /// # let account = client.account(); /// # let secret = ClientSecret::parse("secret")?; - /// let (sid, submit_url) = account.request_3pid_msisdn_token( + /// let token_response = account.request_3pid_msisdn_token( /// &secret, /// "FR", /// "0123456789", @@ -514,7 +502,11 @@ impl Account { /// // Wait for the user to confirm that the token was submitted or prompt /// // the user for the token and send it to submit_url. /// - /// let response = account.add_3pid(&secret, &sid, None).await; + /// let uiaa_response = account.add_3pid( + /// &secret, + /// &token_response.sid, + /// None + /// ).await; /// /// // Proceed with UIAA. /// @@ -529,15 +521,14 @@ impl Account { country: &str, phone_number: &str, send_attempt: UInt, - ) -> Result<(Box, Option)> { + ) -> Result { let request = request_3pid_management_token_via_msisdn::Request::new( client_secret, country, phone_number, send_attempt, ); - let response = self.client.send(request, None).await?; - Ok((response.sid, response.submit_url)) + Ok(self.client.send(request, None).await?) } /// Add a [Third Party Identifier][3pid] on the homeserver for this @@ -574,12 +565,11 @@ impl Account { client_secret: &ClientSecret, sid: &SessionId, auth_data: Option>, - ) -> Result<()> { + ) -> Result { let request = assign!(add_3pid::Request::new(client_secret, sid), { auth: auth_data, }); - self.client.send(request, None).await?; - Ok(()) + Ok(self.client.send(request, None).await?) } /// Delete a [Third Party Identifier][3pid] from the homeserver for this @@ -615,12 +605,16 @@ impl Account { /// # let homeserver = Url::parse("http://localhost:8080")?; /// # let client = Client::new(homeserver).await?; /// # let account = client.account(); - /// match account.delete_3pid("paul@matrix.org", Medium::Email, None).await? { - /// ThirdPartyIdRemovalStatus::Success => { - /// println!("3PID unbound from the Identity Server"); + /// match account + /// .delete_3pid("paul@matrix.org", Medium::Email, None) + /// .await? + /// .id_server_unbind_result + /// { + /// ThirdPartyIdRemovalStatus::Success => { + /// println!("3PID unbound from the Identity Server"); + /// } + /// _ => println!("Could not unbind 3PID from the Identity Server"), /// } - /// _ => println!("Could not unbind 3PID from the Identity Server"), - /// } /// /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` @@ -632,11 +626,10 @@ impl Account { address: &str, medium: Medium, id_server: Option<&str>, - ) -> Result { + ) -> Result { let request = assign!(delete_3pid::Request::new(medium, address), { id_server: id_server, }); - let response = self.client.send(request, None).await?; - Ok(response.id_server_unbind_result) + Ok(self.client.send(request, None).await?) } } From 0436780292858e8163fe3ef3d2906e909d714012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 21 Dec 2021 20:40:31 +0100 Subject: [PATCH 12/22] feat(sdk): Allow to add info and thumbnail to attachments --- crates/matrix-sdk/examples/image_bot.rs | 6 +- crates/matrix-sdk/src/attachment.rs | 154 ++++++++++++ crates/matrix-sdk/src/client.rs | 320 ++++++++++++++++++++++-- crates/matrix-sdk/src/encryption/mod.rs | 94 ++++++- crates/matrix-sdk/src/lib.rs | 2 + crates/matrix-sdk/src/room/joined.rs | 34 ++- 6 files changed, 575 insertions(+), 35 deletions(-) create mode 100644 crates/matrix-sdk/src/attachment.rs diff --git a/crates/matrix-sdk/examples/image_bot.rs b/crates/matrix-sdk/examples/image_bot.rs index 103b1cc10..2398d2032 100644 --- a/crates/matrix-sdk/examples/image_bot.rs +++ b/crates/matrix-sdk/examples/image_bot.rs @@ -9,6 +9,7 @@ use std::{ use matrix_sdk::{ self, + attachment::Thumbnail, config::SyncSettings, room::Room, ruma::events::room::message::{ @@ -38,8 +39,11 @@ async fn on_room_message(event: SyncRoomMessageEvent, room: Room, image: Arc> = None; - room.send_attachment("cat", &mime::IMAGE_JPEG, &mut *image, None).await.unwrap(); + room.send_attachment("cat", &mime::IMAGE_JPEG, &mut *image, None, none_thumbnail, None) + .await + .unwrap(); image.seek(SeekFrom::Start(0)).unwrap(); diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs new file mode 100644 index 000000000..a33c5556f --- /dev/null +++ b/crates/matrix-sdk/src/attachment.rs @@ -0,0 +1,154 @@ +use std::io::Read; + +use ruma::{ + assign, + events::room::{ + message::{AudioInfo, FileInfo, VideoInfo}, + ImageInfo, ThumbnailInfo, + }, + UInt, +}; + +/// Base metadata about an image. +#[derive(Debug, Clone)] +pub struct BaseImageInfo { + /// The height of the image in pixels. + pub height: Option, + /// The width of the image in pixels. + pub width: Option, + /// The file size of the image in bytes. + pub size: Option, + /// The [BlurHash](https://blurha.sh/) for this image. + pub blurhash: Option, +} + +/// Base metadata about a video. +#[derive(Debug, Clone)] +pub struct BaseVideoInfo { + /// The duration of the video in milliseconds. + pub duration: Option, + /// The height of the video in pixels. + pub height: Option, + /// The width of the video in pixels. + pub width: Option, + /// The file size of the video in bytes. + pub size: Option, + /// The [BlurHash](https://blurha.sh/) for this video. + pub blurhash: Option, +} + +/// Base metadata about an audio clip. +#[derive(Debug, Clone)] +pub struct BaseAudioInfo { + /// The duration of the audio clip in milliseconds. + pub duration: Option, + /// The file size of the audio clip in bytes. + pub size: Option, +} + +/// Base metadata about a file. +#[derive(Debug, Clone)] +pub struct BaseFileInfo { + /// The size of the file in bytes. + pub size: Option, +} + +/// Types of metadata for an attachment. +#[derive(Debug)] +pub enum AttachmentInfo { + /// The metadata of an image. + Image(BaseImageInfo), + /// The metadata of a video. + Video(BaseVideoInfo), + /// The metadata of an audio clip. + Audio(BaseAudioInfo), + /// The metadata of a file. + File(BaseFileInfo), +} + +impl From for ImageInfo { + fn from(info: AttachmentInfo) -> Self { + match info { + AttachmentInfo::Image(info) => assign!(ImageInfo::new(), { + height: info.height, + width: info.width, + size: info.size, + blurhash: info.blurhash, + }), + _ => ImageInfo::new(), + } + } +} + +impl From for VideoInfo { + fn from(info: AttachmentInfo) -> Self { + match info { + AttachmentInfo::Video(info) => assign!(VideoInfo::new(), { + duration: info.duration, + height: info.height, + width: info.width, + size: info.size, + blurhash: info.blurhash, + }), + _ => VideoInfo::new(), + } + } +} + +impl From for AudioInfo { + fn from(info: AttachmentInfo) -> Self { + match info { + AttachmentInfo::Audio(info) => assign!(AudioInfo::new(), { + duration: info.duration, + size: info.size, + }), + _ => AudioInfo::new(), + } + } +} + +impl From for FileInfo { + fn from(info: AttachmentInfo) -> Self { + match info { + AttachmentInfo::File(info) => assign!(FileInfo::new(), { + size: info.size, + }), + _ => FileInfo::new(), + } + } +} + +#[derive(Debug, Clone)] +/// Base metadata about a thumbnail. +pub struct BaseThumbnailInfo { + /// The height of the thumbnail in pixels. + pub height: Option, + /// The width of the thumbnail in pixels. + pub width: Option, + /// The file size of the thumbnail in bytes. + pub size: Option, +} + +impl From for ThumbnailInfo { + fn from(info: BaseThumbnailInfo) -> Self { + assign!(ThumbnailInfo::new(), { + height: info.height, + width: info.width, + size: info.size, + }) + } +} + +/// A thumbnail to upload and send for an attachment. +#[derive(Debug)] +pub struct Thumbnail<'a, R: Read> { + /// A `Reader` that will be used to fetch the raw bytes of the thumbnail. + pub reader: &'a mut R, + /// The type of the thumbnail, this will be used as the content-type header. + pub content_type: &'a mime::Mime, + /// The metadata of the thumbnail. + pub info: Option, +} + +/// Typed `None` for an `>`. +pub const NONE_THUMBNAIL: Option> = None; diff --git a/crates/matrix-sdk/src/client.rs b/crates/matrix-sdk/src/client.rs index 5493e9e9a..4e77d643c 100644 --- a/crates/matrix-sdk/src/client.rs +++ b/crates/matrix-sdk/src/client.rs @@ -69,6 +69,7 @@ use tracing::{error, info, instrument, warn}; use url::Url; use crate::{ + attachment::{AttachmentInfo, Thumbnail}, config::{ClientConfig, RequestConfig}, error::{HttpError, HttpResult}, event_handler::{EventHandler, EventHandlerData, EventHandlerResult, EventKind, SyncEvent}, @@ -2321,42 +2322,95 @@ impl Client { } /// Upload the file to be read from `reader` and construct an attachment - /// message with `body` and the specified `content_type`. - pub(crate) async fn prepare_attachment_message( + /// message with `body`, `content_type`, `info` and `thumbnail`. + pub(crate) async fn prepare_attachment_message( &self, body: &str, content_type: &Mime, reader: &mut R, + info: Option, + thumbnail: Option>, ) -> Result { + let (thumbnail_url, thumbnail_info) = if let Some(thumbnail) = thumbnail { + let response = self.upload(thumbnail.content_type, thumbnail.reader).await?; + let url = response.content_uri; + + use ruma::events::room::ThumbnailInfo; + let thumbnail_info = assign!( + thumbnail.info.as_ref().map(|info| ThumbnailInfo::from(info.clone())).unwrap_or_default(), + { mimetype: Some(thumbnail.content_type.as_ref().to_owned()) } + ); + + (Some(url), Some(Box::new(thumbnail_info))) + } else { + (None, None) + }; + let response = self.upload(content_type, reader).await?; let url = response.content_uri; - use ruma::events::room::message; + use ruma::events::room::{self, message}; Ok(match content_type.type_() { mime::IMAGE => { // TODO create a thumbnail using the image crate?. + let info = assign!( + info.map(room::ImageInfo::from).unwrap_or_default(), + { + mimetype: Some(content_type.as_ref().to_owned()), + thumbnail_url, + thumbnail_info + } + ); message::MessageType::Image(message::ImageMessageEventContent::plain( body.to_owned(), url, - None, + Some(Box::new(info)), + )) + } + mime::AUDIO => { + let info = assign!( + info.map(message::AudioInfo::from).unwrap_or_default(), + { + mimetype: Some(content_type.as_ref().to_owned()), + } + ); + message::MessageType::Audio(message::AudioMessageEventContent::plain( + body.to_owned(), + url, + Some(Box::new(info)), + )) + } + mime::VIDEO => { + let info = assign!( + info.map(message::VideoInfo::from).unwrap_or_default(), + { + mimetype: Some(content_type.as_ref().to_owned()), + thumbnail_url, + thumbnail_info + } + ); + message::MessageType::Video(message::VideoMessageEventContent::plain( + body.to_owned(), + url, + Some(Box::new(info)), + )) + } + _ => { + let info = assign!( + info.map(message::FileInfo::from).unwrap_or_default(), + { + mimetype: Some(content_type.as_ref().to_owned()), + thumbnail_url, + thumbnail_info + } + ); + message::MessageType::File(message::FileMessageEventContent::plain( + body.to_owned(), + url, + Some(Box::new(info)), )) } - mime::AUDIO => message::MessageType::Audio(message::AudioMessageEventContent::plain( - body.to_owned(), - url, - None, - )), - mime::VIDEO => message::MessageType::Video(message::VideoMessageEventContent::plain( - body.to_owned(), - url, - None, - )), - _ => message::MessageType::File(message::FileMessageEventContent::plain( - body.to_owned(), - url, - None, - )), }) } } @@ -2406,6 +2460,10 @@ pub(crate) mod test { use super::{Client, Session, Url}; use crate::{ + attachment::{ + AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, Thumbnail, + NONE_THUMBNAIL, + }, config::{ClientConfig, RequestConfig, SyncSettings}, HttpError, RoomMember, }; @@ -3188,6 +3246,11 @@ pub(crate) mod test { let _m = mock("PUT", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/send/".to_string())) .with_status(200) .match_header("authorization", "Bearer 1234") + .match_body(Matcher::PartialJson(json!({ + "info": { + "mimetype": "image/jpeg" + } + }))) .with_body(test_json::EVENT_ID.to_string()) .create(); @@ -3216,12 +3279,227 @@ pub(crate) mod test { let mut media = Cursor::new("Hello world"); - let response = - room.send_attachment("image", &mime::IMAGE_JPEG, &mut media, None).await.unwrap(); + let response = room + .send_attachment("image", &mime::IMAGE_JPEG, &mut media, None, NONE_THUMBNAIL, None) + .await + .unwrap(); assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) } + #[async_test] + async fn room_attachment_send_info() { + let client = logged_in_client().await; + + let _m = mock("PUT", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/send/".to_string())) + .with_status(200) + .match_header("authorization", "Bearer 1234") + .match_body(Matcher::PartialJson(json!({ + "info": { + "mimetype": "image/jpeg", + "h": 600, + "w": 800, + } + }))) + .with_body(test_json::EVENT_ID.to_string()) + .create(); + + let upload_mock = mock("POST", Matcher::Regex(r"^/_matrix/media/r0/upload".to_string())) + .with_status(200) + .match_header("content-type", "image/jpeg") + .with_body( + json!({ + "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + }) + .to_string(), + ) + .create(); + + let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string())) + .with_status(200) + .match_header("authorization", "Bearer 1234") + .with_body(test_json::SYNC.to_string()) + .create(); + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + let mut media = Cursor::new("Hello world"); + + let info = AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(600)), + width: Some(uint!(800)), + size: None, + blurhash: None, + }); + + let response = room + .send_attachment( + "image", + &mime::IMAGE_JPEG, + &mut media, + Some(info), + NONE_THUMBNAIL, + None, + ) + .await + .unwrap(); + + upload_mock.assert(); + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) + } + + #[async_test] + async fn room_attachment_send_wrong_info() { + let client = logged_in_client().await; + + let _m = mock("PUT", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/send/".to_string())) + .with_status(200) + .match_header("authorization", "Bearer 1234") + .match_body(Matcher::PartialJson(json!({ + "info": { + "mimetype": "image/jpeg", + "h": 600, + "w": 800, + } + }))) + .with_body(test_json::EVENT_ID.to_string()) + .create(); + + let _m = mock("POST", Matcher::Regex(r"^/_matrix/media/r0/upload".to_string())) + .with_status(200) + .match_header("content-type", "image/jpeg") + .with_body( + json!({ + "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + }) + .to_string(), + ) + .create(); + + let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string())) + .with_status(200) + .match_header("authorization", "Bearer 1234") + .with_body(test_json::SYNC.to_string()) + .create(); + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + let mut media = Cursor::new("Hello world"); + + let info = AttachmentInfo::Video(BaseVideoInfo { + height: Some(uint!(600)), + width: Some(uint!(800)), + duration: Some(uint!(3600)), + size: None, + blurhash: None, + }); + + let response = room + .send_attachment( + "image", + &mime::IMAGE_JPEG, + &mut media, + Some(info), + NONE_THUMBNAIL, + None, + ) + .await; + + assert!(response.is_err()) + } + + #[async_test] + async fn room_attachment_send_info_thumbnail() { + let client = logged_in_client().await; + + let _m = mock("PUT", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/send/".to_string())) + .with_status(200) + .match_header("authorization", "Bearer 1234") + .match_body(Matcher::PartialJson(json!({ + "info": { + "mimetype": "image/jpeg", + "h": 600, + "w": 800, + "thumbnail_info": { + "h": 360, + "w": 480, + "mimetype":"image/jpeg", + "size": 3600, + }, + "thumbnail_url": "mxc://example.com/AQwafuaFswefuhsfAFAgsw", + } + }))) + .with_body(test_json::EVENT_ID.to_string()) + .create(); + + let upload_mock = mock("POST", Matcher::Regex(r"^/_matrix/media/r0/upload".to_string())) + .with_status(200) + .match_header("content-type", "image/jpeg") + .with_body( + json!({ + "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + }) + .to_string(), + ) + .expect(2) + .create(); + + let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string())) + .with_status(200) + .match_header("authorization", "Bearer 1234") + .with_body(test_json::SYNC.to_string()) + .create(); + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + let mut media = Cursor::new("Hello world"); + + let info = AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(600)), + width: Some(uint!(800)), + size: None, + blurhash: None, + }); + + let mut thumbnail_reader = Cursor::new("Thumbnail"); + let thumbnail = Thumbnail { + reader: &mut thumbnail_reader, + content_type: &mime::IMAGE_JPEG, + info: Some(BaseThumbnailInfo { + height: Some(uint!(360)), + width: Some(uint!(480)), + size: Some(uint!(3600)), + }), + }; + + let response = room + .send_attachment( + "image", + &mime::IMAGE_JPEG, + &mut media, + Some(info), + Some(thumbnail), + None, + ) + .await + .unwrap(); + + upload_mock.assert(); + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) + } + #[async_test] async fn room_redact() { let client = logged_in_client().await; diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 90c05e667..376594b90 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -283,6 +283,7 @@ use ruma::{ use tracing::{debug, instrument, trace, warn}; use crate::{ + attachment::{AttachmentInfo, Thumbnail}, encryption::{ identities::{Device, UserDevices}, verification::{SasVerification, Verification, VerificationRequest}, @@ -729,14 +730,45 @@ impl Client { } /// Encrypt and upload the file to be read from `reader` and construct an - /// attachment message with `body` and the specified `content_type`. + /// attachment message with `body`, `content_type`, `info` and `thumbnail`. #[cfg(feature = "encryption")] - pub(crate) async fn prepare_encrypted_attachment_message( + pub(crate) async fn prepare_encrypted_attachment_message( &self, body: &str, content_type: &mime::Mime, reader: &mut R, + info: Option, + thumbnail: Option>, ) -> Result { + let (thumbnail_file, thumbnail_info) = + if let Some(thumbnail) = thumbnail { + let mut reader = matrix_sdk_base::crypto::AttachmentEncryptor::new(thumbnail.reader); + + let response = self.upload(thumbnail.content_type, &mut reader).await?; + + let file: ruma::events::room::EncryptedFile = { + let keys = reader.finish(); + ruma::events::room::EncryptedFileInit { + url: response.content_uri, + key: keys.web_key, + iv: keys.iv, + hashes: keys.hashes, + v: keys.version, + } + .into() + }; + + use ruma::events::room::ThumbnailInfo; + let thumbnail_info = assign!( + thumbnail.info.as_ref().map(|info| ThumbnailInfo::from(info.clone())).unwrap_or_default(), + { mimetype: Some(thumbnail.content_type.as_ref().to_owned()) } + ); + + (Some(Box::new(file)), Some(Box::new(thumbnail_info))) + } else { + (None, None) + }; + let mut reader = matrix_sdk_base::crypto::AttachmentEncryptor::new(reader); let response = self.upload(content_type, &mut reader).await?; @@ -753,18 +785,66 @@ impl Client { .into() }; - use ruma::events::room::message; + use ruma::events::room::{self, message}; Ok(match content_type.type_() { mime::IMAGE => { - message::MessageType::Image(message::ImageMessageEventContent::encrypted(body.to_owned(), file)) + let info = assign!( + info.map(room::ImageInfo::from).unwrap_or_default(), + { + mimetype: Some(content_type.as_ref().to_owned()), + thumbnail_file, + thumbnail_info + } + ); + let content = assign!( + message::ImageMessageEventContent::encrypted(body.to_owned(), file), + { info: Some(Box::new(info)) } + ); + message::MessageType::Image(content) } mime::AUDIO => { - message::MessageType::Audio(message::AudioMessageEventContent::encrypted(body.to_owned(), file)) + let info = assign!( + info.map(message::AudioInfo::from).unwrap_or_default(), + { + mimetype: Some(content_type.as_ref().to_owned()), + } + ); + let content = assign!( + message::AudioMessageEventContent::encrypted(body.to_owned(), file), + { info: Some(Box::new(info)) } + ); + message::MessageType::Audio(content) } mime::VIDEO => { - message::MessageType::Video(message::VideoMessageEventContent::encrypted(body.to_owned(), file)) + let info = assign!( + info.map(message::VideoInfo::from).unwrap_or_default(), + { + mimetype: Some(content_type.as_ref().to_owned()), + thumbnail_file, + thumbnail_info + } + ); + let content = assign!( + message::VideoMessageEventContent::encrypted(body.to_owned(), file), + { info: Some(Box::new(info)) } + ); + message::MessageType::Video(content) + } + _ => { + let info = assign!( + info.map(message::FileInfo::from).unwrap_or_default(), + { + mimetype: Some(content_type.as_ref().to_owned()), + thumbnail_file, + thumbnail_info + } + ); + let content = assign!( + message::FileMessageEventContent::encrypted(body.to_owned(), file), + { info: Some(Box::new(info)) } + ); + message::MessageType::File(content) } - _ => message::MessageType::File(message::FileMessageEventContent::encrypted(body.to_owned(), file)), }) } diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 552bd11b2..48b7df5f8 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -46,6 +46,8 @@ pub use reqwest; #[doc(no_inline)] pub use ruma; +/// Types and traits for attachments. +pub mod attachment; mod client; pub mod config; mod error; diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index 979e513d8..c9e059a56 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -31,7 +31,12 @@ use tracing::debug; #[cfg(feature = "encryption")] use tracing::instrument; -use crate::{error::HttpResult, room::Common, BaseRoom, Client, Result, RoomType}; +use crate::{ + attachment::{AttachmentInfo, Thumbnail}, + error::HttpResult, + room::Common, + BaseRoom, Client, Result, RoomType, +}; const TYPING_NOTICE_TIMEOUT: Duration = Duration::from_secs(4); const TYPING_NOTICE_RESEND_TIMEOUT: Duration = Duration::from_secs(3); @@ -597,6 +602,12 @@ impl Joined { /// * `reader` - A `Reader` that will be used to fetch the raw bytes of the /// media. /// + /// * `info` - The metadata of the media. If the + /// `AttachmentInfo` type doesn't match the `content_type`, it is ignored. + /// + /// * `thumbnail` - The thumbnail of the media. If the `content_type` does + /// not support it (eg audio clips), it is ignored. + /// /// * `txn_id` - A unique ID that can be attached to a `MessageEvent` /// held in its unsigned field as `transaction_id`. If not given one is /// created for the message. @@ -605,7 +616,7 @@ impl Joined { /// /// ```no_run /// # use std::{path::PathBuf, fs::File, io::Read}; - /// # use matrix_sdk::{Client, ruma::room_id}; + /// # use matrix_sdk::{Client, ruma::room_id, attachment::NONE_THUMBNAIL}; /// # use url::Url; /// # use mime; /// # use futures::executor::block_on; @@ -622,26 +633,37 @@ impl Joined { /// &mime::IMAGE_JPEG, /// &mut image, /// None, + /// NONE_THUMBNAIL, + /// None, /// ).await?; /// } /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` - pub async fn send_attachment( + pub async fn send_attachment( &self, body: &str, content_type: &Mime, reader: &mut R, + info: Option, + thumbnail: Option>, txn_id: Option<&TransactionId>, ) -> Result { #[cfg(feature = "encryption")] let content = if self.is_encrypted() { - self.client.prepare_encrypted_attachment_message(body, content_type, reader).await? + self.client + .prepare_encrypted_attachment_message(body, content_type, reader, info, thumbnail) + .await? } else { - self.client.prepare_attachment_message(body, content_type, reader).await? + self.client + .prepare_attachment_message(body, content_type, reader, info, thumbnail) + .await? }; #[cfg(not(feature = "encryption"))] - let content = self.client.prepare_attachment_message(body, content_type, reader).await?; + let content = self + .client + .prepare_attachment_message(body, content_type, reader, info, thumbnail) + .await?; self.send(RoomMessageEventContent::new(content), txn_id).await } From 5dc7dfd4c86a0449fd4ce814084c0578bd7dd301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 28 Dec 2021 18:51:30 +0100 Subject: [PATCH 13/22] feat(sdk): Add method to generate thumbnails from images --- crates/matrix-sdk/Cargo.toml | 25 +++++- crates/matrix-sdk/README.md | 2 + crates/matrix-sdk/src/attachment.rs | 131 ++++++++++++++++++++++++++++ crates/matrix-sdk/src/error.rs | 22 +++++ crates/matrix-sdk/src/lib.rs | 8 ++ 5 files changed, 187 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 2adc756dc..c3177ac79 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -36,13 +36,16 @@ rustls-tls = ["reqwest/rustls-tls"] socks = ["reqwest/socks"] sso_login = ["warp", "rand", "tokio-stream"] appservice = ["ruma/appservice-api-s", "ruma/appservice-api-helper"] +image_proc = ["image"] +image_rayon = ["image/jpeg_rayon"] docsrs = [ "encryption", "sled_cryptostore", "sled_state_store", "sso_login", - "qrcode" + "qrcode", + "image_proc", ] [dependencies] @@ -66,6 +69,26 @@ url = "2.2.2" zeroize = "1.3.0" async-stream = "0.3.2" +[dependencies.image] +version = "0.23.14" +default-features = false +features = [ + "gif", + "jpeg", + "ico", + "png", + "pnm", + "tga", + "tiff", + "webp", + "bmp", + "hdr", + "dxt", + "dds", + "farbfeld", +] +optional = true + [dependencies.matrix-sdk-base] version = "0.4.0" path = "../matrix-sdk-base" diff --git a/crates/matrix-sdk/README.md b/crates/matrix-sdk/README.md index 0d2a98dc0..634f0a935 100644 --- a/crates/matrix-sdk/README.md +++ b/crates/matrix-sdk/README.md @@ -64,6 +64,8 @@ The following crate feature flags are available: | `anyhow` | No | Better logging for event handlers that return `anyhow::Result` | | `encryption` | Yes | End-to-end encryption support | | `eyre` | No | Better logging for event handlers that return `eyre::Result` | +| `image_proc` | No | Enables image processing to generate thumbnails | +| `image_rayon` | No | Enables faster image processing | | `markdown` | No | Support to send Markdown-formatted messages | | `qrcode` | Yes | QR code verification support | | `sled_cryptostore` | Yes | Persistent storage for E2EE related data | diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index a33c5556f..369223389 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -1,5 +1,9 @@ use std::io::Read; +#[cfg(feature = "image_proc")] +use std::io::{BufRead, Seek}; +#[cfg(feature = "image_proc")] +use image::GenericImageView; use ruma::{ assign, events::room::{ @@ -9,6 +13,9 @@ use ruma::{ UInt, }; +#[cfg(feature = "image_proc")] +use crate::ImageError; + /// Base metadata about an image. #[derive(Debug, Clone)] pub struct BaseImageInfo { @@ -152,3 +159,127 @@ pub struct Thumbnail<'a, R: Read> { /// Typed `None` for an `>`. pub const NONE_THUMBNAIL: Option> = None; + +/// Generate a thumbnail for an image. +/// +/// This is a convenience method that uses the +/// [image](https://github.com/image-rs/image) crate. +/// +/// # Arguments +/// * `content_type` - The type of the media, this will be used as the +/// content-type header. +/// +/// * `reader` - A `Reader` that will be used to fetch the raw bytes of the +/// media. +/// +/// * `size` - The size of the thumbnail in pixels as a `(width, height)` tuple. +/// If set to `None`, defaults to `(800, 600)`. +/// +/// # Examples +/// +/// ```no_run +/// # use std::{path::PathBuf, fs::File, io::{BufReader, Read, Seek}}; +/// # use matrix_sdk::{Client, attachment::{Thumbnail, generate_image_thumbnail}, ruma::room_id}; +/// # use url::Url; +/// # use mime; +/// # use futures::executor::block_on; +/// # block_on(async { +/// # let homeserver = Url::parse("http://localhost:8080")?; +/// # let mut client = Client::new(homeserver)?; +/// # let room_id = room_id!("!test:localhost"); +/// let path = PathBuf::from("/home/example/my-cat.jpg"); +/// let mut image = BufReader::new(File::open(path)?); +/// +/// let (thumbnail_data, thumbnail_info) = generate_image_thumbnail( +/// &mime::IMAGE_JPEG, +/// &mut image, +/// None +/// )?; +/// let thumbnail = Thumbnail { +/// reader: &mut thumbnail_data.as_slice(), +/// content_type: &mime::IMAGE_JPEG, +/// info: Some(thumbnail_info), +/// }; +/// +/// image.rewind()?; +/// +/// if let Some(room) = client.get_joined_room(&room_id) { +/// room.send_attachment( +/// "My favorite cat", +/// &mime::IMAGE_JPEG, +/// &mut image, +/// None, +/// Some(thumbnail), +/// None, +/// ).await?; +/// } +/// # Result::<_, matrix_sdk::Error>::Ok(()) }); +/// ``` +#[cfg(feature = "image_proc")] +pub fn generate_image_thumbnail( + content_type: &mime::Mime, + reader: &mut R, + size: Option<(u32, u32)>, +) -> Result<(Vec, BaseThumbnailInfo), ImageError> { + let image_format = image_format_from_mime_type(content_type); + if image_format.is_none() { + return Err(ImageError::FormatNotSupported); + } + + let image_format = image_format.unwrap(); + + let image = image::load(reader, image_format)?; + let (original_width, original_height) = image.dimensions(); + + let (width, height) = size.unwrap_or((800, 600)); + + // Don't generate a thumbnail if it would be bigger than or equal to the + // original. + if height >= original_height && width >= original_width { + return Err(ImageError::ThumbnailBiggerThanOriginal); + } + + let thumbnail = image.thumbnail(width, height); + let (thumbnail_width, thumbnail_height) = thumbnail.dimensions(); + + let mut data: Vec = vec![]; + thumbnail.write_to(&mut data, image_format)?; + let data_size = data.len() as u32; + + Ok(( + data, + BaseThumbnailInfo { + width: Some(thumbnail_width.into()), + height: Some(thumbnail_height.into()), + size: Some(data_size.into()), + }, + )) +} + +// FIXME: Replace this method by ImageFormat::from_mime_type after "image" +// crate's next release. +/// Return the image format specified by a MIME type. +#[cfg(feature = "image_proc")] +fn image_format_from_mime_type(mime_type: M) -> Option +where + M: AsRef, +{ + match mime_type.as_ref() { + "image/avif" => Some(image::ImageFormat::Avif), + "image/jpeg" => Some(image::ImageFormat::Jpeg), + "image/png" => Some(image::ImageFormat::Png), + "image/gif" => Some(image::ImageFormat::Gif), + "image/webp" => Some(image::ImageFormat::WebP), + "image/tiff" => Some(image::ImageFormat::Tiff), + "image/x-targa" | "image/x-tga" => Some(image::ImageFormat::Tga), + "image/vnd-ms.dds" => Some(image::ImageFormat::Dds), + "image/bmp" => Some(image::ImageFormat::Bmp), + "image/x-icon" => Some(image::ImageFormat::Ico), + "image/vnd.radiance" => Some(image::ImageFormat::Hdr), + "image/x-portable-bitmap" + | "image/x-portable-graymap" + | "image/x-portable-pixmap" + | "image/x-portable-anymap" => Some(image::ImageFormat::Pnm), + _ => None, + } +} diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 423563984..00a0e7595 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -157,6 +157,11 @@ pub enum Error { /// An error encountered when trying to parse a user tag name. #[error(transparent)] UserTagName(#[from] InvalidUserTagName), + + /// An error while processing images. + #[cfg(feature = "image_proc")] + #[error(transparent)] + ImageError(#[from] ImageError), } /// Error for the room key importing functionality. @@ -257,3 +262,20 @@ impl From for Error { Error::Http(HttpError::Reqwest(e)) } } + +/// All possible errors that can happen during image processing. +#[cfg(feature = "image_proc")] +#[derive(Error, Debug)] +pub enum ImageError { + /// Error processing the image data. + #[error(transparent)] + Proc(#[from] image::ImageError), + + /// The image format is not supported. + #[error("the image format is not supported")] + FormatNotSupported, + + /// The thumbnail size is bigger than the original image. + #[error("the thumbnail size is bigger than the original image size")] + ThumbnailBiggerThanOriginal, +} diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 48b7df5f8..9860e8a3a 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -36,6 +36,12 @@ compile_error!("only one of 'native-tls' or 'rustls-tls' features can be enabled #[cfg(all(feature = "sso_login", target_arch = "wasm32"))] compile_error!("'sso_login' cannot be enabled on 'wasm32' arch"); +#[cfg(all(feature = "image_rayon", target_arch = "wasm32"))] +compile_error!("'image_rayon' cannot be enabled on 'wasm32' arch"); + +#[cfg(all(feature = "image_rayon", not(feature = "image_proc")))] +compile_error!("'image_rayon' only works with 'image_proc' feature"); + pub use bytes; pub use matrix_sdk_base::{ media, Room as BaseRoom, RoomInfo, RoomMember as BaseRoomMember, RoomType, Session, @@ -62,6 +68,8 @@ mod sync; pub mod encryption; pub use client::{Client, LoopCtrl}; +#[cfg(feature = "image_proc")] +pub use error::ImageError; pub use error::{Error, HttpError, HttpResult, Result}; pub use http_client::HttpSend; pub use room_member::RoomMember; From 78489391d2dcee9a674c190fca1a2c3282446286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 28 Dec 2021 18:51:56 +0100 Subject: [PATCH 14/22] feat(sdk): Add method to send attachments with generated thumbnails --- crates/matrix-sdk/src/client.rs | 1 - crates/matrix-sdk/src/room/joined.rs | 89 ++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/client.rs b/crates/matrix-sdk/src/client.rs index 4e77d643c..7278f7b9b 100644 --- a/crates/matrix-sdk/src/client.rs +++ b/crates/matrix-sdk/src/client.rs @@ -2353,7 +2353,6 @@ impl Client { use ruma::events::room::{self, message}; Ok(match content_type.type_() { mime::IMAGE => { - // TODO create a thumbnail using the image crate?. let info = assign!( info.map(room::ImageInfo::from).unwrap_or_default(), { diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index c9e059a56..fbfab6a79 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "image_proc")] +use std::io::{BufReader, Seek}; #[cfg(feature = "encryption")] use std::sync::Arc; use std::{io::Read, ops::Deref}; @@ -31,6 +33,8 @@ use tracing::debug; #[cfg(feature = "encryption")] use tracing::instrument; +#[cfg(feature = "image_proc")] +use crate::attachment::generate_image_thumbnail; use crate::{ attachment::{AttachmentInfo, Thumbnail}, error::HttpResult, @@ -668,6 +672,91 @@ impl Joined { self.send(RoomMessageEventContent::new(content), txn_id).await } + /// Send an attachment with a generated thumbnail to this room. + /// + /// This is a convenience method that calls the + /// [`attachment::generate_image_thumbnail()`] and afterwards the + /// [`send_attachment()`](#method.send_attachment). + /// + /// Thumbnails can only be generated for supported image attachments. For + /// more information, see the [image](https://github.com/image-rs/image) + /// crate. + /// + /// If the thumbnail generation fails, this will return an + /// [`ImageError`](../enum.ImageError.html). + /// + /// # Arguments + /// * `body` - A textual representation of the media that is going to be + /// uploaded. Usually the file name. + /// + /// * `content_type` - The type of the media, this will be used as the + /// content-type header. + /// + /// * `reader` - A `Reader` that will be used to fetch the raw bytes of the + /// media. + /// + /// * `info` - The metadata of the media. If the + /// `AttachmentInfo` type doesn't match the `content_type`, it is ignored. + /// + /// * `thumbnail_size` - The size of the thumbnail in pixels as a + /// `(width, height)` tuple. If set to `None`, defaults to `(800, 600)`. + /// + /// * `txn_id` - A unique `Uuid` that can be attached to a `MessageEvent` + /// held in its unsigned field as `transaction_id`. If not given one is + /// created for the message. + /// + /// # Examples + /// + /// ```no_run + /// # use std::{path::PathBuf, fs::File, io::Read}; + /// # use matrix_sdk::{Client, ruma::room_id}; + /// # use url::Url; + /// # use mime; + /// # use futures::executor::block_on; + /// # block_on(async { + /// # let homeserver = Url::parse("http://localhost:8080")?; + /// # let mut client = Client::new(homeserver)?; + /// # let room_id = room_id!("!test:localhost"); + /// let path = PathBuf::from("/home/example/my-cat.jpg"); + /// let mut image = File::open(path)?; + /// + /// if let Some(room) = client.get_joined_room(&room_id) { + /// room.send_attachment_with_generated_thumbnail( + /// "My favorite cat", + /// &mime::IMAGE_JPEG, + /// &mut image, + /// None, + /// None, + /// None, + /// ).await?; + /// } + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); + /// ``` + /// [`attachment::generate_image_thumbnail()`]: + /// ../attachment/fn.generate_image_thumbnail.html + #[cfg(feature = "image_proc")] + pub async fn send_attachment_with_generated_thumbnail( + &self, + body: &str, + content_type: &Mime, + reader: &mut R, + info: Option, + thumbnail_size: Option<(u32, u32)>, + txn_id: Option, + ) -> Result { + let mut reader = BufReader::new(reader); + + let (thumbnail_data, thumbnail_info) = + generate_image_thumbnail(content_type, &mut reader, thumbnail_size)?; + let thumbnail = Thumbnail { + reader: &mut thumbnail_data.as_slice(), + content_type: &mime::IMAGE_JPEG, + info: Some(thumbnail_info), + }; + reader.rewind()?; + self.send_attachment(body, content_type, &mut reader, info, Some(thumbnail), txn_id).await + } + /// Send a room state event to the homeserver. /// /// Returns the parsed response from the server. From e926d7e9282590dca9ebecdd6c6ce3159a0891e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sun, 6 Feb 2022 18:23:15 +0100 Subject: [PATCH 15/22] chore: Update image dependency --- crates/matrix-sdk/Cargo.toml | 2 +- crates/matrix-sdk/src/attachment.rs | 34 +++------------------------- crates/matrix-sdk/src/room/joined.rs | 2 +- 3 files changed, 5 insertions(+), 33 deletions(-) diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index c3177ac79..57cab1619 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -70,7 +70,7 @@ zeroize = "1.3.0" async-stream = "0.3.2" [dependencies.image] -version = "0.23.14" +version = "0.24.0" default-features = false features = [ "gif", diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index 369223389..955bb6947 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -1,6 +1,6 @@ use std::io::Read; #[cfg(feature = "image_proc")] -use std::io::{BufRead, Seek}; +use std::io::{BufRead, Cursor, Seek}; #[cfg(feature = "image_proc")] use image::GenericImageView; @@ -221,7 +221,7 @@ pub fn generate_image_thumbnail( reader: &mut R, size: Option<(u32, u32)>, ) -> Result<(Vec, BaseThumbnailInfo), ImageError> { - let image_format = image_format_from_mime_type(content_type); + let image_format = image::ImageFormat::from_mime_type(content_type); if image_format.is_none() { return Err(ImageError::FormatNotSupported); } @@ -243,7 +243,7 @@ pub fn generate_image_thumbnail( let (thumbnail_width, thumbnail_height) = thumbnail.dimensions(); let mut data: Vec = vec![]; - thumbnail.write_to(&mut data, image_format)?; + thumbnail.write_to(&mut Cursor::new(&mut data), image_format)?; let data_size = data.len() as u32; Ok(( @@ -255,31 +255,3 @@ pub fn generate_image_thumbnail( }, )) } - -// FIXME: Replace this method by ImageFormat::from_mime_type after "image" -// crate's next release. -/// Return the image format specified by a MIME type. -#[cfg(feature = "image_proc")] -fn image_format_from_mime_type(mime_type: M) -> Option -where - M: AsRef, -{ - match mime_type.as_ref() { - "image/avif" => Some(image::ImageFormat::Avif), - "image/jpeg" => Some(image::ImageFormat::Jpeg), - "image/png" => Some(image::ImageFormat::Png), - "image/gif" => Some(image::ImageFormat::Gif), - "image/webp" => Some(image::ImageFormat::WebP), - "image/tiff" => Some(image::ImageFormat::Tiff), - "image/x-targa" | "image/x-tga" => Some(image::ImageFormat::Tga), - "image/vnd-ms.dds" => Some(image::ImageFormat::Dds), - "image/bmp" => Some(image::ImageFormat::Bmp), - "image/x-icon" => Some(image::ImageFormat::Ico), - "image/vnd.radiance" => Some(image::ImageFormat::Hdr), - "image/x-portable-bitmap" - | "image/x-portable-graymap" - | "image/x-portable-pixmap" - | "image/x-portable-anymap" => Some(image::ImageFormat::Pnm), - _ => None, - } -} diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index fbfab6a79..653f2374f 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -742,7 +742,7 @@ impl Joined { reader: &mut R, info: Option, thumbnail_size: Option<(u32, u32)>, - txn_id: Option, + txn_id: Option<&TransactionId>, ) -> Result { let mut reader = BufReader::new(reader); From c2c9d5ecc0e3f7cb4d7476a84ddae85f7763635a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sun, 6 Feb 2022 18:33:21 +0100 Subject: [PATCH 16/22] fix(sdk): Enable image_proc with image_rayon --- crates/matrix-sdk/Cargo.toml | 2 +- crates/matrix-sdk/src/lib.rs | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 57cab1619..fa5b13bd4 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -37,7 +37,7 @@ socks = ["reqwest/socks"] sso_login = ["warp", "rand", "tokio-stream"] appservice = ["ruma/appservice-api-s", "ruma/appservice-api-helper"] image_proc = ["image"] -image_rayon = ["image/jpeg_rayon"] +image_rayon = ["image_proc", "image/jpeg_rayon"] docsrs = [ "encryption", diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 9860e8a3a..678edae5a 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -39,9 +39,6 @@ compile_error!("'sso_login' cannot be enabled on 'wasm32' arch"); #[cfg(all(feature = "image_rayon", target_arch = "wasm32"))] compile_error!("'image_rayon' cannot be enabled on 'wasm32' arch"); -#[cfg(all(feature = "image_rayon", not(feature = "image_proc")))] -compile_error!("'image_rayon' only works with 'image_proc' feature"); - pub use bytes; pub use matrix_sdk_base::{ media, Room as BaseRoom, RoomInfo, RoomMember as BaseRoomMember, RoomType, Session, From 409afc668400c62007d5d10bf55bb0ae89b9eb52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 8 Feb 2022 08:50:15 +0100 Subject: [PATCH 17/22] feat(sdk): Create AttachmentConfig struct --- .../src/file_encryption/attachments.rs | 8 +- crates/matrix-sdk/examples/image_bot.rs | 5 +- crates/matrix-sdk/src/attachment.rs | 133 ++++++++++-- crates/matrix-sdk/src/client.rs | 71 ++----- crates/matrix-sdk/src/room/joined.rs | 201 +++++++++--------- 5 files changed, 250 insertions(+), 168 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs b/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs index ca571c9d6..fff72b69c 100644 --- a/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs +++ b/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs @@ -147,7 +147,7 @@ impl<'a, R: Read + 'a> AttachmentDecryptor<'a, R> { } /// A wrapper that transparently encrypts anything that implements `Read`. -pub struct AttachmentEncryptor<'a, R: Read + 'a> { +pub struct AttachmentEncryptor<'a, R: Read + ?Sized + 'a> { finished: bool, inner: &'a mut R, web_key: JsonWebKey, @@ -157,7 +157,7 @@ pub struct AttachmentEncryptor<'a, R: Read + 'a> { sha: Sha256, } -impl<'a, R: 'a + Read + std::fmt::Debug> std::fmt::Debug for AttachmentEncryptor<'a, R> { +impl<'a, R: 'a + Read + std::fmt::Debug + ?Sized> std::fmt::Debug for AttachmentEncryptor<'a, R> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("AttachmentEncryptor") .field("inner", &self.inner) @@ -166,7 +166,7 @@ impl<'a, R: 'a + Read + std::fmt::Debug> std::fmt::Debug for AttachmentEncryptor } } -impl<'a, R: Read + 'a> Read for AttachmentEncryptor<'a, R> { +impl<'a, R: Read + ?Sized + 'a> Read for AttachmentEncryptor<'a, R> { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let read_bytes = self.inner.read(buf)?; @@ -185,7 +185,7 @@ impl<'a, R: Read + 'a> Read for AttachmentEncryptor<'a, R> { } } -impl<'a, R: Read + 'a> AttachmentEncryptor<'a, R> { +impl<'a, R: Read + ?Sized + 'a> AttachmentEncryptor<'a, R> { /// Wrap the given reader encrypting all the data we read from it. /// /// After all the reads are done, and all the data is encrypted that we wish diff --git a/crates/matrix-sdk/examples/image_bot.rs b/crates/matrix-sdk/examples/image_bot.rs index 2398d2032..abf7281e7 100644 --- a/crates/matrix-sdk/examples/image_bot.rs +++ b/crates/matrix-sdk/examples/image_bot.rs @@ -9,7 +9,7 @@ use std::{ use matrix_sdk::{ self, - attachment::Thumbnail, + attachment::AttachmentConfig, config::SyncSettings, room::Room, ruma::events::room::message::{ @@ -39,9 +39,8 @@ async fn on_room_message(event: SyncRoomMessageEvent, room: Room, image: Arc> = None; - room.send_attachment("cat", &mime::IMAGE_JPEG, &mut *image, None, none_thumbnail, None) + room.send_attachment("cat", &mime::IMAGE_JPEG, &mut *image, AttachmentConfig::new()) .await .unwrap(); diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index 955bb6947..82d800677 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -10,7 +10,7 @@ use ruma::{ message::{AudioInfo, FileInfo, VideoInfo}, ImageInfo, ThumbnailInfo, }, - UInt, + TransactionId, UInt, }; #[cfg(feature = "image_proc")] @@ -157,8 +157,114 @@ pub struct Thumbnail<'a, R: Read> { pub info: Option, } -/// Typed `None` for an `>`. -pub const NONE_THUMBNAIL: Option> = None; +impl Thumbnail<'static, &'static [u8]> { + /// Typed `None` for an `>`. + pub const NONE: Option> = None; +} + +/// Configuration for sending an attachment. +#[derive(Debug)] +pub struct AttachmentConfig<'a, R: Read> { + pub(crate) txn_id: Option<&'a TransactionId>, + pub(crate) info: Option, + pub(crate) thumbnail: Option>, + #[cfg(feature = "image_proc")] + pub(crate) generate_thumbnail: bool, + #[cfg(feature = "image_proc")] + pub(crate) thumbnail_size: Option<(u32, u32)>, +} + +impl AttachmentConfig<'static, &'static [u8]> { + /// Create a new default `AttachmentConfig` without providing a thumbnail. + /// + /// To provide a thumbnail use [`with_thumbnail()`]. + pub fn new() -> Self { + Self { + txn_id: Default::default(), + info: Default::default(), + thumbnail: None, + #[cfg(feature = "image_proc")] + generate_thumbnail: Default::default(), + #[cfg(feature = "image_proc")] + thumbnail_size: Default::default(), + } + } + + /// Generate the thumbnail to send for this media. + /// + /// Uses [`attachment::generate_image_thumbnail()`]. + /// + /// Thumbnails can only be generated for supported image attachments. For + /// more information, see the [image](https://github.com/image-rs/image) + /// crate. + /// + /// # Arguments + /// + /// * `size` - The size of the thumbnail in pixels as a `(width, height)` + /// tuple. If set to `None`, defaults to `(800, 600)`. + #[cfg(feature = "image_proc")] + #[must_use] + pub fn generate_thumbnail(mut self, size: Option<(u32, u32)>) -> Self { + self.generate_thumbnail = true; + self.thumbnail_size = size; + self + } +} + +impl Default for AttachmentConfig<'static, &'static [u8]> { + fn default() -> Self { + Self::new() + } +} + +impl<'a, R: Read> AttachmentConfig<'a, R> { + /// Create a new default `AttachmentConfig` with `thumbnail`. + /// + /// # Arguments + /// + /// * `thumbnail` - The thumbnail of the media. If the `content_type` does + /// not support it (eg audio clips), it is ignored. + /// + /// To generate automatically a thumbnail from an image, use + /// [`new()`] and + /// [`generate_thumbnail()`]. + pub fn with_thumbnail(thumbnail: Thumbnail<'a, R>) -> Self { + Self { + txn_id: Default::default(), + info: Default::default(), + thumbnail: Some(thumbnail), + #[cfg(feature = "image_proc")] + generate_thumbnail: Default::default(), + #[cfg(feature = "image_proc")] + thumbnail_size: Default::default(), + } + } + + /// Set the transaction ID to send. + /// + /// # Arguments + /// + /// * `txn_id` - A unique ID that can be attached to a `MessageEvent` held + /// in its unsigned field as `transaction_id`. If not given, one is created + /// for the message. + #[must_use] + pub fn txn_id(mut self, txn_id: &'a TransactionId) -> Self { + self.txn_id = Some(txn_id); + self + } + + /// Set the media metadata to send. + /// + /// # Arguments + /// + /// * `info` - The metadata of the media. If the `AttachmentInfo` type + /// doesn't match the `content_type`, it is ignored. + #[must_use] + pub fn info(mut self, info: AttachmentInfo) -> Self { + self.info = Some(info); + self + } +} /// Generate a thumbnail for an image. /// @@ -178,14 +284,18 @@ pub const NONE_THUMBNAIL: Option> = None; /// # Examples /// /// ```no_run -/// # use std::{path::PathBuf, fs::File, io::{BufReader, Read, Seek}}; -/// # use matrix_sdk::{Client, attachment::{Thumbnail, generate_image_thumbnail}, ruma::room_id}; +/// # use std::{path::PathBuf, fs::File, io::{BufReader, Cursor, Read, Seek}}; +/// # use matrix_sdk::{ +/// # Client, +/// # attachment::{AttachmentConfig, Thumbnail, generate_image_thumbnail}, +/// # ruma::room_id +/// # }; /// # use url::Url; /// # use mime; /// # use futures::executor::block_on; /// # block_on(async { /// # let homeserver = Url::parse("http://localhost:8080")?; -/// # let mut client = Client::new(homeserver)?; +/// # let mut client = Client::new(homeserver).await?; /// # let room_id = room_id!("!test:localhost"); /// let path = PathBuf::from("/home/example/my-cat.jpg"); /// let mut image = BufReader::new(File::open(path)?); @@ -195,11 +305,12 @@ pub const NONE_THUMBNAIL: Option> = None; /// &mut image, /// None /// )?; -/// let thumbnail = Thumbnail { -/// reader: &mut thumbnail_data.as_slice(), +/// let mut cursor = Cursor::new(thumbnail_data); +/// let config = AttachmentConfig::with_thumbnail(Thumbnail { +/// reader: &mut cursor, /// content_type: &mime::IMAGE_JPEG, /// info: Some(thumbnail_info), -/// }; +/// }); /// /// image.rewind()?; /// @@ -208,9 +319,7 @@ pub const NONE_THUMBNAIL: Option> = None; /// "My favorite cat", /// &mime::IMAGE_JPEG, /// &mut image, -/// None, -/// Some(thumbnail), -/// None, +/// config, /// ).await?; /// } /// # Result::<_, matrix_sdk::Error>::Ok(()) }); diff --git a/crates/matrix-sdk/src/client.rs b/crates/matrix-sdk/src/client.rs index 7278f7b9b..51aa85e59 100644 --- a/crates/matrix-sdk/src/client.rs +++ b/crates/matrix-sdk/src/client.rs @@ -1623,7 +1623,7 @@ impl Client { pub async fn upload( &self, content_type: &Mime, - reader: &mut impl Read, + reader: &mut (impl Read + ?Sized), ) -> Result { let mut data = Vec::new(); reader.read_to_end(&mut data)?; @@ -2460,8 +2460,8 @@ pub(crate) mod test { use super::{Client, Session, Url}; use crate::{ attachment::{ - AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, Thumbnail, - NONE_THUMBNAIL, + AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, + Thumbnail, }, config::{ClientConfig, RequestConfig, SyncSettings}, HttpError, RoomMember, @@ -3279,7 +3279,7 @@ pub(crate) mod test { let mut media = Cursor::new("Hello world"); let response = room - .send_attachment("image", &mime::IMAGE_JPEG, &mut media, None, NONE_THUMBNAIL, None) + .send_attachment("image", &mime::IMAGE_JPEG, &mut media, AttachmentConfig::new()) .await .unwrap(); @@ -3328,24 +3328,15 @@ pub(crate) mod test { let mut media = Cursor::new("Hello world"); - let info = AttachmentInfo::Image(BaseImageInfo { + let config = AttachmentConfig::new().info(AttachmentInfo::Image(BaseImageInfo { height: Some(uint!(600)), width: Some(uint!(800)), size: None, blurhash: None, - }); + })); - let response = room - .send_attachment( - "image", - &mime::IMAGE_JPEG, - &mut media, - Some(info), - NONE_THUMBNAIL, - None, - ) - .await - .unwrap(); + let response = + room.send_attachment("image", &mime::IMAGE_JPEG, &mut media, config).await.unwrap(); upload_mock.assert(); assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) @@ -3393,24 +3384,15 @@ pub(crate) mod test { let mut media = Cursor::new("Hello world"); - let info = AttachmentInfo::Video(BaseVideoInfo { + let config = AttachmentConfig::new().info(AttachmentInfo::Video(BaseVideoInfo { height: Some(uint!(600)), width: Some(uint!(800)), duration: Some(uint!(3600)), size: None, blurhash: None, - }); + })); - let response = room - .send_attachment( - "image", - &mime::IMAGE_JPEG, - &mut media, - Some(info), - NONE_THUMBNAIL, - None, - ) - .await; + let response = room.send_attachment("image", &mime::IMAGE_JPEG, &mut media, config).await; assert!(response.is_err()) } @@ -3465,15 +3447,9 @@ pub(crate) mod test { let mut media = Cursor::new("Hello world"); - let info = AttachmentInfo::Image(BaseImageInfo { - height: Some(uint!(600)), - width: Some(uint!(800)), - size: None, - blurhash: None, - }); - let mut thumbnail_reader = Cursor::new("Thumbnail"); - let thumbnail = Thumbnail { + + let config = AttachmentConfig::with_thumbnail(Thumbnail { reader: &mut thumbnail_reader, content_type: &mime::IMAGE_JPEG, info: Some(BaseThumbnailInfo { @@ -3481,19 +3457,16 @@ pub(crate) mod test { width: Some(uint!(480)), size: Some(uint!(3600)), }), - }; + }) + .info(AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(600)), + width: Some(uint!(800)), + size: None, + blurhash: None, + })); - let response = room - .send_attachment( - "image", - &mime::IMAGE_JPEG, - &mut media, - Some(info), - Some(thumbnail), - None, - ) - .await - .unwrap(); + let response = + room.send_attachment("image", &mime::IMAGE_JPEG, &mut media, config).await.unwrap(); upload_mock.assert(); assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index 653f2374f..f499e0d85 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -1,8 +1,11 @@ #[cfg(feature = "image_proc")] -use std::io::{BufReader, Seek}; +use std::io::Cursor; #[cfg(feature = "encryption")] use std::sync::Arc; -use std::{io::Read, ops::Deref}; +use std::{ + io::{BufReader, Read, Seek}, + ops::Deref, +}; use matrix_sdk_common::instant::{Duration, Instant}; #[cfg(feature = "encryption")] @@ -34,9 +37,9 @@ use tracing::debug; use tracing::instrument; #[cfg(feature = "image_proc")] -use crate::attachment::generate_image_thumbnail; +use crate::{attachment::generate_image_thumbnail, error::ImageError}; use crate::{ - attachment::{AttachmentInfo, Thumbnail}, + attachment::{AttachmentConfig, Thumbnail}, error::HttpResult, room::Common, BaseRoom, Client, Result, RoomType, @@ -606,21 +609,13 @@ impl Joined { /// * `reader` - A `Reader` that will be used to fetch the raw bytes of the /// media. /// - /// * `info` - The metadata of the media. If the - /// `AttachmentInfo` type doesn't match the `content_type`, it is ignored. - /// - /// * `thumbnail` - The thumbnail of the media. If the `content_type` does - /// not support it (eg audio clips), it is ignored. - /// - /// * `txn_id` - A unique ID that can be attached to a `MessageEvent` - /// held in its unsigned field as `transaction_id`. If not given one is - /// created for the message. + /// * `config` - Metadata and configuration for the attachment. /// /// # Examples /// /// ```no_run /// # use std::{path::PathBuf, fs::File, io::Read}; - /// # use matrix_sdk::{Client, ruma::room_id, attachment::NONE_THUMBNAIL}; + /// # use matrix_sdk::{Client, ruma::room_id, attachment::AttachmentConfig}; /// # use url::Url; /// # use mime; /// # use futures::executor::block_on; @@ -636,54 +631,82 @@ impl Joined { /// "My favorite cat", /// &mime::IMAGE_JPEG, /// &mut image, - /// None, - /// NONE_THUMBNAIL, - /// None, + /// AttachmentConfig::new(), /// ).await?; /// } /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` - pub async fn send_attachment( + pub async fn send_attachment<'a, R: Read + Seek, T: Read>( &self, body: &str, content_type: &Mime, reader: &mut R, - info: Option, - thumbnail: Option>, - txn_id: Option<&TransactionId>, + config: AttachmentConfig<'a, T>, ) -> Result { - #[cfg(feature = "encryption")] - let content = if self.is_encrypted() { - self.client - .prepare_encrypted_attachment_message(body, content_type, reader, info, thumbnail) - .await? + let reader = &mut BufReader::new(reader); + + #[cfg(feature = "image_proc")] + let mut cursor; + + if config.thumbnail.is_some() { + self.prepare_and_send_attachment(body, content_type, reader, config).await } else { - self.client - .prepare_attachment_message(body, content_type, reader, info, thumbnail) - .await? - }; + #[cfg(not(feature = "image_proc"))] + let thumbnail = Thumbnail::NONE; - #[cfg(not(feature = "encryption"))] - let content = self - .client - .prepare_attachment_message(body, content_type, reader, info, thumbnail) - .await?; + #[cfg(feature = "image_proc")] + let thumbnail = if config.generate_thumbnail { + match generate_image_thumbnail(content_type, reader, config.thumbnail_size) { + Ok((thumbnail_data, thumbnail_info)) => { + reader.rewind()?; - self.send(RoomMessageEventContent::new(content), txn_id).await + cursor = Cursor::new(thumbnail_data); + Some(Thumbnail { + reader: &mut cursor, + content_type: &mime::IMAGE_JPEG, + info: Some(thumbnail_info), + }) + } + Err(error) + if matches!( + error, + ImageError::ThumbnailBiggerThanOriginal + | ImageError::FormatNotSupported + ) => + { + reader.rewind()?; + None + } + Err(error) => return Err(error.into()), + } + } else { + None + }; + + let config = AttachmentConfig { + txn_id: config.txn_id, + info: config.info, + thumbnail, + #[cfg(feature = "image_proc")] + generate_thumbnail: false, + #[cfg(feature = "image_proc")] + thumbnail_size: None, + }; + + self.prepare_and_send_attachment(body, content_type, reader, config).await + } } - /// Send an attachment with a generated thumbnail to this room. + /// Prepare and send an attachment to this room. + /// + /// This will upload the given data that the reader produces using the + /// [`upload()`](#method.upload) method and post an event to the given room. + /// If the room is encrypted and the encryption feature is enabled the + /// upload will be encrypted. /// /// This is a convenience method that calls the - /// [`attachment::generate_image_thumbnail()`] and afterwards the - /// [`send_attachment()`](#method.send_attachment). - /// - /// Thumbnails can only be generated for supported image attachments. For - /// more information, see the [image](https://github.com/image-rs/image) - /// crate. - /// - /// If the thumbnail generation fails, this will return an - /// [`ImageError`](../enum.ImageError.html). + /// [`Client::upload()`](#Client::method.upload) and afterwards the + /// [`send()`](#method.send). /// /// # Arguments /// * `body` - A textual representation of the media that is going to be @@ -695,66 +718,44 @@ impl Joined { /// * `reader` - A `Reader` that will be used to fetch the raw bytes of the /// media. /// - /// * `info` - The metadata of the media. If the - /// `AttachmentInfo` type doesn't match the `content_type`, it is ignored. - /// - /// * `thumbnail_size` - The size of the thumbnail in pixels as a - /// `(width, height)` tuple. If set to `None`, defaults to `(800, 600)`. - /// - /// * `txn_id` - A unique `Uuid` that can be attached to a `MessageEvent` - /// held in its unsigned field as `transaction_id`. If not given one is - /// created for the message. - /// - /// # Examples - /// - /// ```no_run - /// # use std::{path::PathBuf, fs::File, io::Read}; - /// # use matrix_sdk::{Client, ruma::room_id}; - /// # use url::Url; - /// # use mime; - /// # use futures::executor::block_on; - /// # block_on(async { - /// # let homeserver = Url::parse("http://localhost:8080")?; - /// # let mut client = Client::new(homeserver)?; - /// # let room_id = room_id!("!test:localhost"); - /// let path = PathBuf::from("/home/example/my-cat.jpg"); - /// let mut image = File::open(path)?; - /// - /// if let Some(room) = client.get_joined_room(&room_id) { - /// room.send_attachment_with_generated_thumbnail( - /// "My favorite cat", - /// &mime::IMAGE_JPEG, - /// &mut image, - /// None, - /// None, - /// None, - /// ).await?; - /// } - /// # Result::<_, matrix_sdk::Error>::Ok(()) }); - /// ``` - /// [`attachment::generate_image_thumbnail()`]: - /// ../attachment/fn.generate_image_thumbnail.html - #[cfg(feature = "image_proc")] - pub async fn send_attachment_with_generated_thumbnail( + /// * `config` - Metadata and configuration for the attachment. + async fn prepare_and_send_attachment<'a, R: Read, T: Read>( &self, body: &str, content_type: &Mime, reader: &mut R, - info: Option, - thumbnail_size: Option<(u32, u32)>, - txn_id: Option<&TransactionId>, + config: AttachmentConfig<'a, T>, ) -> Result { - let mut reader = BufReader::new(reader); - - let (thumbnail_data, thumbnail_info) = - generate_image_thumbnail(content_type, &mut reader, thumbnail_size)?; - let thumbnail = Thumbnail { - reader: &mut thumbnail_data.as_slice(), - content_type: &mime::IMAGE_JPEG, - info: Some(thumbnail_info), + #[cfg(feature = "encryption")] + let content = if self.is_encrypted() { + self.client + .prepare_encrypted_attachment_message( + body, + content_type, + reader, + config.info, + config.thumbnail, + ) + .await? + } else { + self.client + .prepare_attachment_message( + body, + content_type, + reader, + config.info, + config.thumbnail, + ) + .await? }; - reader.rewind()?; - self.send_attachment(body, content_type, &mut reader, info, Some(thumbnail), txn_id).await + + #[cfg(not(feature = "encryption"))] + let content = self + .client + .prepare_attachment_message(body, content_type, reader, config.info, config.thumbnail) + .await?; + + self.send(RoomMessageEventContent::new(content), config.txn_id).await } /// Send a room state event to the homeserver. From ebd913f50f633d2c97f6b57fde5b47e02558154e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 15 Feb 2022 18:05:19 +0100 Subject: [PATCH 18/22] fix(sdk): Fix dead links in AttachmentConfig docs --- crates/matrix-sdk/src/attachment.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index 82d800677..21b2b7fbb 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -177,7 +177,7 @@ pub struct AttachmentConfig<'a, R: Read> { impl AttachmentConfig<'static, &'static [u8]> { /// Create a new default `AttachmentConfig` without providing a thumbnail. /// - /// To provide a thumbnail use [`with_thumbnail()`]. + /// To provide a thumbnail use [`AttachmentConfig::with_thumbnail()`]. pub fn new() -> Self { Self { txn_id: Default::default(), @@ -192,7 +192,7 @@ impl AttachmentConfig<'static, &'static [u8]> { /// Generate the thumbnail to send for this media. /// - /// Uses [`attachment::generate_image_thumbnail()`]. + /// Uses [`generate_image_thumbnail()`]. /// /// Thumbnails can only be generated for supported image attachments. For /// more information, see the [image](https://github.com/image-rs/image) @@ -218,7 +218,7 @@ impl Default for AttachmentConfig<'static, &'static [u8]> { } impl<'a, R: Read> AttachmentConfig<'a, R> { - /// Create a new default `AttachmentConfig` with `thumbnail`. + /// Create a new default `AttachmentConfig` with a `thumbnail`. /// /// # Arguments /// @@ -226,8 +226,8 @@ impl<'a, R: Read> AttachmentConfig<'a, R> { /// not support it (eg audio clips), it is ignored. /// /// To generate automatically a thumbnail from an image, use - /// [`new()`] and - /// [`generate_thumbnail()`]. + /// [`AttachmentConfig::new()`] and + /// [`AttachmentConfig::generate_thumbnail()`]. pub fn with_thumbnail(thumbnail: Thumbnail<'a, R>) -> Self { Self { txn_id: Default::default(), From de8aa7b4f707cf847bfc2df8040bdc0f14aef7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 15 Feb 2022 18:11:03 +0100 Subject: [PATCH 19/22] fix(sdk): Simplify code in room::Joined::send_attachment --- crates/matrix-sdk/src/room/joined.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index f499e0d85..ac72455d6 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -667,13 +667,9 @@ impl Joined { info: Some(thumbnail_info), }) } - Err(error) - if matches!( - error, - ImageError::ThumbnailBiggerThanOriginal - | ImageError::FormatNotSupported - ) => - { + Err( + ImageError::ThumbnailBiggerThanOriginal | ImageError::FormatNotSupported, + ) => { reader.rewind()?; None } From 69b0b04e7019bd85be777ad5c1164ab509102e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 15 Feb 2022 18:43:56 +0100 Subject: [PATCH 20/22] fix(sdk): Add ruma feature for blurhash support --- crates/matrix-sdk/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index fa5b13bd4..aafcd3357 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -101,7 +101,7 @@ default_features = false [dependencies.ruma] git = "https://github.com/ruma/ruma/" rev = "b9f32bc6327542d382d4eb42ec43623495c50e66" -features = ["client-api-c", "compat", "rand"] +features = ["client-api-c", "compat", "rand", "unstable-msc2448"] [dependencies.tokio-stream] version = "0.1.6" From 9e545c34ba83682c9a2e856845ea54af7d32b6ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 15 Feb 2022 19:00:39 +0100 Subject: [PATCH 21/22] fix(sdk): Fix clippy warning --- crates/matrix-sdk/src/room/joined.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index ac72455d6..87bdeb2f0 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -636,12 +636,12 @@ impl Joined { /// } /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` - pub async fn send_attachment<'a, R: Read + Seek, T: Read>( + pub async fn send_attachment( &self, body: &str, content_type: &Mime, reader: &mut R, - config: AttachmentConfig<'a, T>, + config: AttachmentConfig<'_, T>, ) -> Result { let reader = &mut BufReader::new(reader); @@ -715,12 +715,12 @@ impl Joined { /// media. /// /// * `config` - Metadata and configuration for the attachment. - async fn prepare_and_send_attachment<'a, R: Read, T: Read>( + async fn prepare_and_send_attachment( &self, body: &str, content_type: &Mime, reader: &mut R, - config: AttachmentConfig<'a, T>, + config: AttachmentConfig<'_, T>, ) -> Result { #[cfg(feature = "encryption")] let content = if self.is_encrypted() { From 2f7d271c167466daaecd9288ada3e4d9e2c6ff80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 15 Feb 2022 19:15:06 +0100 Subject: [PATCH 22/22] fix(sdk): Add license to attachment.rs --- crates/matrix-sdk/src/attachment.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index 21b2b7fbb..591aaf6f1 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -1,3 +1,17 @@ +// Copyright 2022 Kévin Commaille +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + use std::io::Read; #[cfg(feature = "image_proc")] use std::io::{BufRead, Cursor, Seek};