mirror of
https://gitlab.com/famedly/conduit.git
synced 2025-07-02 16:38:36 +00:00
Merge remote-tracking branch 'origin/next' into url_previews
This commit is contained in:
commit
9ddfb8cb5e
36 changed files with 2047 additions and 813 deletions
|
@ -315,7 +315,11 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe
|
|||
pub async fn change_password_route(
|
||||
body: Ruma<change_password::v3::Request>,
|
||||
) -> Result<change_password::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_user = body
|
||||
.sender_user
|
||||
.as_ref()
|
||||
// In the future password changes could be performed with UIA with 3PIDs, but we don't support that currently
|
||||
.ok_or_else(|| Error::BadRequest(ErrorKind::MissingToken, "Missing access token."))?;
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
|
@ -402,7 +406,11 @@ pub async fn whoami_route(body: Ruma<whoami::v3::Request>) -> Result<whoami::v3:
|
|||
pub async fn deactivate_route(
|
||||
body: Ruma<deactivate::v3::Request>,
|
||||
) -> Result<deactivate::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_user = body
|
||||
.sender_user
|
||||
.as_ref()
|
||||
// In the future password changes could be performed with UIA with SSO, but we don't support that currently
|
||||
.ok_or_else(|| Error::BadRequest(ErrorKind::MissingToken, "Missing access token."))?;
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
|
|
|
@ -1,12 +1,26 @@
|
|||
// Unauthenticated media is deprecated
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{service::media::{FileMeta, UrlPreviewData}, services, utils, Error, Result, Ruma};
|
||||
use ruma::api::client::{
|
||||
error::{ErrorKind, RetryAfter},
|
||||
media::{
|
||||
create_content, get_content, get_content_as_filename, get_content_thumbnail,
|
||||
get_media_config, get_media_preview
|
||||
use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE};
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{
|
||||
authenticated_media::{
|
||||
get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
|
||||
},
|
||||
error::{ErrorKind, RetryAfter},
|
||||
media::{
|
||||
self, create_content, get_media_preview,
|
||||
},
|
||||
},
|
||||
federation::authenticated_media::{self as federation_media, FileOrLocation},
|
||||
},
|
||||
http_headers::{ContentDisposition, ContentDispositionType},
|
||||
media::Method,
|
||||
ServerName, UInt,
|
||||
};
|
||||
|
||||
use {
|
||||
|
@ -22,9 +36,20 @@ const MXC_LENGTH: usize = 32;
|
|||
///
|
||||
/// Returns max upload size.
|
||||
pub async fn get_media_config_route(
|
||||
_body: Ruma<get_media_config::v3::Request>,
|
||||
) -> Result<get_media_config::v3::Response> {
|
||||
Ok(get_media_config::v3::Response {
|
||||
_body: Ruma<media::get_media_config::v3::Request>,
|
||||
) -> Result<media::get_media_config::v3::Response> {
|
||||
Ok(media::get_media_config::v3::Response {
|
||||
upload_size: services().globals.max_request_size().into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v1/media/config`
|
||||
///
|
||||
/// Returns max upload size.
|
||||
pub async fn get_media_config_auth_route(
|
||||
_body: Ruma<get_media_config::v1::Request>,
|
||||
) -> Result<get_media_config::v1::Response> {
|
||||
Ok(get_media_config::v1::Response {
|
||||
upload_size: services().globals.max_request_size().into(),
|
||||
})
|
||||
}
|
||||
|
@ -301,10 +326,10 @@ pub async fn create_content_route(
|
|||
.media
|
||||
.create(
|
||||
mxc.clone(),
|
||||
body.filename
|
||||
.as_ref()
|
||||
.map(|filename| "inline; filename=".to_owned() + filename)
|
||||
.as_deref(),
|
||||
Some(
|
||||
ContentDisposition::new(ContentDispositionType::Inline)
|
||||
.with_filename(body.filename.clone()),
|
||||
),
|
||||
body.content_type.as_deref(),
|
||||
&body.file,
|
||||
)
|
||||
|
@ -318,28 +343,67 @@ pub async fn create_content_route(
|
|||
|
||||
pub async fn get_remote_content(
|
||||
mxc: &str,
|
||||
server_name: &ruma::ServerName,
|
||||
server_name: &ServerName,
|
||||
media_id: String,
|
||||
) -> Result<get_content::v3::Response, Error> {
|
||||
let content_response = services()
|
||||
) -> Result<get_content::v1::Response, Error> {
|
||||
let content_response = match services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
server_name,
|
||||
get_content::v3::Request {
|
||||
allow_remote: false,
|
||||
server_name: server_name.to_owned(),
|
||||
media_id,
|
||||
federation_media::get_content::v1::Request {
|
||||
media_id: media_id.clone(),
|
||||
timeout_ms: Duration::from_secs(20),
|
||||
allow_redirect: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
{
|
||||
Ok(federation_media::get_content::v1::Response {
|
||||
metadata: _,
|
||||
content: FileOrLocation::File(content),
|
||||
}) => get_content::v1::Response {
|
||||
file: content.file,
|
||||
content_type: content.content_type,
|
||||
content_disposition: content.content_disposition,
|
||||
},
|
||||
|
||||
Ok(federation_media::get_content::v1::Response {
|
||||
metadata: _,
|
||||
content: FileOrLocation::Location(url),
|
||||
}) => get_location_content(url).await?,
|
||||
Err(Error::BadRequest(ErrorKind::Unrecognized, _)) => {
|
||||
let media::get_content::v3::Response {
|
||||
file,
|
||||
content_type,
|
||||
content_disposition,
|
||||
..
|
||||
} = services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
server_name,
|
||||
media::get_content::v3::Request {
|
||||
server_name: server_name.to_owned(),
|
||||
media_id,
|
||||
timeout_ms: Duration::from_secs(20),
|
||||
allow_remote: false,
|
||||
allow_redirect: true,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
get_content::v1::Response {
|
||||
file,
|
||||
content_type,
|
||||
content_disposition,
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
services()
|
||||
.media
|
||||
.create(
|
||||
mxc.to_owned(),
|
||||
content_response.content_disposition.as_deref(),
|
||||
content_response.content_disposition.clone(),
|
||||
content_response.content_type.as_deref(),
|
||||
&content_response.file,
|
||||
)
|
||||
|
@ -354,31 +418,57 @@ pub async fn get_remote_content(
|
|||
///
|
||||
/// - Only allows federation if `allow_remote` is true
|
||||
pub async fn get_content_route(
|
||||
body: Ruma<get_content::v3::Request>,
|
||||
) -> Result<get_content::v3::Response> {
|
||||
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
|
||||
body: Ruma<media::get_content::v3::Request>,
|
||||
) -> Result<media::get_content::v3::Response> {
|
||||
let get_content::v1::Response {
|
||||
file,
|
||||
content_disposition,
|
||||
content_type,
|
||||
} = get_content(&body.server_name, body.media_id.clone(), body.allow_remote).await?;
|
||||
|
||||
if let Some(FileMeta {
|
||||
Ok(media::get_content::v3::Response {
|
||||
file,
|
||||
content_type,
|
||||
content_disposition,
|
||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}`
|
||||
///
|
||||
/// Load media from our server or over federation.
|
||||
pub async fn get_content_auth_route(
|
||||
body: Ruma<get_content::v1::Request>,
|
||||
) -> Result<get_content::v1::Response> {
|
||||
get_content(&body.server_name, body.media_id.clone(), true).await
|
||||
}
|
||||
|
||||
async fn get_content(
|
||||
server_name: &ServerName,
|
||||
media_id: String,
|
||||
allow_remote: bool,
|
||||
) -> Result<get_content::v1::Response, Error> {
|
||||
let mxc = format!("mxc://{}/{}", server_name, media_id);
|
||||
|
||||
if let Ok(Some(FileMeta {
|
||||
content_disposition,
|
||||
content_type,
|
||||
file,
|
||||
}) = services().media.get(mxc.clone()).await?
|
||||
})) = services().media.get(mxc.clone()).await
|
||||
{
|
||||
Ok(get_content::v3::Response {
|
||||
Ok(get_content::v1::Response {
|
||||
file,
|
||||
content_type,
|
||||
content_disposition,
|
||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||
content_disposition: Some(content_disposition),
|
||||
})
|
||||
} else if &*body.server_name != services().globals.server_name() && body.allow_remote {
|
||||
} else if server_name != services().globals.server_name() && allow_remote {
|
||||
let remote_content_response =
|
||||
get_remote_content(&mxc, &body.server_name, body.media_id.clone()).await?;
|
||||
get_remote_content(&mxc, server_name, media_id.clone()).await?;
|
||||
|
||||
Ok(get_content::v3::Response {
|
||||
Ok(get_content::v1::Response {
|
||||
content_disposition: remote_content_response.content_disposition,
|
||||
content_type: remote_content_response.content_type,
|
||||
file: remote_content_response.file,
|
||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||
})
|
||||
} else {
|
||||
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
||||
|
@ -391,29 +481,74 @@ pub async fn get_content_route(
|
|||
///
|
||||
/// - Only allows federation if `allow_remote` is true
|
||||
pub async fn get_content_as_filename_route(
|
||||
body: Ruma<get_content_as_filename::v3::Request>,
|
||||
) -> Result<get_content_as_filename::v3::Response> {
|
||||
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
|
||||
body: Ruma<media::get_content_as_filename::v3::Request>,
|
||||
) -> Result<media::get_content_as_filename::v3::Response> {
|
||||
let get_content_as_filename::v1::Response {
|
||||
file,
|
||||
content_type,
|
||||
content_disposition,
|
||||
} = get_content_as_filename(
|
||||
&body.server_name,
|
||||
body.media_id.clone(),
|
||||
body.filename.clone(),
|
||||
body.allow_remote,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(FileMeta {
|
||||
Ok(media::get_content_as_filename::v3::Response {
|
||||
file,
|
||||
content_type,
|
||||
content_disposition,
|
||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}/{fileName}`
|
||||
///
|
||||
/// Load media from our server or over federation, permitting desired filename.
|
||||
pub async fn get_content_as_filename_auth_route(
|
||||
body: Ruma<get_content_as_filename::v1::Request>,
|
||||
) -> Result<get_content_as_filename::v1::Response, Error> {
|
||||
get_content_as_filename(
|
||||
&body.server_name,
|
||||
body.media_id.clone(),
|
||||
body.filename.clone(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_content_as_filename(
|
||||
server_name: &ServerName,
|
||||
media_id: String,
|
||||
filename: String,
|
||||
allow_remote: bool,
|
||||
) -> Result<get_content_as_filename::v1::Response, Error> {
|
||||
let mxc = format!("mxc://{}/{}", server_name, media_id);
|
||||
|
||||
if let Ok(Some(FileMeta {
|
||||
file, content_type, ..
|
||||
}) = services().media.get(mxc.clone()).await?
|
||||
})) = services().media.get(mxc.clone()).await
|
||||
{
|
||||
Ok(get_content_as_filename::v3::Response {
|
||||
Ok(get_content_as_filename::v1::Response {
|
||||
file,
|
||||
content_type,
|
||||
content_disposition: Some(format!("inline; filename={}", body.filename)),
|
||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||
content_disposition: Some(
|
||||
ContentDisposition::new(ContentDispositionType::Inline)
|
||||
.with_filename(Some(filename.clone())),
|
||||
),
|
||||
})
|
||||
} else if &*body.server_name != services().globals.server_name() && body.allow_remote {
|
||||
} else if server_name != services().globals.server_name() && allow_remote {
|
||||
let remote_content_response =
|
||||
get_remote_content(&mxc, &body.server_name, body.media_id.clone()).await?;
|
||||
get_remote_content(&mxc, server_name, media_id.clone()).await?;
|
||||
|
||||
Ok(get_content_as_filename::v3::Response {
|
||||
content_disposition: Some(format!("inline: filename={}", body.filename)),
|
||||
Ok(get_content_as_filename::v1::Response {
|
||||
content_disposition: Some(
|
||||
ContentDisposition::new(ContentDispositionType::Inline)
|
||||
.with_filename(Some(filename.clone())),
|
||||
),
|
||||
content_type: remote_content_response.content_type,
|
||||
file: remote_content_response.file,
|
||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||
})
|
||||
} else {
|
||||
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
||||
|
@ -426,62 +561,169 @@ pub async fn get_content_as_filename_route(
|
|||
///
|
||||
/// - Only allows federation if `allow_remote` is true
|
||||
pub async fn get_content_thumbnail_route(
|
||||
body: Ruma<get_content_thumbnail::v3::Request>,
|
||||
) -> Result<get_content_thumbnail::v3::Response> {
|
||||
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
|
||||
body: Ruma<media::get_content_thumbnail::v3::Request>,
|
||||
) -> Result<media::get_content_thumbnail::v3::Response> {
|
||||
let get_content_thumbnail::v1::Response { file, content_type } = get_content_thumbnail(
|
||||
&body.server_name,
|
||||
body.media_id.clone(),
|
||||
body.height,
|
||||
body.width,
|
||||
body.method.clone(),
|
||||
body.animated,
|
||||
body.allow_remote,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(FileMeta {
|
||||
Ok(media::get_content_thumbnail::v3::Response {
|
||||
file,
|
||||
content_type,
|
||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v1/media/thumbnail/{serverName}/{mediaId}`
|
||||
///
|
||||
/// Load media thumbnail from our server or over federation.
|
||||
pub async fn get_content_thumbnail_auth_route(
|
||||
body: Ruma<get_content_thumbnail::v1::Request>,
|
||||
) -> Result<get_content_thumbnail::v1::Response> {
|
||||
get_content_thumbnail(
|
||||
&body.server_name,
|
||||
body.media_id.clone(),
|
||||
body.height,
|
||||
body.width,
|
||||
body.method.clone(),
|
||||
body.animated,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_content_thumbnail(
|
||||
server_name: &ServerName,
|
||||
media_id: String,
|
||||
height: UInt,
|
||||
width: UInt,
|
||||
method: Option<Method>,
|
||||
animated: Option<bool>,
|
||||
allow_remote: bool,
|
||||
) -> Result<get_content_thumbnail::v1::Response, Error> {
|
||||
let mxc = format!("mxc://{}/{}", server_name, media_id);
|
||||
|
||||
if let Ok(Some(FileMeta {
|
||||
file, content_type, ..
|
||||
}) = services()
|
||||
})) = services()
|
||||
.media
|
||||
.get_thumbnail(
|
||||
mxc.clone(),
|
||||
body.width
|
||||
width
|
||||
.try_into()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?,
|
||||
body.height
|
||||
height
|
||||
.try_into()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?,
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Height is invalid."))?,
|
||||
)
|
||||
.await?
|
||||
.await
|
||||
{
|
||||
Ok(get_content_thumbnail::v3::Response {
|
||||
file,
|
||||
content_type,
|
||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||
})
|
||||
} else if &*body.server_name != services().globals.server_name() && body.allow_remote {
|
||||
let get_thumbnail_response = services()
|
||||
Ok(get_content_thumbnail::v1::Response { file, content_type })
|
||||
} else if server_name != services().globals.server_name() && allow_remote {
|
||||
let thumbnail_response = match services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
&body.server_name,
|
||||
get_content_thumbnail::v3::Request {
|
||||
allow_remote: false,
|
||||
height: body.height,
|
||||
width: body.width,
|
||||
method: body.method.clone(),
|
||||
server_name: body.server_name.clone(),
|
||||
media_id: body.media_id.clone(),
|
||||
server_name,
|
||||
federation_media::get_content_thumbnail::v1::Request {
|
||||
height,
|
||||
width,
|
||||
method: method.clone(),
|
||||
media_id: media_id.clone(),
|
||||
timeout_ms: Duration::from_secs(20),
|
||||
allow_redirect: false,
|
||||
animated,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
{
|
||||
Ok(federation_media::get_content_thumbnail::v1::Response {
|
||||
metadata: _,
|
||||
content: FileOrLocation::File(content),
|
||||
}) => get_content_thumbnail::v1::Response {
|
||||
file: content.file,
|
||||
content_type: content.content_type,
|
||||
},
|
||||
|
||||
Ok(federation_media::get_content_thumbnail::v1::Response {
|
||||
metadata: _,
|
||||
content: FileOrLocation::Location(url),
|
||||
}) => {
|
||||
let get_content::v1::Response {
|
||||
file, content_type, ..
|
||||
} = get_location_content(url).await?;
|
||||
|
||||
get_content_thumbnail::v1::Response { file, content_type }
|
||||
}
|
||||
Err(Error::BadRequest(ErrorKind::Unrecognized, _)) => {
|
||||
let media::get_content_thumbnail::v3::Response {
|
||||
file, content_type, ..
|
||||
} = services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
server_name,
|
||||
media::get_content_thumbnail::v3::Request {
|
||||
height,
|
||||
width,
|
||||
method: method.clone(),
|
||||
server_name: server_name.to_owned(),
|
||||
media_id: media_id.clone(),
|
||||
timeout_ms: Duration::from_secs(20),
|
||||
allow_redirect: false,
|
||||
animated,
|
||||
allow_remote: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
get_content_thumbnail::v1::Response { file, content_type }
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
services()
|
||||
.media
|
||||
.upload_thumbnail(
|
||||
mxc,
|
||||
None,
|
||||
get_thumbnail_response.content_type.as_deref(),
|
||||
body.width.try_into().expect("all UInts are valid u32s"),
|
||||
body.height.try_into().expect("all UInts are valid u32s"),
|
||||
&get_thumbnail_response.file,
|
||||
thumbnail_response.content_type.as_deref(),
|
||||
width.try_into().expect("all UInts are valid u32s"),
|
||||
height.try_into().expect("all UInts are valid u32s"),
|
||||
&thumbnail_response.file,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(get_thumbnail_response)
|
||||
Ok(thumbnail_response)
|
||||
} else {
|
||||
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_location_content(url: String) -> Result<get_content::v1::Response, Error> {
|
||||
let client = services().globals.default_client();
|
||||
let response = client.get(url).send().await?;
|
||||
let headers = response.headers();
|
||||
|
||||
let content_type = headers
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|header| header.to_str().ok())
|
||||
.map(ToOwned::to_owned);
|
||||
|
||||
let content_disposition = headers
|
||||
.get(CONTENT_DISPOSITION)
|
||||
.map(|header| header.as_bytes())
|
||||
.map(TryFrom::try_from)
|
||||
.and_then(Result::ok);
|
||||
|
||||
let file = response.bytes().await?.to_vec();
|
||||
|
||||
Ok(get_content::v1::Response {
|
||||
file,
|
||||
content_type,
|
||||
content_disposition,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@ pub async fn join_room_by_id_or_alias_route(
|
|||
|
||||
let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias) {
|
||||
Ok(room_id) => {
|
||||
let mut servers = body.server_name.clone();
|
||||
let mut servers = body.via.clone();
|
||||
servers.extend(
|
||||
services()
|
||||
.rooms
|
||||
|
@ -241,6 +241,7 @@ pub async fn kick_user_route(
|
|||
unsigned: None,
|
||||
state_key: Some(body.user_id.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
|
@ -313,6 +314,7 @@ pub async fn ban_user_route(body: Ruma<ban_user::v3::Request>) -> Result<ban_use
|
|||
unsigned: None,
|
||||
state_key: Some(body.user_id.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
|
@ -386,6 +388,7 @@ pub async fn unban_user_route(
|
|||
unsigned: None,
|
||||
state_key: Some(body.user_id.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
|
@ -624,7 +627,7 @@ async fn join_room_by_id_helper(
|
|||
let event_id = format!(
|
||||
"${}",
|
||||
ruma::signatures::reference_hash(&join_event_stub, &room_version_id)
|
||||
.expect("ruma can calculate reference hashes")
|
||||
.expect("Event format validated when event was hashed")
|
||||
);
|
||||
let event_id = <&EventId>::try_from(event_id.as_str())
|
||||
.expect("ruma's reference hashes are valid event ids");
|
||||
|
@ -938,6 +941,7 @@ async fn join_room_by_id_helper(
|
|||
unsigned: None,
|
||||
state_key: Some(sender_user.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
room_id,
|
||||
|
@ -1141,7 +1145,7 @@ async fn validate_and_add_event_id(
|
|||
let event_id = EventId::parse(format!(
|
||||
"${}",
|
||||
ruma::signatures::reference_hash(&value, room_version)
|
||||
.expect("ruma can calculate reference hashes")
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid PDU format"))?
|
||||
))
|
||||
.expect("ruma's reference hashes are valid event ids");
|
||||
|
||||
|
@ -1260,6 +1264,7 @@ pub(crate) async fn invite_helper<'a>(
|
|||
unsigned: None,
|
||||
state_key: Some(user_id.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
room_id,
|
||||
|
@ -1379,6 +1384,7 @@ pub(crate) async fn invite_helper<'a>(
|
|||
unsigned: None,
|
||||
state_key: Some(user_id.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
room_id,
|
||||
|
@ -1506,6 +1512,7 @@ pub async fn leave_room(user_id: &UserId, room_id: &RoomId, reason: Option<Strin
|
|||
unsigned: None,
|
||||
state_key: Some(user_id.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
user_id,
|
||||
room_id,
|
||||
|
@ -1607,7 +1614,7 @@ async fn remote_leave_room(user_id: &UserId, room_id: &RoomId) -> Result<()> {
|
|||
let event_id = EventId::parse(format!(
|
||||
"${}",
|
||||
ruma::signatures::reference_hash(&leave_event_stub, &room_version_id)
|
||||
.expect("ruma can calculate reference hashes")
|
||||
.expect("Event format validated when event was hashed")
|
||||
))
|
||||
.expect("ruma's reference hashes are valid event ids");
|
||||
|
||||
|
|
|
@ -84,6 +84,11 @@ pub async fn send_message_event_route(
|
|||
unsigned: Some(unsigned),
|
||||
state_key: None,
|
||||
redacts: None,
|
||||
timestamp: if body.appservice_info.is_some() {
|
||||
body.timestamp
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
|
|
|
@ -65,6 +65,7 @@ pub async fn set_displayname_route(
|
|||
unsigned: None,
|
||||
state_key: Some(sender_user.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
room_id,
|
||||
))
|
||||
|
@ -200,6 +201,7 @@ pub async fn set_avatar_url_route(
|
|||
unsigned: None,
|
||||
state_key: Some(sender_user.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
room_id,
|
||||
))
|
||||
|
|
|
@ -44,6 +44,7 @@ pub async fn redact_event_route(
|
|||
unsigned: None,
|
||||
state_key: None,
|
||||
redacts: Some(body.event_id.into()),
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
|
|
|
@ -230,6 +230,7 @@ pub async fn create_room_route(
|
|||
unsigned: None,
|
||||
state_key: Some("".to_owned()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
|
@ -258,6 +259,7 @@ pub async fn create_room_route(
|
|||
unsigned: None,
|
||||
state_key: Some(sender_user.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
|
@ -311,6 +313,7 @@ pub async fn create_room_route(
|
|||
unsigned: None,
|
||||
state_key: Some("".to_owned()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
|
@ -334,6 +337,7 @@ pub async fn create_room_route(
|
|||
unsigned: None,
|
||||
state_key: Some("".to_owned()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
|
@ -360,6 +364,7 @@ pub async fn create_room_route(
|
|||
unsigned: None,
|
||||
state_key: Some("".to_owned()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
|
@ -381,6 +386,7 @@ pub async fn create_room_route(
|
|||
unsigned: None,
|
||||
state_key: Some("".to_owned()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
|
@ -403,6 +409,7 @@ pub async fn create_room_route(
|
|||
unsigned: None,
|
||||
state_key: Some("".to_owned()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
|
@ -447,6 +454,7 @@ pub async fn create_room_route(
|
|||
unsigned: None,
|
||||
state_key: Some("".to_owned()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
|
@ -469,6 +477,7 @@ pub async fn create_room_route(
|
|||
unsigned: None,
|
||||
state_key: Some("".to_owned()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
|
@ -629,6 +638,7 @@ pub async fn upgrade_room_route(
|
|||
unsigned: None,
|
||||
state_key: Some("".to_owned()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
|
@ -730,6 +740,7 @@ pub async fn upgrade_room_route(
|
|||
unsigned: None,
|
||||
state_key: Some("".to_owned()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&replacement_room,
|
||||
|
@ -758,6 +769,7 @@ pub async fn upgrade_room_route(
|
|||
unsigned: None,
|
||||
state_key: Some(sender_user.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&replacement_room,
|
||||
|
@ -800,6 +812,7 @@ pub async fn upgrade_room_route(
|
|||
unsigned: None,
|
||||
state_key: Some("".to_owned()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&replacement_room,
|
||||
|
@ -850,6 +863,7 @@ pub async fn upgrade_room_route(
|
|||
unsigned: None,
|
||||
state_key: Some("".to_owned()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
use crate::{services, Result, Ruma};
|
||||
use ruma::api::client::space::get_hierarchy;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::{service::rooms::spaces::PagnationToken, services, Error, Result, Ruma};
|
||||
use ruma::{
|
||||
api::client::{error::ErrorKind, space::get_hierarchy},
|
||||
UInt,
|
||||
};
|
||||
|
||||
/// # `GET /_matrix/client/v1/rooms/{room_id}/hierarchy``
|
||||
///
|
||||
|
@ -9,25 +14,42 @@ pub async fn get_hierarchy_route(
|
|||
) -> Result<get_hierarchy::v1::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let skip = body
|
||||
let limit = body
|
||||
.limit
|
||||
.unwrap_or(UInt::from(10_u32))
|
||||
.min(UInt::from(100_u32));
|
||||
let max_depth = body
|
||||
.max_depth
|
||||
.unwrap_or(UInt::from(3_u32))
|
||||
.min(UInt::from(10_u32));
|
||||
|
||||
let key = body
|
||||
.from
|
||||
.as_ref()
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.unwrap_or(0);
|
||||
.and_then(|s| PagnationToken::from_str(s).ok());
|
||||
|
||||
let limit = body.limit.map_or(10, u64::from).min(100) as usize;
|
||||
|
||||
let max_depth = body.max_depth.map_or(3, u64::from).min(10) as usize + 1; // +1 to skip the space room itself
|
||||
// Should prevent unexpected behaviour in (bad) clients
|
||||
if let Some(token) = &key {
|
||||
if token.suggested_only != body.suggested_only || token.max_depth != max_depth {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"suggested_only and max_depth cannot change on paginated requests",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
services()
|
||||
.rooms
|
||||
.spaces
|
||||
.get_hierarchy(
|
||||
.get_client_hierarchy(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
limit,
|
||||
skip,
|
||||
max_depth,
|
||||
usize::try_from(limit)
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Limit is too great"))?,
|
||||
key.map_or(vec![], |token| token.short_room_ids),
|
||||
usize::try_from(max_depth).map_err(|_| {
|
||||
Error::BadRequest(ErrorKind::InvalidParam, "Max depth is too great")
|
||||
})?,
|
||||
body.suggested_only,
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -10,7 +10,7 @@ use ruma::{
|
|||
room::canonical_alias::RoomCanonicalAliasEventContent, AnyStateEventContent, StateEventType,
|
||||
},
|
||||
serde::Raw,
|
||||
EventId, RoomId, UserId,
|
||||
EventId, MilliSecondsSinceUnixEpoch, RoomId, UserId,
|
||||
};
|
||||
use tracing::log::warn;
|
||||
|
||||
|
@ -32,6 +32,11 @@ pub async fn send_state_event_for_key_route(
|
|||
&body.event_type,
|
||||
&body.body.body, // Yes, I hate it too
|
||||
body.state_key.to_owned(),
|
||||
if body.appservice_info.is_some() {
|
||||
body.timestamp
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -65,6 +70,11 @@ pub async fn send_state_event_for_empty_key_route(
|
|||
&body.event_type.to_string().into(),
|
||||
&body.body.body,
|
||||
body.state_key.to_owned(),
|
||||
if body.appservice_info.is_some() {
|
||||
body.timestamp
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -190,6 +200,7 @@ async fn send_state_event_for_key_helper(
|
|||
event_type: &StateEventType,
|
||||
json: &Raw<AnyStateEventContent>,
|
||||
state_key: String,
|
||||
timestamp: Option<MilliSecondsSinceUnixEpoch>,
|
||||
) -> Result<Arc<EventId>> {
|
||||
let sender_user = sender;
|
||||
|
||||
|
@ -243,6 +254,7 @@ async fn send_state_event_for_key_helper(
|
|||
unsigned: None,
|
||||
state_key: Some(state_key),
|
||||
redacts: None,
|
||||
timestamp,
|
||||
},
|
||||
sender_user,
|
||||
room_id,
|
||||
|
|
|
@ -27,7 +27,10 @@ pub async fn get_supported_versions_route(
|
|||
"v1.4".to_owned(),
|
||||
"v1.5".to_owned(),
|
||||
],
|
||||
unstable_features: BTreeMap::from_iter([("org.matrix.e2e_cross_signing".to_owned(), true)]),
|
||||
unstable_features: BTreeMap::from_iter([
|
||||
("org.matrix.e2e_cross_signing".to_owned(), true),
|
||||
("org.matrix.msc3916.stable".to_owned(), true),
|
||||
]),
|
||||
};
|
||||
|
||||
Ok(resp)
|
||||
|
|
|
@ -7,8 +7,11 @@ use axum::{
|
|||
response::{IntoResponse, Response},
|
||||
RequestExt, RequestPartsExt,
|
||||
};
|
||||
use axum_extra::headers::authorization::Bearer;
|
||||
use axum_extra::{headers::Authorization, typed_header::TypedHeaderRejectionReason, TypedHeader};
|
||||
use axum_extra::{
|
||||
headers::{authorization::Bearer, Authorization},
|
||||
typed_header::TypedHeaderRejectionReason,
|
||||
TypedHeader,
|
||||
};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use http::{Request, StatusCode};
|
||||
use ruma::{
|
||||
|
@ -186,7 +189,7 @@ where
|
|||
|
||||
let origin_signatures = BTreeMap::from_iter([(
|
||||
x_matrix.key.clone(),
|
||||
CanonicalJsonValue::String(x_matrix.sig),
|
||||
CanonicalJsonValue::String(x_matrix.sig.to_string()),
|
||||
)]);
|
||||
|
||||
let signatures = BTreeMap::from_iter([(
|
||||
|
|
|
@ -2,17 +2,25 @@
|
|||
|
||||
use crate::{
|
||||
api::client_server::{self, claim_keys_helper, get_keys_helper},
|
||||
service::pdu::{gen_event_id_canonical_json, PduBuilder},
|
||||
service::{
|
||||
globals::SigningKeys,
|
||||
media::FileMeta,
|
||||
pdu::{gen_event_id_canonical_json, PduBuilder},
|
||||
},
|
||||
services, utils, Error, PduEvent, Result, Ruma,
|
||||
};
|
||||
use axum::{response::IntoResponse, Json};
|
||||
use axum_extra::headers::{CacheControl, Header};
|
||||
use get_profile_information::v1::ProfileField;
|
||||
use http::header::{HeaderValue, AUTHORIZATION};
|
||||
use http::header::AUTHORIZATION;
|
||||
|
||||
use ruma::{
|
||||
api::{
|
||||
client::error::{Error as RumaError, ErrorKind},
|
||||
federation::{
|
||||
authenticated_media::{
|
||||
get_content, get_content_thumbnail, Content, ContentMetadata, FileOrLocation,
|
||||
},
|
||||
authorization::get_event_authorization,
|
||||
backfill::get_backfill,
|
||||
device::get_devices::{self, v1::UserDevice},
|
||||
|
@ -26,6 +34,7 @@ use ruma::{
|
|||
membership::{create_invite, create_join_event, prepare_join_event},
|
||||
openid::get_openid_userinfo,
|
||||
query::{get_profile_information, get_room_information},
|
||||
space::get_hierarchy,
|
||||
transactions::{
|
||||
edu::{DeviceListUpdateContent, DirectDeviceContent, Edu, SigningKeyUpdateContent},
|
||||
send_transaction_message,
|
||||
|
@ -94,13 +103,6 @@ impl FedDest {
|
|||
}
|
||||
}
|
||||
|
||||
fn into_uri_string(self) -> String {
|
||||
match self {
|
||||
Self::Literal(addr) => addr.to_string(),
|
||||
Self::Named(host, ref port) => host + port,
|
||||
}
|
||||
}
|
||||
|
||||
fn hostname(&self) -> String {
|
||||
match &self {
|
||||
Self::Literal(addr) => addr.ip().to_string(),
|
||||
|
@ -136,8 +138,6 @@ where
|
|||
|
||||
debug!("Preparing to send request to {destination}");
|
||||
|
||||
let mut write_destination_to_cache = false;
|
||||
|
||||
let cached_result = services()
|
||||
.globals
|
||||
.actual_destination_cache
|
||||
|
@ -146,14 +146,63 @@ where
|
|||
.get(destination)
|
||||
.cloned();
|
||||
|
||||
let (actual_destination, host) = if let Some(result) = cached_result {
|
||||
result
|
||||
let actual_destination = if let Some(DestinationResponse {
|
||||
actual_destination,
|
||||
dest_type,
|
||||
}) = cached_result
|
||||
{
|
||||
match dest_type {
|
||||
DestType::IsIpOrHasPort => actual_destination,
|
||||
DestType::LookupFailed {
|
||||
well_known_retry,
|
||||
well_known_backoff_mins,
|
||||
} => {
|
||||
if well_known_retry < Instant::now() {
|
||||
find_actual_destination(destination, None, false, Some(well_known_backoff_mins))
|
||||
.await
|
||||
} else {
|
||||
actual_destination
|
||||
}
|
||||
}
|
||||
|
||||
DestType::WellKnown { expires } => {
|
||||
if expires < Instant::now() {
|
||||
find_actual_destination(destination, None, false, None).await
|
||||
} else {
|
||||
actual_destination
|
||||
}
|
||||
}
|
||||
DestType::WellKnownSrv {
|
||||
srv_expires,
|
||||
well_known_expires,
|
||||
well_known_host,
|
||||
} => {
|
||||
if well_known_expires < Instant::now() {
|
||||
find_actual_destination(destination, None, false, None).await
|
||||
} else if srv_expires < Instant::now() {
|
||||
find_actual_destination(destination, Some(well_known_host), true, None).await
|
||||
} else {
|
||||
actual_destination
|
||||
}
|
||||
}
|
||||
DestType::Srv {
|
||||
well_known_retry,
|
||||
well_known_backoff_mins,
|
||||
srv_expires,
|
||||
} => {
|
||||
if well_known_retry < Instant::now() {
|
||||
find_actual_destination(destination, None, false, Some(well_known_backoff_mins))
|
||||
.await
|
||||
} else if srv_expires < Instant::now() {
|
||||
find_actual_destination(destination, None, true, Some(well_known_backoff_mins))
|
||||
.await
|
||||
} else {
|
||||
actual_destination
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
write_destination_to_cache = true;
|
||||
|
||||
let result = find_actual_destination(destination).await;
|
||||
|
||||
(result.0, result.1.into_uri_string())
|
||||
find_actual_destination(destination, None, false, None).await
|
||||
};
|
||||
|
||||
let actual_destination_str = actual_destination.clone().into_https_string();
|
||||
|
@ -162,7 +211,7 @@ where
|
|||
.try_into_http_request::<Vec<u8>>(
|
||||
&actual_destination_str,
|
||||
SendAccessToken::IfRequired(""),
|
||||
&[MatrixVersion::V1_4],
|
||||
&[MatrixVersion::V1_11],
|
||||
)
|
||||
.map_err(|e| {
|
||||
warn!(
|
||||
|
@ -226,13 +275,14 @@ where
|
|||
for s in signature_server {
|
||||
http_request.headers_mut().insert(
|
||||
AUTHORIZATION,
|
||||
HeaderValue::from_str(&format!(
|
||||
format!(
|
||||
"X-Matrix origin=\"{}\",destination=\"{}\",key=\"{}\",sig=\"{}\"",
|
||||
services().globals.server_name(),
|
||||
destination,
|
||||
s.0,
|
||||
s.1
|
||||
))
|
||||
)
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
@ -290,21 +340,10 @@ where
|
|||
if status == 200 {
|
||||
debug!("Parsing response bytes from {destination}");
|
||||
let response = T::IncomingResponse::try_from_http_response(http_response);
|
||||
if response.is_ok() && write_destination_to_cache {
|
||||
services()
|
||||
.globals
|
||||
.actual_destination_cache
|
||||
.write()
|
||||
.await
|
||||
.insert(
|
||||
OwnedServerName::from(destination),
|
||||
(actual_destination, host),
|
||||
);
|
||||
}
|
||||
|
||||
response.map_err(|e| {
|
||||
warn!(
|
||||
"Invalid 200 response from {} on: {} {}",
|
||||
"Invalid 200 response from {} on: {} {:?}",
|
||||
&destination, url, e
|
||||
);
|
||||
Error::BadServerResponse("Server returned bad 200 response.")
|
||||
|
@ -345,142 +384,225 @@ fn add_port_to_hostname(destination_str: &str) -> FedDest {
|
|||
FedDest::Named(host.to_owned(), port.to_owned())
|
||||
}
|
||||
|
||||
/// Returns: actual_destination, host header
|
||||
/// Implemented according to the specification at <https://matrix.org/docs/spec/server_server/r0.1.4#resolving-server-names>
|
||||
#[derive(Clone)]
|
||||
pub struct DestinationResponse {
|
||||
pub actual_destination: FedDest,
|
||||
pub dest_type: DestType,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum DestType {
|
||||
WellKnownSrv {
|
||||
srv_expires: Instant,
|
||||
well_known_expires: Instant,
|
||||
well_known_host: String,
|
||||
},
|
||||
WellKnown {
|
||||
expires: Instant,
|
||||
},
|
||||
Srv {
|
||||
srv_expires: Instant,
|
||||
well_known_retry: Instant,
|
||||
well_known_backoff_mins: u16,
|
||||
},
|
||||
IsIpOrHasPort,
|
||||
LookupFailed {
|
||||
well_known_retry: Instant,
|
||||
well_known_backoff_mins: u16,
|
||||
},
|
||||
}
|
||||
|
||||
/// Implemented according to the specification at <https://spec.matrix.org/v1.11/server-server-api/#resolving-server-names>
|
||||
/// Numbers in comments below refer to bullet points in linked section of specification
|
||||
async fn find_actual_destination(destination: &'_ ServerName) -> (FedDest, FedDest) {
|
||||
async fn find_actual_destination(
|
||||
destination: &'_ ServerName,
|
||||
// The host used to potentially lookup SRV records against, only used when only_request_srv is true
|
||||
well_known_dest: Option<String>,
|
||||
// Should be used when only the SRV lookup has expired
|
||||
only_request_srv: bool,
|
||||
// The backoff time for the last well known failure, if any
|
||||
well_known_backoff_mins: Option<u16>,
|
||||
) -> FedDest {
|
||||
debug!("Finding actual destination for {destination}");
|
||||
let destination_str = destination.as_str().to_owned();
|
||||
let mut hostname = destination_str.clone();
|
||||
let actual_destination = match get_ip_with_port(&destination_str) {
|
||||
Some(host_port) => {
|
||||
debug!("1: IP literal with provided or default port");
|
||||
host_port
|
||||
}
|
||||
None => {
|
||||
if let Some(pos) = destination_str.find(':') {
|
||||
debug!("2: Hostname with included port");
|
||||
let (host, port) = destination_str.split_at(pos);
|
||||
FedDest::Named(host.to_owned(), port.to_owned())
|
||||
let destination_str = destination.to_string();
|
||||
let next_backoff_mins = well_known_backoff_mins
|
||||
// Errors are recommended to be cached for up to an hour
|
||||
.map(|mins| (mins * 2).min(60))
|
||||
.unwrap_or(1);
|
||||
|
||||
let (actual_destination, dest_type) = if only_request_srv {
|
||||
let destination_str = well_known_dest.unwrap_or(destination_str);
|
||||
let (dest, expires) = get_srv_destination(destination_str).await;
|
||||
let well_known_retry =
|
||||
Instant::now() + Duration::from_secs((60 * next_backoff_mins).into());
|
||||
(
|
||||
dest,
|
||||
if let Some(expires) = expires {
|
||||
DestType::Srv {
|
||||
well_known_backoff_mins: next_backoff_mins,
|
||||
srv_expires: expires,
|
||||
|
||||
well_known_retry,
|
||||
}
|
||||
} else {
|
||||
debug!("Requesting well known for {destination}");
|
||||
match request_well_known(destination.as_str()).await {
|
||||
Some(delegated_hostname) => {
|
||||
debug!("3: A .well-known file is available");
|
||||
hostname = add_port_to_hostname(&delegated_hostname).into_uri_string();
|
||||
match get_ip_with_port(&delegated_hostname) {
|
||||
Some(host_and_port) => host_and_port, // 3.1: IP literal in .well-known file
|
||||
None => {
|
||||
if let Some(pos) = delegated_hostname.find(':') {
|
||||
debug!("3.2: Hostname with port in .well-known file");
|
||||
let (host, port) = delegated_hostname.split_at(pos);
|
||||
FedDest::Named(host.to_owned(), port.to_owned())
|
||||
} else {
|
||||
debug!("Delegated hostname has no port in this branch");
|
||||
if let Some(hostname_override) =
|
||||
query_srv_record(&delegated_hostname).await
|
||||
{
|
||||
debug!("3.3: SRV lookup successful");
|
||||
let force_port = hostname_override.port();
|
||||
|
||||
if let Ok(override_ip) = services()
|
||||
.globals
|
||||
.dns_resolver()
|
||||
.lookup_ip(hostname_override.hostname())
|
||||
.await
|
||||
{
|
||||
services()
|
||||
.globals
|
||||
.tls_name_override
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(
|
||||
delegated_hostname.clone(),
|
||||
(
|
||||
override_ip.iter().collect(),
|
||||
force_port.unwrap_or(8448),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
warn!("Using SRV record, but could not resolve to IP");
|
||||
}
|
||||
|
||||
if let Some(port) = force_port {
|
||||
FedDest::Named(delegated_hostname, format!(":{port}"))
|
||||
} else {
|
||||
add_port_to_hostname(&delegated_hostname)
|
||||
}
|
||||
DestType::LookupFailed {
|
||||
well_known_retry,
|
||||
well_known_backoff_mins: next_backoff_mins,
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
match get_ip_with_port(&destination_str) {
|
||||
Some(host_port) => {
|
||||
debug!("1: IP literal with provided or default port");
|
||||
(host_port, DestType::IsIpOrHasPort)
|
||||
}
|
||||
None => {
|
||||
if let Some(pos) = destination_str.find(':') {
|
||||
debug!("2: Hostname with included port");
|
||||
let (host, port) = destination_str.split_at(pos);
|
||||
(
|
||||
FedDest::Named(host.to_owned(), port.to_owned()),
|
||||
DestType::IsIpOrHasPort,
|
||||
)
|
||||
} else {
|
||||
debug!("Requesting well known for {destination_str}");
|
||||
match request_well_known(destination_str.as_str()).await {
|
||||
Some((delegated_hostname, timestamp)) => {
|
||||
debug!("3: A .well-known file is available");
|
||||
match get_ip_with_port(&delegated_hostname) {
|
||||
// 3.1: IP literal in .well-known file
|
||||
Some(host_and_port) => {
|
||||
(host_and_port, DestType::WellKnown { expires: timestamp })
|
||||
}
|
||||
None => {
|
||||
if let Some(pos) = delegated_hostname.find(':') {
|
||||
debug!("3.2: Hostname with port in .well-known file");
|
||||
let (host, port) = delegated_hostname.split_at(pos);
|
||||
(
|
||||
FedDest::Named(host.to_owned(), port.to_owned()),
|
||||
DestType::WellKnown { expires: timestamp },
|
||||
)
|
||||
} else {
|
||||
debug!("3.4: No SRV records, just use the hostname from .well-known");
|
||||
add_port_to_hostname(&delegated_hostname)
|
||||
debug!("Delegated hostname has no port in this branch");
|
||||
let (dest, srv_expires) =
|
||||
get_srv_destination(delegated_hostname.clone()).await;
|
||||
(
|
||||
dest,
|
||||
if let Some(srv_expires) = srv_expires {
|
||||
DestType::WellKnownSrv {
|
||||
srv_expires,
|
||||
well_known_expires: timestamp,
|
||||
well_known_host: delegated_hostname,
|
||||
}
|
||||
} else {
|
||||
DestType::WellKnown { expires: timestamp }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
debug!("4: No .well-known or an error occured");
|
||||
match query_srv_record(&destination_str).await {
|
||||
Some(hostname_override) => {
|
||||
debug!("4: SRV record found");
|
||||
let force_port = hostname_override.port();
|
||||
|
||||
if let Ok(override_ip) = services()
|
||||
.globals
|
||||
.dns_resolver()
|
||||
.lookup_ip(hostname_override.hostname())
|
||||
.await
|
||||
{
|
||||
services()
|
||||
.globals
|
||||
.tls_name_override
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(
|
||||
hostname.clone(),
|
||||
(
|
||||
override_ip.iter().collect(),
|
||||
force_port.unwrap_or(8448),
|
||||
),
|
||||
);
|
||||
None => {
|
||||
debug!("4: No .well-known or an error occured");
|
||||
let (dest, expires) = get_srv_destination(destination_str).await;
|
||||
let well_known_retry = Instant::now()
|
||||
+ Duration::from_secs((60 * next_backoff_mins).into());
|
||||
(
|
||||
dest,
|
||||
if let Some(expires) = expires {
|
||||
DestType::Srv {
|
||||
srv_expires: expires,
|
||||
well_known_retry,
|
||||
well_known_backoff_mins: next_backoff_mins,
|
||||
}
|
||||
} else {
|
||||
warn!("Using SRV record, but could not resolve to IP");
|
||||
}
|
||||
|
||||
if let Some(port) = force_port {
|
||||
FedDest::Named(hostname.clone(), format!(":{port}"))
|
||||
} else {
|
||||
add_port_to_hostname(&hostname)
|
||||
}
|
||||
}
|
||||
None => {
|
||||
debug!("5: No SRV record found");
|
||||
add_port_to_hostname(&destination_str)
|
||||
}
|
||||
DestType::LookupFailed {
|
||||
well_known_retry,
|
||||
well_known_backoff_mins: next_backoff_mins,
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Actual destination: {actual_destination:?}");
|
||||
|
||||
// Can't use get_ip_with_port here because we don't want to add a port
|
||||
// to an IP address if it wasn't specified
|
||||
let hostname = if let Ok(addr) = hostname.parse::<SocketAddr>() {
|
||||
FedDest::Literal(addr)
|
||||
} else if let Ok(addr) = hostname.parse::<IpAddr>() {
|
||||
FedDest::Named(addr.to_string(), ":8448".to_owned())
|
||||
} else if let Some(pos) = hostname.find(':') {
|
||||
let (host, port) = hostname.split_at(pos);
|
||||
FedDest::Named(host.to_owned(), port.to_owned())
|
||||
} else {
|
||||
FedDest::Named(hostname, ":8448".to_owned())
|
||||
let response = DestinationResponse {
|
||||
actual_destination,
|
||||
dest_type,
|
||||
};
|
||||
(actual_destination, hostname)
|
||||
|
||||
services()
|
||||
.globals
|
||||
.actual_destination_cache
|
||||
.write()
|
||||
.await
|
||||
.insert(destination.to_owned(), response.clone());
|
||||
|
||||
response.actual_destination
|
||||
}
|
||||
|
||||
async fn query_given_srv_record(record: &str) -> Option<FedDest> {
|
||||
/// Looks up the SRV records for federation usage
|
||||
///
|
||||
/// If no timestamp is returned, that means no SRV record was found
|
||||
async fn get_srv_destination(delegated_hostname: String) -> (FedDest, Option<Instant>) {
|
||||
if let Some((hostname_override, timestamp)) = query_srv_record(&delegated_hostname).await {
|
||||
debug!("SRV lookup successful");
|
||||
let force_port = hostname_override.port();
|
||||
|
||||
if let Ok(override_ip) = services()
|
||||
.globals
|
||||
.dns_resolver()
|
||||
.lookup_ip(hostname_override.hostname())
|
||||
.await
|
||||
{
|
||||
services()
|
||||
.globals
|
||||
.tls_name_override
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(
|
||||
delegated_hostname.clone(),
|
||||
(override_ip.iter().collect(), force_port.unwrap_or(8448)),
|
||||
);
|
||||
} else {
|
||||
// Removing in case there was previously a SRV record
|
||||
services()
|
||||
.globals
|
||||
.tls_name_override
|
||||
.write()
|
||||
.unwrap()
|
||||
.remove(&delegated_hostname);
|
||||
warn!("Using SRV record, but could not resolve to IP");
|
||||
}
|
||||
|
||||
if let Some(port) = force_port {
|
||||
(
|
||||
FedDest::Named(delegated_hostname, format!(":{port}")),
|
||||
Some(timestamp),
|
||||
)
|
||||
} else {
|
||||
(add_port_to_hostname(&delegated_hostname), Some(timestamp))
|
||||
}
|
||||
} else {
|
||||
// Removing in case there was previously a SRV record
|
||||
services()
|
||||
.globals
|
||||
.tls_name_override
|
||||
.write()
|
||||
.unwrap()
|
||||
.remove(&delegated_hostname);
|
||||
debug!("No SRV records found");
|
||||
(add_port_to_hostname(&delegated_hostname), None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn query_given_srv_record(record: &str) -> Option<(FedDest, Instant)> {
|
||||
services()
|
||||
.globals
|
||||
.dns_resolver()
|
||||
|
@ -488,16 +610,19 @@ async fn query_given_srv_record(record: &str) -> Option<FedDest> {
|
|||
.await
|
||||
.map(|srv| {
|
||||
srv.iter().next().map(|result| {
|
||||
FedDest::Named(
|
||||
result.target().to_string().trim_end_matches('.').to_owned(),
|
||||
format!(":{}", result.port()),
|
||||
(
|
||||
FedDest::Named(
|
||||
result.target().to_string().trim_end_matches('.').to_owned(),
|
||||
format!(":{}", result.port()),
|
||||
),
|
||||
srv.as_lookup().valid_until(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.unwrap_or(None)
|
||||
}
|
||||
|
||||
async fn query_srv_record(hostname: &'_ str) -> Option<FedDest> {
|
||||
async fn query_srv_record(hostname: &'_ str) -> Option<(FedDest, Instant)> {
|
||||
let hostname = hostname.trim_end_matches('.');
|
||||
|
||||
if let Some(host_port) = query_given_srv_record(&format!("_matrix-fed._tcp.{hostname}.")).await
|
||||
|
@ -508,7 +633,7 @@ async fn query_srv_record(hostname: &'_ str) -> Option<FedDest> {
|
|||
}
|
||||
}
|
||||
|
||||
async fn request_well_known(destination: &str) -> Option<String> {
|
||||
async fn request_well_known(destination: &str) -> Option<(String, Instant)> {
|
||||
let response = services()
|
||||
.globals
|
||||
.default_client()
|
||||
|
@ -516,14 +641,40 @@ async fn request_well_known(destination: &str) -> Option<String> {
|
|||
.send()
|
||||
.await;
|
||||
debug!("Got well known response");
|
||||
if let Err(e) = &response {
|
||||
debug!("Well known error: {e:?}");
|
||||
return None;
|
||||
}
|
||||
let text = response.ok()?.text().await;
|
||||
let response = match response {
|
||||
Err(e) => {
|
||||
debug!("Well known error: {e:?}");
|
||||
return None;
|
||||
}
|
||||
Ok(r) => r,
|
||||
};
|
||||
|
||||
let mut headers = response.headers().values();
|
||||
|
||||
let cache_for = CacheControl::decode(&mut headers)
|
||||
.ok()
|
||||
.and_then(|cc| {
|
||||
// Servers should respect the cache control headers present on the response, or use a sensible default when headers are not present.
|
||||
if cc.no_store() || cc.no_cache() {
|
||||
Some(Duration::ZERO)
|
||||
} else {
|
||||
cc.max_age()
|
||||
// Servers should additionally impose a maximum cache time for responses: 48 hours is recommended.
|
||||
.map(|age| age.min(Duration::from_secs(60 * 60 * 48)))
|
||||
}
|
||||
})
|
||||
// The recommended sensible default is 24 hours.
|
||||
.unwrap_or_else(|| Duration::from_secs(60 * 60 * 24));
|
||||
|
||||
let text = response.text().await;
|
||||
debug!("Got well known response text");
|
||||
let body: serde_json::Value = serde_json::from_str(&text.ok()?).ok()?;
|
||||
Some(body.get("m.server")?.as_str()?.to_owned())
|
||||
|
||||
let host = || {
|
||||
let body: serde_json::Value = serde_json::from_str(&text.ok()?).ok()?;
|
||||
body.get("m.server")?.as_str().map(ToOwned::to_owned)
|
||||
};
|
||||
|
||||
host().map(|host| (host, Instant::now() + cache_for))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/federation/v1/version`
|
||||
|
@ -663,17 +814,78 @@ pub fn parse_incoming_pdu(
|
|||
|
||||
let (event_id, value) = match gen_event_id_canonical_json(pdu, &room_version_id) {
|
||||
Ok(t) => t,
|
||||
Err(_) => {
|
||||
Err(e) => {
|
||||
// Event could not be converted to canonical json
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Could not convert event to canonical json.",
|
||||
));
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
Ok((event_id, value, room_id))
|
||||
}
|
||||
|
||||
/// Attempts to parse and append PDU to timeline.
|
||||
/// If no event ID is returned, then the PDU was failed to be parsed.
|
||||
/// If the Ok(()) is returned, then the PDU was successfully appended to the timeline.
|
||||
async fn handle_pdu_in_transaction(
|
||||
origin: &ServerName,
|
||||
pub_key_map: &RwLock<BTreeMap<String, SigningKeys>>,
|
||||
pdu: &RawJsonValue,
|
||||
) -> (Option<OwnedEventId>, Result<()>) {
|
||||
let (event_id, value, room_id) = match parse_incoming_pdu(pdu) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
warn!("Could not parse PDU: {e}");
|
||||
warn!("Full PDU: {:?}", &pdu);
|
||||
return (None, Err(Error::BadServerResponse("Could not parse PDU")));
|
||||
}
|
||||
};
|
||||
|
||||
// Makes use of the m.room.create event. If we cannot fetch this event,
|
||||
// we must have never been in that room.
|
||||
if services().rooms.state.get_room_version(&room_id).is_err() {
|
||||
debug!("Room {room_id} is not known to this server");
|
||||
return (
|
||||
Some(event_id),
|
||||
Err(Error::BadServerResponse("Room is not known to this server")),
|
||||
);
|
||||
}
|
||||
|
||||
// We do not add the event_id field to the pdu here because of signature and hashes checks
|
||||
|
||||
let mutex = Arc::clone(
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_federation
|
||||
.write()
|
||||
.await
|
||||
.entry(room_id.to_owned())
|
||||
.or_default(),
|
||||
);
|
||||
let mutex_lock = mutex.lock().await;
|
||||
let start_time = Instant::now();
|
||||
|
||||
if let Err(e) = services()
|
||||
.rooms
|
||||
.event_handler
|
||||
.handle_incoming_pdu(origin, &event_id, &room_id, value, true, pub_key_map)
|
||||
.await
|
||||
{
|
||||
warn!("Error appending PDU to timeline: {}: {:?}", e, pdu);
|
||||
return (Some(event_id), Err(e));
|
||||
}
|
||||
|
||||
drop(mutex_lock);
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
debug!(
|
||||
"Handling transaction of event {} took {}m{}s",
|
||||
event_id,
|
||||
elapsed.as_secs() / 60,
|
||||
elapsed.as_secs() % 60
|
||||
);
|
||||
|
||||
(Some(event_id), Ok(()))
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/federation/v1/send/{txnId}`
|
||||
///
|
||||
/// Push EDUs and PDUs to this server.
|
||||
|
@ -698,77 +910,11 @@ pub async fn send_transaction_message_route(
|
|||
// let mut auth_cache = EventMap::new();
|
||||
|
||||
for pdu in &body.pdus {
|
||||
let value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| {
|
||||
warn!("Error parsing incoming event {:?}: {:?}", pdu, e);
|
||||
Error::BadServerResponse("Invalid PDU in server response")
|
||||
})?;
|
||||
let room_id: OwnedRoomId = value
|
||||
.get("room_id")
|
||||
.and_then(|id| RoomId::parse(id.as_str()?).ok())
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Invalid room id in pdu",
|
||||
))?;
|
||||
let (event_id, result) =
|
||||
handle_pdu_in_transaction(sender_servername, &pub_key_map, pdu).await;
|
||||
|
||||
if services().rooms.state.get_room_version(&room_id).is_err() {
|
||||
debug!("Server is not in room {room_id}");
|
||||
continue;
|
||||
}
|
||||
|
||||
let r = parse_incoming_pdu(pdu);
|
||||
let (event_id, value, room_id) = match r {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
warn!("Could not parse PDU: {e}");
|
||||
warn!("Full PDU: {:?}", &pdu);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// We do not add the event_id field to the pdu here because of signature and hashes checks
|
||||
|
||||
let mutex = Arc::clone(
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_federation
|
||||
.write()
|
||||
.await
|
||||
.entry(room_id.to_owned())
|
||||
.or_default(),
|
||||
);
|
||||
let mutex_lock = mutex.lock().await;
|
||||
let start_time = Instant::now();
|
||||
resolved_map.insert(
|
||||
event_id.clone(),
|
||||
services()
|
||||
.rooms
|
||||
.event_handler
|
||||
.handle_incoming_pdu(
|
||||
sender_servername,
|
||||
&event_id,
|
||||
&room_id,
|
||||
value,
|
||||
true,
|
||||
&pub_key_map,
|
||||
)
|
||||
.await
|
||||
.map(|_| ()),
|
||||
);
|
||||
drop(mutex_lock);
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
debug!(
|
||||
"Handling transaction of event {} took {}m{}s",
|
||||
event_id,
|
||||
elapsed.as_secs() / 60,
|
||||
elapsed.as_secs() % 60
|
||||
);
|
||||
}
|
||||
|
||||
for pdu in &resolved_map {
|
||||
if let Err(e) = pdu.1 {
|
||||
if matches!(e, Error::BadRequest(ErrorKind::NotFound, _)) {
|
||||
warn!("Incoming PDU failed {:?}", pdu);
|
||||
}
|
||||
if let Some(event_id) = event_id {
|
||||
resolved_map.insert(event_id.clone(), result.map_err(|e| e.sanitized_error()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -937,12 +1083,7 @@ pub async fn send_transaction_message_route(
|
|||
}
|
||||
}
|
||||
|
||||
Ok(send_transaction_message::v1::Response {
|
||||
pdus: resolved_map
|
||||
.into_iter()
|
||||
.map(|(e, r)| (e, r.map_err(|e| e.sanitized_error())))
|
||||
.collect(),
|
||||
})
|
||||
Ok(send_transaction_message::v1::Response { pdus: resolved_map })
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/federation/v1/event/{eventId}`
|
||||
|
@ -1445,6 +1586,7 @@ pub async fn create_join_event_template_route(
|
|||
unsigned: None,
|
||||
state_key: Some(body.user_id.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
&body.user_id,
|
||||
&body.room_id,
|
||||
|
@ -1684,7 +1826,7 @@ pub async fn create_invite_route(
|
|||
let event_id = EventId::parse(format!(
|
||||
"${}",
|
||||
ruma::signatures::reference_hash(&signed_event, &body.room_version)
|
||||
.expect("ruma can calculate reference hashes")
|
||||
.expect("Event format validated when event was hashed")
|
||||
))
|
||||
.expect("ruma's reference hashes are valid event ids");
|
||||
|
||||
|
@ -1753,6 +1895,90 @@ pub async fn create_invite_route(
|
|||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/federation/v1/media/download/{serverName}/{mediaId}`
|
||||
///
|
||||
/// Load media from our server.
|
||||
pub async fn get_content_route(
|
||||
body: Ruma<get_content::v1::Request>,
|
||||
) -> Result<get_content::v1::Response> {
|
||||
let mxc = format!(
|
||||
"mxc://{}/{}",
|
||||
services().globals.server_name(),
|
||||
body.media_id
|
||||
);
|
||||
|
||||
if let Some(FileMeta {
|
||||
content_disposition,
|
||||
content_type,
|
||||
file,
|
||||
}) = services().media.get(mxc.clone()).await?
|
||||
{
|
||||
Ok(get_content::v1::Response::new(
|
||||
ContentMetadata::new(),
|
||||
FileOrLocation::File(Content {
|
||||
file,
|
||||
content_type,
|
||||
content_disposition: Some(content_disposition),
|
||||
}),
|
||||
))
|
||||
} else {
|
||||
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
||||
}
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/federation/v1/media/thumbnail/{serverName}/{mediaId}`
|
||||
///
|
||||
/// Load media thumbnail from our server or over federation.
|
||||
pub async fn get_content_thumbnail_route(
|
||||
body: Ruma<get_content_thumbnail::v1::Request>,
|
||||
) -> Result<get_content_thumbnail::v1::Response> {
|
||||
let mxc = format!(
|
||||
"mxc://{}/{}",
|
||||
services().globals.server_name(),
|
||||
body.media_id
|
||||
);
|
||||
|
||||
let Some(FileMeta {
|
||||
file,
|
||||
content_type,
|
||||
content_disposition,
|
||||
}) = services()
|
||||
.media
|
||||
.get_thumbnail(
|
||||
mxc.clone(),
|
||||
body.width
|
||||
.try_into()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?,
|
||||
body.height
|
||||
.try_into()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?,
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."));
|
||||
};
|
||||
|
||||
services()
|
||||
.media
|
||||
.upload_thumbnail(
|
||||
mxc,
|
||||
content_type.as_deref(),
|
||||
body.width.try_into().expect("all UInts are valid u32s"),
|
||||
body.height.try_into().expect("all UInts are valid u32s"),
|
||||
&file,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(get_content_thumbnail::v1::Response::new(
|
||||
ContentMetadata::new(),
|
||||
FileOrLocation::File(Content {
|
||||
file,
|
||||
content_type,
|
||||
content_disposition: Some(content_disposition),
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/federation/v1/user/devices/{userId}`
|
||||
///
|
||||
/// Gets information on all devices of the user.
|
||||
|
@ -1937,6 +2163,31 @@ pub async fn get_openid_userinfo_route(
|
|||
))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/federation/v1/hierarchy/{roomId}`
|
||||
///
|
||||
/// Gets the space tree in a depth-first manner to locate child rooms of a given space.
|
||||
pub async fn get_hierarchy_route(
|
||||
body: Ruma<get_hierarchy::v1::Request>,
|
||||
) -> Result<get_hierarchy::v1::Response> {
|
||||
let sender_servername = body
|
||||
.sender_servername
|
||||
.as_ref()
|
||||
.expect("server is authenticated");
|
||||
|
||||
if services().rooms.metadata.exists(&body.room_id)? {
|
||||
services()
|
||||
.rooms
|
||||
.spaces
|
||||
.get_federation_hierarchy(&body.room_id, sender_servername, body.suggested_only)
|
||||
.await
|
||||
} else {
|
||||
Err(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Room does not exist.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// # `GET /.well-known/matrix/server`
|
||||
///
|
||||
/// Returns the federation server discovery information.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue