Skip to content

Commit

Permalink
room preview: add support for MSC3266, room summary
Browse files Browse the repository at this point in the history
  • Loading branch information
bnjbvr committed Apr 22, 2024
1 parent fd96360 commit 90c35b6
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 54 deletions.
15 changes: 7 additions & 8 deletions bindings/matrix-sdk-ffi/src/room_preview.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -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
),
}
}
}
90 changes: 82 additions & 8 deletions crates/matrix-sdk/src/room_preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -52,10 +53,11 @@ pub struct RoomPreview {
pub room_type: Option<RoomType>,

/// 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?
///
Expand All @@ -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,
Expand All @@ -95,14 +109,72 @@ impl RoomPreview {

#[instrument(skip(client))]
pub(crate) async fn from_unknown(client: &Client, room_id: &RoomId) -> crate::Result<Self> {
// 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<Self> {
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<Self> {
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());

Expand All @@ -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))
}
}
3 changes: 3 additions & 0 deletions testing/matrix-sdk-integration-testing/assets/ci-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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: ====== "
Expand Down
116 changes: 78 additions & 38 deletions testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -29,6 +30,8 @@ use matrix_sdk::{
AnySyncMessageLikeEvent, InitialStateEvent, Mentions, StateEventType,
},
mxc_uri,
space::SpaceRoomJoinRule,
RoomId,
},
Client, RoomInfo, RoomListEntry, RoomMemberships, RoomState, SlidingSyncList, SlidingSyncMode,
};
Expand Down Expand Up @@ -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).
Expand All @@ -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());
}

0 comments on commit 90c35b6

Please sign in to comment.