diff --git a/matrixgw_backend/Cargo.lock b/matrixgw_backend/Cargo.lock index 9662dae..f44d616 100644 --- a/matrixgw_backend/Cargo.lock +++ b/matrixgw_backend/Cargo.lock @@ -777,6 +777,17 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -2357,6 +2368,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + [[package]] name = "inout" version = "0.1.4" @@ -3048,6 +3068,7 @@ dependencies = [ "env_logger", "futures-util", "hex", + "infer", "ipnet", "jwt-simple", "lazy-regex", diff --git a/matrixgw_backend/Cargo.toml b/matrixgw_backend/Cargo.toml index d146f47..84f0947 100644 --- a/matrixgw_backend/Cargo.toml +++ b/matrixgw_backend/Cargo.toml @@ -32,4 +32,5 @@ url = "2.5.7" ractor = "0.15.9" serde_json = "1.0.145" lazy-regex = "3.4.2" -actix-ws = "0.3.0" \ No newline at end of file +actix-ws = "0.3.0" +infer = "0.19.0" \ No newline at end of file diff --git a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs index f960f78..cc6c22b 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs @@ -1,6 +1,7 @@ use crate::controllers::HttpResult; +use crate::controllers::matrix::media_controller; use crate::extractors::matrix_client_extractor::MatrixClientExtractor; -use actix_web::{HttpResponse, web}; +use actix_web::{HttpRequest, HttpResponse, web}; use futures_util::{StreamExt, stream}; use matrix_sdk::ruma::{OwnedRoomId, OwnedUserId}; use matrix_sdk::{Room, RoomMemberships}; @@ -56,3 +57,20 @@ pub async fn single_room_info( Some(r) => HttpResponse::Ok().json(APIRoomInfo::from_room(&r).await?), }) } + +/// Get room avatar +pub async fn room_avatar( + req: HttpRequest, + client: MatrixClientExtractor, + path: web::Path, +) -> HttpResult { + let Some(room) = client.client.client.get_room(&path.id) else { + return Ok(HttpResponse::NotFound().json("Room not found")); + }; + + let Some(uri) = room.avatar_url() else { + return Ok(HttpResponse::NotFound().json("Room has no avatar")); + }; + + media_controller::serve_media(req, uri).await +} diff --git a/matrixgw_backend/src/controllers/matrix/media_controller.rs b/matrixgw_backend/src/controllers/matrix/media_controller.rs new file mode 100644 index 0000000..781478c --- /dev/null +++ b/matrixgw_backend/src/controllers/matrix/media_controller.rs @@ -0,0 +1,57 @@ +use crate::controllers::HttpResult; +use crate::extractors::matrix_client_extractor::MatrixClientExtractor; +use crate::utils::crypt_utils::sha512; +use actix_web::dev::Payload; +use actix_web::http::header; +use actix_web::{FromRequest, HttpRequest, HttpResponse, web}; +use matrix_sdk::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}; +use matrix_sdk::ruma::events::room::MediaSource; +use matrix_sdk::ruma::{OwnedMxcUri, UInt}; + +#[derive(serde::Deserialize)] +struct MediaQuery { + #[serde(default)] + thumbnail: bool, +} + +/// Serve a media file +pub async fn serve_media(req: HttpRequest, media: OwnedMxcUri) -> HttpResult { + let query = web::Query::::from_request(&req, &mut Payload::None).await?; + let client = MatrixClientExtractor::from_request(&req, &mut Payload::None).await?; + + let media = client + .client + .client + .media() + .get_media_content( + &MediaRequestParameters { + source: MediaSource::Plain(media), + format: match query.thumbnail { + true => MediaFormat::Thumbnail(MediaThumbnailSettings::new( + UInt::new(100).unwrap(), + UInt::new(100).unwrap(), + )), + false => MediaFormat::File, + }, + }, + true, + ) + .await?; + + let digest = sha512(&media); + + let mime_type = infer::get(&media).map(|x| x.mime_type()); + + // Check if the browser already knows the etag + if let Some(c) = req.headers().get(header::IF_NONE_MATCH) + && c.to_str().unwrap_or("") == digest + { + return Ok(HttpResponse::NotModified().finish()); + } + + Ok(HttpResponse::Ok() + .content_type(mime_type.unwrap_or("application/octet-stream")) + .insert_header(("etag", digest)) + .insert_header(("cache-control", "max-age=360000")) + .body(media)) +} diff --git a/matrixgw_backend/src/controllers/matrix/mod.rs b/matrixgw_backend/src/controllers/matrix/mod.rs index 6742d04..5ce5ba4 100644 --- a/matrixgw_backend/src/controllers/matrix/mod.rs +++ b/matrixgw_backend/src/controllers/matrix/mod.rs @@ -1 +1,2 @@ pub mod matrix_room_controller; +pub mod media_controller; diff --git a/matrixgw_backend/src/controllers/mod.rs b/matrixgw_backend/src/controllers/mod.rs index 0384002..cab029c 100644 --- a/matrixgw_backend/src/controllers/mod.rs +++ b/matrixgw_backend/src/controllers/mod.rs @@ -22,6 +22,8 @@ pub enum HttpFailure { InternalError(#[from] anyhow::Error), #[error("Actix web error: {0}")] ActixError(#[from] actix_web::Error), + #[error("Matrix error: {0}")] + MatrixError(#[from] matrix_sdk::Error), } impl ResponseError for HttpFailure { diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index fc9c805..321f59c 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -144,6 +144,10 @@ async fn main() -> std::io::Result<()> { "/api/matrix/room/{id}", web::get().to(matrix_room_controller::single_room_info), ) + .route( + "/api/matrix/room/{id}/avatar", + web::get().to(matrix_room_controller::room_avatar), + ) }) .workers(4) .bind(&AppConfig::get().listen_address)? diff --git a/matrixgw_backend/src/utils/crypt_utils.rs b/matrixgw_backend/src/utils/crypt_utils.rs index 9a22fa9..f1cff8c 100644 --- a/matrixgw_backend/src/utils/crypt_utils.rs +++ b/matrixgw_backend/src/utils/crypt_utils.rs @@ -1,6 +1,11 @@ -use sha2::{Digest, Sha256}; +use sha2::{Digest, Sha256, Sha512}; /// Compute SHA256sum of a given string pub fn sha256str(input: &str) -> String { hex::encode(Sha256::digest(input.as_bytes())) } + +/// Compute SHA256sum of a given byte array +pub fn sha512(input: &[u8]) -> String { + hex::encode(Sha512::digest(input)) +}