mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-18 13:40:55 -04:00
refactor(sdk)!: Move media methods from Client to a new type
This commit is contained in:
committed by
Jonas Platte
parent
79b5854c83
commit
b769827313
@@ -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?)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ impl Account {
|
||||
pub async fn get_avatar(&self, format: MediaFormat) -> Result<Option<Vec<u8>>> {
|
||||
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<OwnedMxcUri> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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<Box<dyn Future<Output = ()> + 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<create_content::v3::Response> {
|
||||
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<Vec<u8>> {
|
||||
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<u8> = match &request.source {
|
||||
MediaSource::Encrypted(file) => {
|
||||
let content: Vec<u8> =
|
||||
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<Option<Vec<u8>>> {
|
||||
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<Option<Vec<u8>>> {
|
||||
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<whoami::v3::Response> {
|
||||
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<AttachmentInfo>,
|
||||
thumbnail: Option<Thumbnail<'_>>,
|
||||
) -> Result<ruma::events::room::message::MessageType> {
|
||||
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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
375
crates/matrix-sdk/src/media.rs
Normal file
375
crates/matrix-sdk/src/media.rs
Normal file
@@ -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<create_content::v3::Response> {
|
||||
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<Vec<u8>> {
|
||||
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<u8> = match &request.source {
|
||||
MediaSource::Encrypted(file) => {
|
||||
let request = get_content::v3::Request::from_url(&file.url)?;
|
||||
let content: Vec<u8> = 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<Option<Vec<u8>>> {
|
||||
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<Option<Vec<u8>>> {
|
||||
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<AttachmentInfo>,
|
||||
thumbnail: Option<Thumbnail<'_>>,
|
||||
) -> Result<ruma::events::room::message::MessageType> {
|
||||
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)),
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -205,7 +205,7 @@ impl Common {
|
||||
pub async fn avatar(&self, format: MediaFormat) -> Result<Option<Vec<u8>>> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ impl RoomMember {
|
||||
pub async fn avatar(&self, format: MediaFormat) -> Result<Option<Vec<u8>>> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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) },
|
||||
|
||||
Reference in New Issue
Block a user