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] 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.