feat: Add support for MSC4560

This commit is contained in:
Ginger committed 2026-10-15 22:56:04 -04:00
1 parent 4f21fa2bda
commit e6ca06a501
14 files changed
+460 -125

No files matched your search

Generated
+11 -11
View File
@@ -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",
+3 -2
View File
@@ -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",
]
+3
View File
@@ -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
+3 -2
View File
@@ -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,
};
+71
View File
@@ -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<crate::State>,
body: Ruma<get_stripped_state::unstable::Request>,
) -> Result<get_stripped_state::unstable::Response> {
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.")))
},
}
}
+3 -6
View File
@@ -13,7 +13,7 @@ pub(crate) async fn get_room_summary(
State(services): State<crate::State>,
body: Ruma<get_summary::v1::Request>,
) -> Result<get_summary::v1::Response> {
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?;
+2
View File
@@ -184,6 +184,7 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
.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: State) -> Router<State> {
.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)
+6 -2
View File
@@ -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 => {
+2
View File
@@ -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::*;
+6 -2
View File
@@ -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 => {
+54
View File
@@ -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<crate::State>,
body: Ruma<get_stripped_state::unstable::Request>,
) -> Result<get_stripped_state::unstable::Response> {
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.")))
},
}
}
+2
View File
@@ -50,5 +50,7 @@ pub fn unstable_features() -> BTreeMap<String, bool> {
("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),
])
}
+49 -19
View File
@@ -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"
+245 -81
View File
@@ -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<rooms::event_handler::Service>,
metadata: Dep<rooms::metadata::Service>,
globals: Dep<globals::Service>,
membership: Dep<rooms::membership::Service>,
sending: Dep<sending::Service>,
state: Dep<rooms::state::Service>,
state_accessor: Dep<rooms::state_accessor::Service>,
@@ -57,13 +71,21 @@ struct SpaceChild {
via: Vec<OwnedServerName>,
}
#[derive(Default)]
#[non_exhaustive]
pub struct RoomStrippedState {
pub state: Vec<Raw<AnyStrippedStateEvent>>,
pub num_joined_members: Option<UInt>,
}
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
services: Services {
event_handler: args
.depend::<rooms::event_handler::Service>("rooms::event_handler"),
metadata: args.depend::<rooms::metadata::Service>("rooms::metadata"),
globals: args.depend::<globals::Service>("globals"),
membership: args.depend::<rooms::membership::Service>("rooms::membership"),
sending: args.depend::<sending::Service>("sending"),
state: args.depend::<rooms::state::Service>("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<Option<(SpaceHierarchyParentSummary, Vec<OwnedRoomId>)>> {
if self.services.metadata.is_disabled(room_id).await {
return Err!(Request(Forbidden("This room is blocked by this server.")));
}
) -> Option<(SpaceHierarchyParentSummary, Vec<OwnedRoomId>)> {
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<StateEventType>,
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<StateEventType>,
num_joined_members: bool,
) -> Accessibility<RoomStrippedState> {
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<StateEventType>,
num_joined_members: bool,
) -> Option<RoomStrippedState> {
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::<OwnedEventId>(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<StateEventType>,
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
}
}