From b769827313ed359876a29948fab75df522b3ce36 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 2 Sep 2022 16:22:37 +0200 Subject: [PATCH] refactor(sdk)!: Move media methods from Client to a new type --- bindings/matrix-sdk-ffi/src/client.rs | 3 +- crates/matrix-sdk/src/account.rs | 8 +- crates/matrix-sdk/src/client/mod.rs | 360 +---------------- crates/matrix-sdk/src/encryption/mod.rs | 4 +- crates/matrix-sdk/src/lib.rs | 6 +- crates/matrix-sdk/src/media.rs | 375 ++++++++++++++++++ crates/matrix-sdk/src/room/common.rs | 2 +- crates/matrix-sdk/src/room/joined.rs | 2 + crates/matrix-sdk/src/room/member.rs | 2 +- crates/matrix-sdk/tests/integration/client.rs | 11 +- 10 files changed, 410 insertions(+), 363 deletions(-) create mode 100644 crates/matrix-sdk/src/media.rs diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index c743e3174..1537e614f 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -251,7 +251,8 @@ impl Client { let source = (*media_source).clone(); RUNTIME.block_on(async move { - Ok(l.get_media_content(&MediaRequest { source, format: MediaFormat::File }, true) + Ok(l.media() + .get_media_content(&MediaRequest { source, format: MediaFormat::File }, true) .await?) }) } diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs index 22c1f2f75..43bebe111 100644 --- a/crates/matrix-sdk/src/account.rs +++ b/crates/matrix-sdk/src/account.rs @@ -177,7 +177,7 @@ impl Account { pub async fn get_avatar(&self, format: MediaFormat) -> Result>> { if let Some(url) = self.get_avatar_url().await? { let request = MediaRequest { source: MediaSource::Plain(url), format }; - Ok(Some(self.client.get_media_content(&request, true).await?)) + Ok(Some(self.client.media().get_media_content(&request, true).await?)) } else { Ok(None) } @@ -189,7 +189,7 @@ impl Account { /// content repository, and set the user's avatar to the MXC URI for the /// uploaded file. /// - /// This is a convenience method for calling [`Client::upload()`], + /// This is a convenience method for calling [`Media::upload()`], /// followed by [`Account::set_avatar_url()`]. /// /// Returns the MXC URI of the uploaded avatar. @@ -208,8 +208,10 @@ impl Account { /// client.account().upload_avatar(&mime::IMAGE_JPEG, &image).await?; /// # anyhow::Ok(()) }); /// ``` + /// + /// [`Media::upload()`]: crate::Media::upload pub async fn upload_avatar(&self, content_type: &Mime, data: &[u8]) -> Result { - let upload_response = self.client.upload(content_type, data).await?; + let upload_response = self.client.media().upload(content_type, data).await?; self.set_avatar_url(Some(&upload_response.content_uri)).await?; Ok(upload_response.content_uri) } diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 3b7d054d5..b9a5f131e 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -14,8 +14,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[cfg(feature = "e2e-encryption")] -use std::io::Read; use std::{ fmt::{self, Debug}, future::Future, @@ -31,15 +29,13 @@ use futures_core::stream::Stream; use futures_signals::signal::Signal; use futures_util::{SinkExt, StreamExt, TryStreamExt}; use matrix_sdk_base::{ - deserialized_responses::SyncResponse, - media::{MediaEventContent, MediaFormat, MediaRequest, MediaThumbnailSize}, - BaseClient, SendOutsideWasm, Session, SessionMeta, SessionTokens, StateStore, SyncOutsideWasm, + deserialized_responses::SyncResponse, BaseClient, SendOutsideWasm, Session, SessionMeta, + SessionTokens, StateStore, SyncOutsideWasm, }; use matrix_sdk_common::{ - instant::{Duration, Instant}, + instant::Instant, locks::{Mutex, RwLock, RwLockReadGuard}, }; -use mime::{self, Mime}; #[cfg(feature = "appservice")] use ruma::TransactionId; use ruma::{ @@ -55,7 +51,6 @@ use ruma::{ }, error::ErrorKind, filter::{create_filter::v3::Request as FilterUploadRequest, FilterDefinition}, - media::{create_content, get_content, get_content_thumbnail}, membership::{join_room_by_id, join_room_by_id_or_alias}, push::get_notifications::v3::Notification, room::create_room, @@ -71,13 +66,12 @@ use ruma::{ room::{ create::RoomCreateEventContent, member::{MembershipState, RoomMemberEventContent}, - MediaSource, }, SyncStateEvent, }, presence::PresenceState, - DeviceId, MxcUri, OwnedDeviceId, OwnedRoomId, OwnedServerName, RoomAliasId, RoomId, - RoomOrAliasId, ServerName, UInt, UserId, + DeviceId, OwnedDeviceId, OwnedRoomId, OwnedServerName, RoomAliasId, RoomId, RoomOrAliasId, + ServerName, UInt, UserId, }; use serde::de::DeserializeOwned; #[cfg(not(target_arch = "wasm32"))] @@ -90,14 +84,13 @@ use url::Url; #[cfg(feature = "e2e-encryption")] use crate::encryption::Encryption; use crate::{ - attachment::{AttachmentInfo, Thumbnail}, config::RequestConfig, error::{HttpError, HttpResult}, event_handler::{ EventHandler, EventHandlerHandle, EventHandlerResult, EventHandlerStore, SyncEvent, }, http_client::HttpClient, - room, Account, Error, RefreshTokenError, Result, RumaApiError, + room, Account, Error, Media, RefreshTokenError, Result, RumaApiError, }; mod builder; @@ -110,11 +103,6 @@ pub use self::{ login_builder::LoginBuilder, }; -/// A conservative upload speed of 1Mbps -const DEFAULT_UPLOAD_SPEED: u64 = 125_000; -/// 5 min minimal upload request timeout, used to clamp the request timeout. -const MIN_UPLOAD_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 5); - #[cfg(not(target_arch = "wasm32"))] type NotificationHandlerFut = Pin + Send>>; #[cfg(target_arch = "wasm32")] @@ -528,6 +516,11 @@ impl Client { Encryption::new(self.clone()) } + /// Get the media manager of the client. + pub fn media(&self) -> Media { + Media::new(self.clone()) + } + /// Register a handler for a specific event type. /// /// The handler is a function or closure with one or more arguments. The @@ -1876,52 +1869,6 @@ impl Client { self.send(request, None).await } - /// Upload some media to the server. - /// - /// # 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. - /// - /// # Examples - /// - /// ```no_run - /// # use std::fs; - /// # use matrix_sdk::{Client, ruma::room_id}; - /// # use url::Url; - /// # use futures::executor::block_on; - /// # use mime; - /// # block_on(async { - /// # let homeserver = Url::parse("http://localhost:8080")?; - /// # let mut client = Client::new(homeserver).await?; - /// let image = fs::read("/home/example/my-cat.jpg")?; - /// - /// let response = client.upload(&mime::IMAGE_JPEG, &image).await?; - /// - /// println!("Cat URI: {}", response.content_uri); - /// # anyhow::Ok(()) }); - /// ``` - pub async fn upload( - &self, - content_type: &Mime, - data: &[u8], - ) -> Result { - let timeout = std::cmp::max( - Duration::from_secs(data.len() as u64 / DEFAULT_UPLOAD_SPEED), - MIN_UPLOAD_REQUEST_TIMEOUT, - ); - - let request = assign!(create_content::v3::Request::new(data), { - content_type: Some(content_type.essence_str()), - }); - - let request_config = self.request_config().timeout(timeout); - Ok(self.send(request, Some(request_config)).await?) - } - /// Send an arbitrary request to the server, without updating client state. /// /// **Warning:** Because this method *does not* update the client state, it @@ -2048,7 +1995,7 @@ impl Client { } } - async fn server_versions(&self) -> HttpResult<&[MatrixVersion]> { + pub(crate) async fn server_versions(&self) -> HttpResult<&[MatrixVersion]> { #[cfg(target_arch = "wasm32")] let server_versions = self.inner.server_versions.get_or_try_init(self.request_server_versions()).await?; @@ -2481,294 +2428,11 @@ impl Client { self.inner.base_client.sync_token().await } - /// Get a media file's content. - /// - /// If the content is encrypted and encryption is enabled, the content will - /// be decrypted. - /// - /// # Arguments - /// - /// * `request` - The `MediaRequest` of the content. - /// - /// * `use_cache` - If we should use the media cache for this request. - pub async fn get_media_content( - &self, - request: &MediaRequest, - use_cache: bool, - ) -> Result> { - let content = if use_cache { - self.inner.base_client.store().get_media_content(request).await? - } else { - None - }; - - if let Some(content) = content { - Ok(content) - } else { - let content: Vec = match &request.source { - MediaSource::Encrypted(file) => { - let content: Vec = - self.send(get_content::v3::Request::from_url(&file.url)?, None).await?.file; - - #[cfg(feature = "e2e-encryption")] - let content = { - let mut cursor = std::io::Cursor::new(content); - let mut reader = matrix_sdk_base::crypto::AttachmentDecryptor::new( - &mut cursor, - file.as_ref().clone().into(), - )?; - - let mut decrypted = Vec::new(); - reader.read_to_end(&mut decrypted)?; - - decrypted - }; - - content - } - MediaSource::Plain(uri) => { - if let MediaFormat::Thumbnail(size) = &request.format { - self.send( - get_content_thumbnail::v3::Request::from_url( - uri, - size.width, - size.height, - )?, - None, - ) - .await? - .file - } else { - self.send(get_content::v3::Request::from_url(uri)?, None).await?.file - } - } - }; - - if use_cache { - self.inner.base_client.store().add_media_content(request, content.clone()).await?; - } - - Ok(content) - } - } - - /// Remove a media file's content from the store. - /// - /// # Arguments - /// - /// * `request` - The `MediaRequest` of the content. - pub async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> { - Ok(self.inner.base_client.store().remove_media_content(request).await?) - } - - /// Delete all the media content corresponding to the given - /// uri from the store. - /// - /// # Arguments - /// - /// * `uri` - The `MxcUri` of the files. - pub async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> { - Ok(self.inner.base_client.store().remove_media_content_for_uri(uri).await?) - } - - /// Get the file of the given media event content. - /// - /// If the content is encrypted and encryption is enabled, the content will - /// be decrypted. - /// - /// Returns `Ok(None)` if the event content has no file. - /// - /// This is a convenience method that calls the - /// [`get_media_content`](#method.get_media_content) method. - /// - /// # Arguments - /// - /// * `event_content` - The media event content. - /// - /// * `use_cache` - If we should use the media cache for this file. - pub async fn get_file( - &self, - event_content: impl MediaEventContent, - use_cache: bool, - ) -> Result>> { - if let Some(source) = event_content.source() { - Ok(Some( - self.get_media_content( - &MediaRequest { source, format: MediaFormat::File }, - use_cache, - ) - .await?, - )) - } else { - Ok(None) - } - } - - /// Remove the file of the given media event content from the cache. - /// - /// This is a convenience method that calls the - /// [`remove_media_content`](#method.remove_media_content) method. - /// - /// # Arguments - /// - /// * `event_content` - The media event content. - pub async fn remove_file(&self, event_content: impl MediaEventContent) -> Result<()> { - if let Some(source) = event_content.source() { - self.remove_media_content(&MediaRequest { source, format: MediaFormat::File }).await? - } - - Ok(()) - } - - /// Get a thumbnail of the given media event content. - /// - /// If the content is encrypted and encryption is enabled, the content will - /// be decrypted. - /// - /// Returns `Ok(None)` if the event content has no thumbnail. - /// - /// This is a convenience method that calls the - /// [`get_media_content`](#method.get_media_content) method. - /// - /// # Arguments - /// - /// * `event_content` - The media event content. - /// - /// * `size` - The _desired_ size of the thumbnail. The actual thumbnail may - /// not match the size specified. - /// - /// * `use_cache` - If we should use the media cache for this thumbnail. - pub async fn get_thumbnail( - &self, - event_content: impl MediaEventContent, - size: MediaThumbnailSize, - use_cache: bool, - ) -> Result>> { - if let Some(source) = event_content.thumbnail_source() { - Ok(Some( - self.get_media_content( - &MediaRequest { source, format: MediaFormat::Thumbnail(size) }, - use_cache, - ) - .await?, - )) - } else { - Ok(None) - } - } - - /// Remove the thumbnail of the given media event content from the cache. - /// - /// This is a convenience method that calls the - /// [`remove_media_content`](#method.remove_media_content) method. - /// - /// # Arguments - /// - /// * `event_content` - The media event content. - /// - /// * `size` - The _desired_ size of the thumbnail. Must match the size - /// requested with [`get_thumbnail`](#method.get_thumbnail). - pub async fn remove_thumbnail( - &self, - event_content: impl MediaEventContent, - size: MediaThumbnailSize, - ) -> Result<()> { - if let Some(source) = event_content.source() { - self.remove_media_content(&MediaRequest { - source, - format: MediaFormat::Thumbnail(size), - }) - .await? - } - - Ok(()) - } - /// Gets information about the owner of a given access token. pub async fn whoami(&self) -> HttpResult { let request = whoami::v3::Request::new(); self.send(request, None).await } - - /// Upload the file bytes in `data` and construct an attachment - /// message with `body`, `content_type`, `info` and `thumbnail`. - pub(crate) async fn prepare_attachment_message( - &self, - body: &str, - content_type: &Mime, - data: &[u8], - info: Option, - thumbnail: Option>, - ) -> Result { - let (thumbnail_source, thumbnail_info) = if let Some(thumbnail) = thumbnail { - let response = self.upload(thumbnail.content_type, thumbnail.data).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(MediaSource::Plain(url)), Some(Box::new(thumbnail_info))) - } else { - (None, None) - }; - - let response = self.upload(content_type, data).await?; - - let url = response.content_uri; - - use ruma::events::room::{self, message}; - Ok(match content_type.type_() { - mime::IMAGE => { - let info = assign!(info.map(room::ImageInfo::from).unwrap_or_default(), { - mimetype: Some(content_type.as_ref().to_owned()), - thumbnail_source, - thumbnail_info, - }); - message::MessageType::Image(message::ImageMessageEventContent::plain( - body.to_owned(), - url, - 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_source, - 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_source, - thumbnail_info - }); - message::MessageType::File(message::FileMessageEventContent::plain( - body.to_owned(), - url, - Some(Box::new(info)), - )) - } - }) - } } // The http mocking library is not supported for wasm32 diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 168ab0bff..90e068b72 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -127,7 +127,7 @@ impl Client { let mut buf = Vec::new(); encryptor.read_to_end(&mut buf)?; - let response = self.upload(thumbnail.content_type, &buf).await?; + let response = self.media().upload(thumbnail.content_type, &buf).await?; let file: ruma::events::room::EncryptedFile = { let keys = encryptor.finish(); @@ -159,7 +159,7 @@ impl Client { let mut buf = Vec::new(); encryptor.read_to_end(&mut buf)?; - let response = self.upload(content_type, &buf).await?; + let response = self.media().upload(content_type, &buf).await?; let file: ruma::events::room::EncryptedFile = { let keys = encryptor.finish(); diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 6b288c639..a065044fb 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -20,8 +20,8 @@ pub use async_trait::async_trait; pub use bytes; pub use matrix_sdk_base::{ - media, DisplayName, Room as BaseRoom, RoomInfo, RoomMember as BaseRoomMember, RoomType, - Session, StateChanges, StoreError, + DisplayName, Room as BaseRoom, RoomInfo, RoomMember as BaseRoomMember, RoomType, Session, + StateChanges, StoreError, }; pub use matrix_sdk_common::*; pub use reqwest; @@ -36,6 +36,7 @@ pub mod config; mod error; pub mod event_handler; mod http_client; +pub mod media; /// High-level room API pub mod room; pub mod store; @@ -52,6 +53,7 @@ pub use client::{Client, ClientBuildError, ClientBuilder, LoginBuilder, LoopCtrl pub use error::ImageError; pub use error::{Error, HttpError, HttpResult, RefreshTokenError, Result, RumaApiError}; pub use http_client::HttpSend; +pub use media::Media; #[cfg(test)] mod test_utils; diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs new file mode 100644 index 000000000..32620cd62 --- /dev/null +++ b/crates/matrix-sdk/src/media.rs @@ -0,0 +1,375 @@ +// Copyright 2021 Kévin Commaille +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// 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. + +//! High-level media API. + +#[cfg(feature = "e2e-encryption")] +use std::io::Read; +use std::time::Duration; + +pub use matrix_sdk_base::media::*; +use mime::Mime; +use ruma::{ + api::client::media::{create_content, get_content, get_content_thumbnail}, + assign, + events::room::MediaSource, + MxcUri, +}; + +use crate::{ + attachment::{AttachmentInfo, Thumbnail}, + Client, Result, +}; + +/// A conservative upload speed of 1Mbps +const DEFAULT_UPLOAD_SPEED: u64 = 125_000; +/// 5 min minimal upload request timeout, used to clamp the request timeout. +const MIN_UPLOAD_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 5); + +/// A high-level API to interact with the media API. +#[derive(Debug, Clone)] +pub struct Media { + /// The underlying HTTP client. + client: Client, +} + +impl Media { + pub(crate) fn new(client: Client) -> Self { + Self { client } + } + + /// Upload some media to the server. + /// + /// # 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. + /// + /// # Examples + /// + /// ```no_run + /// # use std::fs; + /// # use matrix_sdk::{Client, ruma::room_id}; + /// # use url::Url; + /// # use futures::executor::block_on; + /// # use mime; + /// # block_on(async { + /// # let homeserver = Url::parse("http://localhost:8080")?; + /// # let mut client = Client::new(homeserver).await?; + /// let image = fs::read("/home/example/my-cat.jpg")?; + /// + /// let response = client.media().upload(&mime::IMAGE_JPEG, &image).await?; + /// + /// println!("Cat URI: {}", response.content_uri); + /// # anyhow::Ok(()) }); + /// ``` + pub async fn upload( + &self, + content_type: &Mime, + data: &[u8], + ) -> Result { + let timeout = std::cmp::max( + Duration::from_secs(data.len() as u64 / DEFAULT_UPLOAD_SPEED), + MIN_UPLOAD_REQUEST_TIMEOUT, + ); + + let request = assign!(create_content::v3::Request::new(data), { + content_type: Some(content_type.essence_str()), + }); + + let request_config = self.client.request_config().timeout(timeout); + Ok(self.client.send(request, Some(request_config)).await?) + } + + /// Get a media file's content. + /// + /// If the content is encrypted and encryption is enabled, the content will + /// be decrypted. + /// + /// # Arguments + /// + /// * `request` - The `MediaRequest` of the content. + /// + /// * `use_cache` - If we should use the media cache for this request. + pub async fn get_media_content( + &self, + request: &MediaRequest, + use_cache: bool, + ) -> Result> { + let content = + if use_cache { self.client.store().get_media_content(request).await? } else { None }; + + if let Some(content) = content { + Ok(content) + } else { + let content: Vec = match &request.source { + MediaSource::Encrypted(file) => { + let request = get_content::v3::Request::from_url(&file.url)?; + let content: Vec = self.client.send(request, None).await?.file; + + #[cfg(feature = "e2e-encryption")] + let content = { + let mut cursor = std::io::Cursor::new(content); + let mut reader = matrix_sdk_base::crypto::AttachmentDecryptor::new( + &mut cursor, + file.as_ref().clone().into(), + )?; + + let mut decrypted = Vec::new(); + reader.read_to_end(&mut decrypted)?; + + decrypted + }; + + content + } + MediaSource::Plain(uri) => { + if let MediaFormat::Thumbnail(size) = &request.format { + let request = get_content_thumbnail::v3::Request::from_url( + uri, + size.width, + size.height, + )?; + self.client.send(request, None).await?.file + } else { + let request = get_content::v3::Request::from_url(uri)?; + self.client.send(request, None).await?.file + } + } + }; + + if use_cache { + self.client.store().add_media_content(request, content.clone()).await?; + } + + Ok(content) + } + } + + /// Remove a media file's content from the store. + /// + /// # Arguments + /// + /// * `request` - The `MediaRequest` of the content. + pub async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> { + Ok(self.client.store().remove_media_content(request).await?) + } + + /// Delete all the media content corresponding to the given + /// uri from the store. + /// + /// # Arguments + /// + /// * `uri` - The `MxcUri` of the files. + pub async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> { + Ok(self.client.store().remove_media_content_for_uri(uri).await?) + } + + /// Get the file of the given media event content. + /// + /// If the content is encrypted and encryption is enabled, the content will + /// be decrypted. + /// + /// Returns `Ok(None)` if the event content has no file. + /// + /// This is a convenience method that calls the + /// [`get_media_content`](#method.get_media_content) method. + /// + /// # Arguments + /// + /// * `event_content` - The media event content. + /// + /// * `use_cache` - If we should use the media cache for this file. + pub async fn get_file( + &self, + event_content: impl MediaEventContent, + use_cache: bool, + ) -> Result>> { + if let Some(source) = event_content.source() { + Ok(Some( + self.get_media_content( + &MediaRequest { source, format: MediaFormat::File }, + use_cache, + ) + .await?, + )) + } else { + Ok(None) + } + } + + /// Remove the file of the given media event content from the cache. + /// + /// This is a convenience method that calls the + /// [`remove_media_content`](#method.remove_media_content) method. + /// + /// # Arguments + /// + /// * `event_content` - The media event content. + pub async fn remove_file(&self, event_content: impl MediaEventContent) -> Result<()> { + if let Some(source) = event_content.source() { + self.remove_media_content(&MediaRequest { source, format: MediaFormat::File }).await? + } + + Ok(()) + } + + /// Get a thumbnail of the given media event content. + /// + /// If the content is encrypted and encryption is enabled, the content will + /// be decrypted. + /// + /// Returns `Ok(None)` if the event content has no thumbnail. + /// + /// This is a convenience method that calls the + /// [`get_media_content`](#method.get_media_content) method. + /// + /// # Arguments + /// + /// * `event_content` - The media event content. + /// + /// * `size` - The _desired_ size of the thumbnail. The actual thumbnail may + /// not match the size specified. + /// + /// * `use_cache` - If we should use the media cache for this thumbnail. + pub async fn get_thumbnail( + &self, + event_content: impl MediaEventContent, + size: MediaThumbnailSize, + use_cache: bool, + ) -> Result>> { + if let Some(source) = event_content.thumbnail_source() { + Ok(Some( + self.get_media_content( + &MediaRequest { source, format: MediaFormat::Thumbnail(size) }, + use_cache, + ) + .await?, + )) + } else { + Ok(None) + } + } + + /// Remove the thumbnail of the given media event content from the cache. + /// + /// This is a convenience method that calls the + /// [`remove_media_content`](#method.remove_media_content) method. + /// + /// # Arguments + /// + /// * `event_content` - The media event content. + /// + /// * `size` - The _desired_ size of the thumbnail. Must match the size + /// requested with [`get_thumbnail`](#method.get_thumbnail). + pub async fn remove_thumbnail( + &self, + event_content: impl MediaEventContent, + size: MediaThumbnailSize, + ) -> Result<()> { + if let Some(source) = event_content.source() { + self.remove_media_content(&MediaRequest { + source, + format: MediaFormat::Thumbnail(size), + }) + .await? + } + + Ok(()) + } + + /// Upload the file bytes in `data` and construct an attachment + /// message with `body`, `content_type`, `info` and `thumbnail`. + pub(crate) async fn prepare_attachment_message( + &self, + body: &str, + content_type: &Mime, + data: &[u8], + info: Option, + thumbnail: Option>, + ) -> Result { + let (thumbnail_source, thumbnail_info) = if let Some(thumbnail) = thumbnail { + let response = self.upload(thumbnail.content_type, thumbnail.data).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(MediaSource::Plain(url)), Some(Box::new(thumbnail_info))) + } else { + (None, None) + }; + + let response = self.upload(content_type, data).await?; + + let url = response.content_uri; + + use ruma::events::room::{self, message}; + Ok(match content_type.type_() { + mime::IMAGE => { + let info = assign!(info.map(room::ImageInfo::from).unwrap_or_default(), { + mimetype: Some(content_type.as_ref().to_owned()), + thumbnail_source, + thumbnail_info, + }); + message::MessageType::Image(message::ImageMessageEventContent::plain( + body.to_owned(), + url, + 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_source, + 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_source, + thumbnail_info + }); + message::MessageType::File(message::FileMessageEventContent::plain( + body.to_owned(), + url, + Some(Box::new(info)), + )) + } + }) + } +} diff --git a/crates/matrix-sdk/src/room/common.rs b/crates/matrix-sdk/src/room/common.rs index 1ffb51406..1d01b20a9 100644 --- a/crates/matrix-sdk/src/room/common.rs +++ b/crates/matrix-sdk/src/room/common.rs @@ -205,7 +205,7 @@ impl Common { pub async fn avatar(&self, format: MediaFormat) -> Result>> { if let Some(url) = self.avatar_url() { let request = MediaRequest { source: MediaSource::Plain(url.to_owned()), format }; - Ok(Some(self.client.get_media_content(&request, true).await?)) + Ok(Some(self.client.media().get_media_content(&request, true).await?)) } else { Ok(None) } diff --git a/crates/matrix-sdk/src/room/joined.rs b/crates/matrix-sdk/src/room/joined.rs index 35c62e546..d4c91d7fd 100644 --- a/crates/matrix-sdk/src/room/joined.rs +++ b/crates/matrix-sdk/src/room/joined.rs @@ -743,6 +743,7 @@ impl Joined { .await? } else { self.client + .media() .prepare_attachment_message(body, content_type, data, config.info, config.thumbnail) .await? }; @@ -750,6 +751,7 @@ impl Joined { #[cfg(not(feature = "e2e-encryption"))] let content = self .client + .media() .prepare_attachment_message(body, content_type, data, config.info, config.thumbnail) .await?; diff --git a/crates/matrix-sdk/src/room/member.rs b/crates/matrix-sdk/src/room/member.rs index fab810f73..a20b5698d 100644 --- a/crates/matrix-sdk/src/room/member.rs +++ b/crates/matrix-sdk/src/room/member.rs @@ -61,7 +61,7 @@ impl RoomMember { pub async fn avatar(&self, format: MediaFormat) -> Result>> { if let Some(url) = self.avatar_url() { let request = MediaRequest { source: MediaSource::Plain(url.to_owned()), format }; - Ok(Some(self.client.get_media_content(&request, true).await?)) + Ok(Some(self.client.media().get_media_content(&request, true).await?)) } else { Ok(None) } diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index 9ba3587f5..de4329e1f 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -538,9 +538,9 @@ async fn get_media_content() { .mount(&server) .await; - client.get_media_content(&request, true).await.unwrap(); - client.get_media_content(&request, true).await.unwrap(); - client.get_media_content(&request, false).await.unwrap(); + client.media().get_media_content(&request, true).await.unwrap(); + client.media().get_media_content(&request, true).await.unwrap(); + client.media().get_media_content(&request, false).await.unwrap(); } #[async_test] @@ -566,8 +566,8 @@ async fn get_media_file() { .mount(&server) .await; - client.get_file(event_content.clone(), true).await.unwrap(); - client.get_file(event_content.clone(), true).await.unwrap(); + client.media().get_file(event_content.clone(), true).await.unwrap(); + client.media().get_file(event_content.clone(), true).await.unwrap(); Mock::given(method("GET")) .and(path("/_matrix/media/r0/thumbnail/example%2Eorg/image")) @@ -580,6 +580,7 @@ async fn get_media_file() { .await; client + .media() .get_thumbnail( event_content, MediaThumbnailSize { method: Method::Scale, width: uint!(100), height: uint!(100) },