From 40f4fc138b3b1f5ab764bd823be3b9a78a481896 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 25 Oct 2024 14:33:16 +0200 Subject: [PATCH] chore(room_preview): add `RoomListItem::preview_room` (#4152) This method will return a `RoomPreview` for the provided room id. Also added `fn RoomPreview::leave()` action to be able to decline invites or cancel knocks, since there wasn't a `Client::leave_room_by_id` counterpart as there is for join. The PR also deprecates `RoomListItem::invited_room`, since we have a better alternative now. Co-authored-by: Benjamin Bouvier --- bindings/matrix-sdk-ffi/src/client.rs | 23 ++- bindings/matrix-sdk-ffi/src/error.rs | 9 ++ bindings/matrix-sdk-ffi/src/room.rs | 2 +- bindings/matrix-sdk-ffi/src/room_list.rs | 63 +++++++- bindings/matrix-sdk-ffi/src/room_preview.rs | 121 ++++++++++++---- crates/matrix-sdk-base/src/sync.rs | 6 +- crates/matrix-sdk/src/client/mod.rs | 2 +- crates/matrix-sdk/src/room/mod.rs | 6 +- crates/matrix-sdk/src/room_preview.rs | 35 ++++- crates/matrix-sdk/tests/integration/main.rs | 1 + .../tests/integration/room_preview.rs | 137 ++++++++++++++++++ testing/matrix-sdk-test/src/lib.rs | 4 +- .../src/sync_builder/knocked_room.rs | 43 ++++++ .../matrix-sdk-test/src/sync_builder/mod.rs | 23 ++- 14 files changed, 411 insertions(+), 64 deletions(-) create mode 100644 crates/matrix-sdk/tests/integration/room_preview.rs create mode 100644 testing/matrix-sdk-test/src/sync_builder/knocked_room.rs diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index f5f693bae21..f44134fa0ba 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1027,7 +1027,7 @@ impl Client { &self, room_id: String, via_servers: Vec, - ) -> Result { + ) -> Result, ClientError> { let room_id = RoomId::parse(&room_id).context("room_id is not a valid room id")?; let via_servers = via_servers @@ -1040,16 +1040,16 @@ impl Client { // rustc win that one fight. let room_id: &RoomId = &room_id; - let sdk_room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?; + let room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?; - Ok(RoomPreview::from_sdk(sdk_room_preview)) + Ok(Arc::new(RoomPreview::new(self.inner.clone(), room_preview))) } /// Given a room alias, get the preview of a room, to interact with it. pub async fn get_room_preview_from_room_alias( &self, room_alias: String, - ) -> Result { + ) -> Result, ClientError> { let room_alias = RoomAliasId::parse(&room_alias).context("room_alias is not a valid room alias")?; @@ -1057,9 +1057,9 @@ impl Client { // rustc win that one fight. let room_alias: &RoomAliasId = &room_alias; - let sdk_room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?; + let room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?; - Ok(RoomPreview::from_sdk(sdk_room_preview)) + Ok(Arc::new(RoomPreview::new(self.inner.clone(), room_preview))) } /// Waits until an at least partially synced room is received, and returns @@ -1804,7 +1804,7 @@ impl From for SdkOidcPrompt { } /// The rule used for users wishing to join this room. -#[derive(uniffi::Enum)] +#[derive(Debug, Clone, uniffi::Enum)] pub enum JoinRule { /// Anyone can join the room without any prior action. Public, @@ -1830,10 +1830,16 @@ pub enum JoinRule { /// conditions described in a set of [`AllowRule`]s, or they can request /// an invite to the room. KnockRestricted { rules: Vec }, + + /// A custom join rule, up for interpretation by the consumer. + Custom { + /// The string representation for this custom rule. + repr: String, + }, } /// An allow rule which defines a condition that allows joining a room. -#[derive(uniffi::Enum)] +#[derive(Debug, Clone, uniffi::Enum)] pub enum AllowRule { /// Only a member of the `room_id` Room can join the one this rule is used /// in. @@ -1857,6 +1863,7 @@ impl TryFrom for ruma::events::room::join_rules::JoinRule { let rules = allow_rules_from(rules)?; Ok(Self::KnockRestricted(ruma::events::room::join_rules::Restricted::new(rules))) } + JoinRule::Custom { repr } => Ok(serde_json::from_str(&repr)?), } } } diff --git a/bindings/matrix-sdk-ffi/src/error.rs b/bindings/matrix-sdk-ffi/src/error.rs index 6262b765dc4..e644d7974d8 100644 --- a/bindings/matrix-sdk-ffi/src/error.rs +++ b/bindings/matrix-sdk-ffi/src/error.rs @@ -8,6 +8,9 @@ use matrix_sdk::{ }; use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline}; use uniffi::UnexpectedUniFFICallbackError; + +use crate::room_list::RoomListError; + #[derive(Debug, thiserror::Error)] pub enum ClientError { #[error("client error: {msg}")] @@ -128,6 +131,12 @@ impl From for ClientError { } } +impl From for ClientError { + fn from(e: RoomListError) -> Self { + Self::new(e) + } +} + impl From for ClientError { fn from(e: EventCacheError) -> Self { Self::new(e) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index d10aea0e4ba..9f11366a53c 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -45,7 +45,7 @@ use crate::{ TaskHandle, }; -#[derive(Debug, uniffi::Enum)] +#[derive(Debug, Clone, uniffi::Enum)] pub enum Membership { Invited, Joined, diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index 5eae181a6c7..27bc20dcc34 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -1,4 +1,12 @@ -use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration}; +#![allow(deprecated)] + +use std::{ + fmt::Debug, + mem::{ManuallyDrop, MaybeUninit}, + ptr::addr_of_mut, + sync::Arc, + time::Duration, +}; use eyeball_im::VectorDiff; use futures_util::{pin_mut, StreamExt, TryFutureExt}; @@ -16,12 +24,14 @@ use matrix_sdk_ui::{ timeline::default_event_filter, unable_to_decrypt_hook::UtdHookManager, }; +use ruma::{OwnedRoomOrAliasId, OwnedServerName, ServerName}; use tokio::sync::RwLock; use crate::{ error::ClientError, room::{Membership, Room}, room_info::RoomInfo, + room_preview::RoomPreview, timeline::{EventTimelineItem, Timeline}, timeline_event_filter::TimelineEventTypeFilter, TaskHandle, RUNTIME, @@ -48,7 +58,7 @@ pub enum RoomListError { #[error("Event cache ran into an error: {error}")] EventCache { error: String }, #[error("The requested room doesn't match the membership requirements {expected:?}, observed {actual:?}")] - IncorrectRoomMembership { expected: Membership, actual: Membership }, + IncorrectRoomMembership { expected: Vec, actual: Membership }, } impl From for RoomListError { @@ -574,23 +584,60 @@ impl RoomListItem { } /// Builds a `Room` FFI from an invited room without initializing its - /// internal timeline + /// internal timeline. /// - /// An error will be returned if the room is a state different than invited + /// 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 + /// 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: Membership::Invited, + 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 or knocked rooms. + /// + /// An error will be returned if the room is in a state other than invited + /// or knocked. + async fn preview_room(&self, via: Vec) -> Result, ClientError> { + // Validate parameters first. + let server_names: Vec = via + .into_iter() + .map(|server| ServerName::parse(server).map_err(ClientError::from)) + .collect::>()?; + + // Validate internal room state. + let membership = self.membership(); + if !matches!(membership, Membership::Invited | Membership::Knocked) { + return Err(RoomListError::IncorrectRoomMembership { + expected: vec![Membership::Invited, Membership::Knocked], + actual: membership, + } + .into()); + } + + // Do the thing. + let client = self.inner.client(); + let (room_or_alias_id, server_names) = if let Some(alias) = self.inner.canonical_alias() { + let room_or_alias_id: OwnedRoomOrAliasId = alias.into(); + (room_or_alias_id, Vec::new()) + } else { + let room_or_alias_id: OwnedRoomOrAliasId = self.inner.id().to_owned().into(); + (room_or_alias_id, server_names) + }; + + let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?; + + Ok(Arc::new(RoomPreview::new(ManuallyDrop::new(client), room_preview))) + } + /// Build a full `Room` FFI object, filling its associated timeline. /// /// An error will be returned if the room is a state different than joined @@ -598,7 +645,7 @@ impl RoomListItem { fn full_room(&self) -> Result, RoomListError> { if !matches!(self.membership(), Membership::Joined) { return Err(RoomListError::IncorrectRoomMembership { - expected: Membership::Joined, + expected: vec![Membership::Joined], actual: self.membership(), }); } diff --git a/bindings/matrix-sdk-ffi/src/room_preview.rs b/bindings/matrix-sdk-ffi/src/room_preview.rs index 68214bb11ec..08d3b626573 100644 --- a/bindings/matrix-sdk-ffi/src/room_preview.rs +++ b/bindings/matrix-sdk-ffi/src/room_preview.rs @@ -1,9 +1,78 @@ -use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, RoomState}; +use std::mem::ManuallyDrop; + +use anyhow::Context as _; +use async_compat::TOKIO1 as RUNTIME; +use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client}; use ruma::space::SpaceRoomJoinRule; +use tracing::warn; + +use crate::{client::JoinRule, error::ClientError, room::Membership}; + +/// A room preview for a room. It's intended to be used to represent rooms that +/// aren't joined yet. +#[derive(uniffi::Object)] +pub struct RoomPreview { + inner: SdkRoomPreview, + client: ManuallyDrop, +} + +impl Drop for RoomPreview { + fn drop(&mut self) { + // Dropping the inner OlmMachine must happen within a tokio context + // because deadpool drops sqlite connections in the DB pool on tokio's + // blocking threadpool to avoid blocking async worker threads. + let _guard = RUNTIME.enter(); + // SAFETY: self.client is never used again, which is the only requirement + // for ManuallyDrop::drop to be used safely. + unsafe { + ManuallyDrop::drop(&mut self.client); + } + } +} + +#[matrix_sdk_ffi_macros::export] +impl RoomPreview { + /// Returns the room info the preview contains. + pub fn info(&self) -> Result { + let info = &self.inner; + Ok(RoomPreviewInfo { + room_id: info.room_id.to_string(), + canonical_alias: info.canonical_alias.as_ref().map(|alias| alias.to_string()), + name: info.name.clone(), + topic: info.topic.clone(), + avatar_url: info.avatar_url.as_ref().map(|url| url.to_string()), + num_joined_members: info.num_joined_members, + room_type: info.room_type.as_ref().map(|room_type| room_type.to_string()), + is_history_world_readable: info.is_world_readable, + membership: info.state.map(|state| state.into()), + join_rule: info + .join_rule + .clone() + .try_into() + .map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?, + }) + } + + /// Leave the room if the room preview state is either joined, invited or + /// knocked. + /// + /// 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) + } +} + +impl RoomPreview { + pub(crate) fn new(client: ManuallyDrop, inner: SdkRoomPreview) -> Self { + Self { client, inner } + } +} /// The preview of a room, be it invited/joined/left, or not. #[derive(uniffi::Record)] -pub struct RoomPreview { +pub struct RoomPreviewInfo { /// The room id for this room. pub room_id: String, /// The canonical alias for the room. @@ -20,34 +89,28 @@ pub struct RoomPreview { pub room_type: Option, /// Is the history world-readable for this room? pub is_history_world_readable: bool, - /// Is the room joined by the current user? - pub is_joined: bool, - /// Is the current user invited to this room? - pub is_invited: bool, - /// is the join rule public for this room? - pub is_public: bool, - /// Can we knock (or restricted-knock) to this room? - pub can_knock: bool, + /// The membership state for the current user, if known. + pub membership: Option, + /// The join rule for this room (private, public, knock, etc.). + pub join_rule: JoinRule, } -impl RoomPreview { - pub(crate) fn from_sdk(preview: SdkRoomPreview) -> Self { - Self { - room_id: preview.room_id.to_string(), - canonical_alias: preview.canonical_alias.map(|alias| alias.to_string()), - name: preview.name, - topic: preview.topic, - 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.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 == SpaceRoomJoinRule::Public, - can_knock: matches!( - preview.join_rule, - SpaceRoomJoinRule::KnockRestricted | SpaceRoomJoinRule::Knock - ), - } +impl TryFrom for JoinRule { + type Error = (); + + fn try_from(join_rule: SpaceRoomJoinRule) -> Result { + Ok(match join_rule { + SpaceRoomJoinRule::Invite => JoinRule::Invite, + SpaceRoomJoinRule::Knock => JoinRule::Knock, + SpaceRoomJoinRule::Private => JoinRule::Private, + SpaceRoomJoinRule::Restricted => JoinRule::Restricted { rules: Vec::new() }, + SpaceRoomJoinRule::KnockRestricted => JoinRule::KnockRestricted { rules: Vec::new() }, + SpaceRoomJoinRule::Public => JoinRule::Public, + SpaceRoomJoinRule::_Custom(_) => JoinRule::Custom { repr: join_rule.to_string() }, + _ => { + warn!("unhandled SpaceRoomJoinRule: {join_rule}"); + return Err(()); + } + }) } } diff --git a/crates/matrix-sdk-base/src/sync.rs b/crates/matrix-sdk-base/src/sync.rs index 4d47cd904ce..36ee7421ef7 100644 --- a/crates/matrix-sdk-base/src/sync.rs +++ b/crates/matrix-sdk-base/src/sync.rs @@ -19,7 +19,7 @@ use std::{collections::BTreeMap, fmt}; use matrix_sdk_common::{debug::DebugRawEvent, deserialized_responses::SyncTimelineEvent}; use ruma::{ api::client::sync::sync_events::{ - v3::{InvitedRoom as InvitedRoomUpdate, KnockedRoom}, + v3::{InvitedRoom as InvitedRoomUpdate, KnockedRoom as KnockedRoomUpdate}, UnreadNotificationsCount as RumaUnreadNotificationsCount, }, events::{ @@ -78,7 +78,7 @@ pub struct RoomUpdates { /// The rooms that the user has been invited to. pub invite: BTreeMap, /// The rooms that the user has knocked on. - pub knocked: BTreeMap, + pub knocked: BTreeMap, } impl RoomUpdates { @@ -254,7 +254,7 @@ impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> { } } -struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap); +struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap); #[cfg(not(tarpaulin_include))] impl<'a> fmt::Debug for DebugKnockedRoomUpdates<'a> { diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index e2304998034..7066fcba545 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -1009,7 +1009,7 @@ impl Client { }; if let Some(room) = self.get_room(&room_id) { - return Ok(RoomPreview::from_known(&room)); + return Ok(RoomPreview::from_known(&room).await); } RoomPreview::from_unknown(self, room_id, room_or_alias_id, via).await diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 1f237d6d1e1..5561deee315 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -61,7 +61,7 @@ use ruma::{ membership::{ ban_user, forget_room, get_member_events, invite_user::{self, v3::InvitationRecipient}, - join_room_by_id, kick_user, leave_room, unban_user, Invite3pid, + kick_user, leave_room, unban_user, Invite3pid, }, message::send_message_event, read_marker::set_read_marker, @@ -209,9 +209,7 @@ impl Room { false }); - let request = join_room_by_id::v3::Request::new(self.inner.room_id().to_owned()); - let response = self.client.send(request, None).await?; - self.client.base_client().room_joined(&response.room_id).await?; + self.client.join_room_by_id(self.room_id()).await?; if mark_as_direct { self.set_is_direct(true).await?; diff --git a/crates/matrix-sdk/src/room_preview.rs b/crates/matrix-sdk/src/room_preview.rs index 9b8f1da8469..c102b6225ff 100644 --- a/crates/matrix-sdk/src/room_preview.rs +++ b/crates/matrix-sdk/src/room_preview.rs @@ -32,7 +32,7 @@ use tracing::{instrument, warn}; use crate::{Client, Room}; /// The preview of a room, be it invited/joined/left, or not. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct RoomPreview { /// The actual room id for this room. /// @@ -69,6 +69,9 @@ pub struct RoomPreview { /// /// Set to `None` if the room is unknown to the user. pub state: Option, + + /// The `m.room.direct` state of the room, if known. + pub is_direct: Option, } impl RoomPreview { @@ -78,6 +81,7 @@ impl RoomPreview { /// we can do better than that. fn from_room_info( room_info: RoomInfo, + is_direct: Option, num_joined_members: u64, state: Option, ) -> Self { @@ -102,16 +106,23 @@ impl RoomPreview { } }, is_world_readable: *room_info.history_visibility() == HistoryVisibility::WorldReadable, - num_joined_members, state, + is_direct, } } /// Create a room preview from a known room (i.e. one we've been invited to, /// we've joined or we've left). - pub(crate) fn from_known(room: &Room) -> Self { - Self::from_room_info(room.clone_info(), room.joined_members_count(), Some(room.state())) + pub(crate) async fn from_known(room: &Room) -> Self { + let is_direct = room.is_direct().await.ok(); + + Self::from_room_info( + room.clone_info(), + is_direct, + room.joined_members_count(), + Some(room.state()), + ) } #[instrument(skip(client))] @@ -160,12 +171,19 @@ impl RoomPreview { // 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() { + let cached_room = client.get_room(&room_id); + let state = if cached_room.is_none() { None } else { response.membership.map(|membership| RoomState::from(&membership)) }; + let is_direct = if let Some(cached_room) = cached_room { + cached_room.is_direct().await.ok() + } else { + None + }; + Ok(RoomPreview { room_id, canonical_alias: response.canonical_alias, @@ -177,6 +195,7 @@ impl RoomPreview { join_rule: response.join_rule, is_world_readable: response.world_readable, state, + is_direct, }) } @@ -217,8 +236,10 @@ impl RoomPreview { room_info.handle_state_event(&ev.into()); } - let state = client.get_room(room_id).map(|room| room.state()); + let room = client.get_room(room_id); + let state = room.as_ref().map(|room| room.state()); + let is_direct = if let Some(room) = room { room.is_direct().await.ok() } else { None }; - Ok(Self::from_room_info(room_info, num_joined_members, state)) + Ok(Self::from_room_info(room_info, is_direct, num_joined_members, state)) } } diff --git a/crates/matrix-sdk/tests/integration/main.rs b/crates/matrix-sdk/tests/integration/main.rs index 9192d0233f9..13cebccde96 100644 --- a/crates/matrix-sdk/tests/integration/main.rs +++ b/crates/matrix-sdk/tests/integration/main.rs @@ -20,6 +20,7 @@ mod media; mod notification; mod refresh_token; mod room; +mod room_preview; mod send_queue; #[cfg(feature = "experimental-widgets")] mod widget; diff --git a/crates/matrix-sdk/tests/integration/room_preview.rs b/crates/matrix-sdk/tests/integration/room_preview.rs new file mode 100644 index 00000000000..142df0b83ef --- /dev/null +++ b/crates/matrix-sdk/tests/integration/room_preview.rs @@ -0,0 +1,137 @@ +use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server}; +use matrix_sdk_base::RoomState; +use matrix_sdk_test::{ + async_test, InvitedRoomBuilder, JoinedRoomBuilder, KnockedRoomBuilder, SyncResponseBuilder, +}; +use ruma::{room_id, space::SpaceRoomJoinRule, RoomId}; +use serde_json::json; +use wiremock::{ + matchers::{header, method, path_regex}, + Mock, MockServer, ResponseTemplate, +}; + +use crate::mock_sync; + +#[async_test] +async fn test_room_preview_leave_invited() { + let (client, server) = logged_in_client_with_server().await; + let room_id = room_id!("!room:localhost"); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_invited_room(InvitedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + client.sync_once(SyncSettings::default()).await.unwrap(); + server.reset().await; + + mock_leave(room_id, &server).await; + + let room_preview = client.get_room_preview(room_id.into(), Vec::new()).await.unwrap(); + assert_eq!(room_preview.state.unwrap(), RoomState::Invited); + + client.get_room(room_id).unwrap().leave().await.unwrap(); + + assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left); +} + +#[async_test] +async fn test_room_preview_leave_knocked() { + let (client, server) = logged_in_client_with_server().await; + let room_id = room_id!("!room:localhost"); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_knocked_room(KnockedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + client.sync_once(SyncSettings::default()).await.unwrap(); + server.reset().await; + + mock_leave(room_id, &server).await; + + let room_preview = client.get_room_preview(room_id.into(), Vec::new()).await.unwrap(); + assert_eq!(room_preview.state.unwrap(), RoomState::Knocked); + + let room = client.get_room(room_id).unwrap(); + room.leave().await.unwrap(); + + assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left); +} + +#[async_test] +async fn test_room_preview_leave_joined() { + let (client, server) = logged_in_client_with_server().await; + let room_id = room_id!("!room:localhost"); + + let mut sync_builder = SyncResponseBuilder::new(); + sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + client.sync_once(SyncSettings::default()).await.unwrap(); + server.reset().await; + + mock_leave(room_id, &server).await; + + let room_preview = client.get_room_preview(room_id.into(), Vec::new()).await.unwrap(); + assert_eq!(room_preview.state.unwrap(), RoomState::Joined); + + let room = client.get_room(room_id).unwrap(); + room.leave().await.unwrap(); + + assert_eq!(room.state(), RoomState::Left); +} + +#[async_test] +async fn test_room_preview_leave_unknown_room_fails() { + let (client, server) = logged_in_client_with_server().await; + let room_id = room_id!("!room:localhost"); + + mock_unknown_summary(room_id, None, SpaceRoomJoinRule::Knock, &server).await; + + let room_preview = client.get_room_preview(room_id.into(), Vec::new()).await.unwrap(); + assert!(room_preview.state.is_none()); + + assert!(client.get_room(room_id).is_none()); +} + +async fn mock_leave(room_id: &RoomId, server: &MockServer) { + Mock::given(method("POST")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/leave")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "room_id": room_id, + }))) + .mount(server) + .await +} + +async fn mock_unknown_summary( + room_id: &RoomId, + alias: Option, + join_rule: SpaceRoomJoinRule, + server: &MockServer, +) { + let body = if let Some(alias) = alias { + json!({ + "room_id": room_id, + "canonical_alias": alias, + "guest_can_join": true, + "num_joined_members": 1, + "world_readable": true, + "join_rule": join_rule, + }) + } else { + json!({ + "room_id": room_id, + "guest_can_join": true, + "num_joined_members": 1, + "world_readable": true, + "join_rule": join_rule, + }) + }; + Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/unstable/im.nheko.summary/rooms/.*/summary")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(body)) + .mount(server) + .await +} diff --git a/testing/matrix-sdk-test/src/lib.rs b/testing/matrix-sdk-test/src/lib.rs index 0a7c99e2384..1b06e142d56 100644 --- a/testing/matrix-sdk-test/src/lib.rs +++ b/testing/matrix-sdk-test/src/lib.rs @@ -122,8 +122,8 @@ pub use self::{ event_builder::EventBuilder, sync_builder::{ bulk_room_members, EphemeralTestEvent, GlobalAccountDataTestEvent, InvitedRoomBuilder, - JoinedRoomBuilder, LeftRoomBuilder, PresenceTestEvent, RoomAccountDataTestEvent, - StateTestEvent, StrippedStateTestEvent, SyncResponseBuilder, + JoinedRoomBuilder, KnockedRoomBuilder, LeftRoomBuilder, PresenceTestEvent, + RoomAccountDataTestEvent, StateTestEvent, StrippedStateTestEvent, SyncResponseBuilder, }, }; diff --git a/testing/matrix-sdk-test/src/sync_builder/knocked_room.rs b/testing/matrix-sdk-test/src/sync_builder/knocked_room.rs new file mode 100644 index 00000000000..0df9a4e73a6 --- /dev/null +++ b/testing/matrix-sdk-test/src/sync_builder/knocked_room.rs @@ -0,0 +1,43 @@ +use ruma::{ + api::client::sync::sync_events::v3::KnockedRoom, events::AnyStrippedStateEvent, serde::Raw, + OwnedRoomId, RoomId, +}; + +use super::StrippedStateTestEvent; +use crate::DEFAULT_TEST_ROOM_ID; + +pub struct KnockedRoomBuilder { + pub(super) room_id: OwnedRoomId, + pub(super) inner: KnockedRoom, +} + +impl KnockedRoomBuilder { + /// Create a new `KnockedRoomBuilder` for the given room ID. + /// + /// If the room ID is [`DEFAULT_TEST_ROOM_ID`], + /// [`KnockedRoomBuilder::default()`] can be used instead. + pub fn new(room_id: &RoomId) -> Self { + Self { room_id: room_id.to_owned(), inner: Default::default() } + } + + /// Add an event to the state. + pub fn add_state_event(mut self, event: StrippedStateTestEvent) -> Self { + self.inner.knock_state.events.push(event.into_raw_event()); + self + } + + /// Add events to the state in bulk. + pub fn add_state_bulk(mut self, events: I) -> Self + where + I: IntoIterator>, + { + self.inner.knock_state.events.extend(events); + self + } +} + +impl Default for KnockedRoomBuilder { + fn default() -> Self { + Self::new(&DEFAULT_TEST_ROOM_ID) + } +} diff --git a/testing/matrix-sdk-test/src/sync_builder/mod.rs b/testing/matrix-sdk-test/src/sync_builder/mod.rs index ea6d05bd8cb..919fe6356d6 100644 --- a/testing/matrix-sdk-test/src/sync_builder/mod.rs +++ b/testing/matrix-sdk-test/src/sync_builder/mod.rs @@ -4,7 +4,7 @@ use http::Response; use ruma::{ api::{ client::sync::sync_events::v3::{ - InvitedRoom, JoinedRoom, LeftRoom, Response as SyncResponse, + InvitedRoom, JoinedRoom, KnockedRoom, LeftRoom, Response as SyncResponse, }, IncomingResponse, }, @@ -19,12 +19,14 @@ use super::test_json; mod bulk; mod invited_room; mod joined_room; +mod knocked_room; mod left_room; mod test_event; pub use bulk::bulk_room_members; pub use invited_room::InvitedRoomBuilder; pub use joined_room::JoinedRoomBuilder; +pub use knocked_room::KnockedRoomBuilder; pub use left_room::LeftRoomBuilder; pub use test_event::{ EphemeralTestEvent, GlobalAccountDataTestEvent, PresenceTestEvent, RoomAccountDataTestEvent, @@ -45,6 +47,8 @@ pub struct SyncResponseBuilder { invited_rooms: HashMap, /// Updates to left `Room`s. left_rooms: HashMap, + /// Updates to knocked `Room`s. + knocked_rooms: HashMap, /// Events that determine the presence state of a user. presence: Vec>, /// Global account data events. @@ -68,6 +72,7 @@ impl SyncResponseBuilder { pub fn add_joined_room(&mut self, room: JoinedRoomBuilder) -> &mut Self { self.invited_rooms.remove(&room.room_id); self.left_rooms.remove(&room.room_id); + self.knocked_rooms.remove(&room.room_id); self.joined_rooms.insert(room.room_id, room.inner); self } @@ -79,6 +84,7 @@ impl SyncResponseBuilder { pub fn add_invited_room(&mut self, room: InvitedRoomBuilder) -> &mut Self { self.joined_rooms.remove(&room.room_id); self.left_rooms.remove(&room.room_id); + self.knocked_rooms.remove(&room.room_id); self.invited_rooms.insert(room.room_id, room.inner); self } @@ -90,10 +96,23 @@ impl SyncResponseBuilder { pub fn add_left_room(&mut self, room: LeftRoomBuilder) -> &mut Self { self.joined_rooms.remove(&room.room_id); self.invited_rooms.remove(&room.room_id); + self.knocked_rooms.remove(&room.room_id); self.left_rooms.insert(room.room_id, room.inner); self } + /// Add a knocked room to the next sync response. + /// + /// If a room with the same room ID already exists, it is replaced by this + /// one. + pub fn add_knocked_room(&mut self, room: KnockedRoomBuilder) -> &mut Self { + self.joined_rooms.remove(&room.room_id); + self.invited_rooms.remove(&room.room_id); + self.left_rooms.remove(&room.room_id); + self.knocked_rooms.insert(room.room_id, room.inner); + self + } + /// Add a presence event. pub fn add_presence_event(&mut self, event: PresenceTestEvent) -> &mut Self { let val = match event { @@ -169,6 +188,7 @@ impl SyncResponseBuilder { "invite": self.invited_rooms, "join": self.joined_rooms, "leave": self.left_rooms, + "knock": self.knocked_rooms, }, "to_device": { "events": [] @@ -215,6 +235,7 @@ impl SyncResponseBuilder { self.invited_rooms.clear(); self.joined_rooms.clear(); self.left_rooms.clear(); + self.knocked_rooms.clear(); self.presence.clear(); } }