diff --git a/Cargo.lock b/Cargo.lock index fcee7896c..e39e947d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3833,7 +3833,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.13.0" -source = "git+https://github.com/matrix-construct/ruma?rev=d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648#d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648" +source = "git+https://github.com/matrix-construct/ruma?rev=8564227896ed0f20f46eb4d717b7061f4ddd17ae#8564227896ed0f20f46eb4d717b7061f4ddd17ae" dependencies = [ "assign", "js_int", @@ -3852,7 +3852,7 @@ dependencies = [ [[package]] name = "ruma-appservice-api" version = "0.13.0" -source = "git+https://github.com/matrix-construct/ruma?rev=d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648#d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648" +source = "git+https://github.com/matrix-construct/ruma?rev=8564227896ed0f20f46eb4d717b7061f4ddd17ae#8564227896ed0f20f46eb4d717b7061f4ddd17ae" dependencies = [ "js_int", "ruma-common", @@ -3864,7 +3864,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.21.0" -source = "git+https://github.com/matrix-construct/ruma?rev=d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648#d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648" +source = "git+https://github.com/matrix-construct/ruma?rev=8564227896ed0f20f46eb4d717b7061f4ddd17ae#8564227896ed0f20f46eb4d717b7061f4ddd17ae" dependencies = [ "as_variant", "assign", @@ -3889,7 +3889,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.16.0" -source = "git+https://github.com/matrix-construct/ruma?rev=d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648#d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648" +source = "git+https://github.com/matrix-construct/ruma?rev=8564227896ed0f20f46eb4d717b7061f4ddd17ae#8564227896ed0f20f46eb4d717b7061f4ddd17ae" dependencies = [ "as_variant", "base64", @@ -3923,7 +3923,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.31.0" -source = "git+https://github.com/matrix-construct/ruma?rev=d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648#d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648" +source = "git+https://github.com/matrix-construct/ruma?rev=8564227896ed0f20f46eb4d717b7061f4ddd17ae#8564227896ed0f20f46eb4d717b7061f4ddd17ae" dependencies = [ "as_variant", "indexmap", @@ -3950,7 +3950,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.12.0" -source = "git+https://github.com/matrix-construct/ruma?rev=d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648#d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648" +source = "git+https://github.com/matrix-construct/ruma?rev=8564227896ed0f20f46eb4d717b7061f4ddd17ae#8564227896ed0f20f46eb4d717b7061f4ddd17ae" dependencies = [ "bytes", "headers", @@ -3973,7 +3973,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.11.0" -source = "git+https://github.com/matrix-construct/ruma?rev=d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648#d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648" +source = "git+https://github.com/matrix-construct/ruma?rev=8564227896ed0f20f46eb4d717b7061f4ddd17ae#8564227896ed0f20f46eb4d717b7061f4ddd17ae" dependencies = [ "js_int", "thiserror 2.0.18", @@ -3982,7 +3982,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.16.0" -source = "git+https://github.com/matrix-construct/ruma?rev=d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648#d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648" +source = "git+https://github.com/matrix-construct/ruma?rev=8564227896ed0f20f46eb4d717b7061f4ddd17ae#8564227896ed0f20f46eb4d717b7061f4ddd17ae" dependencies = [ "cfg-if", "proc-macro-crate", @@ -3997,7 +3997,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.12.0" -source = "git+https://github.com/matrix-construct/ruma?rev=d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648#d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648" +source = "git+https://github.com/matrix-construct/ruma?rev=8564227896ed0f20f46eb4d717b7061f4ddd17ae#8564227896ed0f20f46eb4d717b7061f4ddd17ae" dependencies = [ "js_int", "ruma-common", @@ -4009,7 +4009,7 @@ dependencies = [ [[package]] name = "ruma-signatures" version = "0.18.0" -source = "git+https://github.com/matrix-construct/ruma?rev=d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648#d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648" +source = "git+https://github.com/matrix-construct/ruma?rev=8564227896ed0f20f46eb4d717b7061f4ddd17ae#8564227896ed0f20f46eb4d717b7061f4ddd17ae" dependencies = [ "base64", "ed25519-dalek", diff --git a/Cargo.toml b/Cargo.toml index a4c492dc3..d26a6c6ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -328,7 +328,7 @@ default-features = false [workspace.dependencies.ruma] git = "https://github.com/matrix-construct/ruma" -rev = "d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648" +rev = "8564227896ed0f20f46eb4d717b7061f4ddd17ae" features = [ "__compat", "appservice-api-c", diff --git a/docker/Dockerfile.complement b/docker/Dockerfile.complement index 0ec7603c3..5cc09443a 100644 --- a/docker/Dockerfile.complement +++ b/docker/Dockerfile.complement @@ -130,7 +130,7 @@ ENTRYPOINT valgrind \ FROM input AS complement-base ARG var_cache ARG var_lib_apt -ARG complement_ref="5ca17e228083b44a38aeed3398ecc94529ae696e" +ARG complement_ref="97a724b5431b4f4086fe71db0901615ae6ab1ae9" ARG complement_tags="conduwuit_blacklist" ARG complement_tests="./tests/..." ARG complement_run=".*" diff --git a/src/api/client/sync/mod.rs b/src/api/client/sync/mod.rs index c2814a06b..1d9b076e1 100644 --- a/src/api/client/sync/mod.rs +++ b/src/api/client/sync/mod.rs @@ -10,7 +10,10 @@ use tuwunel_core::{ }; use tuwunel_service::Services; -pub(crate) use self::{v3::sync_events_route, v5::sync_events_v5_route}; +pub(crate) use self::{ + v3::{calculate_heroes, sync_events_route}, + v5::sync_events_v5_route, +}; async fn load_timeline( services: &Services, diff --git a/src/api/client/sync/v3.rs b/src/api/client/sync/v3.rs index 4f0ff6312..541a36656 100644 --- a/src/api/client/sync/v3.rs +++ b/src/api/client/sync/v3.rs @@ -1305,47 +1305,42 @@ async fn calculate_counts( (Some(joined_member_count), Some(invited_member_count), heroes.await) } -async fn calculate_heroes( +pub(crate) async fn calculate_heroes( services: &Services, room_id: &RoomId, sender_user: &UserId, ) -> Vec { + const LIMIT: usize = 5; + services .state_accessor .room_state_type_pdus(room_id, &StateEventType::RoomMember) .ready_filter_map(Result::ok) - .fold_default(|heroes: Vec<_>, pdu| { - fold_hero(heroes, services, room_id, sender_user, pdu) - }) + .filter_map(|pdu| filter_hero(services, room_id, sender_user, pdu)) + .take(LIMIT) + .collect::>() .await } -async fn fold_hero( - mut heroes: Vec, +async fn filter_hero( services: &Services, room_id: &RoomId, sender_user: &UserId, pdu: Pdu, -) -> Vec { - let Some(user_id): Option<&UserId> = pdu.state_key().map(TryInto::try_into).flat_ok() else { - return heroes; - }; +) -> Option { + let user_id = pdu.state_key().map(TryInto::try_into).flat_ok()?; if user_id == sender_user { - return heroes; + return None; } let Ok(content): Result = pdu.get_content() else { - return heroes; + return None; }; // The membership was and still is invite or join if !matches!(content.membership, MembershipState::Join | MembershipState::Invite) { - return heroes; - } - - if heroes.iter().any(is_equal_to!(user_id)) { - return heroes; + return None; } let (is_invited, is_joined) = join( @@ -1355,11 +1350,10 @@ async fn fold_hero( .await; if !is_joined && is_invited { - return heroes; + return None; } - heroes.push(user_id.to_owned()); - heroes + Some(user_id.to_owned()) } async fn typings_event_for_user( diff --git a/src/api/server/send_join.rs b/src/api/server/send_join.rs index 7e34dd1a4..f2079062a 100644 --- a/src/api/server/send_join.rs +++ b/src/api/server/send_join.rs @@ -5,7 +5,8 @@ use std::borrow::Borrow; use axum::extract::State; use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future::try_join4}; use ruma::{ - OwnedEventId, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId, ServerName, + CanonicalJsonObject, OwnedEventId, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId, + ServerName, api::federation::membership::create_join_event, events::{ StateEventType, @@ -14,21 +15,113 @@ use ruma::{ }; use serde_json::value::RawValue as RawJsonValue; use tuwunel_core::{ - Err, Result, at, err, + Err, Result, at, debug_error, err, + itertools::Itertools, matrix::event::gen_event_id_canonical_json, - utils::stream::{IterStream, TryBroadbandExt}, + utils::{ + BoolExt, + future::{BoolExt as _, ReadyBoolExt}, + stream::{BroadbandExt, IterStream, TryBroadbandExt, TryReadyExt}, + }, warn, }; use tuwunel_service::Services; -use crate::Ruma; +use crate::{Ruma, client::sync::calculate_heroes}; + +/// # `PUT /_matrix/federation/v1/send_join/{roomId}/{eventId}` +/// +/// Submits a signed join event. +pub(crate) async fn create_join_event_v1_route( + State(services): State, + body: Ruma, +) -> Result { + let room_id = &body.room_id; + let origin = body.origin(); + + if let Some(server) = room_id.server_name() + && services + .config + .forbidden_remote_server_names + .is_match(server.host()) + { + warn!( + "Server {origin} tried joining room ID {room_id} through us which has a server name \ + that is globally forbidden. Rejecting." + ); + + return Err!(Request(Forbidden(warn!( + "Room ID server name {server} is banned on this homeserver." + )))); + } + + Ok(create_join_event::v1::Response { + room_state: create_join_event(&services, origin, room_id, &body.pdu, false) + .boxed() + .await?, + }) +} + +/// # `PUT /_matrix/federation/v2/send_join/{roomId}/{eventId}` +/// +/// Submits a signed join event. +pub(crate) async fn create_join_event_v2_route( + State(services): State, + body: Ruma, +) -> Result { + let room_id = &body.room_id; + let origin = body.origin(); + let members_omitted = body.omit_members; + + if let Some(server) = room_id.server_name() + && services + .config + .forbidden_remote_server_names + .is_match(server.host()) + { + warn!( + "Server {origin} tried joining {room_id} through us which has a server name that is \ + globally forbidden. Rejecting.", + ); + + return Err!(Request(Forbidden(warn!( + "Room ID server name {server} is banned on this homeserver." + )))); + } + + // Get the servers in the room BEFORE the join + let servers_in_room = members_omitted + .then_async(|| { + services + .state_cache + .room_servers(room_id) + .map(ToOwned::to_owned) + .collect::>() + }) + .await; + + let create_join_event::v1::RoomState { auth_chain, state, event } = + create_join_event(&services, origin, room_id, &body.pdu, members_omitted) + .boxed() + .await?; + + Ok(create_join_event::v2::Response { + room_state: create_join_event::v2::RoomState { + auth_chain, + state, + event, + servers_in_room, + members_omitted, + }, + }) +} -/// helper method for /send_join v1 and v2 async fn create_join_event( services: &Services, origin: &ServerName, room_id: &RoomId, pdu: &RawJsonValue, + omit_members: bool, ) -> Result { if !services.metadata.exists(room_id).await { return Err!(Request(NotFound("Room is unknown to this server."))); @@ -120,7 +213,7 @@ async fn create_join_event( return Err!(Request(Forbidden("Not allowed to join on behalf of another server."))); } - let state_key: OwnedUserId = serde_json::from_value( + let joining_user: OwnedUserId = serde_json::from_value( value .get("state_key") .ok_or_else(|| err!(Request(BadJson("Event missing state_key property."))))? @@ -129,7 +222,7 @@ async fn create_join_event( ) .map_err(|e| err!(Request(BadJson(warn!("State key is not a valid user ID: {e}")))))?; - if state_key != sender { + if joining_user != sender { return Err!(Request(BadJson("State key does not match sender user."))); } @@ -161,8 +254,13 @@ async fn create_join_event( ))); } - if !super::user_can_perform_restricted_join(services, &state_key, room_id, &room_version) - .await? + if !super::user_can_perform_restricted_join( + services, + &joining_user, + room_id, + &room_version, + ) + .await? { return Err!(Request(UnableToAuthorizeJoin( "Joining user did not pass restricted room's rules." @@ -184,13 +282,51 @@ async fn create_join_event( ) .map_err(|e| err!(Request(BadJson("Event has an invalid origin server name: {e}"))))?; + // MSC3943: Only include heroes when the room has no name and no + // canonical alias (matching Synapse's behavior in PR #14442). + let heroes = omit_members + .then_async(|| { + let has_name = services.state_accessor.state_contains( + shortstatehash, + &StateEventType::RoomName, + "", + ); + + let has_alias = services.state_accessor.state_contains( + shortstatehash, + &StateEventType::RoomCanonicalAlias, + "", + ); + + has_name + .is_false() + .and(has_alias.is_false()) + .then(|_| calculate_heroes(services, room_id, &joining_user)) + }) + .await + .unwrap_or_default(); + // Prestart state gather here since it doesn't involve the new join event. let state_ids = services .state_accessor .state_full_ids(shortstatehash) - .map(at!(1)) - .collect::>() - .boxed(); + .broad_filter_map(async |(ssk, event_id)| { + // Filter state: keep all non-member events, the joining user's + // member event, and hero member events. If get_statekey_from_short + // fails, keep the event (safe default, matching original behavior). + if omit_members + && let Ok((kind, sk)) = services.short.get_statekey_from_short(ssk).await + && kind == StateEventType::RoomMember + && let Ok(user_id) = sk.as_str().try_into() + && joining_user != user_id + && !heroes.contains(&user_id) + { + return None; + } + + Some(event_id) + }) + .collect::>(); let mutex_lock = services .event_handler @@ -208,34 +344,36 @@ async fn create_join_event( drop(mutex_lock); - // Join event for new server. - let event = services - .federation - .format_pdu_into(value, Some(&room_version)) - .map(Some) - .map(Ok); - - // Join event revealed to existing servers. - let broadcast = services.sending.send_pdu_room(room_id, &pdu_id); - // Wait for state gather which the remaining operations depend on. - let state_ids = state_ids.await; - let auth_heads = state_ids.iter().map(Borrow::borrow); - let into_federation_format = |pdu| { + let state_ids = state_ids + .await + .into_iter() + .sorted_unstable() + .collect::>(); + + let into_federation_format = |pdu: CanonicalJsonObject| { services .federation .format_pdu_into(pdu, Some(&room_version)) .map(Ok) }; + // MSC3706: Any events returned within state can be omitted from auth_chain. + let include_auth_event = + |event_id: &OwnedEventId| !omit_members || state_ids.binary_search(event_id).is_err(); + + let auth_heads = state_ids.iter().map(Borrow::borrow); + let auth_chain = services .auth_chain .event_ids_iter(room_id, &room_version, auth_heads) + .ready_try_filter(include_auth_event) .broad_and_then(async |event_id| { services .timeline .get_pdu_json(&event_id) .and_then(into_federation_format) + .inspect_err(|e| debug_error!(?event_id, "auth_chain event not found: {e}")) .await }) .try_collect(); @@ -243,90 +381,29 @@ async fn create_join_event( let state = state_ids .iter() .try_stream() - .broad_and_then(|event_id| { + .broad_and_then(async |event_id| { services .timeline .get_pdu_json(event_id) .and_then(into_federation_format) + .inspect_err(|e| debug_error!(?event_id, "state event not found: {e}")) + .await }) .try_collect(); + // Join event for new server. + let event = services + .federation + .format_pdu_into(value, Some(&room_version)) + .map(Some) + .map(Ok); + + // Join event revealed to existing servers. + let broadcast = services.sending.send_pdu_room(room_id, &pdu_id); + let (auth_chain, state, event, ()) = try_join4(auth_chain, state, event, broadcast) .boxed() .await?; Ok(create_join_event::v1::RoomState { auth_chain, state, event }) } - -/// # `PUT /_matrix/federation/v1/send_join/{roomId}/{eventId}` -/// -/// Submits a signed join event. -pub(crate) async fn create_join_event_v1_route( - State(services): State, - body: Ruma, -) -> Result { - if let Some(server) = body.room_id.server_name() - && services - .config - .forbidden_remote_server_names - .is_match(server.host()) - { - warn!( - "Server {} tried joining room ID {} through us which has a server name that is \ - globally forbidden. Rejecting.", - body.origin(), - &body.room_id, - ); - - return Err!(Request(Forbidden(warn!( - "Room ID server name {server} is banned on this homeserver." - )))); - } - - Ok(create_join_event::v1::Response { - room_state: create_join_event(&services, body.origin(), &body.room_id, &body.pdu) - .boxed() - .await?, - }) -} - -/// # `PUT /_matrix/federation/v2/send_join/{roomId}/{eventId}` -/// -/// Submits a signed join event. -pub(crate) async fn create_join_event_v2_route( - State(services): State, - body: Ruma, -) -> Result { - if let Some(server) = body.room_id.server_name() - && services - .config - .forbidden_remote_server_names - .is_match(server.host()) - { - warn!( - "Server {} tried joining room ID {} through us which has a server name that is \ - globally forbidden. Rejecting.", - body.origin(), - &body.room_id, - ); - - return Err!(Request(Forbidden(warn!( - "Room ID server name {server} is banned on this homeserver." - )))); - } - - let create_join_event::v1::RoomState { auth_chain, state, event } = - create_join_event(&services, body.origin(), &body.room_id, &body.pdu) - .boxed() - .await?; - - Ok(create_join_event::v2::Response { - room_state: create_join_event::v2::RoomState { - members_omitted: false, - auth_chain, - state, - event, - servers_in_room: None, - }, - }) -} diff --git a/src/core/utils/stream/try_ready.rs b/src/core/utils/stream/try_ready.rs index 39849fe5e..7b8bb8e2f 100644 --- a/src/core/utils/stream/try_ready.rs +++ b/src/core/utils/stream/try_ready.rs @@ -4,8 +4,8 @@ use futures::{ future::{Ready, ready}, stream::{ - AndThen, TryFilterMap, TryFold, TryForEach, TrySkipWhile, TryStream, TryStreamExt, - TryTakeWhile, + AndThen, TryFilter, TryFilterMap, TryFold, TryForEach, TrySkipWhile, TryStream, + TryStreamExt, TryTakeWhile, }, }; @@ -26,6 +26,13 @@ where where F: Fn(S::Ok) -> Result; + fn ready_try_filter( + self, + f: F, + ) -> TryFilter, impl FnMut(&S::Ok) -> Ready> + where + F: Fn(&S::Ok) -> bool; + fn ready_try_filter_map( self, f: F, @@ -91,6 +98,18 @@ where self.and_then(move |t| ready(f(t))) } + #[inline] + fn ready_try_filter( + self, + f: F, + ) -> TryFilter, impl FnMut(&S::Ok) -> Ready> + where + F: Fn(&S::Ok) -> bool, + { + self.try_filter(move |t| ready(f(t))) + } + + #[inline] fn ready_try_filter_map( self, f: F, diff --git a/tests/complement/results.jsonl b/tests/complement/results.jsonl index 8b6bb70be..3ea4bff4a 100644 --- a/tests/complement/results.jsonl +++ b/tests/complement/results.jsonl @@ -101,8 +101,6 @@ {"Action":"fail","Test":"TestDelayedEvents/delayed_state_events_can_be_cancelled"} {"Action":"fail","Test":"TestDelayedEvents/delayed_state_events_can_be_restarted"} {"Action":"fail","Test":"TestDelayedEvents/delayed_state_events_can_be_sent_on_request"} -{"Action":"fail","Test":"TestDelayedEvents/delayed_state_is_cancelled_by_new_state_from_another_user"} -{"Action":"fail","Test":"TestDelayedEvents/delayed_state_is_not_cancelled_by_new_state_from_the_same_user"} {"Action":"pass","Test":"TestDelayedEvents/parallel"} {"Action":"pass","Test":"TestDelayedEvents/parallel/cannot_cancel_a_delayed_event_without_a_matching_delay_ID"} {"Action":"pass","Test":"TestDelayedEvents/parallel/cannot_restart_a_delayed_event_without_a_matching_delay_ID"} @@ -219,7 +217,7 @@ {"Action":"pass","Test":"TestJoinViaRoomIDAndServerName"} {"Action":"fail","Test":"TestJson"} {"Action":"fail","Test":"TestJson/Parallel"} -{"Action":"fail","Test":"TestJson/Parallel/Invalid_JSON_special_values"} +{"Action":"pass","Test":"TestJson/Parallel/Invalid_JSON_special_values"} {"Action":"fail","Test":"TestJson/Parallel/Invalid_numerical_values"} {"Action":"fail","Test":"TestJumpToDateEndpoint"} {"Action":"fail","Test":"TestKeyChangesLocal"} @@ -504,6 +502,8 @@ {"Action":"pass","Test":"TestPushSync/Enabling_a_push_rule_wakes_up_an_incremental_/sync"} {"Action":"pass","Test":"TestPushSync/Push_rules_come_down_in_an_initial_/sync"} {"Action":"pass","Test":"TestPushSync/Setting_actions_for_a_push_rule_wakes_up_an_incremental_/sync"} +{"Action":"pass","Test":"TestRedact"} +{"Action":"pass","Test":"TestRedact/Event_content_is_redacted"} {"Action":"pass","Test":"TestRegistration"} {"Action":"pass","Test":"TestRegistration/parallel"} {"Action":"pass","Test":"TestRegistration/parallel/GET_/register/available_returns_M_INVALID_USERNAME_for_invalid_user_name"} @@ -698,8 +698,8 @@ {"Action":"pass","Test":"TestSearch/parallel/Search_results_with_rank_ordering_do_not_include_redacted_events"} {"Action":"pass","Test":"TestSearch/parallel/Search_results_with_recent_ordering_do_not_include_redacted_events"} {"Action":"pass","Test":"TestSearch/parallel/Search_works_across_an_upgraded_room_and_its_predecessor"} -{"Action":"fail","Test":"TestSendAndFetchMessage"} -{"Action":"skip","Test":"TestSendJoinPartialStateResponse"} +{"Action":"pass","Test":"TestSendAndFetchMessage"} +{"Action":"pass","Test":"TestSendJoinPartialStateResponse"} {"Action":"pass","Test":"TestSendMessageWithTxn"} {"Action":"pass","Test":"TestServerCapabilities"} {"Action":"skip","Test":"TestServerNotices"} @@ -710,6 +710,8 @@ {"Action":"pass","Test":"TestSync/parallel/Device_list_tracking/User_is_correctly_listed_when_they_leave,_even_when_lazy_loading_is_enabled"} {"Action":"pass","Test":"TestSync/parallel/Full_state_sync_includes_joined_rooms"} {"Action":"fail","Test":"TestSync/parallel/Get_presence_for_newly_joined_members_in_incremental_sync"} +{"Action":"fail","Test":"TestSync/parallel/Initial_sync_with_lazy-loading_room_members_->_private_room_`state_after`_includes_all_members_from_timeline"} +{"Action":"fail","Test":"TestSync/parallel/Initial_sync_with_lazy-loading_room_members_->_public_room_`state_after`_includes_all_members_from_timeline"} {"Action":"pass","Test":"TestSync/parallel/Newly_joined_room_has_correct_timeline_in_incremental_sync"} {"Action":"fail","Test":"TestSync/parallel/Newly_joined_room_includes_presence_in_incremental_sync"} {"Action":"pass","Test":"TestSync/parallel/Newly_joined_room_is_included_in_an_incremental_sync"}