client_server: use and provide authenticated media API

This commit is contained in:
Lambda 2024-07-21 19:24:29 +00:00
parent 7f6ab63752
commit 79053ad052
4 changed files with 568 additions and 101 deletions

View file

@ -3,17 +3,21 @@ use std::time::Duration;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use http::{ use http::{
header::{CONTENT_DISPOSITION, CONTENT_SECURITY_POLICY, CONTENT_TYPE}, header::{CONTENT_DISPOSITION, CONTENT_SECURITY_POLICY, CONTENT_TYPE},
HeaderName, HeaderValue, HeaderName, HeaderValue, Method,
}; };
use phf::{phf_set, Set}; use phf::{phf_set, Set};
use ruma::{ use ruma::{
api::client::{ api::{
client::{
authenticated_media as authenticated_media_client,
error::ErrorKind, error::ErrorKind,
media::{self as legacy_media, create_content}, media::{self as legacy_media, create_content},
}, },
federation::authenticated_media as authenticated_media_fed,
},
http_headers::{ContentDisposition, ContentDispositionType}, http_headers::{ContentDisposition, ContentDispositionType},
}; };
use tracing::error; use tracing::{debug, error, info, warn};
use crate::{ use crate::{
service::media::FileMeta, service::media::FileMeta,
@ -121,7 +125,7 @@ fn set_header_or_panic(
/// ///
/// Returns max upload size. /// Returns max upload size.
#[allow(deprecated)] // unauthenticated media #[allow(deprecated)] // unauthenticated media
pub(crate) async fn get_media_config_route( pub(crate) async fn get_media_config_legacy_route(
_body: Ar<legacy_media::get_media_config::v3::Request>, _body: Ar<legacy_media::get_media_config::v3::Request>,
) -> Result<Ra<legacy_media::get_media_config::v3::Response>> { ) -> Result<Ra<legacy_media::get_media_config::v3::Response>> {
Ok(Ra(legacy_media::get_media_config::v3::Response { Ok(Ra(legacy_media::get_media_config::v3::Response {
@ -129,6 +133,17 @@ pub(crate) async fn get_media_config_route(
})) }))
} }
/// # `GET /_matrix/client/v1/media/config`
///
/// Returns max upload size.
pub(crate) async fn get_media_config_route(
_body: Ar<authenticated_media_client::get_media_config::v1::Request>,
) -> Result<Ra<authenticated_media_client::get_media_config::v1::Response>> {
Ok(Ra(authenticated_media_client::get_media_config::v1::Response {
upload_size: services().globals.max_request_size().into(),
}))
}
/// # `POST /_matrix/media/r0/upload` /// # `POST /_matrix/media/r0/upload`
/// ///
/// Permanently save media in the server. /// Permanently save media in the server.
@ -163,10 +178,106 @@ pub(crate) async fn create_content_route(
})) }))
} }
#[allow(deprecated)] // unauthenticated media struct RemoteResponse {
pub(crate) async fn get_remote_content( #[allow(unused)]
metadata: authenticated_media_fed::ContentMetadata,
content: authenticated_media_fed::Content,
}
/// Fetches remote media content from a URL specified in a
/// `/_matrix/federation/v1/media/*/{mediaId}` `Location` header
#[tracing::instrument]
async fn get_redirected_content(
location: String,
) -> Result<authenticated_media_fed::Content> {
let location = location.parse().map_err(|error| {
warn!(location, %error, "Invalid redirect location");
Error::BadServerResponse("Invalid redirect location")
})?;
let response = services()
.globals
.federation_client()
.execute(reqwest::Request::new(Method::GET, location))
.await?;
let content_type = response
.headers()
.get(CONTENT_TYPE)
.map(|value| {
value.to_str().map_err(|error| {
error!(
?value,
%error,
"Invalid Content-Type header"
);
Error::BadServerResponse("Invalid Content-Type header")
})
})
.transpose()?
.map(str::to_owned);
let content_disposition = response
.headers()
.get(CONTENT_DISPOSITION)
.map(|value| {
ContentDisposition::try_from(value.as_bytes()).map_err(|error| {
error!(
?value,
%error,
"Invalid Content-Disposition header"
);
Error::BadServerResponse("Invalid Content-Disposition header")
})
})
.transpose()?;
Ok(authenticated_media_fed::Content {
file: response.bytes().await?.to_vec(),
content_type,
content_disposition,
})
}
#[tracing::instrument(skip_all)]
async fn get_remote_content_via_federation_api(
mxc: &MxcData<'_>, mxc: &MxcData<'_>,
) -> Result<legacy_media::get_content::v3::Response, Error> { ) -> Result<RemoteResponse, Error> {
let authenticated_media_fed::get_content::v1::Response {
metadata,
content,
} = services()
.sending
.send_federation_request(
mxc.server_name,
authenticated_media_fed::get_content::v1::Request {
media_id: mxc.media_id.to_owned(),
timeout_ms: Duration::from_secs(20),
},
)
.await?;
let content = match content {
authenticated_media_fed::FileOrLocation::File(content) => {
debug!("Got media from remote server");
content
}
authenticated_media_fed::FileOrLocation::Location(location) => {
debug!(location, "Following redirect");
get_redirected_content(location).await?
}
};
Ok(RemoteResponse {
metadata,
content,
})
}
#[allow(deprecated)] // unauthenticated media
#[tracing::instrument(skip_all)]
async fn get_remote_content_via_legacy_api(
mxc: &MxcData<'_>,
) -> Result<RemoteResponse, Error> {
let content_response = services() let content_response = services()
.sending .sending
.send_federation_request( .send_federation_request(
@ -181,22 +292,53 @@ pub(crate) async fn get_remote_content(
) )
.await?; .await?;
Ok(RemoteResponse {
metadata: authenticated_media_fed::ContentMetadata {},
content: authenticated_media_fed::Content {
file: content_response.file,
content_disposition: content_response.content_disposition,
content_type: content_response.content_type,
},
})
}
#[tracing::instrument]
pub(crate) async fn get_remote_content(
mxc: &MxcData<'_>,
) -> Result<RemoteResponse, Error> {
let fed_result = get_remote_content_via_federation_api(mxc).await;
let response = match fed_result {
Ok(response) => {
debug!("Got remote content via authenticated media API");
response
}
Err(Error::Federation(_, error))
if error.error_kind() == Some(&ErrorKind::Unrecognized) =>
{
info!(
"Remote server does not support authenticated media, falling \
back to deprecated API"
);
get_remote_content_via_legacy_api(mxc).await?
}
Err(e) => {
return Err(e);
}
};
services() services()
.media .media
.create( .create(
mxc.to_string(), mxc.to_string(),
content_response.content_disposition.as_ref(), response.content.content_disposition.as_ref(),
content_response.content_type.as_deref(), response.content.content_type.as_deref(),
&content_response.file, &response.content.file,
) )
.await?; .await?;
Ok(legacy_media::get_content::v3::Response { Ok(response)
file: content_response.file,
content_disposition: content_response.content_disposition,
content_type: content_response.content_type,
cross_origin_resource_policy: Some("cross-origin".to_owned()),
})
} }
/// # `GET /_matrix/media/r0/download/{serverName}/{mediaId}` /// # `GET /_matrix/media/r0/download/{serverName}/{mediaId}`
@ -205,10 +347,71 @@ pub(crate) async fn get_remote_content(
/// ///
/// - Only allows federation if `allow_remote` is true /// - Only allows federation if `allow_remote` is true
#[allow(deprecated)] // unauthenticated media #[allow(deprecated)] // unauthenticated media
pub(crate) async fn get_content_route( pub(crate) async fn get_content_legacy_route(
body: Ar<legacy_media::get_content::v3::Request>, body: Ar<legacy_media::get_content::v3::Request>,
) -> Result<axum::response::Response> { ) -> Result<axum::response::Response> {
get_content_route_ruma(body).await.map(|x| { use authenticated_media_client::get_content::v1::{
Request as AmRequest, Response as AmResponse,
};
use legacy_media::get_content::v3::{
Request as LegacyRequest, Response as LegacyResponse,
};
fn convert_request(
LegacyRequest {
server_name,
media_id,
timeout_ms,
..
}: LegacyRequest,
) -> AmRequest {
AmRequest {
server_name,
media_id,
timeout_ms,
}
}
fn convert_response(
AmResponse {
file,
content_type,
content_disposition,
}: AmResponse,
) -> LegacyResponse {
LegacyResponse {
file,
content_type,
content_disposition,
cross_origin_resource_policy: Some("cross-origin".to_owned()),
}
}
let allow_remote = body.allow_remote;
get_content_route_ruma(body.map_body(convert_request), allow_remote)
.await
.map(|response| {
let response = convert_response(response);
let mut r = Ra(response).into_response();
set_header_or_panic(
&mut r,
CONTENT_SECURITY_POLICY,
content_security_policy(),
);
r
})
}
/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}`
///
/// Load media from our server or over federation.
pub(crate) async fn get_content_route(
body: Ar<authenticated_media_client::get_content::v1::Request>,
) -> Result<axum::response::Response> {
get_content_route_ruma(body, true).await.map(|x| {
let mut r = Ra(x).into_response(); let mut r = Ra(x).into_response();
set_header_or_panic( set_header_or_panic(
@ -221,10 +424,10 @@ pub(crate) async fn get_content_route(
}) })
} }
#[allow(deprecated)] // unauthenticated media
async fn get_content_route_ruma( async fn get_content_route_ruma(
body: Ar<legacy_media::get_content::v3::Request>, body: Ar<authenticated_media_client::get_content::v1::Request>,
) -> Result<legacy_media::get_content::v3::Response> { allow_remote: bool,
) -> Result<authenticated_media_client::get_content::v1::Response> {
let mxc = MxcData::new(&body.server_name, &body.media_id)?; let mxc = MxcData::new(&body.server_name, &body.media_id)?;
if let Some(FileMeta { if let Some(FileMeta {
@ -233,27 +436,25 @@ async fn get_content_route_ruma(
.. ..
}) = services().media.get(mxc.to_string()).await? }) = services().media.get(mxc.to_string()).await?
{ {
Ok(legacy_media::get_content::v3::Response { Ok(authenticated_media_client::get_content::v1::Response {
file, file,
content_disposition: Some(content_disposition_for( content_disposition: Some(content_disposition_for(
content_type.as_deref(), content_type.as_deref(),
None, None,
)), )),
content_type, content_type,
cross_origin_resource_policy: Some("cross-origin".to_owned()),
}) })
} else if &*body.server_name != services().globals.server_name() } else if &*body.server_name != services().globals.server_name()
&& body.allow_remote && allow_remote
{ {
let remote_content_response = get_remote_content(&mxc).await?; let remote_response = get_remote_content(&mxc).await?;
Ok(legacy_media::get_content::v3::Response { Ok(authenticated_media_client::get_content::v1::Response {
file: remote_content_response.file, file: remote_response.content.file,
content_disposition: Some(content_disposition_for( content_disposition: Some(content_disposition_for(
remote_content_response.content_type.as_deref(), remote_response.content.content_type.as_deref(),
None, None,
)), )),
content_type: remote_content_response.content_type, content_type: remote_response.content.content_type,
cross_origin_resource_policy: Some("cross-origin".to_owned()),
}) })
} else { } else {
Err(Error::BadRequest(ErrorKind::NotYetUploaded, "Media not found.")) Err(Error::BadRequest(ErrorKind::NotYetUploaded, "Media not found."))
@ -266,10 +467,75 @@ async fn get_content_route_ruma(
/// ///
/// - Only allows federation if `allow_remote` is true /// - Only allows federation if `allow_remote` is true
#[allow(deprecated)] // unauthenticated media #[allow(deprecated)] // unauthenticated media
pub(crate) async fn get_content_as_filename_route( pub(crate) async fn get_content_as_filename_legacy_route(
body: Ar<legacy_media::get_content_as_filename::v3::Request>, body: Ar<legacy_media::get_content_as_filename::v3::Request>,
) -> Result<axum::response::Response> { ) -> Result<axum::response::Response> {
get_content_as_filename_route_ruma(body).await.map(|x| { use authenticated_media_client::get_content_as_filename::v1::{
Request as AmRequest, Response as AmResponse,
};
use legacy_media::get_content_as_filename::v3::{
Request as LegacyRequest, Response as LegacyResponse,
};
fn convert_request(
LegacyRequest {
server_name,
media_id,
filename,
timeout_ms,
..
}: LegacyRequest,
) -> AmRequest {
AmRequest {
server_name,
media_id,
filename,
timeout_ms,
}
}
fn convert_response(
AmResponse {
file,
content_type,
content_disposition,
}: AmResponse,
) -> LegacyResponse {
LegacyResponse {
file,
content_type,
content_disposition,
cross_origin_resource_policy: Some("cross-origin".to_owned()),
}
}
let allow_remote = body.allow_remote;
get_content_as_filename_route_ruma(
body.map_body(convert_request),
allow_remote,
)
.await
.map(|response| {
let response = convert_response(response);
let mut r = Ra(response).into_response();
set_header_or_panic(
&mut r,
CONTENT_SECURITY_POLICY,
content_security_policy(),
);
r
})
}
/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}/{fileName}`
///
/// Load media from our server or over federation, permitting desired filename.
pub(crate) async fn get_content_as_filename_route(
body: Ar<authenticated_media_client::get_content_as_filename::v1::Request>,
) -> Result<axum::response::Response> {
get_content_as_filename_route_ruma(body, true).await.map(|x| {
let mut r = Ra(x).into_response(); let mut r = Ra(x).into_response();
set_header_or_panic( set_header_or_panic(
@ -282,10 +548,10 @@ pub(crate) async fn get_content_as_filename_route(
}) })
} }
#[allow(deprecated)] // unauthenticated media
pub(crate) async fn get_content_as_filename_route_ruma( pub(crate) async fn get_content_as_filename_route_ruma(
body: Ar<legacy_media::get_content_as_filename::v3::Request>, body: Ar<authenticated_media_client::get_content_as_filename::v1::Request>,
) -> Result<legacy_media::get_content_as_filename::v3::Response> { allow_remote: bool,
) -> Result<authenticated_media_client::get_content_as_filename::v1::Response> {
let mxc = MxcData::new(&body.server_name, &body.media_id)?; let mxc = MxcData::new(&body.server_name, &body.media_id)?;
if let Some(FileMeta { if let Some(FileMeta {
@ -294,74 +560,242 @@ pub(crate) async fn get_content_as_filename_route_ruma(
.. ..
}) = services().media.get(mxc.to_string()).await? }) = services().media.get(mxc.to_string()).await?
{ {
Ok(legacy_media::get_content_as_filename::v3::Response { Ok(authenticated_media_client::get_content_as_filename::v1::Response {
file, file,
content_disposition: Some(content_disposition_for( content_disposition: Some(content_disposition_for(
content_type.as_deref(), content_type.as_deref(),
Some(body.filename.clone()), Some(body.filename.clone()),
)), )),
content_type, content_type,
cross_origin_resource_policy: Some("cross-origin".to_owned()),
}) })
} else if &*body.server_name != services().globals.server_name() } else if &*body.server_name != services().globals.server_name()
&& body.allow_remote && allow_remote
{ {
let remote_content_response = get_remote_content(&mxc).await?; let remote_response = get_remote_content(&mxc).await?;
Ok(legacy_media::get_content_as_filename::v3::Response { Ok(authenticated_media_client::get_content_as_filename::v1::Response {
content_disposition: Some(content_disposition_for( content_disposition: Some(content_disposition_for(
remote_content_response.content_type.as_deref(), remote_response.content.content_type.as_deref(),
Some(body.filename.clone()), Some(body.filename.clone()),
)), )),
content_type: remote_content_response.content_type, content_type: remote_response.content.content_type,
file: remote_content_response.file, file: remote_response.content.file,
cross_origin_resource_policy: Some("cross-origin".to_owned()),
}) })
} else { } else {
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found.")) Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
} }
} }
fn fix_thumbnail_headers(r: &mut axum::response::Response) {
let content_type = r
.headers()
.get(CONTENT_TYPE)
.and_then(|x| std::str::from_utf8(x.as_ref()).ok())
.map(ToOwned::to_owned);
set_header_or_panic(r, CONTENT_SECURITY_POLICY, content_security_policy());
set_header_or_panic(
r,
CONTENT_DISPOSITION,
content_disposition_for(content_type.as_deref(), None)
.to_string()
.try_into()
.expect("generated header value should be valid"),
);
}
/// # `GET /_matrix/media/r0/thumbnail/{serverName}/{mediaId}` /// # `GET /_matrix/media/r0/thumbnail/{serverName}/{mediaId}`
/// ///
/// Load media thumbnail from our server or over federation. /// Load media thumbnail from our server or over federation.
/// ///
/// - Only allows federation if `allow_remote` is true /// - Only allows federation if `allow_remote` is true
#[allow(deprecated)] // unauthenticated media #[allow(deprecated)] // unauthenticated media
pub(crate) async fn get_content_thumbnail_route( pub(crate) async fn get_content_thumbnail_legacy_route(
body: Ar<legacy_media::get_content_thumbnail::v3::Request>, body: Ar<legacy_media::get_content_thumbnail::v3::Request>,
) -> Result<axum::response::Response> { ) -> Result<axum::response::Response> {
get_content_thumbnail_route_ruma(body).await.map(|x| { use authenticated_media_client::get_content_thumbnail::v1::{
let mut r = Ra(x).into_response(); Request as AmRequest, Response as AmResponse,
};
use legacy_media::get_content_thumbnail::v3::{
Request as LegacyRequest, Response as LegacyResponse,
};
let content_type = r fn convert_request(
.headers() LegacyRequest {
.get(CONTENT_TYPE) server_name,
.and_then(|x| std::str::from_utf8(x.as_ref()).ok()) media_id,
.map(ToOwned::to_owned); method,
width,
height,
timeout_ms,
animated,
..
}: LegacyRequest,
) -> AmRequest {
AmRequest {
server_name,
media_id,
method,
width,
height,
timeout_ms,
animated,
}
}
set_header_or_panic( fn convert_response(
&mut r, AmResponse {
CONTENT_SECURITY_POLICY, file,
content_security_policy(), content_type,
); }: AmResponse,
set_header_or_panic( ) -> LegacyResponse {
&mut r, LegacyResponse {
CONTENT_DISPOSITION, file,
content_disposition_for(content_type.as_deref(), None) content_type,
.to_string() cross_origin_resource_policy: Some("cross-origin".to_owned()),
.try_into() }
.expect("generated header value should be valid"), }
);
let allow_remote = body.allow_remote;
get_content_thumbnail_route_ruma(
body.map_body(convert_request),
allow_remote,
)
.await
.map(|response| {
let response = convert_response(response);
let mut r = Ra(response).into_response();
fix_thumbnail_headers(&mut r);
r r
}) })
} }
/// # `GET /_matrix/client/v1/media/thumbnail/{serverName}/{mediaId}`
///
/// Load media thumbnail from our server or over federation.
pub(crate) async fn get_content_thumbnail_route(
body: Ar<authenticated_media_client::get_content_thumbnail::v1::Request>,
) -> Result<axum::response::Response> {
get_content_thumbnail_route_ruma(body, true).await.map(|x| {
let mut r = Ra(x).into_response();
fix_thumbnail_headers(&mut r);
r
})
}
#[tracing::instrument(skip_all)]
async fn get_remote_thumbnail_via_federation_api(
server_name: &ruma::ServerName,
request: authenticated_media_fed::get_content_thumbnail::v1::Request,
) -> Result<RemoteResponse, Error> {
let authenticated_media_fed::get_content_thumbnail::v1::Response {
metadata,
content,
} = services()
.sending
.send_federation_request(server_name, request)
.await?;
let content = match content {
authenticated_media_fed::FileOrLocation::File(content) => {
debug!("Got thumbnail from remote server");
content
}
authenticated_media_fed::FileOrLocation::Location(location) => {
debug!(location, "Following redirect");
get_redirected_content(location).await?
}
};
Ok(RemoteResponse {
metadata,
content,
})
}
#[allow(deprecated)] // unauthenticated media #[allow(deprecated)] // unauthenticated media
#[tracing::instrument(skip_all)]
async fn get_remote_thumbnail_via_legacy_api(
server_name: &ruma::ServerName,
authenticated_media_fed::get_content_thumbnail::v1::Request {
media_id,
method,
width,
height,
timeout_ms,
animated,
}: authenticated_media_fed::get_content_thumbnail::v1::Request,
) -> Result<RemoteResponse, Error> {
let content_response = services()
.sending
.send_federation_request(
server_name,
legacy_media::get_content_thumbnail::v3::Request {
server_name: server_name.to_owned(),
allow_remote: false,
allow_redirect: false,
media_id,
method,
width,
height,
timeout_ms,
animated,
},
)
.await?;
Ok(RemoteResponse {
metadata: authenticated_media_fed::ContentMetadata {},
content: authenticated_media_fed::Content {
file: content_response.file,
content_disposition: None,
content_type: content_response.content_type,
},
})
}
#[tracing::instrument]
pub(crate) async fn get_remote_thumbnail(
server_name: &ruma::ServerName,
request: authenticated_media_fed::get_content_thumbnail::v1::Request,
) -> Result<RemoteResponse, Error> {
let fed_result =
get_remote_thumbnail_via_federation_api(server_name, request.clone())
.await;
let response = match fed_result {
Ok(response) => {
debug!("Got remote content via authenticated media API");
response
}
Err(Error::Federation(_, error))
if error.error_kind() == Some(&ErrorKind::Unrecognized) =>
{
info!(
"Remote server does not support authenticated media, falling \
back to deprecated API"
);
get_remote_thumbnail_via_legacy_api(server_name, request.clone())
.await?
}
Err(e) => {
return Err(e);
}
};
Ok(response)
}
async fn get_content_thumbnail_route_ruma( async fn get_content_thumbnail_route_ruma(
body: Ar<legacy_media::get_content_thumbnail::v3::Request>, body: Ar<authenticated_media_client::get_content_thumbnail::v1::Request>,
) -> Result<legacy_media::get_content_thumbnail::v3::Response> { allow_remote: bool,
) -> Result<authenticated_media_client::get_content_thumbnail::v1::Response> {
let mxc = MxcData::new(&body.server_name, &body.media_id)?; let mxc = MxcData::new(&body.server_name, &body.media_id)?;
let width = body.width.try_into().map_err(|_| { let width = body.width.try_into().map_err(|_| {
Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid.") Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid.")
@ -377,27 +811,24 @@ async fn get_content_thumbnail_route_ruma(
}) = }) =
services().media.get_thumbnail(mxc.to_string(), width, height).await? services().media.get_thumbnail(mxc.to_string(), width, height).await?
{ {
Ok(legacy_media::get_content_thumbnail::v3::Response { Ok(authenticated_media_client::get_content_thumbnail::v1::Response {
file, file,
content_type, content_type,
cross_origin_resource_policy: Some("cross-origin".to_owned()),
}) })
} else if &*body.server_name != services().globals.server_name() } else if &*body.server_name != services().globals.server_name()
&& body.allow_remote && allow_remote
{ {
let get_thumbnail_response = services() let get_thumbnail_response = get_remote_thumbnail(
.sending
.send_federation_request(
&body.server_name, &body.server_name,
legacy_media::get_content_thumbnail::v3::Request { authenticated_media_fed::get_content_thumbnail::v1::Request {
allow_remote: false,
height: body.height, height: body.height,
width: body.width, width: body.width,
method: body.method.clone(), method: body.method.clone(),
server_name: body.server_name.clone(),
media_id: body.media_id.clone(), media_id: body.media_id.clone(),
timeout_ms: Duration::from_secs(20), timeout_ms: Duration::from_secs(20),
allow_redirect: false, // we don't support animated thumbnails, so don't try requesting
// one - we're allowed to ignore the client's request for an
// animated thumbnail
animated: Some(false), animated: Some(false),
}, },
) )
@ -408,17 +839,16 @@ async fn get_content_thumbnail_route_ruma(
.upload_thumbnail( .upload_thumbnail(
mxc.to_string(), mxc.to_string(),
None, None,
get_thumbnail_response.content_type.as_deref(), get_thumbnail_response.content.content_type.as_deref(),
width, width,
height, height,
&get_thumbnail_response.file, &get_thumbnail_response.content.file,
) )
.await?; .await?;
Ok(legacy_media::get_content_thumbnail::v3::Response { Ok(authenticated_media_client::get_content_thumbnail::v1::Response {
file: get_thumbnail_response.file, file: get_thumbnail_response.content.file,
content_type: get_thumbnail_response.content_type, content_type: get_thumbnail_response.content.content_type,
cross_origin_resource_policy: Some("cross-origin".to_owned()),
}) })
} else { } else {
Err(Error::BadRequest(ErrorKind::NotYetUploaded, "Media not found.")) Err(Error::BadRequest(ErrorKind::NotYetUploaded, "Media not found."))

View file

@ -29,10 +29,10 @@ pub(crate) async fn get_supported_versions_route(
"v1.4".to_owned(), "v1.4".to_owned(),
"v1.5".to_owned(), "v1.5".to_owned(),
], ],
unstable_features: BTreeMap::from_iter([( unstable_features: BTreeMap::from_iter([
"org.matrix.e2e_cross_signing".to_owned(), ("org.matrix.e2e_cross_signing".to_owned(), true),
true, ("org.matrix.msc3916.stable".to_owned(), true),
)]), ]),
}; };
Ok(Ra(resp)) Ok(Ra(resp))

View file

@ -24,6 +24,31 @@ pub(crate) struct Ar<T> {
pub(crate) appservice_info: Option<RegistrationInfo>, pub(crate) appservice_info: Option<RegistrationInfo>,
} }
impl<T> Ar<T> {
pub(crate) fn map_body<F, U>(self, f: F) -> Ar<U>
where
F: FnOnce(T) -> U,
{
let Ar {
body,
sender_user,
sender_device,
sender_servername,
json_body,
appservice_info,
} = self;
Ar {
body: f(body),
sender_user,
sender_device,
sender_servername,
json_body,
appservice_info,
}
}
}
impl<T> Deref for Ar<T> { impl<T> Deref for Ar<T> {
type Target = T; type Target = T;

View file

@ -390,12 +390,24 @@ fn routes(config: &Config) -> Router {
.ruma_route(c2s::get_message_events_route) .ruma_route(c2s::get_message_events_route)
.ruma_route(c2s::search_events_route) .ruma_route(c2s::search_events_route)
.ruma_route(c2s::turn_server_route) .ruma_route(c2s::turn_server_route)
.ruma_route(c2s::send_event_to_device_route) .ruma_route(c2s::send_event_to_device_route);
// unauthenticated (legacy) media
let router = router
.ruma_route(c2s::get_media_config_legacy_route)
.ruma_route(c2s::get_content_legacy_route)
.ruma_route(c2s::get_content_as_filename_legacy_route)
.ruma_route(c2s::get_content_thumbnail_legacy_route);
// authenticated media
let router = router
.ruma_route(c2s::get_media_config_route) .ruma_route(c2s::get_media_config_route)
.ruma_route(c2s::create_content_route) .ruma_route(c2s::create_content_route)
.ruma_route(c2s::get_content_route) .ruma_route(c2s::get_content_route)
.ruma_route(c2s::get_content_as_filename_route) .ruma_route(c2s::get_content_as_filename_route)
.ruma_route(c2s::get_content_thumbnail_route) .ruma_route(c2s::get_content_thumbnail_route);
let router = router
.ruma_route(c2s::get_devices_route) .ruma_route(c2s::get_devices_route)
.ruma_route(c2s::get_device_route) .ruma_route(c2s::get_device_route)
.ruma_route(c2s::update_device_route) .ruma_route(c2s::update_device_route)