diff --git a/bindings/matrix-sdk-ffi/src/room_preview.rs b/bindings/matrix-sdk-ffi/src/room_preview.rs index f5f390ef7aa..112a6732fc5 100644 --- a/bindings/matrix-sdk-ffi/src/room_preview.rs +++ b/bindings/matrix-sdk-ffi/src/room_preview.rs @@ -1,8 +1,5 @@ use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, RoomState}; -use ruma::{ - events::room::{history_visibility::HistoryVisibility, join_rules::JoinRule}, - OwnedRoomId, -}; +use ruma::{space::SpaceRoomJoinRule, OwnedRoomId}; /// The preview of a room, be it invited/joined/left, or not. #[derive(uniffi::Record)] @@ -43,12 +40,14 @@ impl RoomPreview { avatar_url: preview.avatar_url.map(|url| url.to_string()), num_joined_members: preview.num_joined_members, room_type: preview.room_type.map(|room_type| room_type.to_string()), - is_history_world_readable: preview.history_visibility - == HistoryVisibility::WorldReadable, + is_history_world_readable: preview.is_world_readable, is_joined: preview.state.map_or(false, |state| state == RoomState::Joined), is_invited: preview.state.map_or(false, |state| state == RoomState::Invited), - is_public: preview.join_rule == JoinRule::Public, - can_knock: matches!(preview.join_rule, JoinRule::KnockRestricted(_) | JoinRule::Knock), + is_public: preview.join_rule == SpaceRoomJoinRule::Public, + can_knock: matches!( + preview.join_rule, + SpaceRoomJoinRule::KnockRestricted | SpaceRoomJoinRule::Knock + ), } } } diff --git a/crates/matrix-sdk/src/room_preview.rs b/crates/matrix-sdk/src/room_preview.rs index 9d9227f8c64..a113f80e152 100644 --- a/crates/matrix-sdk/src/room_preview.rs +++ b/crates/matrix-sdk/src/room_preview.rs @@ -23,6 +23,7 @@ use ruma::{ api::client::{membership::joined_members, state::get_state_events}, events::room::{history_visibility::HistoryVisibility, join_rules::JoinRule}, room::RoomType, + space::SpaceRoomJoinRule, OwnedMxcUri, OwnedRoomAliasId, RoomId, }; use tokio::try_join; @@ -52,10 +53,11 @@ pub struct RoomPreview { pub room_type: Option, /// What's the join rule for this room? - pub join_rule: JoinRule, + pub join_rule: SpaceRoomJoinRule, - /// What's the history visibility for this room? - pub history_visibility: HistoryVisibility, + /// Is the room world-readable (i.e. is its history_visibility set to + /// world_readable)? + pub is_world_readable: bool, /// Has the current user been invited/joined/left this room? /// @@ -79,8 +81,20 @@ impl RoomPreview { topic: room_info.topic().map(ToOwned::to_owned), avatar_url: room_info.avatar_url().map(ToOwned::to_owned), room_type: room_info.room_type().cloned(), - join_rule: room_info.join_rule().clone(), - history_visibility: room_info.history_visibility().clone(), + join_rule: match room_info.join_rule() { + JoinRule::Invite => SpaceRoomJoinRule::Invite, + JoinRule::Knock => SpaceRoomJoinRule::Knock, + JoinRule::Private => SpaceRoomJoinRule::Private, + JoinRule::Restricted(_) => SpaceRoomJoinRule::Restricted, + JoinRule::KnockRestricted(_) => SpaceRoomJoinRule::KnockRestricted, + JoinRule::Public => SpaceRoomJoinRule::Public, + _ => { + // The JoinRule enum is non-exhaustive. Let's do a white lie and pretend it's + // private (a cautious choice). + SpaceRoomJoinRule::Private + } + }, + is_world_readable: *room_info.history_visibility() == HistoryVisibility::WorldReadable, num_joined_members, state, @@ -95,14 +109,72 @@ impl RoomPreview { #[instrument(skip(client))] pub(crate) async fn from_unknown(client: &Client, room_id: &RoomId) -> crate::Result { - // TODO: (optimization) Use the room summary endpoint, if available, as - // described in https://github.com/deepbluev7/matrix-doc/blob/room-summaries/proposals/3266-room-summary.md + // Use the room summary endpoint, if available, as described in + // https://github.com/deepbluev7/matrix-doc/blob/room-summaries/proposals/3266-room-summary.md + match Self::from_room_summary(client, room_id).await { + Ok(res) => return Ok(res), + Err(err) => { + warn!("error when previewing room from the room summary endpoint: {err}"); + } + } // TODO: (optimization) Use the room search directory, if available: // - if the room directory visibility is public, // - then use a public room filter set to this room id // Resort to using the room state endpoint, as well as the joined members one. + Self::from_state_events(client, room_id).await + } + + /// Get a [`RoomPreview`] using MSC3266, if available on the remote server. + /// + /// Will fail with a 404 if the API is not available. + /// + /// This method is exposed for testing purposes; clients should prefer + /// `Client::get_room_preview` in general over this. + pub async fn from_room_summary(client: &Client, room_id: &RoomId) -> crate::Result { + let request = ruma::api::client::room::get_summary::msc3266::Request::new( + room_id.to_owned().into(), + Vec::new(), + ); + + let response = client.send(request, None).await?; + + // The server returns a `Left` room state for rooms the user has not joined. Be + // more precise than that, and set it to `None` if we haven't joined + // that room. + let state = if client.get_room(room_id).is_none() { + None + } else { + response.membership.map(|membership| RoomState::from(&membership)) + }; + + Ok(RoomPreview { + canonical_alias: response.canonical_alias, + name: response.name, + topic: response.topic, + avatar_url: response.avatar_url, + num_joined_members: response.num_joined_members.into(), + room_type: response.room_type, + join_rule: response.join_rule, + is_world_readable: response.world_readable, + state, + }) + } + + /// Get a [`RoomPreview`] using the room state endpoint. + /// + /// This is always available on a remote server, but will only work if one + /// of these two conditions is true: + /// + /// - the user has joined the room at some point (i.e. they're still joined + /// or they've joined + /// it and left it later). + /// - the room has an history visibility set to world-readable. + /// + /// This method is exposed for testing purposes; clients should prefer + /// `Client::get_room_preview` in general over this. + pub async fn from_state_events(client: &Client, room_id: &RoomId) -> crate::Result { let state_request = get_state_events::v3::Request::new(room_id.to_owned()); let joined_members_request = joined_members::v3::Request::new(room_id.to_owned()); @@ -128,6 +200,8 @@ impl RoomPreview { room_info.handle_state_event(&ev.into()); } - Ok(Self::from_room_info(room_info, num_joined_members, None)) + let state = client.get_room(room_id).map(|room| room.state()); + + Ok(Self::from_room_info(room_info, num_joined_members, state)) } } diff --git a/testing/matrix-sdk-integration-testing/assets/ci-start.sh b/testing/matrix-sdk-integration-testing/assets/ci-start.sh index 911878063b2..2c7e9b3efdd 100644 --- a/testing/matrix-sdk-integration-testing/assets/ci-start.sh +++ b/testing/matrix-sdk-integration-testing/assets/ci-start.sh @@ -56,6 +56,9 @@ rc_invites: per_issuer: per_second: 1000 burst_count: 1000 + +experimental_features: + msc3266_enabled: true """ >> /data/homeserver.yaml echo " ====== Starting server with: ====== " diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index b179cc46e9e..90134197e77 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -10,6 +10,7 @@ use futures_util::{pin_mut, FutureExt, StreamExt as _}; use matrix_sdk::{ bytes::Bytes, config::SyncSettings, + room_preview::RoomPreview, ruma::{ api::client::{ receipt::create_receipt::v3::ReceiptType, @@ -29,6 +30,8 @@ use matrix_sdk::{ AnySyncMessageLikeEvent, InitialStateEvent, Mentions, StateEventType, }, mxc_uri, + space::SpaceRoomJoinRule, + RoomId, }, Client, RoomInfo, RoomListEntry, RoomMemberships, RoomState, SlidingSyncList, SlidingSyncMode, }; @@ -1082,14 +1085,24 @@ async fn test_room_preview() -> Result<()> { InitialStateEvent::new(RoomHistoryVisibilityEventContent::new(HistoryVisibility::WorldReadable)).to_raw_any(), InitialStateEvent::new(RoomJoinRulesEventContent::new(JoinRule::Invite)).to_raw_any(), ], - //TODO: this doesn't allow preview => could be tested! - //preset: Some(RoomPreset::PrivateChat), })) .await?; room.set_avatar_url(mxc_uri!("mxc://localhost/alice"), None).await?; + // Alice creates another room, and still doesn't invite Bob. + let private_room = alice + .create_room(assign!(CreateRoomRequest::new(), { + name: Some("Alice's Room 2".to_owned()), + initial_state: vec![ + InitialStateEvent::new(RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared)).to_raw_any(), + InitialStateEvent::new(RoomJoinRulesEventContent::new(JoinRule::Public)).to_raw_any(), + ], + })) + .await?; + let room_id = room.room_id(); + let private_room_id = private_room.room_id(); // Wait for Alice's stream to stabilize (stop updating when we haven't received // successful updates for more than 2 seconds). @@ -1104,49 +1117,76 @@ async fn test_room_preview() -> Result<()> { } } - let preview = alice.get_room_preview(room_id).await?; - assert_eq!(preview.canonical_alias.unwrap().alias(), room_alias); - assert_eq!(preview.name.unwrap(), "Alice's Room"); - assert_eq!(preview.topic.unwrap(), "Discussing Alice's Topic"); - assert_eq!(preview.avatar_url.unwrap(), mxc_uri!("mxc://localhost/alice")); - assert_eq!(preview.num_joined_members, 1); - assert!(preview.room_type.is_none()); - // Because of the preset: - assert_eq!(preview.join_rule, JoinRule::Invite); - assert_eq!(preview.history_visibility, HistoryVisibility::WorldReadable); - assert_eq!(preview.state, Some(RoomState::Joined)); + get_room_preview_with_room_state(&alice, &bob, &room_alias, room_id, private_room_id).await; + get_room_preview_with_room_summary(&alice, &bob, &room_alias, room_id, private_room_id).await; - // Bob definitely doesn't know about the room, but they can get a preview of the - // room too. - let preview = bob.get_room_preview(room_id).await?; + { + // Dummy test for `Client::get_room_preview` which may call one or the other + // methods. + let preview = alice.get_room_preview(room_id).await.unwrap(); + assert_room_preview(&preview, &room_alias); + assert_eq!(preview.state, Some(RoomState::Joined)); + } - assert_eq!(preview.canonical_alias.unwrap().alias(), room_alias); - assert_eq!(preview.name.unwrap(), "Alice's Room"); - assert_eq!(preview.topic.unwrap(), "Discussing Alice's Topic"); - assert_eq!(preview.avatar_url.unwrap(), mxc_uri!("mxc://localhost/alice")); + Ok(()) +} + +fn assert_room_preview(preview: &RoomPreview, room_alias: &str) { + assert_eq!(preview.canonical_alias.as_ref().unwrap().alias(), room_alias); + assert_eq!(preview.name.as_ref().unwrap(), "Alice's Room"); + assert_eq!(preview.topic.as_ref().unwrap(), "Discussing Alice's Topic"); + assert_eq!(preview.avatar_url.as_ref().unwrap(), mxc_uri!("mxc://localhost/alice")); assert_eq!(preview.num_joined_members, 1); assert!(preview.room_type.is_none()); - assert_eq!(preview.join_rule, JoinRule::Invite); - assert_eq!(preview.history_visibility, HistoryVisibility::WorldReadable); + assert_eq!(preview.join_rule, SpaceRoomJoinRule::Invite); + assert!(preview.is_world_readable); +} - // Only difference with Alice's room is here: since Bob hasn't joined the room, - // they don't have any associated room state. - assert_eq!(preview.state, None); +async fn get_room_preview_with_room_state( + alice: &Client, + bob: &Client, + room_alias: &str, + room_id: &RoomId, + public_no_history_room_id: &RoomId, +) { + // Alice has joined the room, so they get the full details. + let preview = RoomPreview::from_state_events(alice, room_id).await.unwrap(); + assert_room_preview(&preview, room_alias); + assert_eq!(preview.state, Some(RoomState::Joined)); - // Now Alice creates another room, with a private preset, and still doesn't - // invite Bob. - let room = alice - .create_room(assign!(CreateRoomRequest::new(), { - initial_state: vec![ - InitialStateEvent::new(RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared)).to_raw_any(), - InitialStateEvent::new(RoomJoinRulesEventContent::new(JoinRule::Invite)).to_raw_any(), - ], - })) - .await?; + // Bob definitely doesn't know about the room, but they can get a preview of the + // room too. + let preview = RoomPreview::from_state_events(bob, room_id).await.unwrap(); + assert_room_preview(&preview, room_alias); + assert!(preview.state.is_none()); - // So Bob can't preview it. - let preview_result = bob.get_room_preview(room.room_id()).await; + // Bob can't preview the second room, because its history visibility is neither + // world-readable, nor have they joined the room before. + let preview_result = RoomPreview::from_state_events(bob, public_no_history_room_id).await; assert_eq!(preview_result.unwrap_err().as_client_api_error().unwrap().status_code, 403); +} - Ok(()) +async fn get_room_preview_with_room_summary( + alice: &Client, + bob: &Client, + room_alias: &str, + room_id: &RoomId, + public_no_history_room_id: &RoomId, +) { + // Alice has joined the room, so they get the full details. + let preview = RoomPreview::from_room_summary(alice, room_id).await.unwrap(); + assert_room_preview(&preview, room_alias); + assert_eq!(preview.state, Some(RoomState::Joined)); + + // Bob definitely doesn't know about the room, but they can get a preview of the + // room too. + let preview = RoomPreview::from_room_summary(bob, room_id).await.unwrap(); + assert_room_preview(&preview, room_alias); + assert!(preview.state.is_none()); + + // Bob can preview the second room with the room summary (because its join rule + // is set to public, or because Alice is a member of that room). + let preview = RoomPreview::from_room_summary(bob, public_no_history_room_id).await.unwrap(); + assert_eq!(preview.name.unwrap(), "Alice's Room 2"); + assert!(preview.state.is_none()); }