From e6ca06a501ac3601b0bbe3a1f26c18cbc0798b49 Mon Sep 17 00:00:00 2001 From: Ginger Date: Mon, 12 Oct 2026 14:13:15 -0400 Subject: [PATCH] feat: Add support for MSC4560 --- Cargo.lock | 22 +- Cargo.toml | 5 +- changelog.d/+01d550e6.feature.md | 3 + src/api/client/room/mod.rs | 5 +- src/api/client/room/stripped_state.rs | 71 ++++++ src/api/client/room/summary.rs | 9 +- src/api/router.rs | 2 + src/api/server/make_join.rs | 8 +- src/api/server/mod.rs | 2 + src/api/server/send_join.rs | 8 +- src/api/server/stripped_state.rs | 54 +++++ src/core/matrix/versions.rs | 2 + src/service/rooms/membership/mod.rs | 68 ++++-- src/service/rooms/summary/mod.rs | 326 +++++++++++++++++++------- 14 files changed, 460 insertions(+), 125 deletions(-) create mode 100644 changelog.d/+01d550e6.feature.md create mode 100644 src/api/client/room/stripped_state.rs create mode 100644 src/api/server/stripped_state.rs diff --git a/Cargo.lock b/Cargo.lock index deac9fff4..6d72cea40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5019,7 +5019,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.17.0" -source = "git+https://github.com/ruma/ruma.git?rev=f5e5e1c885f598036fd4326c566e9d1928027d5d#f5e5e1c885f598036fd4326c566e9d1928027d5d" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=ef98d8d0b111f563fab960c3f696493979fba3a7#ef98d8d0b111f563fab960c3f696493979fba3a7" dependencies = [ "assign", "js_int", @@ -5038,7 +5038,7 @@ dependencies = [ [[package]] name = "ruma-appservice-api" version = "0.17.0" -source = "git+https://github.com/ruma/ruma.git?rev=f5e5e1c885f598036fd4326c566e9d1928027d5d#f5e5e1c885f598036fd4326c566e9d1928027d5d" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=ef98d8d0b111f563fab960c3f696493979fba3a7#ef98d8d0b111f563fab960c3f696493979fba3a7" dependencies = [ "http", "js_int", @@ -5051,7 +5051,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.25.0" -source = "git+https://github.com/ruma/ruma.git?rev=f5e5e1c885f598036fd4326c566e9d1928027d5d#f5e5e1c885f598036fd4326c566e9d1928027d5d" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=ef98d8d0b111f563fab960c3f696493979fba3a7#ef98d8d0b111f563fab960c3f696493979fba3a7" dependencies = [ "as_variant", "assign", @@ -5073,7 +5073,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.20.0" -source = "git+https://github.com/ruma/ruma.git?rev=f5e5e1c885f598036fd4326c566e9d1928027d5d#f5e5e1c885f598036fd4326c566e9d1928027d5d" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=ef98d8d0b111f563fab960c3f696493979fba3a7#ef98d8d0b111f563fab960c3f696493979fba3a7" dependencies = [ "as_variant", "base64 0.23.1", @@ -5106,7 +5106,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.35.0" -source = "git+https://github.com/ruma/ruma.git?rev=f5e5e1c885f598036fd4326c566e9d1928027d5d#f5e5e1c885f598036fd4326c566e9d1928027d5d" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=ef98d8d0b111f563fab960c3f696493979fba3a7#ef98d8d0b111f563fab960c3f696493979fba3a7" dependencies = [ "as_variant", "indexmap 2.14.2", @@ -5127,7 +5127,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.16.0" -source = "git+https://github.com/ruma/ruma.git?rev=f5e5e1c885f598036fd4326c566e9d1928027d5d#f5e5e1c885f598036fd4326c566e9d1928027d5d" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=ef98d8d0b111f563fab960c3f696493979fba3a7#ef98d8d0b111f563fab960c3f696493979fba3a7" dependencies = [ "bytes", "http", @@ -5149,7 +5149,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.12.1" -source = "git+https://github.com/ruma/ruma.git?rev=f5e5e1c885f598036fd4326c566e9d1928027d5d#f5e5e1c885f598036fd4326c566e9d1928027d5d" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=ef98d8d0b111f563fab960c3f696493979fba3a7#ef98d8d0b111f563fab960c3f696493979fba3a7" dependencies = [ "js_int", "thiserror 2.0.21", @@ -5158,7 +5158,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.20.0" -source = "git+https://github.com/ruma/ruma.git?rev=f5e5e1c885f598036fd4326c566e9d1928027d5d#f5e5e1c885f598036fd4326c566e9d1928027d5d" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=ef98d8d0b111f563fab960c3f696493979fba3a7#ef98d8d0b111f563fab960c3f696493979fba3a7" dependencies = [ "as_variant", "cfg-if", @@ -5174,7 +5174,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.16.0" -source = "git+https://github.com/ruma/ruma.git?rev=f5e5e1c885f598036fd4326c566e9d1928027d5d#f5e5e1c885f598036fd4326c566e9d1928027d5d" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=ef98d8d0b111f563fab960c3f696493979fba3a7#ef98d8d0b111f563fab960c3f696493979fba3a7" dependencies = [ "js_int", "ruma-common", @@ -5186,7 +5186,7 @@ dependencies = [ [[package]] name = "ruma-signatures" version = "0.22.0" -source = "git+https://github.com/ruma/ruma.git?rev=f5e5e1c885f598036fd4326c566e9d1928027d5d#f5e5e1c885f598036fd4326c566e9d1928027d5d" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=ef98d8d0b111f563fab960c3f696493979fba3a7#ef98d8d0b111f563fab960c3f696493979fba3a7" dependencies = [ "base64 0.23.1", "ed25519-dalek 3.0.0", @@ -5203,7 +5203,7 @@ dependencies = [ [[package]] name = "ruma-state-res" version = "0.18.0" -source = "git+https://github.com/ruma/ruma.git?rev=f5e5e1c885f598036fd4326c566e9d1928027d5d#f5e5e1c885f598036fd4326c566e9d1928027d5d" +source = "git+https://github.com/gingershaped/ruwuma.git?rev=ef98d8d0b111f563fab960c3f696493979fba3a7#ef98d8d0b111f563fab960c3f696493979fba3a7" dependencies = [ "js_int", "ruma-common", diff --git a/Cargo.toml b/Cargo.toml index fc1b2ef88..c70d5de05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -347,8 +347,8 @@ version = "1.1.1" # Used for matrix spec type definitions and helpers [workspace.dependencies.ruma] -git = "https://github.com/ruma/ruma.git" -rev = "f5e5e1c885f598036fd4326c566e9d1928027d5d" +git = "https://github.com/gingershaped/ruwuma.git" +rev = "ef98d8d0b111f563fab960c3f696493979fba3a7" features = [ "appservice-api-c", "client-api", @@ -388,6 +388,7 @@ features = [ "unstable-msc4494", "unstable-msc4484", "unstable-msc4540", + "unstable-msc4560", "unstable-extensible-events", ] diff --git a/changelog.d/+01d550e6.feature.md b/changelog.d/+01d550e6.feature.md new file mode 100644 index 000000000..bb9a865ac --- /dev/null +++ b/changelog.d/+01d550e6.feature.md @@ -0,0 +1,3 @@ +Implemented unstable support for [MSC4560] (Stripped state requests over federation). Contributed by @ginger. + +[MSC4560]: https://github.com/matrix-org/matrix-spec-proposals/pull/4560 diff --git a/src/api/client/room/mod.rs b/src/api/client/room/mod.rs index e249e18b6..1d3125d3e 100644 --- a/src/api/client/room/mod.rs +++ b/src/api/client/room/mod.rs @@ -2,11 +2,12 @@ mod aliases; mod create; mod event; mod initial_sync; +mod stripped_state; mod summary; mod upgrade; pub(crate) use self::{ aliases::get_room_aliases_route, create::create_room_route, event::get_room_event_route, - initial_sync::room_initial_sync_route, summary::get_room_summary, - upgrade::upgrade_room_route, + initial_sync::room_initial_sync_route, stripped_state::get_stripped_state_route, + summary::get_room_summary, upgrade::upgrade_room_route, }; diff --git a/src/api/client/room/stripped_state.rs b/src/api/client/room/stripped_state.rs new file mode 100644 index 000000000..d30655721 --- /dev/null +++ b/src/api/client/room/stripped_state.rs @@ -0,0 +1,71 @@ +use axum::extract::State; +use conduwuit::{Err, Result}; +use ruma::{ + api::client::state::get_stripped_state, assign, events::ALLOWED_STRIPPED_STATE_EVENTS, +}; +use service::rooms::summary::{Accessibility, RoomStrippedState}; + +use crate::{Ruma, router::ClientIdentity}; + +/// # `POST /_matrix/client/unstable/org.continuwuity.msc4560.stripped_state/{room_id_or_alias}` ([MSC4560]) +/// +/// Returns a room's stripped state. +/// +/// [MSC4560]: https://github.com/matrix-org/matrix-spec-proposals/pull/4560 +#[tracing::instrument(skip_all, name = "stripped_state", level = "info")] +pub(crate) async fn get_stripped_state_route( + State(services): State, + body: Ruma, +) -> Result { + let user_id = body.identity.as_ref().and_then(ClientIdentity::sender_user); + + if body.via.len() > get_stripped_state::unstable::MAX_VIAS { + return Err!(Request(TooLarge("Too many vias, please be less indecisive."))); + } + + if let Some(forbidden_type) = body + .state + .iter() + .find(|event_type| !ALLOWED_STRIPPED_STATE_EVENTS.contains(event_type)) + { + return Err!(Request(InvalidParam( + "Event type {forbidden_type} may not be requested as stripped state." + ))); + } + + let (room_id, _) = services + .rooms + .alias + .resolve_with_servers(&body.room_id_or_alias, Some(body.via.clone())) + .await?; + + if services.rooms.metadata.is_banned(&room_id).await { + return Err!(Request(Forbidden("This room is banned on this homeserver."))); + } + + match services + .rooms + .summary + .get_stripped_state_for_user( + user_id, + &room_id, + &body.via, + &body.state, + body.num_joined_members, + ) + .await + { + | Accessibility::Accessible(( + via, + RoomStrippedState { state, num_joined_members, .. }, + )) => Ok( + assign!(get_stripped_state::unstable::Response::new(via), { state, num_joined_members }), + ), + | Accessibility::Inaccessible => { + Err!(Request(Forbidden("You may not request this room's stripped state."))) + }, + | Accessibility::NotFound => { + Err!(Request(NotFound("This room does not exist."))) + }, + } +} diff --git a/src/api/client/room/summary.rs b/src/api/client/room/summary.rs index da6958242..836dba890 100644 --- a/src/api/client/room/summary.rs +++ b/src/api/client/room/summary.rs @@ -13,7 +13,7 @@ pub(crate) async fn get_room_summary( State(services): State, body: Ruma, ) -> Result { - let (room_id, servers) = services + let (room_id, _) = services .rooms .alias .resolve_with_servers(&body.room_id_or_alias, Some(body.via.clone())) @@ -27,12 +27,9 @@ pub(crate) async fn get_room_summary( .rooms .summary .get_room_summary_for_user( - body.identity - .as_ref() - .map(ClientIdentity::expect_sender_user) - .transpose()?, + body.identity.as_ref().and_then(ClientIdentity::sender_user), &room_id, - &servers, + &body.via, ) .await?; diff --git a/src/api/router.rs b/src/api/router.rs index 5fbe21134..09681c0b1 100644 --- a/src/api/router.rs +++ b/src/api/router.rs @@ -184,6 +184,7 @@ pub fn build(router: Router, state: State) -> Router { .ruma_route(&client::get_mutual_rooms_unstable_route) .ruma_route(&client::get_mutual_rooms_route) .ruma_route(&client::get_room_summary) + .ruma_route(&client::get_stripped_state_route) .ruma_route(&client::get_suspended_status) .ruma_route(&client::put_suspended_status) .ruma_route(&client::get_lock_status) @@ -229,6 +230,7 @@ pub fn build(router: Router, state: State) -> Router { .ruma_route(&server::get_keys_route) .ruma_route(&server::claim_keys_route) .ruma_route(&server::get_hierarchy_route) + .ruma_route(&server::get_stripped_state_route) .ruma_route(&server::well_known_server) .ruma_route(&server::get_content_route) .ruma_route(&server::get_content_thumbnail_route) diff --git a/src/api/server/make_join.rs b/src/api/server/make_join.rs index f9776933c..dcb74d433 100644 --- a/src/api/server/make_join.rs +++ b/src/api/server/make_join.rs @@ -13,7 +13,7 @@ use ruma::{ }, }; use serde_json::value::to_raw_value; -use service::rooms::membership::JoinConditionResult; +use service::rooms::membership::{JoinConditionEntity, JoinConditionResult}; use crate::Ruma; @@ -95,7 +95,11 @@ pub(crate) async fn create_join_event_template_route( match services .rooms .membership - .evaluate_join_conditions(&body.room_id, &body.user_id, &conditions) + .evaluate_join_conditions( + &body.room_id, + JoinConditionEntity::User(&body.user_id), + &conditions, + ) .await { | JoinConditionResult::Satisfied => { diff --git a/src/api/server/mod.rs b/src/api/server/mod.rs index 5c1ff3f79..b115ccabb 100644 --- a/src/api/server/mod.rs +++ b/src/api/server/mod.rs @@ -18,6 +18,7 @@ pub(super) mod send_knock; pub(super) mod send_leave; pub(super) mod state; pub(super) mod state_ids; +pub(super) mod stripped_state; pub(super) mod user; pub(super) mod version; pub(super) mod well_known; @@ -42,6 +43,7 @@ pub(super) use send_knock::*; pub(super) use send_leave::*; pub(super) use state::*; pub(super) use state_ids::*; +pub(super) use stripped_state::*; pub(super) use user::*; pub(super) use version::*; pub(super) use well_known::*; diff --git a/src/api/server/send_join.rs b/src/api/server/send_join.rs index aa91957d0..d25fd8156 100644 --- a/src/api/server/send_join.rs +++ b/src/api/server/send_join.rs @@ -20,7 +20,7 @@ use ruma::{ room_version_rules::RoomVersionRules, }; use serde_json::value::to_raw_value; -use service::rooms::membership::JoinConditionResult; +use service::rooms::membership::{JoinConditionEntity, JoinConditionResult}; use crate::{Ruma, server::utils::validate_any_membership_event}; @@ -123,7 +123,11 @@ async fn create_join_event( match services .rooms .membership - .evaluate_join_conditions(room_id, &sender, &conditions) + .evaluate_join_conditions( + room_id, + JoinConditionEntity::User(&sender), + &conditions, + ) .await { | JoinConditionResult::Satisfied => { diff --git a/src/api/server/stripped_state.rs b/src/api/server/stripped_state.rs new file mode 100644 index 000000000..9680583ce --- /dev/null +++ b/src/api/server/stripped_state.rs @@ -0,0 +1,54 @@ +use axum::extract::State; +use conduwuit::{Err, Result}; +use ruma::{ + api::federation::event::get_stripped_state, assign, events::ALLOWED_STRIPPED_STATE_EVENTS, +}; +use service::rooms::summary::{Accessibility, RoomStrippedState}; + +use crate::Ruma; + +/// # `POST /_matrix/federation/unstable/org.continuwuity.msc4560.stripped_state/{room_id}` ([MSC4560]) +/// +/// Returns a room's stripped state. +/// +/// [MSC4560]: https://github.com/matrix-org/matrix-spec-proposals/pull/4560 +pub(crate) async fn get_stripped_state_route( + State(services): State, + body: Ruma, +) -> Result { + if let Some(forbidden_type) = body + .state + .iter() + .find(|event_type| !ALLOWED_STRIPPED_STATE_EVENTS.contains(event_type)) + { + return Err!(Request(InvalidParam( + "Event type {forbidden_type} may not be requested as stripped state." + ))); + } + + if services.rooms.metadata.is_banned(&body.room_id).await { + return Err!(Request(Forbidden("This room is banned on this homeserver."))); + } + + match services + .rooms + .summary + .get_local_stripped_state_for_server( + &body.identity, + &body.room_id, + &body.state, + body.num_joined_members, + ) + .await + { + | Accessibility::Accessible(RoomStrippedState { state, num_joined_members, .. }) => Ok( + assign!(get_stripped_state::unstable::Response::default(), { state, num_joined_members }), + ), + | Accessibility::Inaccessible => { + Err!(Request(Forbidden("You may not access this room's stripped state."))) + }, + | Accessibility::NotFound => { + Err!(Request(NotFound("This room is not known to this server."))) + }, + } +} diff --git a/src/core/matrix/versions.rs b/src/core/matrix/versions.rs index ae681e3ca..3ea538b62 100644 --- a/src/core/matrix/versions.rs +++ b/src/core/matrix/versions.rs @@ -50,5 +50,7 @@ pub fn unstable_features() -> BTreeMap { ("uk.timedout.msc4494".to_owned(), true), // server administration OAuth scope (https://github.com/matrix-org/matrix-spec-proposals/pull/4484) ("org.continuwuity.msc4484.unstable".to_owned(), true), + // stripped state over federation (https://github.com/matrix-org/matrix-spec-proposals/pull/4560) + ("org.continuwuity.msc4560".to_owned(), true), ]) } diff --git a/src/service/rooms/membership/mod.rs b/src/service/rooms/membership/mod.rs index b8a2ed299..5445b15ac 100644 --- a/src/service/rooms/membership/mod.rs +++ b/src/service/rooms/membership/mod.rs @@ -15,7 +15,7 @@ use database::Database; use futures::{FutureExt, StreamExt, TryFutureExt, join}; use ruma::{ CanonicalJsonObject, CanonicalJsonValue, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId, - RoomVersionId, UserId, + RoomVersionId, ServerName, UserId, api::{ error::{ErrorKind, IncompatibleRoomVersionErrorData}, federation, @@ -52,6 +52,20 @@ pub enum JoinConditionResult { UnableToAuthorizeJoin, } +#[derive(Copy, Clone, Debug)] +pub enum JoinConditionEntity<'id> { + User(&'id UserId), + Server(&'id ServerName), +} + +impl<'id> From<&'id UserId> for JoinConditionEntity<'id> { + fn from(value: &'id UserId) -> Self { Self::User(value) } +} + +impl<'id> From<&'id ServerName> for JoinConditionEntity<'id> { + fn from(value: &'id ServerName) -> Self { Self::Server(value) } +} + pub struct Service { services: Services, } @@ -250,7 +264,7 @@ impl Service { // This is a restricted room, check if we can complete the join // requirements locally. if self - .evaluate_join_conditions(room_id, sender_user, conditions) + .evaluate_join_conditions(room_id, sender_user.into(), conditions) .await == JoinConditionResult::Satisfied { @@ -834,7 +848,7 @@ impl Service { pub async fn evaluate_join_conditions( &self, room_id: &RoomId, - user_id: &UserId, + entity: JoinConditionEntity<'_>, conditions: &Restricted, ) -> JoinConditionResult { if conditions.allow.is_empty() { @@ -845,7 +859,7 @@ impl Service { let mut unable_to_authorize = false; for rule in &conditions.allow { - match self.evaluate_allow_rule(room_id, user_id, rule).await { + match self.evaluate_allow_rule(room_id, entity, rule).await { | JoinConditionResult::Satisfied => { // This allow rule is satisfied, the user can join. return JoinConditionResult::Satisfied; @@ -879,7 +893,7 @@ impl Service { async fn evaluate_allow_rule( &self, room_id: &RoomId, - user_id: &UserId, + entity: JoinConditionEntity<'_>, rule: &AllowRule, ) -> JoinConditionResult { match rule { @@ -890,17 +904,25 @@ impl Service { .server_in_room(self.services.globals.server_name(), condition_room) .await { - if self - .services - .state_cache - .is_joined(user_id, condition_room) - .await - { + let is_joined = match entity { + | JoinConditionEntity::User(user_id) => + self.services + .state_cache + .is_joined(user_id, condition_room) + .await, + | JoinConditionEntity::Server(server) => + self.services + .state_cache + .server_in_room(server, room_id) + .await, + }; + + if is_joined { debug!( - ?user_id, + ?entity, ?room_id, ?condition_room, - "User may join via membership in {condition_room}" + "Entity may join via membership" ); JoinConditionResult::Satisfied } else { @@ -910,14 +932,22 @@ impl Service { JoinConditionResult::UnableToAuthorizeJoin } }, - | other if other.rule_type() == "fi.mau.spam_checker" => - self.services - .antispam - .meowlnir_accept_make_join(room_id.to_owned(), user_id.to_owned()) - .await, + | other if other.rule_type() == "fi.mau.spam_checker" => { + if let JoinConditionEntity::User(user_id) = entity { + self.services + .antispam + .meowlnir_accept_make_join(room_id.to_owned(), user_id.to_owned()) + .await + } else { + // We can't currently check server names against the spam + // checker, so we just assume that they + // can't access the room for now. + JoinConditionResult::Unsatisfied + } + }, | unknown => { debug!( - ?user_id, + ?entity, ?room_id, rule_type = unknown.rule_type(), "Could not check unknown allow rule" diff --git a/src/service/rooms/summary/mod.rs b/src/service/rooms/summary/mod.rs index f2d56eb52..014e2fa57 100644 --- a/src/service/rooms/summary/mod.rs +++ b/src/service/rooms/summary/mod.rs @@ -1,4 +1,8 @@ -use std::{collections::HashSet, sync::Arc}; +use std::{ + collections::{BTreeSet, HashSet}, + pin::pin, + sync::Arc, +}; use conduwuit::{ Err, Event, Result, debug, info, @@ -10,18 +14,27 @@ use ruma::{ OwnedEventId, OwnedRoomId, OwnedServerName, RoomId, ServerName, UInt, UserId, api::{ client::space::SpaceHierarchyRoomsChunk, - federation::space::{SpaceHierarchyParentSummary, get_hierarchy}, + error::ErrorKind, + federation::{ + event::get_stripped_state, + space::{SpaceHierarchyParentSummary, get_hierarchy}, + }, }, assign, events::{ - StateEventType, + AnyStrippedStateEvent, StateEventType, + room::member::MembershipState, space::child::{HierarchySpaceChildEvent, SpaceChildEventContent}, }, - room::{JoinRuleSummary, RestrictedSummary, RoomSummary, RoomType}, + room::{JoinRule, RoomSummary, RoomType}, serde::Raw, }; -use crate::{Dep, rooms, sending}; +use crate::{ + Dep, globals, + rooms::{self, membership::JoinConditionResult}, + sending, +}; pub struct Service { services: Services, @@ -29,7 +42,8 @@ pub struct Service { struct Services { event_handler: Dep, - metadata: Dep, + globals: Dep, + membership: Dep, sending: Dep, state: Dep, state_accessor: Dep, @@ -57,13 +71,21 @@ struct SpaceChild { via: Vec, } +#[derive(Default)] +#[non_exhaustive] +pub struct RoomStrippedState { + pub state: Vec>, + pub num_joined_members: Option, +} + impl crate::Service for Service { fn build(args: crate::Args<'_>) -> Result> { Ok(Arc::new(Self { services: Services { event_handler: args .depend::("rooms::event_handler"), - metadata: args.depend::("rooms::metadata"), + globals: args.depend::("globals"), + membership: args.depend::("rooms::membership"), sending: args.depend::("sending"), state: args.depend::("rooms::state"), state_accessor: args @@ -92,7 +114,7 @@ impl Service { summary } else if let Some((SpaceHierarchyParentSummary { summary, .. }, _)) = - self.fetch_remote_summary(room_id, via, false).await? + self.fetch_remote_room_summary(room_id, via, false).await { // A via has this room. @@ -105,7 +127,7 @@ impl Service { }; // Check if the room is visible to the querying user. - if !self.user_may_see_summary(querying_user, &summary).await { + if !self.user_may_see_summary(querying_user, room_id).await { return Ok(Accessibility::Inaccessible); } @@ -138,8 +160,8 @@ impl Service { SpaceHierarchyParentSummary { summary, children_state, .. }, inaccessible_children, )) = self - .fetch_remote_summary(room_id, via, suggested_only) - .await? + .fetch_remote_room_summary(room_id, via, suggested_only) + .await { // A via has this room. @@ -152,10 +174,7 @@ impl Service { }; // Check if the room is visible to the querying user. - if !self - .user_may_see_summary(querying_user, &summary.summary) - .await - { + if !self.user_may_see_summary(querying_user, room_id).await { return Ok(Accessibility::Inaccessible); } @@ -312,7 +331,7 @@ impl Service { }; // Check if the server can see the root room's summary - if !self.server_may_see_summary(querying_server, &summary).await { + if !self.server_may_see_summary(querying_server, room_id).await { return Accessibility::Inaccessible; } @@ -331,7 +350,10 @@ impl Service { .broad_then(async |child| { let summary = { if let Some(summary) = self.build_local_room_summary(&child.state_key).await { - if self.server_may_see_summary(querying_server, &summary).await { + if self + .server_may_see_summary(querying_server, &child.state_key) + .await + { Accessibility::Accessible(summary) } else { Accessibility::Inaccessible @@ -439,20 +461,14 @@ impl Service { } /// Query remote servers for the summary of a room. - async fn fetch_remote_summary( + async fn fetch_remote_room_summary( &self, room_id: &RoomId, via: &[OwnedServerName], suggested_only: bool, - ) -> Result)>> { - if self.services.metadata.is_disabled(room_id).await { - return Err!(Request(Forbidden("This room is blocked by this server."))); - } - + ) -> Option<(SpaceHierarchyParentSummary, Vec)> { if via.is_empty() { - return Err!(Request(MissingParam( - "No servers were provided with which to query this room's summary." - ))); + return None; } info!(%room_id, ?via, "Asking for room summary over federation"); @@ -477,7 +493,7 @@ impl Service { } info!(%room_id, %server, "Got room summary"); - return Ok(Some((room, inaccessible_children))); + return Some((room, inaccessible_children)); }, | Err(err) => { info!(%room_id, %server, %err, "Server could not provide a summary for this room"); @@ -486,7 +502,7 @@ impl Service { } info!(%room_id, "No servers queried could provide a summary for this room"); - Ok(None) + None } /// Get the stripped m.space.child events of a room. @@ -536,54 +552,207 @@ impl Service { .await } + pub async fn get_stripped_state_for_user( + &self, + querying_user: Option<&UserId>, + room_id: &RoomId, + via: &[OwnedServerName], + state: &BTreeSet, + num_joined_members: bool, + ) -> Accessibility<(OwnedServerName, RoomStrippedState)> { + if let Some(state) = self + .build_local_stripped_state(room_id, state, num_joined_members) + .await + { + if self.user_may_see_summary(querying_user, room_id).await { + Accessibility::Accessible((self.services.globals.server_name().to_owned(), state)) + } else { + Accessibility::Inaccessible + } + } else { + self.fetch_remote_stripped_state(room_id, via, state, num_joined_members) + .await + } + } + + pub async fn get_local_stripped_state_for_server( + &self, + querying_server: &ServerName, + room_id: &RoomId, + state: &BTreeSet, + num_joined_members: bool, + ) -> Accessibility { + if let Some(state) = self + .build_local_stripped_state(room_id, state, num_joined_members) + .await + { + if self.server_may_see_summary(querying_server, room_id).await { + Accessibility::Accessible(state) + } else { + Accessibility::Inaccessible + } + } else { + Accessibility::NotFound + } + } + + async fn build_local_stripped_state( + &self, + room_id: &RoomId, + state: &BTreeSet, + num_joined_members: bool, + ) -> Option { + if !self + .services + .state_cache + .server_in_room(self.services.globals.server_name(), room_id) + .await + { + return None; + } + + let current_shortstatehash = self + .services + .state + .get_room_shortstatehash(room_id) + .await + .expect("room should have state"); + + let state = state + .iter() + .stream() + .broad_flat_map(|event_type| { + self.services + .state_accessor + .state_keys_with_ids::(current_shortstatehash, event_type) + }) + .broad_filter_map(async |(_, event_id)| { + self.services.timeline.get_pdu(&event_id).ok().await + }) + .ready_filter_map(|pdu| Some(pdu.into_format())) + .collect() + .await; + + let num_joined_members = if num_joined_members { + Some( + self.services + .state_cache + .room_joined_count(room_id) + .await + .expect("room should have a joined count") + .try_into() + .expect("joined count should fit into uint"), + ) + } else { + None + }; + + Some(RoomStrippedState { state, num_joined_members }) + } + + async fn fetch_remote_stripped_state( + &self, + room_id: &RoomId, + via: &[OwnedServerName], + state: &BTreeSet, + num_joined_members: bool, + ) -> Accessibility<(OwnedServerName, RoomStrippedState)> { + if via.is_empty() { + return Accessibility::NotFound; + } + + let request = assign!(get_stripped_state::unstable::Request::new(room_id.to_owned()), { + state: state.to_owned(), + num_joined_members + }); + + let mut responses = pin!(via.iter().stream().broad_then(async |server| { + match self + .services + .sending + .send_federation_request(server, request.clone()) + .await + { + | Ok(response) => Accessibility::Accessible((server.to_owned(), response)), + | Err(err) => match err.kind() { + | ErrorKind::Forbidden => Accessibility::Inaccessible, + | _ => Accessibility::NotFound, + }, + } + })); + + let mut got_inaccessible = false; + + while let Some(response) = responses.next().await { + match response { + | Accessibility::Accessible((server, response)) => { + return Accessibility::Accessible((server, RoomStrippedState { + state: response.state, + num_joined_members: response.num_joined_members, + })); + }, + | Accessibility::Inaccessible => { + got_inaccessible = true; + }, + | Accessibility::NotFound => { + continue; + }, + } + } + + if got_inaccessible { + Accessibility::Inaccessible + } else { + Accessibility::NotFound + } + } + /// Determine if a user (possibly anonymous) may view a room summary. async fn user_may_see_summary( &self, querying_user: Option<&UserId>, - summary: &RoomSummary, + room_id: &RoomId, ) -> bool { // Anyone can view the summary of world-readable rooms. - if summary.world_readable { - return true; - } - - // If the user is joined or invited they may always view the summary. - if let Some(querying_user) = querying_user - && (self - .services - .state_cache - .is_joined(querying_user, &summary.room_id) - .await - || self - .services - .state_cache - .is_invited(querying_user, &summary.room_id) - .await) + if self + .services + .state_accessor + .is_world_readable(room_id) + .await { return true; } + // If the user is joined or invited they may always view the summary. + if let Some(querying_user) = querying_user { + let membership = self + .services + .state_accessor + .get_member(room_id, querying_user) + .await + .ok() + .map_or_else(|| MembershipState::Leave, |content| content.membership); + + if matches!(membership, MembershipState::Join | MembershipState::Invite) { + return true; + } + } + // Otherwise, visibility depends on the join rule. - match (&summary.join_rule, querying_user) { + let join_rule = self.services.state_accessor.get_join_rules(room_id).await; + + match (join_rule, querying_user) { // Anyone can view summaries for `public`, `knock`, and `knock_restricted` rooms. - | ( - JoinRuleSummary::Public - | JoinRuleSummary::Knock - | JoinRuleSummary::KnockRestricted(_), - _, - ) => true, + | (JoinRule::Public | JoinRule::Knock | JoinRule::KnockRestricted(_), _) => true, // The user may be able to view the summary for a `restricted` room, even if they - // aren't invited, provided they're in one of the allowed rooms. - | ( - JoinRuleSummary::Restricted(RestrictedSummary { allowed_room_ids, .. }), - Some(querying_user), - ) => + // aren't invited, provided they fulfill the join rules. + | (JoinRule::Restricted(conditions), Some(querying_user)) => self.services - .state_cache - .rooms_joined(querying_user) - .ready_any(|room_id| allowed_room_ids.contains(&room_id)) - .await, + .membership + .evaluate_join_conditions(room_id, querying_user.into(), &conditions) + .await + == JoinConditionResult::Satisfied, // In all other cases, the user may not view the summary. | _ => false, @@ -594,13 +763,13 @@ impl Service { async fn server_may_see_summary( &self, querying_server: &ServerName, - summary: &RoomSummary, + room_id: &RoomId, ) -> bool { // Servers may not see summaries of rooms they're ACLed from. if self .services .event_handler - .acl_check(querying_server, &summary.room_id) + .acl_check(querying_server, room_id) .await .is_err() { @@ -613,32 +782,27 @@ impl Service { if self .services .state_cache - .server_in_room(querying_server, &summary.room_id) + .server_in_room(querying_server, room_id) .await { return true; } - // For restricted rooms, federation responses must include the summary - // when a user on the querying server can satisfy one of the join - // conditions. - if let JoinRuleSummary::Restricted(RestrictedSummary { allowed_room_ids, .. }) = - &summary.join_rule - { - for room_id in allowed_room_ids { - if self - .services - .state_cache - .server_in_room(querying_server, room_id) - .await - { - return true; - } - } + let join_rule = self.services.state_accessor.get_join_rules(room_id).await; + + // Servers may see summaries in restricted rooms if they satisfy the + // join conditions. + if let JoinRule::Restricted(conditions) = join_rule { + return self + .services + .membership + .evaluate_join_conditions(room_id, querying_server.into(), &conditions) + .await + == JoinConditionResult::Satisfied; } // If the server isn't in the room, the same visibility rules apply as // for anonymous summary requests. - self.user_may_see_summary(None, summary).await + self.user_may_see_summary(None, room_id).await } }