diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 43e46cec290..d3a27801bad 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -886,6 +886,24 @@ impl Client { self.inner.rooms().into_iter().map(|room| Arc::new(Room::new(room))).collect() } + /// Get a room by its ID. + /// + /// # Arguments + /// + /// * `room_id` - The ID of the room to get. + /// + /// # Returns + /// + /// A `Result` containing an optional room, or a `ClientError`. + /// This method will not initialize the room's timeline or populate it with + /// events. + pub fn get_room(&self, room_id: String) -> Result>, ClientError> { + let room_id = RoomId::parse(room_id)?; + let sdk_room = self.inner.get_room(&room_id); + let room = sdk_room.map(|room| Arc::new(Room::new(room))); + Ok(room) + } + pub fn get_dm_room(&self, user_id: String) -> Result>, ClientError> { let user_id = UserId::parse(user_id)?; let sdk_room = self.inner.get_dm_room(&user_id); diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 8a481300040..bfe3d896d61 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -161,21 +161,6 @@ impl Room { self.inner.active_room_call_participants().iter().map(|u| u.to_string()).collect() } - /// For rooms one is invited to, retrieves the room member information for - /// the user who invited the logged-in user to a room. - pub async fn inviter(&self) -> Option { - if self.inner.state() == RoomState::Invited { - self.inner - .invite_details() - .await - .ok() - .and_then(|a| a.inviter) - .and_then(|m| m.try_into().ok()) - } else { - None - } - } - /// Forces the currently active room key, which is used to encrypt messages, /// to be rotated. /// diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index a566dac2905..08edd781418 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -573,24 +573,6 @@ impl RoomListItem { self.inner.inner_room().state().into() } - /// Builds a `Room` FFI from an invited room without initializing its - /// internal timeline. - /// - /// An error will be returned if the room is a state different than invited. - /// - /// ⚠️ Holding on to this room instance after it has been joined is not - /// safe. Use `full_room` instead. - #[deprecated(note = "Please use `preview_room` instead.")] - fn invited_room(&self) -> Result, RoomListError> { - if !matches!(self.membership(), Membership::Invited) { - return Err(RoomListError::IncorrectRoomMembership { - expected: vec![Membership::Invited], - actual: self.membership(), - }); - } - Ok(Arc::new(Room::new(self.inner.inner_room().clone()))) - } - /// Builds a `RoomPreview` from a room list item. This is intended for /// invited, knocked or banned rooms. async fn preview_room(&self, via: Vec) -> Result, ClientError> { diff --git a/bindings/matrix-sdk-ffi/src/room_preview.rs b/bindings/matrix-sdk-ffi/src/room_preview.rs index e91459257d8..d9e7ba697a1 100644 --- a/bindings/matrix-sdk-ffi/src/room_preview.rs +++ b/bindings/matrix-sdk-ffi/src/room_preview.rs @@ -51,11 +51,23 @@ impl RoomPreview { /// Leave the room if the room preview state is either joined, invited or /// knocked. /// + /// If rejecting an invite then also forget it as an extra layer of + /// protection against spam attacks. + /// /// Will return an error otherwise. pub async fn leave(&self) -> Result<(), ClientError> { let room = self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?; - room.leave().await.map_err(Into::into) + + let should_forget = matches!(room.state(), matrix_sdk::RoomState::Invited); + + room.leave().await.map_err(ClientError::from)?; + + if should_forget { + _ = self.forget().await; + } + + Ok(()) } /// Get the user who created the invite, if any. diff --git a/crates/matrix-sdk/src/room_preview.rs b/crates/matrix-sdk/src/room_preview.rs index d56709baef4..315643a4c30 100644 --- a/crates/matrix-sdk/src/room_preview.rs +++ b/crates/matrix-sdk/src/room_preview.rs @@ -31,7 +31,7 @@ use ruma::{ use tokio::try_join; use tracing::{instrument, warn}; -use crate::{room_directory_search::RoomDirectorySearch, Client, Room}; +use crate::{room_directory_search::RoomDirectorySearch, Client, Error, Room}; /// The preview of a room, be it invited/joined/left, or not. #[derive(Debug, Clone)] @@ -172,8 +172,21 @@ impl RoomPreview { } } - // Resort to using the room state endpoint, as well as the joined members one. - Self::from_state_events(client, &room_id).await + // Try using the room state endpoint, as well as the joined members one. + match Self::from_state_events(client, &room_id).await { + Ok(res) => return Ok(res), + Err(err) => { + warn!("error when building room preview from state events: {err}"); + } + } + + // Finally, if everything else fails, try to build the room from information + // that the client itself might have about it. + if let Some(room) = client.get_room(&room_id) { + Ok(Self::from_known_room(&room).await) + } else { + Err(Error::InsufficientData) + } } /// Get a [`RoomPreview`] by searching in the room directory for the diff --git a/crates/matrix-sdk/src/test_utils/mocks/mod.rs b/crates/matrix-sdk/src/test_utils/mocks/mod.rs index 6e71e9dad03..9d0683b9cd3 100644 --- a/crates/matrix-sdk/src/test_utils/mocks/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mocks/mod.rs @@ -989,6 +989,13 @@ impl MatrixMockServer { .and(path_regex(r"^/_matrix/client/v3/keys/signatures/upload")); self.mock_endpoint(mock, UploadCrossSigningSignaturesEndpoint).expect_default_access_token() } + + /// Creates a prebuilt mock for the endpoint used to leave a room. + pub fn mock_room_leave(&self) -> MockEndpoint<'_, RoomLeaveEndpoint> { + let mock = + Mock::given(method("POST")).and(path_regex(r"^/_matrix/client/v3/rooms/.*/leave")); + self.mock_endpoint(mock, RoomLeaveEndpoint).expect_default_access_token() + } } /// Parameter to [`MatrixMockServer::sync_room`]. @@ -2498,3 +2505,16 @@ impl<'a> MockEndpoint<'a, UploadCrossSigningSignaturesEndpoint> { self.respond_with(ResponseTemplate::new(200).set_body_json(json!({}))) } } + +/// A prebuilt mock for the room leave endpoint. +pub struct RoomLeaveEndpoint; + +impl<'a> MockEndpoint<'a, RoomLeaveEndpoint> { + /// Returns a successful response with some default data for the given room + /// id. + pub fn ok(self, room_id: &RoomId) -> MatrixMock<'a> { + self.respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "room_id": room_id, + }))) + } +} diff --git a/crates/matrix-sdk/tests/integration/room_preview.rs b/crates/matrix-sdk/tests/integration/room_preview.rs index 9b05d6fe9a1..eadcf3f9e62 100644 --- a/crates/matrix-sdk/tests/integration/room_preview.rs +++ b/crates/matrix-sdk/tests/integration/room_preview.rs @@ -1,5 +1,9 @@ +use assert_matches::assert_matches; use js_int::uint; -use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server}; +use matrix_sdk::{ + config::SyncSettings, + test_utils::{logged_in_client_with_server, mocks::MatrixMockServer}, +}; use matrix_sdk_base::RoomState; use matrix_sdk_test::{ async_test, InvitedRoomBuilder, JoinedRoomBuilder, KnockedRoomBuilder, SyncResponseBuilder, @@ -50,6 +54,34 @@ async fn test_room_preview_leave_invited() { assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left); } +#[async_test] +async fn test_room_preview_invite_leave_room_summary_msc3266_disabled() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let room_id = room_id!("!room:localhost"); + + server.sync_room(&client, InvitedRoomBuilder::new(room_id)).await; + + // A preview should be built from the sync data above + let preview = client + .get_room_preview(room_id.into(), Vec::new()) + .await + .expect("Room preview should be retrieved"); + + assert_eq!(preview.room_id, room_id); + assert_matches!(preview.state.unwrap(), RoomState::Invited); + + server.mock_room_leave().ok(room_id).expect(1).mount().await; + + client.get_room(room_id).unwrap().leave().await.unwrap(); + + assert_matches!(client.get_room(room_id).unwrap().state(), RoomState::Left); + assert_matches!( + client.get_room_preview(room_id.into(), Vec::new()).await.unwrap().state.unwrap(), + RoomState::Left + ); +} + #[async_test] async fn test_room_preview_leave_knocked() { let (client, server) = logged_in_client_with_server().await;