matrix-sdk: Add docs and cleanup the media upload methods.

master
Damir Jelić 2020-09-15 17:16:16 +02:00
parent 3ac3be501f
commit c500c06e4b
3 changed files with 196 additions and 72 deletions

View File

@ -29,7 +29,8 @@ use std::{
#[cfg(feature = "encryption")] #[cfg(feature = "encryption")]
use dashmap::DashMap; use dashmap::DashMap;
use futures_timer::Delay as sleep; use futures_timer::Delay as sleep;
use reqwest::header::{HeaderValue, InvalidHeaderValue}; use http::HeaderValue;
use reqwest::header::InvalidHeaderValue;
use url::Url; use url::Url;
#[cfg(feature = "encryption")] #[cfg(feature = "encryption")]
use zeroize::Zeroizing; use zeroize::Zeroizing;
@ -68,7 +69,17 @@ use matrix_sdk_common::{
}, },
}, },
assign, assign,
identifiers::ServerName, events::{
room::{
message::{
AudioMessageEventContent, FileMessageEventContent, ImageMessageEventContent,
MessageEventContent, VideoMessageEventContent,
},
EncryptedFile,
},
AnyMessageEventContent,
},
identifiers::{EventId, RoomId, RoomIdOrAliasId, ServerName, UserId},
instant::{Duration, Instant}, instant::{Duration, Instant},
js_int::UInt, js_int::UInt,
locks::RwLock, locks::RwLock,
@ -90,15 +101,7 @@ use matrix_sdk_common::{
}; };
use crate::{ use crate::{
events::{
room::{
message::{FileMessageEventContent, MessageEventContent},
EncryptedFile,
},
AnyMessageEventContent,
},
http_client::{client_with_config, HttpClient, HttpSend}, http_client::{client_with_config, HttpClient, HttpSend},
identifiers::{EventId, RoomId, RoomIdOrAliasId, UserId},
Error, EventEmitter, OutgoingRequest, Result, Error, EventEmitter, OutgoingRequest, Result,
}; };
@ -1017,9 +1020,9 @@ impl Client {
/// ///
/// * `content` - The content of the message event. /// * `content` - The content of the message event.
/// ///
/// * `txn_id` - A unique `Uuid` that can be attached to a `MessageEvent` held /// * `txn_id` - A unique `Uuid` that can be attached to a `MessageEvent`
/// in its unsigned field as `transaction_id`. If not given one is created for the /// held in its unsigned field as `transaction_id`. If not given one is
/// message. /// created for the message.
/// ///
/// # Example /// # Example
/// ```no_run /// ```no_run
@ -1081,78 +1084,179 @@ impl Client {
} }
} }
#[allow(missing_docs)] /// Send an attachment to a room.
///
/// This will upload the given data that the reader produces using the
/// [`upload()`](#method.upload) method and post an event to the given room.
/// If the room is encrypted and the encryption feature is enabled the
/// upload will be encrypted.
///
/// This is a convenience method that calls the [`upload()`](#method.upload)
/// and afterwards the [`room_send()`](#method.room_send).
///
/// # Arguments
/// * `room_id` - The id of the room that should receive the media event.
///
/// * `body` - A textual representation of the media that is going to be
/// uploaded. Usually the file name.
///
/// * `content_type` - The type of the media, this will be used as the
/// content-type header.
///
/// * `reader` - A `Reader` that will be used to fetch the raw bytes of the
/// media.
///
/// * `txn_id` - A unique `Uuid` that can be attached to a `MessageEvent`
/// held in its unsigned field as `transaction_id`. If not given one is
/// created for the message.
///
/// # Examples
///
/// ```no_run
/// # use std::{path::PathBuf, fs::File, io::Read};
/// # use matrix_sdk::{Client, identifiers::room_id};
/// # use matrix_sdk_base::crypto::AttachmentEncryptor;
/// # use url::Url;
/// # use futures::executor::block_on;
/// # block_on(async {
/// # let homeserver = Url::parse("http://localhost:8080").unwrap();
/// # let mut client = Client::new(homeserver).unwrap();
/// # let room_id = room_id!("!test:localhost");
/// let path = PathBuf::from("/home/example/my-cat.jpg");
/// let mut image = File::open(path).unwrap();
///
/// let response = client
/// .room_send_attachment(&room_id, "My favorite cat", "image/jpg", &mut image, None)
/// .await
/// .expect("Can't upload my cat.");
/// # });
/// ```
pub async fn room_send_attachment<R: Read>( pub async fn room_send_attachment<R: Read>(
&self, &self,
room_id: &RoomId, room_id: &RoomId,
body: &str,
content_type: &str,
reader: &mut R, reader: &mut R,
txn_id: Option<Uuid>, txn_id: Option<Uuid>,
) -> Result<send_message_event::Response> { ) -> Result<send_message_event::Response> {
#[cfg(not(feature = "encryption"))] let (new_content_type, reader, keys) =
let encrypted = false; if cfg!(feature = "encryption") && self.is_room_encrypted(room_id).await {
let encryptor = AttachmentEncryptor::new(reader);
#[cfg(feature = "encryption")]
let encrypted = {
let room = self.base_client.get_joined_room(room_id).await;
match room {
Some(r) => r.read().await.is_encrypted(),
None => false,
}
};
#[cfg(feature = "encryption")]
if encrypted {
let mut data = Vec::new();
let mut encryptor = AttachmentEncryptor::new(reader);
encryptor.read_to_end(&mut data).unwrap();
let keys = encryptor.finish(); let keys = encryptor.finish();
let upload = self.upload("application/octet-stream", data).await?; ("application/octet-stream", reader, Some(keys))
} else {
let content = EncryptedFile { (content_type, reader, None)
url: upload.content_uri,
key: keys.web_key,
iv: keys.iv,
hashes: keys.hashes,
v: keys.version,
}; };
let content = AnyMessageEventContent::RoomMessage(MessageEventContent::File( let upload = self.upload(new_content_type, reader).await?;
FileMessageEventContent {
body: "test".to_owned(),
filename: None,
info: None,
url: None,
file: Some(Box::new(content)),
},
));
return self.room_send(room_id, content, txn_id).await; let url = upload.content_uri.clone();
};
let mut data = Vec::new(); let encrypted_file = keys.map(move |k| {
reader.read_to_end(&mut data).unwrap(); Box::new(EncryptedFile {
let upload = self.upload("application/octet-stream", data).await?; url,
key: k.web_key,
iv: k.iv,
hashes: k.hashes,
v: k.version,
})
});
let content = AnyMessageEventContent::RoomMessage(MessageEventContent::File( let content = if content_type.starts_with("image") {
FileMessageEventContent { // TODO create a thumbnail using the image crate?.
body: "test".to_owned(), MessageEventContent::Image(ImageMessageEventContent {
filename: None, body: body.to_owned(),
info: None, info: None,
url: Some(upload.content_uri), url: Some(upload.content_uri),
file: None, file: encrypted_file,
}, })
)); } else if content_type.starts_with("audio") {
MessageEventContent::Audio(AudioMessageEventContent {
body: body.to_owned(),
info: None,
url: Some(upload.content_uri),
file: encrypted_file,
})
} else if content_type.starts_with("video") {
MessageEventContent::Video(VideoMessageEventContent {
body: body.to_owned(),
info: None,
url: Some(upload.content_uri),
file: encrypted_file,
})
} else {
MessageEventContent::File(FileMessageEventContent {
filename: None,
body: body.to_owned(),
info: None,
url: Some(upload.content_uri),
file: encrypted_file,
})
};
self.room_send(room_id, content, txn_id).await self.room_send(
room_id,
AnyMessageEventContent::RoomMessage(content),
txn_id,
)
.await
} }
async fn upload(&self, content_type: &str, data: Vec<u8>) -> Result<create_content::Response> { /// 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::{path::PathBuf, fs::File, io::Read};
/// # use matrix_sdk::{Client, identifiers::room_id};
/// # use matrix_sdk_base::crypto::AttachmentEncryptor;
/// # use url::Url;
/// # use futures::executor::block_on;
/// # block_on(async {
/// # let homeserver = Url::parse("http://localhost:8080").unwrap();
/// # let mut client = Client::new(homeserver).unwrap();
/// let path = PathBuf::from("/home/example/my-cat.jpg");
/// let mut image = File::open(path).unwrap();
///
/// let response = client
/// .upload("image/jpg", &mut image)
/// .await
/// .expect("Can't upload my cat.");
///
/// println!("Cat URI: {}", response.content_uri);
///
/// // Upload an encrypted cat, err file.
/// let path = PathBuf::from("/home/example/my-secret-cat.jpg");
/// let mut image = File::open(path).unwrap();
/// let mut encryptor = AttachmentEncryptor::new(&mut image);
///
/// let response = client
/// .upload("image/jpg", &mut encryptor)
/// .await
/// .expect("Can't upload my cat.");
/// println!("Secret cat URI: {}", response.content_uri);
/// # });
/// ```
pub async fn upload(
&self,
content_type: &str,
reader: &mut impl Read,
) -> Result<create_content::Response> {
let mut data = Vec::new();
reader.read_to_end(&mut data)?;
let request = create_content::Request::new(content_type, data); let request = create_content::Request::new(content_type, data);
self.send(request).await self.http_client.upload(request).await
} }
/// Send an arbitrary request to the server, without updating client state. /// Send an arbitrary request to the server, without updating client state.

View File

@ -21,6 +21,7 @@ use matrix_sdk_common::{
}; };
use reqwest::Error as ReqwestError; use reqwest::Error as ReqwestError;
use serde_json::Error as JsonError; use serde_json::Error as JsonError;
use std::io::Error as IoError;
use thiserror::Error; use thiserror::Error;
#[cfg(feature = "encryption")] #[cfg(feature = "encryption")]
@ -44,6 +45,10 @@ pub enum Error {
#[error(transparent)] #[error(transparent)]
SerdeJson(#[from] JsonError), SerdeJson(#[from] JsonError),
/// An IO error happened.
#[error(transparent)]
IO(#[from] IoError),
/// An error converting between ruma_client_api types and Hyper types. /// An error converting between ruma_client_api types and Hyper types.
#[error("can't parse the JSON response as a Matrix response")] #[error("can't parse the JSON response as a Matrix response")]
RumaResponse(RumaResponseError<RumaClientError>), RumaResponse(RumaResponseError<RumaClientError>),

View File

@ -19,7 +19,7 @@ use reqwest::{Client, Response};
use tracing::trace; use tracing::trace;
use url::Url; use url::Url;
use matrix_sdk_common::{locks::RwLock, FromHttpResponseError}; use matrix_sdk_common::{api::r0::media::create_content, locks::RwLock, FromHttpResponseError};
use matrix_sdk_common_macros::async_trait; use matrix_sdk_common_macros::async_trait;
use crate::{ClientConfig, Error, OutgoingRequest, Result, Session}; use crate::{ClientConfig, Error, OutgoingRequest, Result, Session};
@ -87,6 +87,7 @@ impl HttpClient {
&self, &self,
request: Request, request: Request,
session: Arc<RwLock<Option<Session>>>, session: Arc<RwLock<Option<Session>>>,
content_type: Option<HeaderValue>,
) -> Result<http::Response<Vec<u8>>> { ) -> Result<http::Response<Vec<u8>>> {
let mut request = { let mut request = {
let read_guard; let read_guard;
@ -106,21 +107,35 @@ impl HttpClient {
}; };
if let HttpMethod::POST | HttpMethod::PUT | HttpMethod::DELETE = *request.method() { if let HttpMethod::POST | HttpMethod::PUT | HttpMethod::DELETE = *request.method() {
request.headers_mut().append( if let Some(content_type) = content_type {
http::header::CONTENT_TYPE, request
HeaderValue::from_static("application/json"), .headers_mut()
); .append(http::header::CONTENT_TYPE, content_type);
}
} }
self.inner.send_request(request).await self.inner.send_request(request).await
} }
pub async fn upload(
&self,
request: create_content::Request<'_>,
) -> Result<create_content::Response> {
let response = self
.send_request(request, self.session.clone(), None)
.await?;
Ok(create_content::Response::try_from(response)?)
}
pub async fn send<Request>(&self, request: Request) -> Result<Request::IncomingResponse> pub async fn send<Request>(&self, request: Request) -> Result<Request::IncomingResponse>
where where
Request: OutgoingRequest, Request: OutgoingRequest,
Error: From<FromHttpResponseError<Request::EndpointError>>, Error: From<FromHttpResponseError<Request::EndpointError>>,
{ {
let response = self.send_request(request, self.session.clone()).await?; let content_type = HeaderValue::from_static("application/json");
let response = self
.send_request(request, self.session.clone(), Some(content_type))
.await?;
trace!("Got response: {:?}", response); trace!("Got response: {:?}", response);