From c67d5afaf48ccb8cd1fd414c8feb5649b729dd61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 15 Feb 2022 17:42:12 +0100 Subject: [PATCH 01/12] feat(sdk): Add method to get homeserver capabilities --- crates/matrix-sdk/src/client.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/matrix-sdk/src/client.rs b/crates/matrix-sdk/src/client.rs index c529dc002..07fa7891c 100644 --- a/crates/matrix-sdk/src/client.rs +++ b/crates/matrix-sdk/src/client.rs @@ -43,6 +43,7 @@ use ruma::{ client::{ r0::{ account::{register, whoami}, + capabilities::{get_capabilities, Capabilities}, device::{delete_devices, get_devices}, directory::{get_public_rooms, get_public_rooms_filtered}, filter::{create_filter::Request as FilterUploadRequest, FilterDefinition}, @@ -346,6 +347,33 @@ impl Client { .await } + /// Get the capabilities of the homeserver. + /// + /// This method should be used to check what features are supported by the + /// homeserver. + /// + /// # Example + /// ```no_run + /// # use futures::executor::block_on; + /// # use matrix_sdk::Client; + /// # use url::Url; + /// # block_on(async { + /// # let homeserver = Url::parse("http://example.com")?; + /// let client = Client::new(homeserver).await?; + /// + /// let capabilities = client.get_capabilities().await?; + /// + /// if capabilities.change_password.enabled { + /// // Change password + /// } + /// + /// # Result::<_, anyhow::Error>::Ok(()) }); + /// ``` + pub async fn get_capabilities(&self) -> HttpResult { + let res = self.send(get_capabilities::Request::new(), None).await?; + Ok(res.capabilities) + } + /// Process a [transaction] received from the homeserver /// /// # Arguments From 0436780292858e8163fe3ef3d2906e909d714012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 21 Dec 2021 20:40:31 +0100 Subject: [PATCH 02/12] feat(sdk): Allow to add info and thumbnail to attachments --- crates/matrix-sdk/examples/image_bot.rs | 6 +- crates/matrix-sdk/src/attachment.rs | 154 ++++++++++++ crates/matrix-sdk/src/client.rs | 320 ++++++++++++++++++++++-- crates/matrix-sdk/src/encryption/mod.rs | 94 ++++++- crates/matrix-sdk/src/lib.rs | 2 + crates/matrix-sdk/src/room/joined.rs | 34 ++- 6 files changed, 575 insertions(+), 35 deletions(-) create mode 100644 crates/matrix-sdk/src/attachment.rs diff --git a/crates/matrix-sdk/examples/image_bot.rs b/crates/matrix-sdk/examples/image_bot.rs index 103b1cc10..2398d2032 100644 --- a/crates/matrix-sdk/examples/image_bot.rs +++ b/crates/matrix-sdk/examples/image_bot.rs @@ -9,6 +9,7 @@ use std::{ use matrix_sdk::{ self, + attachment::Thumbnail, config::SyncSettings, room::Room, ruma::events::room::message::{ @@ -38,8 +39,11 @@ async fn on_room_message(event: SyncRoomMessageEvent, room: Room, image: Arc> = None; - room.send_attachment("cat", &mime::IMAGE_JPEG, &mut *image, None).await.unwrap(); + room.send_attachment("cat", &mime::IMAGE_JPEG, &mut *image, None, none_thumbnail, None) + .await + .unwrap(); image.seek(SeekFrom::Start(0)).unwrap(); diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs new file mode 100644 index 000000000..a33c5556f --- /dev/null +++ b/crates/matrix-sdk/src/attachment.rs @@ -0,0 +1,154 @@ +use std::io::Read; + +use ruma::{ + assign, + events::room::{ + message::{AudioInfo, FileInfo, VideoInfo}, + ImageInfo, ThumbnailInfo, + }, + UInt, +}; + +/// Base metadata about an image. +#[derive(Debug, Clone)] +pub struct BaseImageInfo { + /// The height of the image in pixels. + pub height: Option, + /// The width of the image in pixels. + pub width: Option, + /// The file size of the image in bytes. + pub size: Option, + /// The [BlurHash](https://blurha.sh/) for this image. + pub blurhash: Option, +} + +/// Base metadata about a video. +#[derive(Debug, Clone)] +pub struct BaseVideoInfo { + /// The duration of the video in milliseconds. + pub duration: Option, + /// The height of the video in pixels. + pub height: Option, + /// The width of the video in pixels. + pub width: Option, + /// The file size of the video in bytes. + pub size: Option, + /// The [BlurHash](https://blurha.sh/) for this video. + pub blurhash: Option, +} + +/// Base metadata about an audio clip. +#[derive(Debug, Clone)] +pub struct BaseAudioInfo { + /// The duration of the audio clip in milliseconds. + pub duration: Option, + /// The file size of the audio clip in bytes. + pub size: Option, +} + +/// Base metadata about a file. +#[derive(Debug, Clone)] +pub struct BaseFileInfo { + /// The size of the file in bytes. + pub size: Option, +} + +/// Types of metadata for an attachment. +#[derive(Debug)] +pub enum AttachmentInfo { + /// The metadata of an image. + Image(BaseImageInfo), + /// The metadata of a video. + Video(BaseVideoInfo), + /// The metadata of an audio clip. + Audio(BaseAudioInfo), + /// The metadata of a file. + File(BaseFileInfo), +} + +impl From for ImageInfo { + fn from(info: AttachmentInfo) -> Self { + match info { + AttachmentInfo::Image(info) => assign!(ImageInfo::new(), { + height: info.height, + width: info.width, + size: info.size, + blurhash: info.blurhash, + }), + _ => ImageInfo::new(), + } + } +} + +impl From for VideoInfo { + fn from(info: AttachmentInfo) -> Self { + match info { + AttachmentInfo::Video(info) => assign!(VideoInfo::new(), { + duration: info.duration, + height: info.height, + width: info.width, + size: info.size, + blurhash: info.blurhash, + }), + _ => VideoInfo::new(), + } + } +} + +impl From for AudioInfo { + fn from(info: AttachmentInfo) -> Self { + match info { + AttachmentInfo::Audio(info) => assign!(AudioInfo::new(), { + duration: info.duration, + size: info.size, + }), + _ => AudioInfo::new(), + } + } +} + +impl From for FileInfo { + fn from(info: AttachmentInfo) -> Self { + match info { + AttachmentInfo::File(info) => assign!(FileInfo::new(), { + size: info.size, + }), + _ => FileInfo::new(), + } + } +} + +#[derive(Debug, Clone)] +/// Base metadata about a thumbnail. +pub struct BaseThumbnailInfo { + /// The height of the thumbnail in pixels. + pub height: Option, + /// The width of the thumbnail in pixels. + pub width: Option, + /// The file size of the thumbnail in bytes. + pub size: Option, +} + +impl From for ThumbnailInfo { + fn from(info: BaseThumbnailInfo) -> Self { + assign!(ThumbnailInfo::new(), { + height: info.height, + width: info.width, + size: info.size, + }) + } +} + +/// A thumbnail to upload and send for an attachment. +#[derive(Debug)] +pub struct Thumbnail<'a, R: Read> { + /// A `Reader` that will be used to fetch the raw bytes of the thumbnail. + pub reader: &'a mut R, + /// The type of the thumbnail, this will be used as the content-type header. + pub content_type: &'a mime::Mime, + /// The metadata of the thumbnail. + pub info: Option, +} + +/// Typed `None` for an `>`. +pub const NONE_THUMBNAIL: Option> = None; diff --git a/crates/matrix-sdk/src/client.rs b/crates/matrix-sdk/src/client.rs index 5493e9e9a..4e77d643c 100644 --- a/crates/matrix-sdk/src/client.rs +++ b/crates/matrix-sdk/src/client.rs @@ -69,6 +69,7 @@ use tracing::{error, info, instrument, warn}; use url::Url; use crate::{ + attachment::{AttachmentInfo, Thumbnail}, config::{ClientConfig, RequestConfig}, error::{HttpError, HttpResult}, event_handler::{EventHandler, EventHandlerData, EventHandlerResult, EventKind, SyncEvent}, @@ -2321,42 +2322,95 @@ impl Client { } /// Upload the file to be read from `reader` and construct an attachment - /// message with `body` and the specified `content_type`. - pub(crate) async fn prepare_attachment_message( + /// message with `body`, `content_type`, `info` and `thumbnail`. + pub(crate) async fn prepare_attachment_message( &self, body: &str, content_type: &Mime, reader: &mut R, + info: Option, + thumbnail: Option>, ) -> Result { + let (thumbnail_url, thumbnail_info) = if let Some(thumbnail) = thumbnail { + let response = self.upload(thumbnail.content_type, thumbnail.reader).await?; + let url = response.content_uri; + + use ruma::events::room::ThumbnailInfo; + let thumbnail_info = assign!( + thumbnail.info.as_ref().map(|info| ThumbnailInfo::from(info.clone())).unwrap_or_default(), + { mimetype: Some(thumbnail.content_type.as_ref().to_owned()) } + ); + + (Some(url), Some(Box::new(thumbnail_info))) + } else { + (None, None) + }; + let response = self.upload(content_type, reader).await?; let url = response.content_uri; - use ruma::events::room::message; + use ruma::events::room::{self, message}; Ok(match content_type.type_() { mime::IMAGE => { // TODO create a thumbnail using the image crate?. + let info = assign!( + info.map(room::ImageInfo::from).unwrap_or_default(), + { + mimetype: Some(content_type.as_ref().to_owned()), + thumbnail_url, + thumbnail_info + } + ); message::MessageType::Image(message::ImageMessageEventContent::plain( body.to_owned(), url, - None, + Some(Box::new(info)), + )) + } + mime::AUDIO => { + let info = assign!( + info.map(message::AudioInfo::from).unwrap_or_default(), + { + mimetype: Some(content_type.as_ref().to_owned()), + } + ); + message::MessageType::Audio(message::AudioMessageEventContent::plain( + body.to_owned(), + url, + Some(Box::new(info)), + )) + } + mime::VIDEO => { + let info = assign!( + info.map(message::VideoInfo::from).unwrap_or_default(), + { + mimetype: Some(content_type.as_ref().to_owned()), + thumbnail_url, + thumbnail_info + } + ); + message::MessageType::Video(message::VideoMessageEventContent::plain( + body.to_owned(), + url, + Some(Box::new(info)), + )) + } + _ => { + let info = assign!( + info.map(message::FileInfo::from).unwrap_or_default(), + { + mimetype: Some(content_type.as_ref().to_owned()), + thumbnail_url, + thumbnail_info + } + ); + message::MessageType::File(message::FileMessageEventContent::plain( + body.to_owned(), + url, + Some(Box::new(info)), )) } - mime::AUDIO => message::MessageType::Audio(message::AudioMessageEventContent::plain( - body.to_owned(), - url, - None, - )), - mime::VIDEO => message::MessageType::Video(message::VideoMessageEventContent::plain( - body.to_owned(), - url, - None, - )), - _ => message::MessageType::File(message::FileMessageEventContent::plain( - body.to_owned(), - url, - None, - )), }) } } @@ -2406,6 +2460,10 @@ pub(crate) mod test { use super::{Client, Session, Url}; use crate::{ + attachment::{ + AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, Thumbnail, + NONE_THUMBNAIL, + }, config::{ClientConfig, RequestConfig, SyncSettings}, HttpError, RoomMember, }; @@ -3188,6 +3246,11 @@ pub(crate) mod test { let _m = mock("PUT", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/send/".to_string())) .with_status(200) .match_header("authorization", "Bearer 1234") + .match_body(Matcher::PartialJson(json!({ + "info": { + "mimetype": "image/jpeg" + } + }))) .with_body(test_json::EVENT_ID.to_string()) .create(); @@ -3216,12 +3279,227 @@ pub(crate) mod test { let mut media = Cursor::new("Hello world"); - let response = - room.send_attachment("image", &mime::IMAGE_JPEG, &mut media, None).await.unwrap(); + let response = room + .send_attachment("image", &mime::IMAGE_JPEG, &mut media, None, NONE_THUMBNAIL, None) + .await + .unwrap(); assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) } + #[async_test] + async fn room_attachment_send_info() { + let client = logged_in_client().await; + + let _m = mock("PUT", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/send/".to_string())) + .with_status(200) + .match_header("authorization", "Bearer 1234") + .match_body(Matcher::PartialJson(json!({ + "info": { + "mimetype": "image/jpeg", + "h": 600, + "w": 800, + } + }))) + .with_body(test_json::EVENT_ID.to_string()) + .create(); + + let upload_mock = mock("POST", Matcher::Regex(r"^/_matrix/media/r0/upload".to_string())) + .with_status(200) + .match_header("content-type", "image/jpeg") + .with_body( + json!({ + "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + }) + .to_string(), + ) + .create(); + + let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string())) + .with_status(200) + .match_header("authorization", "Bearer 1234") + .with_body(test_json::SYNC.to_string()) + .create(); + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + let mut media = Cursor::new("Hello world"); + + let info = AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(600)), + width: Some(uint!(800)), + size: None, + blurhash: None, + }); + + let response = room + .send_attachment( + "image", + &mime::IMAGE_JPEG, + &mut media, + Some(info), + NONE_THUMBNAIL, + None, + ) + .await + .unwrap(); + + upload_mock.assert(); + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) + } + + #[async_test] + async fn room_attachment_send_wrong_info() { + let client = logged_in_client().await; + + let _m = mock("PUT", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/send/".to_string())) + .with_status(200) + .match_header("authorization", "Bearer 1234") + .match_body(Matcher::PartialJson(json!({ + "info": { + "mimetype": "image/jpeg", + "h": 600, + "w": 800, + } + }))) + .with_body(test_json::EVENT_ID.to_string()) + .create(); + + let _m = mock("POST", Matcher::Regex(r"^/_matrix/media/r0/upload".to_string())) + .with_status(200) + .match_header("content-type", "image/jpeg") + .with_body( + json!({ + "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + }) + .to_string(), + ) + .create(); + + let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string())) + .with_status(200) + .match_header("authorization", "Bearer 1234") + .with_body(test_json::SYNC.to_string()) + .create(); + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + let mut media = Cursor::new("Hello world"); + + let info = AttachmentInfo::Video(BaseVideoInfo { + height: Some(uint!(600)), + width: Some(uint!(800)), + duration: Some(uint!(3600)), + size: None, + blurhash: None, + }); + + let response = room + .send_attachment( + "image", + &mime::IMAGE_JPEG, + &mut media, + Some(info), + NONE_THUMBNAIL, + None, + ) + .await; + + assert!(response.is_err()) + } + + #[async_test] + async fn room_attachment_send_info_thumbnail() { + let client = logged_in_client().await; + + let _m = mock("PUT", Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/send/".to_string())) + .with_status(200) + .match_header("authorization", "Bearer 1234") + .match_body(Matcher::PartialJson(json!({ + "info": { + "mimetype": "image/jpeg", + "h": 600, + "w": 800, + "thumbnail_info": { + "h": 360, + "w": 480, + "mimetype":"image/jpeg", + "size": 3600, + }, + "thumbnail_url": "mxc://example.com/AQwafuaFswefuhsfAFAgsw", + } + }))) + .with_body(test_json::EVENT_ID.to_string()) + .create(); + + let upload_mock = mock("POST", Matcher::Regex(r"^/_matrix/media/r0/upload".to_string())) + .with_status(200) + .match_header("content-type", "image/jpeg") + .with_body( + json!({ + "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + }) + .to_string(), + ) + .expect(2) + .create(); + + let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string())) + .with_status(200) + .match_header("authorization", "Bearer 1234") + .with_body(test_json::SYNC.to_string()) + .create(); + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); + + let mut media = Cursor::new("Hello world"); + + let info = AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(600)), + width: Some(uint!(800)), + size: None, + blurhash: None, + }); + + let mut thumbnail_reader = Cursor::new("Thumbnail"); + let thumbnail = Thumbnail { + reader: &mut thumbnail_reader, + content_type: &mime::IMAGE_JPEG, + info: Some(BaseThumbnailInfo { + height: Some(uint!(360)), + width: Some(uint!(480)), + size: Some(uint!(3600)), + }), + }; + + let response = room + .send_attachment( + "image", + &mime::IMAGE_JPEG, + &mut media, + Some(info), + Some(thumbnail), + None, + ) + .await + .unwrap(); + + upload_mock.assert(); + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) + } + #[async_test] async fn room_redact() { let client = logged_in_client().await; diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 90c05e667..376594b90 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -283,6 +283,7 @@ use ruma::{ use tracing::{debug, instrument, trace, warn}; use crate::{ + attachment::{AttachmentInfo, Thumbnail}, encryption::{ identities::{Device, UserDevices}, verification::{SasVerification, Verification, VerificationRequest}, @@ -729,14 +730,45 @@ impl Client { } /// Encrypt and upload the file to be read from `reader` and construct an - /// attachment message with `body` and the specified `content_type`. + /// attachment message with `body`, `content_type`, `info` and `thumbnail`. #[cfg(feature = "encryption")] - pub(crate) async fn prepare_encrypted_attachment_message( + pub(crate) async fn prepare_encrypted_attachment_message( &self, body: &str, content_type: &mime::Mime, reader: &mut R, + info: Option, + thumbnail: Option>, ) -> Result { + let (thumbnail_file, thumbnail_info) = + if let Some(thumbnail) = thumbnail { + let mut reader = matrix_sdk_base::crypto::AttachmentEncryptor::new(thumbnail.reader); + + let response = self.upload(thumbnail.content_type, &mut reader).await?; + + let file: ruma::events::room::EncryptedFile = { + let keys = reader.finish(); + ruma::events::room::EncryptedFileInit { + url: response.content_uri, + key: keys.web_key, + iv: keys.iv, + hashes: keys.hashes, + v: keys.version, + } + .into() + }; + + use ruma::events::room::ThumbnailInfo; + let thumbnail_info = assign!( + thumbnail.info.as_ref().map(|info| ThumbnailInfo::from(info.clone())).unwrap_or_default(), + { mimetype: Some(thumbnail.content_type.as_ref().to_owned()) } + ); + + (Some(Box::new(file)), Some(Box::new(thumbnail_info))) + } else { + (None, None) + }; + let mut reader = matrix_sdk_base::crypto::AttachmentEncryptor::new(reader); let response = self.upload(content_type, &mut reader).await?; @@ -753,18 +785,66 @@ impl Client { .into() }; - use ruma::events::room::message; + use ruma::events::room::{self, message}; Ok(match content_type.type_() { mime::IMAGE => { - message::MessageType::Image(message::ImageMessageEventContent::encrypted(body.to_owned(), file)) + let info = assign!( + info.map(room::ImageInfo::from).unwrap_or_default(), + { + mimetype: Some(content_type.as_ref().to_owned()), + thumbnail_file, + thumbnail_info + } + ); + let content = assign!( + message::ImageMessageEventContent::encrypted(body.to_owned(), file), + { info: Some(Box::new(info)) } + ); + message::MessageType::Image(content) } mime::AUDIO => { - message::MessageType::Audio(message::AudioMessageEventContent::encrypted(body.to_owned(), file)) + let info = assign!( + info.map(message::AudioInfo::from).unwrap_or_default(), + { + mimetype: Some(content_type.as_ref().to_owned()), + } + ); + let content = assign!( + message::AudioMessageEventContent::encrypted(body.to_owned(), file), + { info: Some(Box::new(info)) } + ); + message::MessageType::Audio(content) } mime::VIDEO => { - message::MessageType::Video(message::VideoMessageEventContent::encrypted(body.to_owned(), file)) + let info = assign!( + info.map(message::VideoInfo::from).unwrap_or_default(), + { + mimetype: Some(content_type.as_ref().to_owned()), + thumbnail_file, + thumbnail_info + } + ); + let content = assign!( + message::VideoMessageEventContent::encrypted(body.to_owned(), file), + { info: Some(Box::new(info)) } + ); + message::MessageType::Video(content) + } + _ => { + let info = assign!( + info.map(message::FileInfo::from).unwrap_or_default(), + { + mimetype: Some(content_type.as_ref().to_owned()), + thumbnail_file, + thumbnail_info + } + ); + let content = assign!( + message::FileMessageEventContent::encrypted(body.to_owned(), file), + { info: Some(Box::new(info)) } + ); + message::MessageType::File(content) } - _ => message::MessageType::File(message::FileMessageEventContent::encrypted(body.to_owned(), file)), }) } diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 552bd11b2..48b7df5f8 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -46,6 +46,8 @@ pub use reqwest; #[doc(no_inline)] pub use ruma; +/// Types and traits for attachments. +pub mod attachment; mod client; pub mod config; mod error; diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index 979e513d8..c9e059a56 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -31,7 +31,12 @@ use tracing::debug; #[cfg(feature = "encryption")] use tracing::instrument; -use crate::{error::HttpResult, room::Common, BaseRoom, Client, Result, RoomType}; +use crate::{ + attachment::{AttachmentInfo, Thumbnail}, + error::HttpResult, + room::Common, + BaseRoom, Client, Result, RoomType, +}; const TYPING_NOTICE_TIMEOUT: Duration = Duration::from_secs(4); const TYPING_NOTICE_RESEND_TIMEOUT: Duration = Duration::from_secs(3); @@ -597,6 +602,12 @@ impl Joined { /// * `reader` - A `Reader` that will be used to fetch the raw bytes of the /// media. /// + /// * `info` - The metadata of the media. If the + /// `AttachmentInfo` type doesn't match the `content_type`, it is ignored. + /// + /// * `thumbnail` - The thumbnail of the media. If the `content_type` does + /// not support it (eg audio clips), it is ignored. + /// /// * `txn_id` - A unique ID that can be attached to a `MessageEvent` /// held in its unsigned field as `transaction_id`. If not given one is /// created for the message. @@ -605,7 +616,7 @@ impl Joined { /// /// ```no_run /// # use std::{path::PathBuf, fs::File, io::Read}; - /// # use matrix_sdk::{Client, ruma::room_id}; + /// # use matrix_sdk::{Client, ruma::room_id, attachment::NONE_THUMBNAIL}; /// # use url::Url; /// # use mime; /// # use futures::executor::block_on; @@ -622,26 +633,37 @@ impl Joined { /// &mime::IMAGE_JPEG, /// &mut image, /// None, + /// NONE_THUMBNAIL, + /// None, /// ).await?; /// } /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` - pub async fn send_attachment( + pub async fn send_attachment( &self, body: &str, content_type: &Mime, reader: &mut R, + info: Option, + thumbnail: Option>, txn_id: Option<&TransactionId>, ) -> Result { #[cfg(feature = "encryption")] let content = if self.is_encrypted() { - self.client.prepare_encrypted_attachment_message(body, content_type, reader).await? + self.client + .prepare_encrypted_attachment_message(body, content_type, reader, info, thumbnail) + .await? } else { - self.client.prepare_attachment_message(body, content_type, reader).await? + self.client + .prepare_attachment_message(body, content_type, reader, info, thumbnail) + .await? }; #[cfg(not(feature = "encryption"))] - let content = self.client.prepare_attachment_message(body, content_type, reader).await?; + let content = self + .client + .prepare_attachment_message(body, content_type, reader, info, thumbnail) + .await?; self.send(RoomMessageEventContent::new(content), txn_id).await } From 5dc7dfd4c86a0449fd4ce814084c0578bd7dd301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 28 Dec 2021 18:51:30 +0100 Subject: [PATCH 03/12] feat(sdk): Add method to generate thumbnails from images --- crates/matrix-sdk/Cargo.toml | 25 +++++- crates/matrix-sdk/README.md | 2 + crates/matrix-sdk/src/attachment.rs | 131 ++++++++++++++++++++++++++++ crates/matrix-sdk/src/error.rs | 22 +++++ crates/matrix-sdk/src/lib.rs | 8 ++ 5 files changed, 187 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 2adc756dc..c3177ac79 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -36,13 +36,16 @@ rustls-tls = ["reqwest/rustls-tls"] socks = ["reqwest/socks"] sso_login = ["warp", "rand", "tokio-stream"] appservice = ["ruma/appservice-api-s", "ruma/appservice-api-helper"] +image_proc = ["image"] +image_rayon = ["image/jpeg_rayon"] docsrs = [ "encryption", "sled_cryptostore", "sled_state_store", "sso_login", - "qrcode" + "qrcode", + "image_proc", ] [dependencies] @@ -66,6 +69,26 @@ url = "2.2.2" zeroize = "1.3.0" async-stream = "0.3.2" +[dependencies.image] +version = "0.23.14" +default-features = false +features = [ + "gif", + "jpeg", + "ico", + "png", + "pnm", + "tga", + "tiff", + "webp", + "bmp", + "hdr", + "dxt", + "dds", + "farbfeld", +] +optional = true + [dependencies.matrix-sdk-base] version = "0.4.0" path = "../matrix-sdk-base" diff --git a/crates/matrix-sdk/README.md b/crates/matrix-sdk/README.md index 0d2a98dc0..634f0a935 100644 --- a/crates/matrix-sdk/README.md +++ b/crates/matrix-sdk/README.md @@ -64,6 +64,8 @@ The following crate feature flags are available: | `anyhow` | No | Better logging for event handlers that return `anyhow::Result` | | `encryption` | Yes | End-to-end encryption support | | `eyre` | No | Better logging for event handlers that return `eyre::Result` | +| `image_proc` | No | Enables image processing to generate thumbnails | +| `image_rayon` | No | Enables faster image processing | | `markdown` | No | Support to send Markdown-formatted messages | | `qrcode` | Yes | QR code verification support | | `sled_cryptostore` | Yes | Persistent storage for E2EE related data | diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index a33c5556f..369223389 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -1,5 +1,9 @@ use std::io::Read; +#[cfg(feature = "image_proc")] +use std::io::{BufRead, Seek}; +#[cfg(feature = "image_proc")] +use image::GenericImageView; use ruma::{ assign, events::room::{ @@ -9,6 +13,9 @@ use ruma::{ UInt, }; +#[cfg(feature = "image_proc")] +use crate::ImageError; + /// Base metadata about an image. #[derive(Debug, Clone)] pub struct BaseImageInfo { @@ -152,3 +159,127 @@ pub struct Thumbnail<'a, R: Read> { /// Typed `None` for an `>`. pub const NONE_THUMBNAIL: Option> = None; + +/// Generate a thumbnail for an image. +/// +/// This is a convenience method that uses the +/// [image](https://github.com/image-rs/image) crate. +/// +/// # Arguments +/// * `content_type` - The type of the media, this will be used as the +/// content-type header. +/// +/// * `reader` - A `Reader` that will be used to fetch the raw bytes of the +/// media. +/// +/// * `size` - The size of the thumbnail in pixels as a `(width, height)` tuple. +/// If set to `None`, defaults to `(800, 600)`. +/// +/// # Examples +/// +/// ```no_run +/// # use std::{path::PathBuf, fs::File, io::{BufReader, Read, Seek}}; +/// # use matrix_sdk::{Client, attachment::{Thumbnail, generate_image_thumbnail}, ruma::room_id}; +/// # use url::Url; +/// # use mime; +/// # use futures::executor::block_on; +/// # block_on(async { +/// # let homeserver = Url::parse("http://localhost:8080")?; +/// # let mut client = Client::new(homeserver)?; +/// # let room_id = room_id!("!test:localhost"); +/// let path = PathBuf::from("/home/example/my-cat.jpg"); +/// let mut image = BufReader::new(File::open(path)?); +/// +/// let (thumbnail_data, thumbnail_info) = generate_image_thumbnail( +/// &mime::IMAGE_JPEG, +/// &mut image, +/// None +/// )?; +/// let thumbnail = Thumbnail { +/// reader: &mut thumbnail_data.as_slice(), +/// content_type: &mime::IMAGE_JPEG, +/// info: Some(thumbnail_info), +/// }; +/// +/// image.rewind()?; +/// +/// if let Some(room) = client.get_joined_room(&room_id) { +/// room.send_attachment( +/// "My favorite cat", +/// &mime::IMAGE_JPEG, +/// &mut image, +/// None, +/// Some(thumbnail), +/// None, +/// ).await?; +/// } +/// # Result::<_, matrix_sdk::Error>::Ok(()) }); +/// ``` +#[cfg(feature = "image_proc")] +pub fn generate_image_thumbnail( + content_type: &mime::Mime, + reader: &mut R, + size: Option<(u32, u32)>, +) -> Result<(Vec, BaseThumbnailInfo), ImageError> { + let image_format = image_format_from_mime_type(content_type); + if image_format.is_none() { + return Err(ImageError::FormatNotSupported); + } + + let image_format = image_format.unwrap(); + + let image = image::load(reader, image_format)?; + let (original_width, original_height) = image.dimensions(); + + let (width, height) = size.unwrap_or((800, 600)); + + // Don't generate a thumbnail if it would be bigger than or equal to the + // original. + if height >= original_height && width >= original_width { + return Err(ImageError::ThumbnailBiggerThanOriginal); + } + + let thumbnail = image.thumbnail(width, height); + let (thumbnail_width, thumbnail_height) = thumbnail.dimensions(); + + let mut data: Vec = vec![]; + thumbnail.write_to(&mut data, image_format)?; + let data_size = data.len() as u32; + + Ok(( + data, + BaseThumbnailInfo { + width: Some(thumbnail_width.into()), + height: Some(thumbnail_height.into()), + size: Some(data_size.into()), + }, + )) +} + +// FIXME: Replace this method by ImageFormat::from_mime_type after "image" +// crate's next release. +/// Return the image format specified by a MIME type. +#[cfg(feature = "image_proc")] +fn image_format_from_mime_type(mime_type: M) -> Option +where + M: AsRef, +{ + match mime_type.as_ref() { + "image/avif" => Some(image::ImageFormat::Avif), + "image/jpeg" => Some(image::ImageFormat::Jpeg), + "image/png" => Some(image::ImageFormat::Png), + "image/gif" => Some(image::ImageFormat::Gif), + "image/webp" => Some(image::ImageFormat::WebP), + "image/tiff" => Some(image::ImageFormat::Tiff), + "image/x-targa" | "image/x-tga" => Some(image::ImageFormat::Tga), + "image/vnd-ms.dds" => Some(image::ImageFormat::Dds), + "image/bmp" => Some(image::ImageFormat::Bmp), + "image/x-icon" => Some(image::ImageFormat::Ico), + "image/vnd.radiance" => Some(image::ImageFormat::Hdr), + "image/x-portable-bitmap" + | "image/x-portable-graymap" + | "image/x-portable-pixmap" + | "image/x-portable-anymap" => Some(image::ImageFormat::Pnm), + _ => None, + } +} diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 423563984..00a0e7595 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -157,6 +157,11 @@ pub enum Error { /// An error encountered when trying to parse a user tag name. #[error(transparent)] UserTagName(#[from] InvalidUserTagName), + + /// An error while processing images. + #[cfg(feature = "image_proc")] + #[error(transparent)] + ImageError(#[from] ImageError), } /// Error for the room key importing functionality. @@ -257,3 +262,20 @@ impl From for Error { Error::Http(HttpError::Reqwest(e)) } } + +/// All possible errors that can happen during image processing. +#[cfg(feature = "image_proc")] +#[derive(Error, Debug)] +pub enum ImageError { + /// Error processing the image data. + #[error(transparent)] + Proc(#[from] image::ImageError), + + /// The image format is not supported. + #[error("the image format is not supported")] + FormatNotSupported, + + /// The thumbnail size is bigger than the original image. + #[error("the thumbnail size is bigger than the original image size")] + ThumbnailBiggerThanOriginal, +} diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 48b7df5f8..9860e8a3a 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -36,6 +36,12 @@ compile_error!("only one of 'native-tls' or 'rustls-tls' features can be enabled #[cfg(all(feature = "sso_login", target_arch = "wasm32"))] compile_error!("'sso_login' cannot be enabled on 'wasm32' arch"); +#[cfg(all(feature = "image_rayon", target_arch = "wasm32"))] +compile_error!("'image_rayon' cannot be enabled on 'wasm32' arch"); + +#[cfg(all(feature = "image_rayon", not(feature = "image_proc")))] +compile_error!("'image_rayon' only works with 'image_proc' feature"); + pub use bytes; pub use matrix_sdk_base::{ media, Room as BaseRoom, RoomInfo, RoomMember as BaseRoomMember, RoomType, Session, @@ -62,6 +68,8 @@ mod sync; pub mod encryption; pub use client::{Client, LoopCtrl}; +#[cfg(feature = "image_proc")] +pub use error::ImageError; pub use error::{Error, HttpError, HttpResult, Result}; pub use http_client::HttpSend; pub use room_member::RoomMember; From 78489391d2dcee9a674c190fca1a2c3282446286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 28 Dec 2021 18:51:56 +0100 Subject: [PATCH 04/12] feat(sdk): Add method to send attachments with generated thumbnails --- crates/matrix-sdk/src/client.rs | 1 - crates/matrix-sdk/src/room/joined.rs | 89 ++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/client.rs b/crates/matrix-sdk/src/client.rs index 4e77d643c..7278f7b9b 100644 --- a/crates/matrix-sdk/src/client.rs +++ b/crates/matrix-sdk/src/client.rs @@ -2353,7 +2353,6 @@ impl Client { use ruma::events::room::{self, message}; Ok(match content_type.type_() { mime::IMAGE => { - // TODO create a thumbnail using the image crate?. let info = assign!( info.map(room::ImageInfo::from).unwrap_or_default(), { diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index c9e059a56..fbfab6a79 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "image_proc")] +use std::io::{BufReader, Seek}; #[cfg(feature = "encryption")] use std::sync::Arc; use std::{io::Read, ops::Deref}; @@ -31,6 +33,8 @@ use tracing::debug; #[cfg(feature = "encryption")] use tracing::instrument; +#[cfg(feature = "image_proc")] +use crate::attachment::generate_image_thumbnail; use crate::{ attachment::{AttachmentInfo, Thumbnail}, error::HttpResult, @@ -668,6 +672,91 @@ impl Joined { self.send(RoomMessageEventContent::new(content), txn_id).await } + /// Send an attachment with a generated thumbnail to this room. + /// + /// This is a convenience method that calls the + /// [`attachment::generate_image_thumbnail()`] and afterwards the + /// [`send_attachment()`](#method.send_attachment). + /// + /// Thumbnails can only be generated for supported image attachments. For + /// more information, see the [image](https://github.com/image-rs/image) + /// crate. + /// + /// If the thumbnail generation fails, this will return an + /// [`ImageError`](../enum.ImageError.html). + /// + /// # Arguments + /// * `body` - A textual representation of the media that is going to be + /// uploaded. Usually the file name. + /// + /// * `content_type` - The type of the media, this will be used as the + /// content-type header. + /// + /// * `reader` - A `Reader` that will be used to fetch the raw bytes of the + /// media. + /// + /// * `info` - The metadata of the media. If the + /// `AttachmentInfo` type doesn't match the `content_type`, it is ignored. + /// + /// * `thumbnail_size` - The size of the thumbnail in pixels as a + /// `(width, height)` tuple. If set to `None`, defaults to `(800, 600)`. + /// + /// * `txn_id` - A unique `Uuid` that can be attached to a `MessageEvent` + /// held in its unsigned field as `transaction_id`. If not given one is + /// created for the message. + /// + /// # Examples + /// + /// ```no_run + /// # use std::{path::PathBuf, fs::File, io::Read}; + /// # use matrix_sdk::{Client, ruma::room_id}; + /// # use url::Url; + /// # use mime; + /// # use futures::executor::block_on; + /// # block_on(async { + /// # let homeserver = Url::parse("http://localhost:8080")?; + /// # let mut client = Client::new(homeserver)?; + /// # let room_id = room_id!("!test:localhost"); + /// let path = PathBuf::from("/home/example/my-cat.jpg"); + /// let mut image = File::open(path)?; + /// + /// if let Some(room) = client.get_joined_room(&room_id) { + /// room.send_attachment_with_generated_thumbnail( + /// "My favorite cat", + /// &mime::IMAGE_JPEG, + /// &mut image, + /// None, + /// None, + /// None, + /// ).await?; + /// } + /// # Result::<_, matrix_sdk::Error>::Ok(()) }); + /// ``` + /// [`attachment::generate_image_thumbnail()`]: + /// ../attachment/fn.generate_image_thumbnail.html + #[cfg(feature = "image_proc")] + pub async fn send_attachment_with_generated_thumbnail( + &self, + body: &str, + content_type: &Mime, + reader: &mut R, + info: Option, + thumbnail_size: Option<(u32, u32)>, + txn_id: Option, + ) -> Result { + let mut reader = BufReader::new(reader); + + let (thumbnail_data, thumbnail_info) = + generate_image_thumbnail(content_type, &mut reader, thumbnail_size)?; + let thumbnail = Thumbnail { + reader: &mut thumbnail_data.as_slice(), + content_type: &mime::IMAGE_JPEG, + info: Some(thumbnail_info), + }; + reader.rewind()?; + self.send_attachment(body, content_type, &mut reader, info, Some(thumbnail), txn_id).await + } + /// Send a room state event to the homeserver. /// /// Returns the parsed response from the server. From e926d7e9282590dca9ebecdd6c6ce3159a0891e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sun, 6 Feb 2022 18:23:15 +0100 Subject: [PATCH 05/12] chore: Update image dependency --- crates/matrix-sdk/Cargo.toml | 2 +- crates/matrix-sdk/src/attachment.rs | 34 +++------------------------- crates/matrix-sdk/src/room/joined.rs | 2 +- 3 files changed, 5 insertions(+), 33 deletions(-) diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index c3177ac79..57cab1619 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -70,7 +70,7 @@ zeroize = "1.3.0" async-stream = "0.3.2" [dependencies.image] -version = "0.23.14" +version = "0.24.0" default-features = false features = [ "gif", diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index 369223389..955bb6947 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -1,6 +1,6 @@ use std::io::Read; #[cfg(feature = "image_proc")] -use std::io::{BufRead, Seek}; +use std::io::{BufRead, Cursor, Seek}; #[cfg(feature = "image_proc")] use image::GenericImageView; @@ -221,7 +221,7 @@ pub fn generate_image_thumbnail( reader: &mut R, size: Option<(u32, u32)>, ) -> Result<(Vec, BaseThumbnailInfo), ImageError> { - let image_format = image_format_from_mime_type(content_type); + let image_format = image::ImageFormat::from_mime_type(content_type); if image_format.is_none() { return Err(ImageError::FormatNotSupported); } @@ -243,7 +243,7 @@ pub fn generate_image_thumbnail( let (thumbnail_width, thumbnail_height) = thumbnail.dimensions(); let mut data: Vec = vec![]; - thumbnail.write_to(&mut data, image_format)?; + thumbnail.write_to(&mut Cursor::new(&mut data), image_format)?; let data_size = data.len() as u32; Ok(( @@ -255,31 +255,3 @@ pub fn generate_image_thumbnail( }, )) } - -// FIXME: Replace this method by ImageFormat::from_mime_type after "image" -// crate's next release. -/// Return the image format specified by a MIME type. -#[cfg(feature = "image_proc")] -fn image_format_from_mime_type(mime_type: M) -> Option -where - M: AsRef, -{ - match mime_type.as_ref() { - "image/avif" => Some(image::ImageFormat::Avif), - "image/jpeg" => Some(image::ImageFormat::Jpeg), - "image/png" => Some(image::ImageFormat::Png), - "image/gif" => Some(image::ImageFormat::Gif), - "image/webp" => Some(image::ImageFormat::WebP), - "image/tiff" => Some(image::ImageFormat::Tiff), - "image/x-targa" | "image/x-tga" => Some(image::ImageFormat::Tga), - "image/vnd-ms.dds" => Some(image::ImageFormat::Dds), - "image/bmp" => Some(image::ImageFormat::Bmp), - "image/x-icon" => Some(image::ImageFormat::Ico), - "image/vnd.radiance" => Some(image::ImageFormat::Hdr), - "image/x-portable-bitmap" - | "image/x-portable-graymap" - | "image/x-portable-pixmap" - | "image/x-portable-anymap" => Some(image::ImageFormat::Pnm), - _ => None, - } -} diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index fbfab6a79..653f2374f 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -742,7 +742,7 @@ impl Joined { reader: &mut R, info: Option, thumbnail_size: Option<(u32, u32)>, - txn_id: Option, + txn_id: Option<&TransactionId>, ) -> Result { let mut reader = BufReader::new(reader); From c2c9d5ecc0e3f7cb4d7476a84ddae85f7763635a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sun, 6 Feb 2022 18:33:21 +0100 Subject: [PATCH 06/12] fix(sdk): Enable image_proc with image_rayon --- crates/matrix-sdk/Cargo.toml | 2 +- crates/matrix-sdk/src/lib.rs | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 57cab1619..fa5b13bd4 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -37,7 +37,7 @@ socks = ["reqwest/socks"] sso_login = ["warp", "rand", "tokio-stream"] appservice = ["ruma/appservice-api-s", "ruma/appservice-api-helper"] image_proc = ["image"] -image_rayon = ["image/jpeg_rayon"] +image_rayon = ["image_proc", "image/jpeg_rayon"] docsrs = [ "encryption", diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 9860e8a3a..678edae5a 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -39,9 +39,6 @@ compile_error!("'sso_login' cannot be enabled on 'wasm32' arch"); #[cfg(all(feature = "image_rayon", target_arch = "wasm32"))] compile_error!("'image_rayon' cannot be enabled on 'wasm32' arch"); -#[cfg(all(feature = "image_rayon", not(feature = "image_proc")))] -compile_error!("'image_rayon' only works with 'image_proc' feature"); - pub use bytes; pub use matrix_sdk_base::{ media, Room as BaseRoom, RoomInfo, RoomMember as BaseRoomMember, RoomType, Session, From 409afc668400c62007d5d10bf55bb0ae89b9eb52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 8 Feb 2022 08:50:15 +0100 Subject: [PATCH 07/12] feat(sdk): Create AttachmentConfig struct --- .../src/file_encryption/attachments.rs | 8 +- crates/matrix-sdk/examples/image_bot.rs | 5 +- crates/matrix-sdk/src/attachment.rs | 133 ++++++++++-- crates/matrix-sdk/src/client.rs | 71 ++----- crates/matrix-sdk/src/room/joined.rs | 201 +++++++++--------- 5 files changed, 250 insertions(+), 168 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs b/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs index ca571c9d6..fff72b69c 100644 --- a/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs +++ b/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs @@ -147,7 +147,7 @@ impl<'a, R: Read + 'a> AttachmentDecryptor<'a, R> { } /// A wrapper that transparently encrypts anything that implements `Read`. -pub struct AttachmentEncryptor<'a, R: Read + 'a> { +pub struct AttachmentEncryptor<'a, R: Read + ?Sized + 'a> { finished: bool, inner: &'a mut R, web_key: JsonWebKey, @@ -157,7 +157,7 @@ pub struct AttachmentEncryptor<'a, R: Read + 'a> { sha: Sha256, } -impl<'a, R: 'a + Read + std::fmt::Debug> std::fmt::Debug for AttachmentEncryptor<'a, R> { +impl<'a, R: 'a + Read + std::fmt::Debug + ?Sized> std::fmt::Debug for AttachmentEncryptor<'a, R> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("AttachmentEncryptor") .field("inner", &self.inner) @@ -166,7 +166,7 @@ impl<'a, R: 'a + Read + std::fmt::Debug> std::fmt::Debug for AttachmentEncryptor } } -impl<'a, R: Read + 'a> Read for AttachmentEncryptor<'a, R> { +impl<'a, R: Read + ?Sized + 'a> Read for AttachmentEncryptor<'a, R> { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let read_bytes = self.inner.read(buf)?; @@ -185,7 +185,7 @@ impl<'a, R: Read + 'a> Read for AttachmentEncryptor<'a, R> { } } -impl<'a, R: Read + 'a> AttachmentEncryptor<'a, R> { +impl<'a, R: Read + ?Sized + 'a> AttachmentEncryptor<'a, R> { /// Wrap the given reader encrypting all the data we read from it. /// /// After all the reads are done, and all the data is encrypted that we wish diff --git a/crates/matrix-sdk/examples/image_bot.rs b/crates/matrix-sdk/examples/image_bot.rs index 2398d2032..abf7281e7 100644 --- a/crates/matrix-sdk/examples/image_bot.rs +++ b/crates/matrix-sdk/examples/image_bot.rs @@ -9,7 +9,7 @@ use std::{ use matrix_sdk::{ self, - attachment::Thumbnail, + attachment::AttachmentConfig, config::SyncSettings, room::Room, ruma::events::room::message::{ @@ -39,9 +39,8 @@ async fn on_room_message(event: SyncRoomMessageEvent, room: Room, image: Arc> = None; - room.send_attachment("cat", &mime::IMAGE_JPEG, &mut *image, None, none_thumbnail, None) + room.send_attachment("cat", &mime::IMAGE_JPEG, &mut *image, AttachmentConfig::new()) .await .unwrap(); diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index 955bb6947..82d800677 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -10,7 +10,7 @@ use ruma::{ message::{AudioInfo, FileInfo, VideoInfo}, ImageInfo, ThumbnailInfo, }, - UInt, + TransactionId, UInt, }; #[cfg(feature = "image_proc")] @@ -157,8 +157,114 @@ pub struct Thumbnail<'a, R: Read> { pub info: Option, } -/// Typed `None` for an `>`. -pub const NONE_THUMBNAIL: Option> = None; +impl Thumbnail<'static, &'static [u8]> { + /// Typed `None` for an `>`. + pub const NONE: Option> = None; +} + +/// Configuration for sending an attachment. +#[derive(Debug)] +pub struct AttachmentConfig<'a, R: Read> { + pub(crate) txn_id: Option<&'a TransactionId>, + pub(crate) info: Option, + pub(crate) thumbnail: Option>, + #[cfg(feature = "image_proc")] + pub(crate) generate_thumbnail: bool, + #[cfg(feature = "image_proc")] + pub(crate) thumbnail_size: Option<(u32, u32)>, +} + +impl AttachmentConfig<'static, &'static [u8]> { + /// Create a new default `AttachmentConfig` without providing a thumbnail. + /// + /// To provide a thumbnail use [`with_thumbnail()`]. + pub fn new() -> Self { + Self { + txn_id: Default::default(), + info: Default::default(), + thumbnail: None, + #[cfg(feature = "image_proc")] + generate_thumbnail: Default::default(), + #[cfg(feature = "image_proc")] + thumbnail_size: Default::default(), + } + } + + /// Generate the thumbnail to send for this media. + /// + /// Uses [`attachment::generate_image_thumbnail()`]. + /// + /// Thumbnails can only be generated for supported image attachments. For + /// more information, see the [image](https://github.com/image-rs/image) + /// crate. + /// + /// # Arguments + /// + /// * `size` - The size of the thumbnail in pixels as a `(width, height)` + /// tuple. If set to `None`, defaults to `(800, 600)`. + #[cfg(feature = "image_proc")] + #[must_use] + pub fn generate_thumbnail(mut self, size: Option<(u32, u32)>) -> Self { + self.generate_thumbnail = true; + self.thumbnail_size = size; + self + } +} + +impl Default for AttachmentConfig<'static, &'static [u8]> { + fn default() -> Self { + Self::new() + } +} + +impl<'a, R: Read> AttachmentConfig<'a, R> { + /// Create a new default `AttachmentConfig` with `thumbnail`. + /// + /// # Arguments + /// + /// * `thumbnail` - The thumbnail of the media. If the `content_type` does + /// not support it (eg audio clips), it is ignored. + /// + /// To generate automatically a thumbnail from an image, use + /// [`new()`] and + /// [`generate_thumbnail()`]. + pub fn with_thumbnail(thumbnail: Thumbnail<'a, R>) -> Self { + Self { + txn_id: Default::default(), + info: Default::default(), + thumbnail: Some(thumbnail), + #[cfg(feature = "image_proc")] + generate_thumbnail: Default::default(), + #[cfg(feature = "image_proc")] + thumbnail_size: Default::default(), + } + } + + /// Set the transaction ID to send. + /// + /// # Arguments + /// + /// * `txn_id` - A unique ID that can be attached to a `MessageEvent` held + /// in its unsigned field as `transaction_id`. If not given, one is created + /// for the message. + #[must_use] + pub fn txn_id(mut self, txn_id: &'a TransactionId) -> Self { + self.txn_id = Some(txn_id); + self + } + + /// Set the media metadata to send. + /// + /// # Arguments + /// + /// * `info` - The metadata of the media. If the `AttachmentInfo` type + /// doesn't match the `content_type`, it is ignored. + #[must_use] + pub fn info(mut self, info: AttachmentInfo) -> Self { + self.info = Some(info); + self + } +} /// Generate a thumbnail for an image. /// @@ -178,14 +284,18 @@ pub const NONE_THUMBNAIL: Option> = None; /// # Examples /// /// ```no_run -/// # use std::{path::PathBuf, fs::File, io::{BufReader, Read, Seek}}; -/// # use matrix_sdk::{Client, attachment::{Thumbnail, generate_image_thumbnail}, ruma::room_id}; +/// # use std::{path::PathBuf, fs::File, io::{BufReader, Cursor, Read, Seek}}; +/// # use matrix_sdk::{ +/// # Client, +/// # attachment::{AttachmentConfig, Thumbnail, generate_image_thumbnail}, +/// # ruma::room_id +/// # }; /// # use url::Url; /// # use mime; /// # use futures::executor::block_on; /// # block_on(async { /// # let homeserver = Url::parse("http://localhost:8080")?; -/// # let mut client = Client::new(homeserver)?; +/// # let mut client = Client::new(homeserver).await?; /// # let room_id = room_id!("!test:localhost"); /// let path = PathBuf::from("/home/example/my-cat.jpg"); /// let mut image = BufReader::new(File::open(path)?); @@ -195,11 +305,12 @@ pub const NONE_THUMBNAIL: Option> = None; /// &mut image, /// None /// )?; -/// let thumbnail = Thumbnail { -/// reader: &mut thumbnail_data.as_slice(), +/// let mut cursor = Cursor::new(thumbnail_data); +/// let config = AttachmentConfig::with_thumbnail(Thumbnail { +/// reader: &mut cursor, /// content_type: &mime::IMAGE_JPEG, /// info: Some(thumbnail_info), -/// }; +/// }); /// /// image.rewind()?; /// @@ -208,9 +319,7 @@ pub const NONE_THUMBNAIL: Option> = None; /// "My favorite cat", /// &mime::IMAGE_JPEG, /// &mut image, -/// None, -/// Some(thumbnail), -/// None, +/// config, /// ).await?; /// } /// # Result::<_, matrix_sdk::Error>::Ok(()) }); diff --git a/crates/matrix-sdk/src/client.rs b/crates/matrix-sdk/src/client.rs index 7278f7b9b..51aa85e59 100644 --- a/crates/matrix-sdk/src/client.rs +++ b/crates/matrix-sdk/src/client.rs @@ -1623,7 +1623,7 @@ impl Client { pub async fn upload( &self, content_type: &Mime, - reader: &mut impl Read, + reader: &mut (impl Read + ?Sized), ) -> Result { let mut data = Vec::new(); reader.read_to_end(&mut data)?; @@ -2460,8 +2460,8 @@ pub(crate) mod test { use super::{Client, Session, Url}; use crate::{ attachment::{ - AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, Thumbnail, - NONE_THUMBNAIL, + AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, + Thumbnail, }, config::{ClientConfig, RequestConfig, SyncSettings}, HttpError, RoomMember, @@ -3279,7 +3279,7 @@ pub(crate) mod test { let mut media = Cursor::new("Hello world"); let response = room - .send_attachment("image", &mime::IMAGE_JPEG, &mut media, None, NONE_THUMBNAIL, None) + .send_attachment("image", &mime::IMAGE_JPEG, &mut media, AttachmentConfig::new()) .await .unwrap(); @@ -3328,24 +3328,15 @@ pub(crate) mod test { let mut media = Cursor::new("Hello world"); - let info = AttachmentInfo::Image(BaseImageInfo { + let config = AttachmentConfig::new().info(AttachmentInfo::Image(BaseImageInfo { height: Some(uint!(600)), width: Some(uint!(800)), size: None, blurhash: None, - }); + })); - let response = room - .send_attachment( - "image", - &mime::IMAGE_JPEG, - &mut media, - Some(info), - NONE_THUMBNAIL, - None, - ) - .await - .unwrap(); + let response = + room.send_attachment("image", &mime::IMAGE_JPEG, &mut media, config).await.unwrap(); upload_mock.assert(); assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) @@ -3393,24 +3384,15 @@ pub(crate) mod test { let mut media = Cursor::new("Hello world"); - let info = AttachmentInfo::Video(BaseVideoInfo { + let config = AttachmentConfig::new().info(AttachmentInfo::Video(BaseVideoInfo { height: Some(uint!(600)), width: Some(uint!(800)), duration: Some(uint!(3600)), size: None, blurhash: None, - }); + })); - let response = room - .send_attachment( - "image", - &mime::IMAGE_JPEG, - &mut media, - Some(info), - NONE_THUMBNAIL, - None, - ) - .await; + let response = room.send_attachment("image", &mime::IMAGE_JPEG, &mut media, config).await; assert!(response.is_err()) } @@ -3465,15 +3447,9 @@ pub(crate) mod test { let mut media = Cursor::new("Hello world"); - let info = AttachmentInfo::Image(BaseImageInfo { - height: Some(uint!(600)), - width: Some(uint!(800)), - size: None, - blurhash: None, - }); - let mut thumbnail_reader = Cursor::new("Thumbnail"); - let thumbnail = Thumbnail { + + let config = AttachmentConfig::with_thumbnail(Thumbnail { reader: &mut thumbnail_reader, content_type: &mime::IMAGE_JPEG, info: Some(BaseThumbnailInfo { @@ -3481,19 +3457,16 @@ pub(crate) mod test { width: Some(uint!(480)), size: Some(uint!(3600)), }), - }; + }) + .info(AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(600)), + width: Some(uint!(800)), + size: None, + blurhash: None, + })); - let response = room - .send_attachment( - "image", - &mime::IMAGE_JPEG, - &mut media, - Some(info), - Some(thumbnail), - None, - ) - .await - .unwrap(); + let response = + room.send_attachment("image", &mime::IMAGE_JPEG, &mut media, config).await.unwrap(); upload_mock.assert(); assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index 653f2374f..f499e0d85 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -1,8 +1,11 @@ #[cfg(feature = "image_proc")] -use std::io::{BufReader, Seek}; +use std::io::Cursor; #[cfg(feature = "encryption")] use std::sync::Arc; -use std::{io::Read, ops::Deref}; +use std::{ + io::{BufReader, Read, Seek}, + ops::Deref, +}; use matrix_sdk_common::instant::{Duration, Instant}; #[cfg(feature = "encryption")] @@ -34,9 +37,9 @@ use tracing::debug; use tracing::instrument; #[cfg(feature = "image_proc")] -use crate::attachment::generate_image_thumbnail; +use crate::{attachment::generate_image_thumbnail, error::ImageError}; use crate::{ - attachment::{AttachmentInfo, Thumbnail}, + attachment::{AttachmentConfig, Thumbnail}, error::HttpResult, room::Common, BaseRoom, Client, Result, RoomType, @@ -606,21 +609,13 @@ impl Joined { /// * `reader` - A `Reader` that will be used to fetch the raw bytes of the /// media. /// - /// * `info` - The metadata of the media. If the - /// `AttachmentInfo` type doesn't match the `content_type`, it is ignored. - /// - /// * `thumbnail` - The thumbnail of the media. If the `content_type` does - /// not support it (eg audio clips), it is ignored. - /// - /// * `txn_id` - A unique ID that can be attached to a `MessageEvent` - /// held in its unsigned field as `transaction_id`. If not given one is - /// created for the message. + /// * `config` - Metadata and configuration for the attachment. /// /// # Examples /// /// ```no_run /// # use std::{path::PathBuf, fs::File, io::Read}; - /// # use matrix_sdk::{Client, ruma::room_id, attachment::NONE_THUMBNAIL}; + /// # use matrix_sdk::{Client, ruma::room_id, attachment::AttachmentConfig}; /// # use url::Url; /// # use mime; /// # use futures::executor::block_on; @@ -636,54 +631,82 @@ impl Joined { /// "My favorite cat", /// &mime::IMAGE_JPEG, /// &mut image, - /// None, - /// NONE_THUMBNAIL, - /// None, + /// AttachmentConfig::new(), /// ).await?; /// } /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` - pub async fn send_attachment( + pub async fn send_attachment<'a, R: Read + Seek, T: Read>( &self, body: &str, content_type: &Mime, reader: &mut R, - info: Option, - thumbnail: Option>, - txn_id: Option<&TransactionId>, + config: AttachmentConfig<'a, T>, ) -> Result { - #[cfg(feature = "encryption")] - let content = if self.is_encrypted() { - self.client - .prepare_encrypted_attachment_message(body, content_type, reader, info, thumbnail) - .await? + let reader = &mut BufReader::new(reader); + + #[cfg(feature = "image_proc")] + let mut cursor; + + if config.thumbnail.is_some() { + self.prepare_and_send_attachment(body, content_type, reader, config).await } else { - self.client - .prepare_attachment_message(body, content_type, reader, info, thumbnail) - .await? - }; + #[cfg(not(feature = "image_proc"))] + let thumbnail = Thumbnail::NONE; - #[cfg(not(feature = "encryption"))] - let content = self - .client - .prepare_attachment_message(body, content_type, reader, info, thumbnail) - .await?; + #[cfg(feature = "image_proc")] + let thumbnail = if config.generate_thumbnail { + match generate_image_thumbnail(content_type, reader, config.thumbnail_size) { + Ok((thumbnail_data, thumbnail_info)) => { + reader.rewind()?; - self.send(RoomMessageEventContent::new(content), txn_id).await + cursor = Cursor::new(thumbnail_data); + Some(Thumbnail { + reader: &mut cursor, + content_type: &mime::IMAGE_JPEG, + info: Some(thumbnail_info), + }) + } + Err(error) + if matches!( + error, + ImageError::ThumbnailBiggerThanOriginal + | ImageError::FormatNotSupported + ) => + { + reader.rewind()?; + None + } + Err(error) => return Err(error.into()), + } + } else { + None + }; + + let config = AttachmentConfig { + txn_id: config.txn_id, + info: config.info, + thumbnail, + #[cfg(feature = "image_proc")] + generate_thumbnail: false, + #[cfg(feature = "image_proc")] + thumbnail_size: None, + }; + + self.prepare_and_send_attachment(body, content_type, reader, config).await + } } - /// Send an attachment with a generated thumbnail to this room. + /// Prepare and send an attachment to this room. + /// + /// This will upload the given data that the reader produces using the + /// [`upload()`](#method.upload) method and post an event to the given room. + /// If the room is encrypted and the encryption feature is enabled the + /// upload will be encrypted. /// /// This is a convenience method that calls the - /// [`attachment::generate_image_thumbnail()`] and afterwards the - /// [`send_attachment()`](#method.send_attachment). - /// - /// Thumbnails can only be generated for supported image attachments. For - /// more information, see the [image](https://github.com/image-rs/image) - /// crate. - /// - /// If the thumbnail generation fails, this will return an - /// [`ImageError`](../enum.ImageError.html). + /// [`Client::upload()`](#Client::method.upload) and afterwards the + /// [`send()`](#method.send). /// /// # Arguments /// * `body` - A textual representation of the media that is going to be @@ -695,66 +718,44 @@ impl Joined { /// * `reader` - A `Reader` that will be used to fetch the raw bytes of the /// media. /// - /// * `info` - The metadata of the media. If the - /// `AttachmentInfo` type doesn't match the `content_type`, it is ignored. - /// - /// * `thumbnail_size` - The size of the thumbnail in pixels as a - /// `(width, height)` tuple. If set to `None`, defaults to `(800, 600)`. - /// - /// * `txn_id` - A unique `Uuid` that can be attached to a `MessageEvent` - /// held in its unsigned field as `transaction_id`. If not given one is - /// created for the message. - /// - /// # Examples - /// - /// ```no_run - /// # use std::{path::PathBuf, fs::File, io::Read}; - /// # use matrix_sdk::{Client, ruma::room_id}; - /// # use url::Url; - /// # use mime; - /// # use futures::executor::block_on; - /// # block_on(async { - /// # let homeserver = Url::parse("http://localhost:8080")?; - /// # let mut client = Client::new(homeserver)?; - /// # let room_id = room_id!("!test:localhost"); - /// let path = PathBuf::from("/home/example/my-cat.jpg"); - /// let mut image = File::open(path)?; - /// - /// if let Some(room) = client.get_joined_room(&room_id) { - /// room.send_attachment_with_generated_thumbnail( - /// "My favorite cat", - /// &mime::IMAGE_JPEG, - /// &mut image, - /// None, - /// None, - /// None, - /// ).await?; - /// } - /// # Result::<_, matrix_sdk::Error>::Ok(()) }); - /// ``` - /// [`attachment::generate_image_thumbnail()`]: - /// ../attachment/fn.generate_image_thumbnail.html - #[cfg(feature = "image_proc")] - pub async fn send_attachment_with_generated_thumbnail( + /// * `config` - Metadata and configuration for the attachment. + async fn prepare_and_send_attachment<'a, R: Read, T: Read>( &self, body: &str, content_type: &Mime, reader: &mut R, - info: Option, - thumbnail_size: Option<(u32, u32)>, - txn_id: Option<&TransactionId>, + config: AttachmentConfig<'a, T>, ) -> Result { - let mut reader = BufReader::new(reader); - - let (thumbnail_data, thumbnail_info) = - generate_image_thumbnail(content_type, &mut reader, thumbnail_size)?; - let thumbnail = Thumbnail { - reader: &mut thumbnail_data.as_slice(), - content_type: &mime::IMAGE_JPEG, - info: Some(thumbnail_info), + #[cfg(feature = "encryption")] + let content = if self.is_encrypted() { + self.client + .prepare_encrypted_attachment_message( + body, + content_type, + reader, + config.info, + config.thumbnail, + ) + .await? + } else { + self.client + .prepare_attachment_message( + body, + content_type, + reader, + config.info, + config.thumbnail, + ) + .await? }; - reader.rewind()?; - self.send_attachment(body, content_type, &mut reader, info, Some(thumbnail), txn_id).await + + #[cfg(not(feature = "encryption"))] + let content = self + .client + .prepare_attachment_message(body, content_type, reader, config.info, config.thumbnail) + .await?; + + self.send(RoomMessageEventContent::new(content), config.txn_id).await } /// Send a room state event to the homeserver. From ebd913f50f633d2c97f6b57fde5b47e02558154e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 15 Feb 2022 18:05:19 +0100 Subject: [PATCH 08/12] fix(sdk): Fix dead links in AttachmentConfig docs --- crates/matrix-sdk/src/attachment.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index 82d800677..21b2b7fbb 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -177,7 +177,7 @@ pub struct AttachmentConfig<'a, R: Read> { impl AttachmentConfig<'static, &'static [u8]> { /// Create a new default `AttachmentConfig` without providing a thumbnail. /// - /// To provide a thumbnail use [`with_thumbnail()`]. + /// To provide a thumbnail use [`AttachmentConfig::with_thumbnail()`]. pub fn new() -> Self { Self { txn_id: Default::default(), @@ -192,7 +192,7 @@ impl AttachmentConfig<'static, &'static [u8]> { /// Generate the thumbnail to send for this media. /// - /// Uses [`attachment::generate_image_thumbnail()`]. + /// Uses [`generate_image_thumbnail()`]. /// /// Thumbnails can only be generated for supported image attachments. For /// more information, see the [image](https://github.com/image-rs/image) @@ -218,7 +218,7 @@ impl Default for AttachmentConfig<'static, &'static [u8]> { } impl<'a, R: Read> AttachmentConfig<'a, R> { - /// Create a new default `AttachmentConfig` with `thumbnail`. + /// Create a new default `AttachmentConfig` with a `thumbnail`. /// /// # Arguments /// @@ -226,8 +226,8 @@ impl<'a, R: Read> AttachmentConfig<'a, R> { /// not support it (eg audio clips), it is ignored. /// /// To generate automatically a thumbnail from an image, use - /// [`new()`] and - /// [`generate_thumbnail()`]. + /// [`AttachmentConfig::new()`] and + /// [`AttachmentConfig::generate_thumbnail()`]. pub fn with_thumbnail(thumbnail: Thumbnail<'a, R>) -> Self { Self { txn_id: Default::default(), From de8aa7b4f707cf847bfc2df8040bdc0f14aef7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 15 Feb 2022 18:11:03 +0100 Subject: [PATCH 09/12] fix(sdk): Simplify code in room::Joined::send_attachment --- crates/matrix-sdk/src/room/joined.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index f499e0d85..ac72455d6 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -667,13 +667,9 @@ impl Joined { info: Some(thumbnail_info), }) } - Err(error) - if matches!( - error, - ImageError::ThumbnailBiggerThanOriginal - | ImageError::FormatNotSupported - ) => - { + Err( + ImageError::ThumbnailBiggerThanOriginal | ImageError::FormatNotSupported, + ) => { reader.rewind()?; None } From 69b0b04e7019bd85be777ad5c1164ab509102e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 15 Feb 2022 18:43:56 +0100 Subject: [PATCH 10/12] fix(sdk): Add ruma feature for blurhash support --- crates/matrix-sdk/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index fa5b13bd4..aafcd3357 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -101,7 +101,7 @@ default_features = false [dependencies.ruma] git = "https://github.com/ruma/ruma/" rev = "b9f32bc6327542d382d4eb42ec43623495c50e66" -features = ["client-api-c", "compat", "rand"] +features = ["client-api-c", "compat", "rand", "unstable-msc2448"] [dependencies.tokio-stream] version = "0.1.6" From 9e545c34ba83682c9a2e856845ea54af7d32b6ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 15 Feb 2022 19:00:39 +0100 Subject: [PATCH 11/12] fix(sdk): Fix clippy warning --- crates/matrix-sdk/src/room/joined.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index ac72455d6..87bdeb2f0 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -636,12 +636,12 @@ impl Joined { /// } /// # Result::<_, matrix_sdk::Error>::Ok(()) }); /// ``` - pub async fn send_attachment<'a, R: Read + Seek, T: Read>( + pub async fn send_attachment( &self, body: &str, content_type: &Mime, reader: &mut R, - config: AttachmentConfig<'a, T>, + config: AttachmentConfig<'_, T>, ) -> Result { let reader = &mut BufReader::new(reader); @@ -715,12 +715,12 @@ impl Joined { /// media. /// /// * `config` - Metadata and configuration for the attachment. - async fn prepare_and_send_attachment<'a, R: Read, T: Read>( + async fn prepare_and_send_attachment( &self, body: &str, content_type: &Mime, reader: &mut R, - config: AttachmentConfig<'a, T>, + config: AttachmentConfig<'_, T>, ) -> Result { #[cfg(feature = "encryption")] let content = if self.is_encrypted() { From 2f7d271c167466daaecd9288ada3e4d9e2c6ff80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 15 Feb 2022 19:15:06 +0100 Subject: [PATCH 12/12] fix(sdk): Add license to attachment.rs --- crates/matrix-sdk/src/attachment.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index 21b2b7fbb..591aaf6f1 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -1,3 +1,17 @@ +// Copyright 2022 Kévin Commaille +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + use std::io::Read; #[cfg(feature = "image_proc")] use std::io::{BufRead, Cursor, Seek};