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/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-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 b8f07da89..15f00bf0d 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -65,8 +65,8 @@ tokio = { version = "1.7.1", optional = true, default-features = false, features [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/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/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-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-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 7fab3932f..430149dd1 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/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-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 e8ee7a2d4..6b6a0ed5c 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -58,8 +58,8 @@ http = { version = "0.2.4", 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-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/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-crypto/src/machine.rs b/crates/matrix-sdk-crypto/src/machine.rs index e3f719934..c0d27cae3 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/olm/group_sessions/inbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs index 8710baef5..5e5c2ae1b 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs @@ -368,7 +368,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/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(), 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-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-indexeddb/src/cryptostore.rs b/crates/matrix-sdk-indexeddb/src/cryptostore.rs index 166db79c0..9a6339ff5 100644 --- a/crates/matrix-sdk-indexeddb/src/cryptostore.rs +++ b/crates/matrix-sdk-indexeddb/src/cryptostore.rs @@ -431,7 +431,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-indexeddb/src/state_store.rs b/crates/matrix-sdk-indexeddb/src/state_store.rs index 29aebe389..131fcdbd0 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store.rs @@ -581,7 +581,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::>()) @@ -726,7 +726,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)).into()); 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..aafcd3357 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_proc", "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.24.0" +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" @@ -77,8 +100,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", "unstable-msc2448"] [dependencies.tokio-stream] version = "0.1.6" diff --git a/crates/matrix-sdk/README.md b/crates/matrix-sdk/README.md index 6afa11495..634f0a935 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?; @@ -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/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/examples/image_bot.rs b/crates/matrix-sdk/examples/image_bot.rs index 103b1cc10..abf7281e7 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::AttachmentConfig, config::SyncSettings, room::Room, ruma::events::room::message::{ @@ -39,7 +40,9 @@ async fn on_room_message(event: SyncRoomMessageEvent, room: Room, image: Arc 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; + /// # block_on(async { + /// # let homeserver = Url::parse("http://example.com")?; + /// let user = "example"; + /// let client = Client::new(homeserver).await?; + /// client.login(user, "password", None, None).await?; + /// + /// 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)?; + 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; + /// # block_on(async { + /// # let homeserver = Url::parse("http://example.com")?; + /// let user = "example"; + /// let client = Client::new(homeserver).await?; + /// client.login(user, "password", None, None).await?; + /// + /// 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)?; + let request = set_display_name::Request::new(&user_id, name); + self.client.send(request, None).await?; + Ok(()) + } + + /// 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; + /// # block_on(async { + /// # let homeserver = Url::parse("http://example.com")?; + /// # let user = "example"; + /// let client = Client::new(homeserver).await?; + /// client.login(user, "password", None, None).await?; + /// + /// 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)?; + 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 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<()> { + 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; + /// # block_on(async { + /// # let homeserver = Url::parse("http://example.com")?; + /// # let user = "example"; + /// let client = Client::new(homeserver).await?; + /// client.login(user, "password", None, None).await?; + /// + /// 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? { + 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 URI for the + /// uploaded file. + /// + /// This is a convenience method for calling [`Client::upload()`], + /// followed by [`Account::set_avatar_url()`]. + /// + /// Returns the MXC URI of the uploaded avatar. + /// + /// # 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")?; + /// # let client = Client::new(homeserver).await?; + /// let path = Path::new("/home/example/selfie.jpg"); + /// let mut image = File::open(&path)?; + /// + /// client.account().upload_avatar(&mime::IMAGE_JPEG, &mut image).await?; + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); + /// ``` + 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(upload_response.content_uri) + } + + /// Get the profile of the account. + /// + /// Allows to get both the display name and avatar URL in a single call. + /// + /// # 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")?; + /// # let client = Client::new(homeserver).await?; + /// 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 { + let user_id = self.client.user_id().await.ok_or(Error::AuthenticationRequired)?; + let request = get_profile::Request::new(&user_id); + Ok(self.client.send(request, None).await?) + } + + /// 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 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 + /// # 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")?; + /// # let client = Client::new(homeserver).await?; + /// 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 + /// [`ErrorKind::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, + }); + Ok(self.client.send(request, None).await?) + } + + /// 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. + /// + /// # 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")?; + /// # 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 + pub async fn deactivate( + &self, + id_server: Option<&str>, + auth_data: Option>, + ) -> Result { + let request = assign!(deactivate::Request::new(), { + id_server, + auth: auth_data, + }); + Ok(self.client.send(request, None).await?) + } + + /// 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")?; + /// # let client = Client::new(homeserver).await?; + /// let threepids = client.account().get_3pids().await?.threepids; + /// + /// 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(); + Ok(self.client.send(request, None).await?) + } + + /// 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 [`Account::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 + /// + /// * `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 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 + /// # 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")?; + /// # let client = Client::new(homeserver).await?; + /// # let account = client.account(); + /// # let secret = ClientSecret::parse("secret")?; + /// let token_response = 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 uiaa_response = account.add_3pid( + /// &secret, + /// &token_response.sid, + /// None + /// ).await; + /// + /// // Proceed with UIAA. + /// + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); + /// ``` + /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types + /// [`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, + email: &str, + send_attempt: UInt, + ) -> Result { + let request = request_3pid_management_token_via_email::Request::new( + client_secret, + email, + send_attempt, + ); + Ok(self.client.send(request, None).await?) + } + + /// 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 [`Account::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 + /// + /// * `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 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 + /// # 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")?; + /// # let client = Client::new(homeserver).await?; + /// # let account = client.account(); + /// # let secret = ClientSecret::parse("secret")?; + /// let token_response = 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 uiaa_response = account.add_3pid( + /// &secret, + /// &token_response.sid, + /// None + /// ).await; + /// + /// // Proceed with UIAA. + /// + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); + /// ``` + /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types + /// [`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, + country: &str, + phone_number: &str, + send_attempt: UInt, + ) -> Result { + let request = request_3pid_management_token_via_msisdn::Request::new( + client_secret, + country, + phone_number, + send_attempt, + ); + Ok(self.client.send(request, None).await?) + } + + /// 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. + /// + /// 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 + /// [`Account::request_3pid_email_token()`] or + /// [`Account::request_3pid_msisdn_token()`]. + /// + /// * `sid` - The session ID returned in + /// [`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 + /// 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, + }); + Ok(self.client.send(request, None).await?) + } + + /// 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 previously. + /// + /// # Returns + /// + /// * [`ThirdPartyIdRemovalStatus::Success`] if the 3PID was also unbound + /// from the identity server. + /// + /// * [`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 + /// # 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")?; + /// # let client = Client::new(homeserver).await?; + /// # let account = client.account(); + /// 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"), + /// } + /// + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); + /// ``` + /// [3pid]: https://spec.matrix.org/v1.2/appendices/#3pid-types + /// [`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, + medium: Medium, + id_server: Option<&str>, + ) -> Result { + let request = assign!(delete_3pid::Request::new(medium, address), { + id_server: id_server, + }); + Ok(self.client.send(request, None).await?) + } +} diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs new file mode 100644 index 000000000..591aaf6f1 --- /dev/null +++ b/crates/matrix-sdk/src/attachment.rs @@ -0,0 +1,380 @@ +// 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}; + +#[cfg(feature = "image_proc")] +use image::GenericImageView; +use ruma::{ + assign, + events::room::{ + message::{AudioInfo, FileInfo, VideoInfo}, + ImageInfo, ThumbnailInfo, + }, + TransactionId, UInt, +}; + +#[cfg(feature = "image_proc")] +use crate::ImageError; + +/// 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, +} + +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 [`AttachmentConfig::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 [`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 a `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 + /// [`AttachmentConfig::new()`] and + /// [`AttachmentConfig::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. +/// +/// 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, 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).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)?); +/// +/// let (thumbnail_data, thumbnail_info) = generate_image_thumbnail( +/// &mime::IMAGE_JPEG, +/// &mut image, +/// None +/// )?; +/// 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()?; +/// +/// if let Some(room) = client.get_joined_room(&room_id) { +/// room.send_attachment( +/// "My favorite cat", +/// &mime::IMAGE_JPEG, +/// &mut image, +/// config, +/// ).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::ImageFormat::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 Cursor::new(&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()), + }, + )) +} diff --git a/crates/matrix-sdk/src/client.rs b/crates/matrix-sdk/src/client.rs index c529dc002..8d1e964eb 100644 --- a/crates/matrix-sdk/src/client.rs +++ b/crates/matrix-sdk/src/client.rs @@ -43,12 +43,12 @@ 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}, 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}, @@ -69,11 +69,12 @@ 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}, http_client::{client_with_config, HttpClient}, - room, Error, Result, + room, Account, Error, Result, }; /// A conservative upload speed of 1Mbps @@ -231,7 +232,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?; @@ -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 @@ -398,161 +426,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. @@ -1622,7 +1503,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)?; @@ -2321,42 +2202,94 @@ 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, - )), }) } } @@ -2367,13 +2300,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}; @@ -2412,6 +2339,10 @@ pub(crate) mod test { use super::{Client, Session, Url}; use crate::{ + attachment::{ + AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, + Thumbnail, + }, config::{ClientConfig, RequestConfig, SyncSettings}, HttpError, RoomMember, }; @@ -2447,7 +2378,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 +2400,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) @@ -3194,6 +3125,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(); @@ -3222,12 +3158,200 @@ 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, AttachmentConfig::new()) + .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 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, config).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 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, config).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 mut thumbnail_reader = Cursor::new("Thumbnail"); + + let config = AttachmentConfig::with_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)), + }), + }) + .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, config).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/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..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}, @@ -367,14 +368,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 +408,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 +447,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()); @@ -499,14 +500,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 { @@ -730,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?; @@ -754,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/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 552bd11b2..2d1c4a48b 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -36,6 +36,9 @@ 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"); + pub use bytes; pub use matrix_sdk_base::{ media, Room as BaseRoom, RoomInfo, RoomMember as BaseRoomMember, RoomType, Session, @@ -46,6 +49,9 @@ pub use reqwest; #[doc(no_inline)] pub use ruma; +mod account; +/// Types and traits for attachments. +pub mod attachment; mod client; pub mod config; mod error; @@ -59,7 +65,10 @@ mod sync; #[cfg(feature = "encryption")] pub mod encryption; +pub use account::Account; 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; 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 diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index 979e513d8..87bdeb2f0 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -1,6 +1,11 @@ +#[cfg(feature = "image_proc")] +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")] @@ -31,7 +36,14 @@ use tracing::debug; #[cfg(feature = "encryption")] use tracing::instrument; -use crate::{error::HttpResult, room::Common, BaseRoom, Client, Result, RoomType}; +#[cfg(feature = "image_proc")] +use crate::{attachment::generate_image_thumbnail, error::ImageError}; +use crate::{ + attachment::{AttachmentConfig, 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,15 +609,13 @@ impl Joined { /// * `reader` - A `Reader` that will be used to fetch the raw bytes of the /// media. /// - /// * `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}; + /// # use matrix_sdk::{Client, ruma::room_id, attachment::AttachmentConfig}; /// # use url::Url; /// # use mime; /// # use futures::executor::block_on; @@ -621,29 +631,127 @@ impl Joined { /// "My favorite cat", /// &mime::IMAGE_JPEG, /// &mut image, - /// None, + /// AttachmentConfig::new(), /// ).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, - txn_id: Option<&TransactionId>, + config: AttachmentConfig<'_, T>, + ) -> Result { + 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 { + #[cfg(not(feature = "image_proc"))] + let thumbnail = Thumbnail::NONE; + + #[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()?; + + cursor = Cursor::new(thumbnail_data); + Some(Thumbnail { + reader: &mut cursor, + content_type: &mime::IMAGE_JPEG, + info: Some(thumbnail_info), + }) + } + Err( + 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 + } + } + + /// 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 + /// [`Client::upload()`](#Client::method.upload) and afterwards the + /// [`send()`](#method.send). + /// + /// # 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. + /// + /// * `config` - Metadata and configuration for the attachment. + async fn prepare_and_send_attachment( + &self, + body: &str, + content_type: &Mime, + reader: &mut R, + config: AttachmentConfig<'_, T>, ) -> 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, + config.info, + config.thumbnail, + ) + .await? } else { - self.client.prepare_attachment_message(body, content_type, reader).await? + self.client + .prepare_attachment_message( + body, + content_type, + reader, + config.info, + config.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, config.info, config.thumbnail) + .await?; - self.send(RoomMessageEventContent::new(content), txn_id).await + self.send(RoomMessageEventContent::new(content), config.txn_id).await } /// Send a room state event to the homeserver.