diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 165ce75bdab..b0771bcd2e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,7 @@ jobs: - markdown - socks - sso-login + - image-proc steps: - name: Checkout diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ccc9d6cf5c3..17149f1af42 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -102,7 +102,7 @@ jobs: run: | rustup run stable cargo tarpaulin \ --skip-clean --profile cov --out xml \ - --features experimental-widgets,testing + --features experimental-widgets,testing,image-proc env: CARGO_PROFILE_COV_INHERITS: 'dev' CARGO_PROFILE_COV_DEBUG: 1 diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 16e8f112e6d..40bdad98b97 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -11,6 +11,10 @@ Breaking changes: `Media::get_file`/`Media::remove_file`/`Media::get_thumbnail`/`Media::remove_thumbnail` - A custom sliding sync proxy set with `ClientBuilder::sliding_sync_proxy` now takes precedence over a discovered proxy. - `Client::get_profile` was moved to `Account` and renamed to `Account::fetch_user_profile_of`. `Account::get_profile` was renamed to `Account::fetch_user_profile`. +- `generate_image_thumbnail` now returns a `Thumbnail`. +- It is now possible to select the format of a generated thumbnail. + - `generate_image_thumbnail` takes a `ThumbnailFormat`. + - `AttachmentConfig::generate_thumbnail` takes a `ThumbnailFormat`. Additions: diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index f1f9349783b..4afbf1b6c97 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -20,6 +20,8 @@ use std::time::Duration; #[cfg(feature = "image-proc")] use image::GenericImageView; +#[cfg(feature = "image-proc")] +pub use image::ImageFormat; use ruma::{ assign, events::{ @@ -188,7 +190,7 @@ pub struct Thumbnail { } /// Configuration for sending an attachment. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct AttachmentConfig { pub(crate) txn_id: Option, pub(crate) info: Option, @@ -200,6 +202,8 @@ pub struct AttachmentConfig { pub(crate) generate_thumbnail: bool, #[cfg(feature = "image-proc")] pub(crate) thumbnail_size: Option<(u32, u32)>, + #[cfg(feature = "image-proc")] + pub(crate) thumbnail_format: ThumbnailFormat, } impl AttachmentConfig { @@ -207,18 +211,7 @@ impl AttachmentConfig { /// /// To provide a thumbnail use [`AttachmentConfig::with_thumbnail()`]. pub fn new() -> Self { - Self { - txn_id: Default::default(), - info: Default::default(), - thumbnail: None, - caption: None, - formatted_caption: None, - mentions: Default::default(), - #[cfg(feature = "image-proc")] - generate_thumbnail: Default::default(), - #[cfg(feature = "image-proc")] - thumbnail_size: Default::default(), - } + Self::default() } /// Generate the thumbnail to send for this media. @@ -229,15 +222,21 @@ impl AttachmentConfig { /// more information, see the [image](https://github.com/image-rs/image) /// crate. /// + /// If generating the thumbnail failed, the error will be logged and sending + /// the attachment will proceed without a thumbnail. + /// /// # Arguments /// /// * `size` - The size of the thumbnail in pixels as a `(width, height)` /// tuple. If set to `None`, defaults to `(800, 600)`. + /// + /// * `format` - The image format to use to encode the thumbnail. #[cfg(feature = "image-proc")] #[must_use] - pub fn generate_thumbnail(mut self, size: Option<(u32, u32)>) -> Self { + pub fn generate_thumbnail(mut self, size: Option<(u32, u32)>, format: ThumbnailFormat) -> Self { self.generate_thumbnail = true; self.thumbnail_size = size; + self.thumbnail_format = format; self } @@ -252,18 +251,7 @@ impl AttachmentConfig { /// [`AttachmentConfig::new()`] and /// [`AttachmentConfig::generate_thumbnail()`]. pub fn with_thumbnail(thumbnail: Thumbnail) -> Self { - Self { - txn_id: Default::default(), - info: Default::default(), - thumbnail: Some(thumbnail), - caption: None, - formatted_caption: None, - mentions: Default::default(), - #[cfg(feature = "image-proc")] - generate_thumbnail: Default::default(), - #[cfg(feature = "image-proc")] - thumbnail_size: Default::default(), - } + Self { thumbnail: Some(thumbnail), ..Default::default() } } /// Set the transaction ID to send. @@ -322,12 +310,6 @@ impl AttachmentConfig { } } -impl Default for AttachmentConfig { - fn default() -> Self { - Self::new() - } -} - /// Generate a thumbnail for an image. /// /// This is a convenience method that uses the @@ -335,13 +317,15 @@ impl Default for AttachmentConfig { /// /// # Arguments /// * `content_type` - The type of the media, this will be used as the -/// content-type header. +/// content-type header. /// /// * `reader` - A `Reader` that will be used to fetch the raw bytes of the -/// media. +/// media. /// /// * `size` - The size of the thumbnail in pixels as a `(width, height)` tuple. -/// If set to `None`, defaults to `(800, 600)`. +/// If set to `None`, defaults to `(800, 600)`. +/// +/// * `format` - The image format to use to encode the thumbnail. /// /// # Examples /// @@ -349,7 +333,7 @@ impl Default for AttachmentConfig { /// use std::{io::Cursor, path::PathBuf}; /// /// use matrix_sdk::attachment::{ -/// generate_image_thumbnail, AttachmentConfig, Thumbnail, +/// generate_image_thumbnail, AttachmentConfig, Thumbnail, ThumbnailFormat, /// }; /// use mime; /// # use matrix_sdk::{Client, ruma::room_id }; @@ -363,13 +347,13 @@ impl Default for AttachmentConfig { /// let image = tokio::fs::read(path).await?; /// /// let cursor = Cursor::new(&image); -/// let (thumbnail_data, thumbnail_info) = -/// generate_image_thumbnail(&mime::IMAGE_JPEG, cursor, None)?; -/// let config = AttachmentConfig::with_thumbnail(Thumbnail { -/// data: thumbnail_data, -/// content_type: mime::IMAGE_JPEG, -/// info: Some(thumbnail_info), -/// }); +/// let thumbnail = generate_image_thumbnail( +/// &mime::IMAGE_JPEG, +/// cursor, +/// None, +/// ThumbnailFormat::Original, +/// )?; +/// let config = AttachmentConfig::with_thumbnail(thumbnail); /// /// if let Some(room) = client.get_room(&room_id) { /// room.send_attachment( @@ -387,13 +371,13 @@ pub fn generate_image_thumbnail( content_type: &mime::Mime, reader: 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); - } + format: ThumbnailFormat, +) -> Result { + use std::str::FromStr; - let image_format = image_format.unwrap(); + let Some(image_format) = ImageFormat::from_mime_type(content_type) else { + return Err(ImageError::FormatNotSupported); + }; let image = image::load(reader, image_format)?; let (original_width, original_height) = image.dimensions(); @@ -409,16 +393,48 @@ pub fn generate_image_thumbnail( let thumbnail = image.thumbnail(width, height); let (thumbnail_width, thumbnail_height) = thumbnail.dimensions(); + let thumbnail_format = match format { + ThumbnailFormat::Always(format) => format, + ThumbnailFormat::Fallback(format) if !image_format.writing_enabled() => format, + ThumbnailFormat::Fallback(_) | ThumbnailFormat::Original => image_format, + }; + let mut data: Vec = vec![]; - thumbnail.write_to(&mut Cursor::new(&mut data), image_format)?; + thumbnail.write_to(&mut Cursor::new(&mut data), thumbnail_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()), - }, - )) + let content_type = mime::Mime::from_str(thumbnail_format.to_mime_type())?; + + let info = BaseThumbnailInfo { + width: Some(thumbnail_width.into()), + height: Some(thumbnail_height.into()), + size: Some(data_size.into()), + }; + + Ok(Thumbnail { data, content_type, info: Some(info) }) +} + +/// The format to use for encoding the thumbnail. +#[cfg(feature = "image-proc")] +#[derive(Debug, Default, Clone, Copy)] +pub enum ThumbnailFormat { + /// Always use this format. + /// + /// Will always return an error if this format is not writable by the + /// `image` crate. + Always(ImageFormat), + /// Try to use the same format as the original image, and fallback to this + /// one if the original format is not writable. + /// + /// Will return an error if both the original format and this format are not + /// writable by the `image` crate. + Fallback(ImageFormat), + /// Only try to use the format of the original image. + /// + /// Will return an error if the original format is not writable by the + /// `image` crate. + /// + /// This is the default. + #[default] + Original, } diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index f74e30b806b..0b74ff4737b 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -404,6 +404,10 @@ pub enum ImageError { #[error(transparent)] Proc(#[from] image::ImageError), + /// Error parsing the mimetype of the image. + #[error(transparent)] + Mime(#[from] mime::FromStrError), + /// The image format is not supported. #[error("the image format is not supported")] FormatNotSupported, diff --git a/crates/matrix-sdk/src/room/futures.rs b/crates/matrix-sdk/src/room/futures.rs index aed304f95eb..c68e441a852 100644 --- a/crates/matrix-sdk/src/room/futures.rs +++ b/crates/matrix-sdk/src/room/futures.rs @@ -35,15 +35,12 @@ use ruma::{ use tracing::{debug, info, Instrument, Span}; use super::Room; +#[cfg(feature = "image-proc")] +use crate::{attachment::generate_image_thumbnail, error::ImageError}; use crate::{ attachment::AttachmentConfig, utils::IntoRawMessageLikeEventContent, Result, TransmissionProgress, }; -#[cfg(feature = "image-proc")] -use crate::{ - attachment::{generate_image_thumbnail, Thumbnail}, - error::ImageError, -}; /// Future returned by [`Room::send`]. #[allow(missing_debug_implementations)] @@ -275,8 +272,6 @@ impl<'a> IntoFuture for SendAttachment<'a> { #[cfg(not(feature = "image-proc"))] let thumbnail = None; - #[cfg(feature = "image-proc")] - let data_slot; #[cfg(feature = "image-proc")] let (data, thumbnail) = if config.generate_thumbnail { let content_type = content_type.clone(); @@ -285,6 +280,7 @@ impl<'a> IntoFuture for SendAttachment<'a> { &content_type, Cursor::new(&data), config.thumbnail_size, + config.thumbnail_format, ); (data, res) }; @@ -298,19 +294,15 @@ impl<'a> IntoFuture for SendAttachment<'a> { let (data, res) = make_thumbnail(data); let thumbnail = match res { - Ok((thumbnail_data, thumbnail_info)) => { - data_slot = thumbnail_data; - Some(Thumbnail { - data: data_slot, - content_type: mime::IMAGE_JPEG, - info: Some(thumbnail_info), - }) + Ok(thumbnail) => Some(thumbnail), + Err(error) => { + if matches!(error, ImageError::ThumbnailBiggerThanOriginal) { + debug!("Not generating thumbnail: {error}"); + } else { + tracing::warn!("Failed to generate thumbnail: {error}"); + } + None } - Err( - ImageError::ThumbnailBiggerThanOriginal - | ImageError::FormatNotSupported, - ) => None, - Err(error) => return Err(error.into()), }; (data, thumbnail) @@ -329,6 +321,8 @@ impl<'a> IntoFuture for SendAttachment<'a> { generate_thumbnail: false, #[cfg(feature = "image-proc")] thumbnail_size: None, + #[cfg(feature = "image-proc")] + thumbnail_format: Default::default(), }; room.prepare_and_send_attachment( diff --git a/crates/matrix-sdk/tests/integration/room/attachment/matrix-rusty.jpg b/crates/matrix-sdk/tests/integration/room/attachment/matrix-rusty.jpg new file mode 100644 index 00000000000..e004351249e Binary files /dev/null and b/crates/matrix-sdk/tests/integration/room/attachment/matrix-rusty.jpg differ diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs new file mode 100644 index 00000000000..175a0378815 --- /dev/null +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -0,0 +1,521 @@ +use std::time::Duration; + +#[cfg(feature = "image-proc")] +use matrix_sdk::attachment::{ImageFormat, ThumbnailFormat}; +use matrix_sdk::{ + attachment::{ + AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, + Thumbnail, + }, + config::SyncSettings, + test_utils::logged_in_client_with_server, +}; +use matrix_sdk_test::{async_test, test_json, DEFAULT_TEST_ROOM_ID}; +use ruma::{event_id, events::Mentions, owned_user_id, uint}; +use serde_json::json; +use wiremock::{ + matchers::{body_partial_json, header, method, path, path_regex}, + Mock, ResponseTemplate, +}; + +use crate::{mock_encryption_state, mock_sync}; + +#[async_test] +async fn test_room_attachment_send() { + let (client, server) = logged_in_client_with_server().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .and(body_partial_json(json!({ + "info": { + "mimetype": "image/jpeg", + } + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/_matrix/media/r0/upload")) + .and(header("authorization", "Bearer 1234")) + .and(header("content-type", "image/jpeg")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + }))) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + mock_encryption_state(&server, false).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + + let response = room + .send_attachment( + "image", + &mime::IMAGE_JPEG, + b"Hello world".to_vec(), + AttachmentConfig::new(), + ) + .await + .unwrap(); + + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) +} + +#[async_test] +async fn test_room_attachment_send_info() { + let (client, server) = logged_in_client_with_server().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .and(body_partial_json(json!({ + "info": { + "mimetype": "image/jpeg", + "h": 600, + "w": 800, + } + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/_matrix/media/r0/upload")) + .and(header("authorization", "Bearer 1234")) + .and(header("content-type", "image/jpeg")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + }))) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + mock_encryption_state(&server, false).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + + let config = AttachmentConfig::new() + .info(AttachmentInfo::Image(BaseImageInfo { + height: Some(uint!(600)), + width: Some(uint!(800)), + size: None, + blurhash: None, + })) + .caption(Some("image caption".to_owned())); + + let response = room + .send_attachment("image.jpg", &mime::IMAGE_JPEG, b"Hello world".to_vec(), config) + .await + .unwrap(); + + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) +} + +#[async_test] +async fn test_room_attachment_send_wrong_info() { + let (client, server) = logged_in_client_with_server().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .and(body_partial_json(json!({ + "info": { + "mimetype": "image/jpeg", + "h": 600, + "w": 800, + } + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/_matrix/media/r0/upload")) + .and(header("authorization", "Bearer 1234")) + .and(header("content-type", "image/jpeg")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + }))) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + mock_encryption_state(&server, false).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + + let config = AttachmentConfig::new() + .info(AttachmentInfo::Video(BaseVideoInfo { + height: Some(uint!(600)), + width: Some(uint!(800)), + duration: Some(Duration::from_millis(3600)), + size: None, + blurhash: None, + })) + .caption(Some("image caption".to_owned())); + + let response = + room.send_attachment("image.jpg", &mime::IMAGE_JPEG, b"Hello world".to_vec(), config).await; + + response.unwrap_err(); +} + +#[async_test] +async fn test_room_attachment_send_info_thumbnail() { + let (client, server) = logged_in_client_with_server().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .and(body_partial_json(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", + } + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/_matrix/media/r0/upload")) + .and(header("authorization", "Bearer 1234")) + .and(header("content-type", "image/jpeg")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + }))) + .expect(2) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + mock_encryption_state(&server, false).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + + let config = AttachmentConfig::with_thumbnail(Thumbnail { + data: b"Thumbnail".to_vec(), + 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, b"Hello world".to_vec(), config) + .await + .unwrap(); + + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) +} + +#[async_test] +async fn test_room_attachment_send_mentions() { + let (client, server) = logged_in_client_with_server().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .and(body_partial_json(json!({ + "m.mentions": { + "user_ids": ["@user:localhost"], + } + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/_matrix/media/r0/upload")) + .and(header("authorization", "Bearer 1234")) + .and(header("content-type", "image/jpeg")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" + }))) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + mock_encryption_state(&server, false).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + + let response = room + .send_attachment( + "image", + &mime::IMAGE_JPEG, + b"Hello world".to_vec(), + AttachmentConfig::new() + .mentions(Some(Mentions::with_user_ids([owned_user_id!("@user:localhost")]))), + ) + .await + .unwrap(); + + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) +} + +#[cfg(feature = "image-proc")] +const IMAGE_BYTES: &[u8] = include_bytes!("matrix-rusty.jpg"); + +#[cfg(feature = "image-proc")] +#[async_test] +async fn test_room_attachment_generate_thumbnail_original_format() { + let (client, server) = logged_in_client_with_server().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .and(body_partial_json(json!({ + "url": "mxc://localhost/AQwafuaFswefuhsfAFAgsw", + "info": { + "mimetype": "image/jpeg", + "thumbnail_info": { + "h": 600, + "w": 600, + "mimetype":"image/jpeg", + }, + "thumbnail_url": "mxc://localhost/AQwafuaFswefuhsfAFAgsw", + } + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/_matrix/media/r0/upload")) + .and(header("authorization", "Bearer 1234")) + .and(header("content-type", "image/jpeg")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": "mxc://localhost/AQwafuaFswefuhsfAFAgsw" + }))) + .expect(2) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + mock_encryption_state(&server, false).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + + let config = AttachmentConfig::new().generate_thumbnail(None, ThumbnailFormat::Original); + + let response = room + .send_attachment("image", &mime::IMAGE_JPEG, IMAGE_BYTES.to_vec(), config) + .await + .unwrap(); + + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) +} + +#[cfg(feature = "image-proc")] +#[async_test] +async fn test_room_attachment_generate_thumbnail_always_format() { + let (client, server) = logged_in_client_with_server().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .and(body_partial_json(json!({ + "url": "mxc://localhost/original", + "info": { + "mimetype": "image/jpeg", + "thumbnail_info": { + "h": 600, + "w": 600, + "mimetype":"image/png", + }, + "thumbnail_url": "mxc://localhost/thumbnail", + } + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/_matrix/media/r0/upload")) + .and(header("authorization", "Bearer 1234")) + .and(header("content-type", "image/jpeg")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": "mxc://localhost/original" + }))) + .expect(1) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/_matrix/media/r0/upload")) + .and(header("authorization", "Bearer 1234")) + .and(header("content-type", "image/png")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": "mxc://localhost/thumbnail" + }))) + .expect(1) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + mock_encryption_state(&server, false).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + + let config = + AttachmentConfig::new().generate_thumbnail(None, ThumbnailFormat::Always(ImageFormat::Png)); + + let response = room + .send_attachment("image", &mime::IMAGE_JPEG, IMAGE_BYTES.to_vec(), config) + .await + .unwrap(); + + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) +} + +#[cfg(feature = "image-proc")] +#[async_test] +async fn test_room_attachment_generate_thumbnail_not_fallback_format() { + let (client, server) = logged_in_client_with_server().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .and(body_partial_json(json!({ + "url": "mxc://localhost/AQwafuaFswefuhsfAFAgsw", + "info": { + "mimetype": "image/jpeg", + "thumbnail_info": { + "h": 600, + "w": 600, + "mimetype":"image/jpeg", + }, + "thumbnail_url": "mxc://localhost/AQwafuaFswefuhsfAFAgsw", + } + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/_matrix/media/r0/upload")) + .and(header("authorization", "Bearer 1234")) + .and(header("content-type", "image/jpeg")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": "mxc://localhost/AQwafuaFswefuhsfAFAgsw" + }))) + .expect(2) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + mock_encryption_state(&server, false).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + + let config = AttachmentConfig::new() + .generate_thumbnail(None, ThumbnailFormat::Fallback(ImageFormat::Png)); + + let response = room + .send_attachment("image", &mime::IMAGE_JPEG, IMAGE_BYTES.to_vec(), config) + .await + .unwrap(); + + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) +} + +#[cfg(feature = "image-proc")] +#[async_test] +async fn test_room_attachment_generate_thumbnail_bigger_than_image() { + let (client, server) = logged_in_client_with_server().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .and(body_partial_json(json!({ + "url": "mxc://localhost/original", + "info": { + "mimetype": "image/jpeg", + } + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/_matrix/media/r0/upload")) + .and(header("authorization", "Bearer 1234")) + .and(header("content-type", "image/jpeg")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "content_uri": "mxc://localhost/original" + }))) + .expect(1) + .mount(&server) + .await; + + mock_sync(&server, &*test_json::SYNC, None).await; + mock_encryption_state(&server, false).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let _response = client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + + let config = + AttachmentConfig::new().generate_thumbnail(Some((1400, 1400)), ThumbnailFormat::Original); + + let response = room + .send_attachment("image", &mime::IMAGE_JPEG, IMAGE_BYTES.to_vec(), config) + .await + .unwrap(); + + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) +} diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 5c8bc1203bb..191062d40c2 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -5,10 +5,6 @@ use std::{ use futures_util::future::join_all; use matrix_sdk::{ - attachment::{ - AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, - Thumbnail, - }, config::SyncSettings, room::{Receipts, ReportedContentScore, RoomMemberRole}, }; @@ -20,15 +16,12 @@ use matrix_sdk_test::{ use ruma::{ api::client::{membership::Invite3pidInit, receipt::create_receipt::v3::ReceiptType}, assign, event_id, - events::{ - receipt::ReceiptThread, room::message::RoomMessageEventContent, Mentions, TimelineEventType, - }, - int, mxc_uri, owned_event_id, owned_user_id, room_id, thirdparty, uint, user_id, OwnedUserId, - TransactionId, + events::{receipt::ReceiptThread, room::message::RoomMessageEventContent, TimelineEventType}, + int, mxc_uri, owned_event_id, room_id, thirdparty, user_id, OwnedUserId, TransactionId, }; use serde_json::json; use wiremock::{ - matchers::{body_json, body_partial_json, header, method, path, path_regex}, + matchers::{body_json, body_partial_json, header, method, path_regex}, Mock, ResponseTemplate, }; @@ -336,279 +329,6 @@ async fn test_room_message_send() { assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) } -#[async_test] -async fn test_room_attachment_send() { - let (client, server) = logged_in_client_with_server().await; - - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .and(body_partial_json(json!({ - "info": { - "mimetype": "image/jpeg", - } - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) - .mount(&server) - .await; - - Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) - .and(header("authorization", "Bearer 1234")) - .and(header("content-type", "image/jpeg")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" - }))) - .mount(&server) - .await; - - mock_sync(&server, &*test_json::SYNC, None).await; - mock_encryption_state(&server, false).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); - - let response = room - .send_attachment( - "image", - &mime::IMAGE_JPEG, - b"Hello world".to_vec(), - AttachmentConfig::new(), - ) - .await - .unwrap(); - - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) -} - -#[async_test] -async fn test_room_attachment_send_info() { - let (client, server) = logged_in_client_with_server().await; - - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .and(body_partial_json(json!({ - "info": { - "mimetype": "image/jpeg", - "h": 600, - "w": 800, - } - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) - .mount(&server) - .await; - - Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) - .and(header("authorization", "Bearer 1234")) - .and(header("content-type", "image/jpeg")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" - }))) - .mount(&server) - .await; - - mock_sync(&server, &*test_json::SYNC, None).await; - mock_encryption_state(&server, false).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); - - let config = AttachmentConfig::new() - .info(AttachmentInfo::Image(BaseImageInfo { - height: Some(uint!(600)), - width: Some(uint!(800)), - size: None, - blurhash: None, - })) - .caption(Some("image caption".to_owned())); - - let response = room - .send_attachment("image.jpg", &mime::IMAGE_JPEG, b"Hello world".to_vec(), config) - .await - .unwrap(); - - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) -} - -#[async_test] -async fn test_room_attachment_send_wrong_info() { - let (client, server) = logged_in_client_with_server().await; - - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .and(body_partial_json(json!({ - "info": { - "mimetype": "image/jpeg", - "h": 600, - "w": 800, - } - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) - .mount(&server) - .await; - - Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) - .and(header("authorization", "Bearer 1234")) - .and(header("content-type", "image/jpeg")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" - }))) - .mount(&server) - .await; - - mock_sync(&server, &*test_json::SYNC, None).await; - mock_encryption_state(&server, false).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); - - let config = AttachmentConfig::new() - .info(AttachmentInfo::Video(BaseVideoInfo { - height: Some(uint!(600)), - width: Some(uint!(800)), - duration: Some(Duration::from_millis(3600)), - size: None, - blurhash: None, - })) - .caption(Some("image caption".to_owned())); - - let response = - room.send_attachment("image.jpg", &mime::IMAGE_JPEG, b"Hello world".to_vec(), config).await; - - response.unwrap_err(); -} - -#[async_test] -async fn test_room_attachment_send_info_thumbnail() { - let (client, server) = logged_in_client_with_server().await; - - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .and(body_partial_json(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", - } - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) - .mount(&server) - .await; - - Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) - .and(header("authorization", "Bearer 1234")) - .and(header("content-type", "image/jpeg")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" - }))) - .expect(2) - .mount(&server) - .await; - - mock_sync(&server, &*test_json::SYNC, None).await; - mock_encryption_state(&server, false).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); - - let config = AttachmentConfig::with_thumbnail(Thumbnail { - data: b"Thumbnail".to_vec(), - 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, b"Hello world".to_vec(), config) - .await - .unwrap(); - - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) -} - -#[async_test] -async fn test_room_attachment_send_mentions() { - let (client, server) = logged_in_client_with_server().await; - - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .and(body_partial_json(json!({ - "m.mentions": { - "user_ids": ["@user:localhost"], - } - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) - .mount(&server) - .await; - - Mock::given(method("POST")) - .and(path("/_matrix/media/r0/upload")) - .and(header("authorization", "Bearer 1234")) - .and(header("content-type", "image/jpeg")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw" - }))) - .mount(&server) - .await; - - mock_sync(&server, &*test_json::SYNC, None).await; - mock_encryption_state(&server, false).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); - - let response = room - .send_attachment( - "image", - &mime::IMAGE_JPEG, - b"Hello world".to_vec(), - AttachmentConfig::new() - .mentions(Some(Mentions::with_user_ids([owned_user_id!("@user:localhost")]))), - ) - .await - .unwrap(); - - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) -} - #[async_test] async fn test_room_redact() { let (client, server) = synced_client().await; diff --git a/crates/matrix-sdk/tests/integration/room/mod.rs b/crates/matrix-sdk/tests/integration/room/mod.rs index 196d5836e3d..1dea955bc1c 100644 --- a/crates/matrix-sdk/tests/integration/room/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/mod.rs @@ -1,3 +1,4 @@ +mod attachment; mod common; mod joined; mod left; diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index a4e4732680a..6bb8e888d90 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -69,6 +69,7 @@ enum FeatureSet { Markdown, Socks, SsoLogin, + ImageProc, } #[derive(Subcommand, PartialEq, Eq, PartialOrd, Ord)] @@ -218,6 +219,7 @@ fn run_feature_tests(cmd: Option) -> Result<()> { (FeatureSet::Markdown, "--features markdown,testing"), (FeatureSet::Socks, "--features socks,testing"), (FeatureSet::SsoLogin, "--features sso-login,testing"), + (FeatureSet::ImageProc, "--features image-proc,testing"), ]); let run = |arg_set: &str| {