diff --git a/Cargo.toml b/Cargo.toml index 3c710bf..2b458a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -172,6 +172,8 @@ features = [ "ring-compat", "state-res", "unstable-msc2448", + #[cfg(feature = "msc3266")] + "unstable-msc3266", "unstable-msc4186", ] git = "https://github.com/ruma/ruma.git" @@ -189,10 +191,11 @@ nix = { version = "0.30", features = ["resource"] } backend_rocksdb = ["rocksdb"] backend_sqlite = ["sqlite"] conduit_bin = ["axum"] -default = ["backend_rocksdb", "backend_sqlite", "conduit_bin", "systemd"] +default = ["backend_rocksdb", "backend_sqlite", "conduit_bin", "proposal_msc3266", "systemd"] jemalloc = ["tikv-jemallocator"] sqlite = ["parking_lot", "rusqlite", "tokio/signal"] systemd = ["sd-notify"] +proposal_msc3266 = ["msc3266"] [[bin]] name = "conduit" diff --git a/src/api/client_server/mod.rs b/src/api/client_server/mod.rs index e99bc04..039f4c5 100644 --- a/src/api/client_server/mod.rs +++ b/src/api/client_server/mod.rs @@ -25,6 +25,8 @@ mod search; mod session; mod space; mod state; +#[cfg(feature = "msc3266")] +mod summary; mod sync; mod tag; mod thirdparty; @@ -62,6 +64,8 @@ pub use room::*; pub use search::*; pub use session::*; pub use space::*; +#[cfg(feature = "msc3266")] +pub use summary::*; pub use state::*; pub use sync::*; pub use tag::*; diff --git a/src/api/client_server/summary.rs b/src/api/client_server/summary.rs new file mode 100644 index 0000000..030dd43 --- /dev/null +++ b/src/api/client_server/summary.rs @@ -0,0 +1,85 @@ +//! `MSC3266` ([MSC]) room-summary +//! +//! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3266 + +use std::str::FromStr; + +//use crate::{service::rooms::spaces::PagnationToken, services, Error, Result, Ruma}; +use crate::{service::rooms::::PagnationToken, services, Error, Result, Ruma}; +use ruma::{ + api::client::{error::ErrorKind, summary::get_summary, space::get_hierarchy}, + UInt, +}; + +/// # `GET /_matrix/client/v1/room_summary/{roomIdOrAlias}` +/// +/// Retrieves a summary for a room. +/// +/// The API returns a summary of the given room identified via its id, or room alias. +/// +/// * is either already a member +/// * or has the necessary permissions to join.u +/// +/// (For example, the user may be a member of a room mentioned in an allow condition +/// in the join rules of a restricted room.) +/// +/// Servers MAY allow unauthenticated access to this API if at least one of +/// the following conditions holds true: +/// +/// * The room has a [join rule](#mroomjoin_rules) of `public`, `knock` or +/// `knock_restricted`. +/// * The room has a `world_readable` [history visibility](#room-history-visibility). +/// +/// Servers should consider rate limiting federation requests more heavily, +/// if the client is unauthenticated. +/// +/// Improvements yet to be defined: +/// Clients should note that requests for rooms where the user's membership +/// is `invite` or `knock` might yield outdated, partial or even no data +/// since the server may not have access to the current state of the room. + +pub async fn get_room_summary_route( + body: Ruma, +) -> Result { + let sender_user = body.sender_user.as_ref().expect("user is authenticated"); + + let limit = body + .limit + .unwrap_or(UInt::from(10_u32)) + .min(UInt::from(100_u32)); + let via = body + .via + .unwrap_or(UInt::from(3_u32)) + .min(UInt::from(10_u32)); + + let key = body + .from + .as_ref() + .and_then(|s| PagnationToken::from_str(s).ok()); + + // 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_client_hierarchy( + sender_user, + &body.room_id, + 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 +} diff --git a/src/main.rs b/src/main.rs index 93362f2..1c71bef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -456,6 +456,8 @@ fn routes(config: &Config) -> Router { .ruma_route(client_server::get_relating_events_with_rel_type_and_event_type_route) .ruma_route(client_server::get_relating_events_with_rel_type_route) .ruma_route(client_server::get_relating_events_route) + #cfg[(feature = "msc3266")] + .ruma_route(client_server::get_room_summary_route) .ruma_route(client_server::get_hierarchy_route) .ruma_route(client_server::well_known_client) .route( @@ -505,6 +507,8 @@ fn routes(config: &Config) -> Router { .ruma_route(server_server::get_keys_route) .ruma_route(server_server::claim_keys_route) .ruma_route(server_server::get_openid_userinfo_route) + #cfg[(feature = "msc3266")] + .ruma_route(client_server::get_room_summary_route) .ruma_route(server_server::get_hierarchy_route) .ruma_route(server_server::well_known_server) } else { diff --git a/src/service/rooms/mod.rs b/src/service/rooms/mod.rs index 2f0c334..a5b22d8 100644 --- a/src/service/rooms/mod.rs +++ b/src/service/rooms/mod.rs @@ -11,6 +11,8 @@ pub mod pdu_metadata; pub mod search; pub mod short; pub mod spaces; +#cfg[(feature = "msc3266")] +pub mod summary; pub mod state; pub mod state_accessor; pub mod state_cache; diff --git a/src/service/rooms/summary/mod.rs b/src/service/rooms/summary/mod.rs new file mode 100644 index 0000000..2571776 --- /dev/null +++ b/src/service/rooms/summary/mod.rs @@ -0,0 +1,204 @@ +//! `MSC3266` ([MSC]) room-summary +//! +//! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3266 + +use std::{ + collections::VecDeque, + fmt::{Display, Formatter}, + str::FromStr, +}; + +use lru_cache::LruCache; +use ruma::{ + api::{ + client::{ + self, + error::ErrorKind, + room::summary::get_summary + }, + }, + events::{ + room::{ + avatar::RoomAvatarEventContent, + canonical_alias::RoomCanonicalAliasEventContent, + create::RoomCreateEventContent, + join_rules::{JoinRule, RoomJoinRulesEventContent}, + topic::RoomTopicEventContent, + }, + StateEventType, + }, + serde::Raw, + OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, RoomId, ServerName, + UInt, UserId, +}; +use tokio::sync::Mutex; +use tracing::{debug, error, info, warn}; + +use crate::{services, Error, Result}; + +#[derive(Debug, PartialEq)] +pub struct SummaryRequest { + /// The Alias or ID of the room to be summarized. + #[ruma_api(path)] + pub room_id_or_alias: Vec, + /// A list of servers the homeserver should attempt to use to peek at the room. + /// + /// Defaults to an empty `Vec`. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[ruma_api(query)] + pub via: Vec, +} + +impl Display for SummaryRequest { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}_{}", + self.room_id_or_alias + .iter() + .map(|b| b.to_string()) + .collect::>() + .join(","), + self.via + ) + } +} + +//impl Service { +//} + +/// With the given identifier, checks if a room is accessible +fn is_accessible( + current_room: &OwnedRoomId, + join_rule: &RoomJoinRule, + identifier: &Identifier<'_>, + allowed_room_ids: &Vec, +) -> bool { + // Note: unwrap_or_default for bool means false + match identifier { + Identifier::ServerName(server_name) => { + let room_id: &RoomId = current_room; + + // Checks if ACLs allow for the server to participate + if services() + .rooms + .event_handler + .acl_check(server_name, room_id) + .is_err() + { + return false; + } + } + Identifier::UserId(user_id) => { + if services() + .rooms + .state_cache + .is_joined(user_id, current_room) + .unwrap_or_default() + || services() + .rooms + .state_cache + .is_invited(user_id, current_room) + .unwrap_or_default() + { + return true; + } + } + } // Takes care of join rules + match join_rule { + PublicRoomJoinRule::Restricted => { + SpaceRoomJoinRule::Restricted => { + for room in allowed_room_ids { + match identifier { + Identifier::UserId(user) => { + if services() + .rooms + .state_cache + .is_joined(user, room) + .unwrap_or_default() + { + return true; + } + } + Identifier::ServerName(server) => { + if services() + .rooms + .state_cache + .server_in_room(server, room) + .unwrap_or_default() + { + return true; + } + } + } + } + false + } + SpaceRoomJoinRule::Public + | SpaceRoomJoinRule::Knock + | SpaceRoomJoinRule::KnockRestricted => true, + SpaceRoomJoinRule::Invite | SpaceRoomJoinRule::Private => false, + // Custom join rule + _ => false, + } +} + +#[cfg(test)] +mod tests { + use ruma::{ + OwnedRoomOrAliasId, RoomOrAliasId}; + use crate::IdParseError; + use super::*; + + #[test] + fn valid_room_id_or_alias_id_with_a_room_alias_id() { + assert_eq!( + <&RoomOrAliasId>::try_from("#conduit:example.com") + .expect("Failed to create RoomAliasId.") + .as_str(), + "#conduit:example.com" + ); + + } + + #[test] + fn valid_room_id_or_alias_id_with_a_room_id() { + assert_eq!( + <&RoomOrAliasId>::try_from("!29fhd83h92h0:example.com") + .expect("Failed to create RoomId.") + .as_str(), + "!29fhd83h92h0:example.com" + ); + + } + + #[test] + fn missing_sigil_for_room_id_or_alias_id() { + assert_eq!( + <&RoomOrAliasId>::try_from("ruma:example.com").unwrap_err(), + IdParseError::MissingLeadingSigil + ); + } + + #[test] + fn valid_room_summary_for_room_id() { + assert_eq!( + SummaryRequest { + room_id_or_alias: vec!["!29fhd83h92h0:example.com"], + via: vec!("matrix.org", "my_homeserver.io"), + } + SummaryRequest.try_from(), + "!29fhd83h92h0:example.com_matrix.org,my_homeserver.org" + ); + + fn valid_room_summary_for_room_alias() { + assert_eq!( + SummaryRequest { + room_id_or_alias: vec!["#Admin:example.com"], + via: vec!("matrix.org", "my_homeserver.io"), + } + .to_string(), + "#Admin:example.com_matrix.org,my_homeserver.org" + ); + } +}