From c500c06e4b01ac81566ff5de20732d9450863ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 15 Sep 2020 17:16:16 +0200 Subject: [PATCH] matrix-sdk: Add docs and cleanup the media upload methods. --- matrix_sdk/src/client.rs | 236 ++++++++++++++++++++++++---------- matrix_sdk/src/error.rs | 5 + matrix_sdk/src/http_client.rs | 27 +++- 3 files changed, 196 insertions(+), 72 deletions(-) diff --git a/matrix_sdk/src/client.rs b/matrix_sdk/src/client.rs index ddd9a70b..767bf55e 100644 --- a/matrix_sdk/src/client.rs +++ b/matrix_sdk/src/client.rs @@ -29,7 +29,8 @@ use std::{ #[cfg(feature = "encryption")] use dashmap::DashMap; use futures_timer::Delay as sleep; -use reqwest::header::{HeaderValue, InvalidHeaderValue}; +use http::HeaderValue; +use reqwest::header::InvalidHeaderValue; use url::Url; #[cfg(feature = "encryption")] use zeroize::Zeroizing; @@ -68,7 +69,17 @@ use matrix_sdk_common::{ }, }, assign, - identifiers::ServerName, + events::{ + room::{ + message::{ + AudioMessageEventContent, FileMessageEventContent, ImageMessageEventContent, + MessageEventContent, VideoMessageEventContent, + }, + EncryptedFile, + }, + AnyMessageEventContent, + }, + identifiers::{EventId, RoomId, RoomIdOrAliasId, ServerName, UserId}, instant::{Duration, Instant}, js_int::UInt, locks::RwLock, @@ -90,15 +101,7 @@ use matrix_sdk_common::{ }; use crate::{ - events::{ - room::{ - message::{FileMessageEventContent, MessageEventContent}, - EncryptedFile, - }, - AnyMessageEventContent, - }, http_client::{client_with_config, HttpClient, HttpSend}, - identifiers::{EventId, RoomId, RoomIdOrAliasId, UserId}, Error, EventEmitter, OutgoingRequest, Result, }; @@ -1017,9 +1020,9 @@ impl Client { /// /// * `content` - The content of the message event. /// - /// * `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. + /// * `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. /// /// # Example /// ```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( &self, room_id: &RoomId, + body: &str, + content_type: &str, reader: &mut R, txn_id: Option, ) -> Result { - #[cfg(not(feature = "encryption"))] - let encrypted = false; + let (new_content_type, reader, keys) = + if cfg!(feature = "encryption") && self.is_room_encrypted(room_id).await { + let encryptor = AttachmentEncryptor::new(reader); + let keys = encryptor.finish(); - #[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 upload = self.upload("application/octet-stream", data).await?; - - let content = EncryptedFile { - url: upload.content_uri, - key: keys.web_key, - iv: keys.iv, - hashes: keys.hashes, - v: keys.version, + ("application/octet-stream", reader, Some(keys)) + } else { + (content_type, reader, None) }; - let content = AnyMessageEventContent::RoomMessage(MessageEventContent::File( - FileMessageEventContent { - body: "test".to_owned(), - filename: None, - info: None, - url: None, - file: Some(Box::new(content)), - }, - )); + let upload = self.upload(new_content_type, reader).await?; - return self.room_send(room_id, content, txn_id).await; - }; + let url = upload.content_uri.clone(); - let mut data = Vec::new(); - reader.read_to_end(&mut data).unwrap(); - let upload = self.upload("application/octet-stream", data).await?; + let encrypted_file = keys.map(move |k| { + Box::new(EncryptedFile { + url, + key: k.web_key, + iv: k.iv, + hashes: k.hashes, + v: k.version, + }) + }); - let content = AnyMessageEventContent::RoomMessage(MessageEventContent::File( - FileMessageEventContent { - body: "test".to_owned(), - filename: None, + let content = if content_type.starts_with("image") { + // TODO create a thumbnail using the image crate?. + MessageEventContent::Image(ImageMessageEventContent { + body: body.to_owned(), info: None, 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) -> Result { + /// 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 { + let mut data = Vec::new(); + reader.read_to_end(&mut 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. diff --git a/matrix_sdk/src/error.rs b/matrix_sdk/src/error.rs index 943c6f05..d7fd62ca 100644 --- a/matrix_sdk/src/error.rs +++ b/matrix_sdk/src/error.rs @@ -21,6 +21,7 @@ use matrix_sdk_common::{ }; use reqwest::Error as ReqwestError; use serde_json::Error as JsonError; +use std::io::Error as IoError; use thiserror::Error; #[cfg(feature = "encryption")] @@ -44,6 +45,10 @@ pub enum Error { #[error(transparent)] SerdeJson(#[from] JsonError), + /// An IO error happened. + #[error(transparent)] + IO(#[from] IoError), + /// An error converting between ruma_client_api types and Hyper types. #[error("can't parse the JSON response as a Matrix response")] RumaResponse(RumaResponseError), diff --git a/matrix_sdk/src/http_client.rs b/matrix_sdk/src/http_client.rs index 1c2594e2..7ef485c0 100644 --- a/matrix_sdk/src/http_client.rs +++ b/matrix_sdk/src/http_client.rs @@ -19,7 +19,7 @@ use reqwest::{Client, Response}; use tracing::trace; 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 crate::{ClientConfig, Error, OutgoingRequest, Result, Session}; @@ -87,6 +87,7 @@ impl HttpClient { &self, request: Request, session: Arc>>, + content_type: Option, ) -> Result>> { let mut request = { let read_guard; @@ -106,21 +107,35 @@ impl HttpClient { }; if let HttpMethod::POST | HttpMethod::PUT | HttpMethod::DELETE = *request.method() { - request.headers_mut().append( - http::header::CONTENT_TYPE, - HeaderValue::from_static("application/json"), - ); + if let Some(content_type) = content_type { + request + .headers_mut() + .append(http::header::CONTENT_TYPE, content_type); + } } self.inner.send_request(request).await } + pub async fn upload( + &self, + request: create_content::Request<'_>, + ) -> Result { + let response = self + .send_request(request, self.session.clone(), None) + .await?; + Ok(create_content::Response::try_from(response)?) + } + pub async fn send(&self, request: Request) -> Result where Request: OutgoingRequest, Error: From>, { - 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);