Skip to content

Commit d60802d

Browse files
committed
room preview: add support for MSC3266, room summary
1 parent 72207a3 commit d60802d

File tree

5 files changed

+171
-54
lines changed

5 files changed

+171
-54
lines changed

Diff for: Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ ruma = { git = "https://github.com/bnjbvr/ruma", rev = "998987943ab20c63bdc31bcf
4646
"compat-arbitrary-length-ids",
4747
"compat-tag-info",
4848
"unstable-msc3401",
49+
"unstable-msc3266",
4950
] }
5051
ruma-common = { git = "https://github.com/bnjbvr/ruma", rev = "998987943ab20c63bdc31bcf05714c42d0175de7" }
5152
once_cell = "1.16.0"

Diff for: bindings/matrix-sdk-ffi/src/room_preview.rs

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, RoomState};
2-
use ruma::{
3-
events::room::{history_visibility::HistoryVisibility, join_rules::JoinRule},
4-
OwnedRoomId,
5-
};
2+
use ruma::{space::SpaceRoomJoinRule, OwnedRoomId};
63

74
/// The preview of a room, be it invited/joined/left, or not.
85
#[derive(uniffi::Record)]
@@ -43,12 +40,14 @@ impl RoomPreview {
4340
avatar_url: preview.avatar_url.map(|url| url.to_string()),
4441
num_joined_members: preview.num_joined_members,
4542
room_type: preview.room_type.map(|room_type| room_type.to_string()),
46-
is_history_world_readable: preview.history_visibility
47-
== HistoryVisibility::WorldReadable,
43+
is_history_world_readable: preview.is_world_readable,
4844
is_joined: preview.state.map_or(false, |state| state == RoomState::Joined),
4945
is_invited: preview.state.map_or(false, |state| state == RoomState::Invited),
50-
is_public: preview.join_rule == JoinRule::Public,
51-
can_knock: matches!(preview.join_rule, JoinRule::KnockRestricted(_) | JoinRule::Knock),
46+
is_public: preview.join_rule == SpaceRoomJoinRule::Public,
47+
can_knock: matches!(
48+
preview.join_rule,
49+
SpaceRoomJoinRule::KnockRestricted | SpaceRoomJoinRule::Knock
50+
),
5251
}
5352
}
5453
}

Diff for: crates/matrix-sdk/src/room_preview.rs

+82-8
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use ruma::{
2323
api::client::{membership::joined_members, state::get_state_events},
2424
events::room::{history_visibility::HistoryVisibility, join_rules::JoinRule},
2525
room::RoomType,
26+
space::SpaceRoomJoinRule,
2627
OwnedMxcUri, OwnedRoomAliasId, RoomId,
2728
};
2829
use tokio::try_join;
@@ -52,10 +53,11 @@ pub struct RoomPreview {
5253
pub room_type: Option<RoomType>,
5354

5455
/// What's the join rule for this room?
55-
pub join_rule: JoinRule,
56+
pub join_rule: SpaceRoomJoinRule,
5657

57-
/// What's the history visibility for this room?
58-
pub history_visibility: HistoryVisibility,
58+
/// Is the room world-readable (i.e. is its history_visibility set to
59+
/// world_readable)?
60+
pub is_world_readable: bool,
5961

6062
/// Has the current user been invited/joined/left this room?
6163
///
@@ -79,8 +81,20 @@ impl RoomPreview {
7981
topic: room_info.topic().map(ToOwned::to_owned),
8082
avatar_url: room_info.avatar_url().map(ToOwned::to_owned),
8183
room_type: room_info.room_type().cloned(),
82-
join_rule: room_info.join_rule().clone(),
83-
history_visibility: room_info.history_visibility().clone(),
84+
join_rule: match room_info.join_rule() {
85+
JoinRule::Invite => SpaceRoomJoinRule::Invite,
86+
JoinRule::Knock => SpaceRoomJoinRule::Knock,
87+
JoinRule::Private => SpaceRoomJoinRule::Private,
88+
JoinRule::Restricted(_) => SpaceRoomJoinRule::Restricted,
89+
JoinRule::KnockRestricted(_) => SpaceRoomJoinRule::KnockRestricted,
90+
JoinRule::Public => SpaceRoomJoinRule::Public,
91+
_ => {
92+
// The JoinRule enum is non-exhaustive. Let's do a white lie and pretend it's
93+
// private (a cautious choice).
94+
SpaceRoomJoinRule::Private
95+
}
96+
},
97+
is_world_readable: *room_info.history_visibility() == HistoryVisibility::WorldReadable,
8498

8599
num_joined_members,
86100
state,
@@ -95,14 +109,72 @@ impl RoomPreview {
95109

96110
#[instrument(skip(client))]
97111
pub(crate) async fn from_unknown(client: &Client, room_id: &RoomId) -> crate::Result<Self> {
98-
// TODO: (optimization) Use the room summary endpoint, if available, as
99-
// described in https://github.com/deepbluev7/matrix-doc/blob/room-summaries/proposals/3266-room-summary.md
112+
// Use the room summary endpoint, if available, as described in
113+
// https://github.com/deepbluev7/matrix-doc/blob/room-summaries/proposals/3266-room-summary.md
114+
match Self::from_room_summary(client, room_id).await {
115+
Ok(res) => return Ok(res),
116+
Err(err) => {
117+
warn!("error when previewing room from the room summary endpoint: {err}");
118+
}
119+
}
100120

101121
// TODO: (optimization) Use the room search directory, if available:
102122
// - if the room directory visibility is public,
103123
// - then use a public room filter set to this room id
104124

105125
// Resort to using the room state endpoint, as well as the joined members one.
126+
Self::from_state_events(client, room_id).await
127+
}
128+
129+
/// Get a [`RoomPreview`] using MSC3266, if available on the remote server.
130+
///
131+
/// Will fail with a 404 if the API is not available.
132+
///
133+
/// This method is exposed for testing purposes; clients should prefer
134+
/// `Client::get_room_preview` in general over this.
135+
pub async fn from_room_summary(client: &Client, room_id: &RoomId) -> crate::Result<Self> {
136+
let request = ruma::api::client::room::get_summary::msc3266::Request::new(
137+
room_id.to_owned().into(),
138+
Vec::new(),
139+
);
140+
141+
let response = client.send(request, None).await?;
142+
143+
// The server returns a `Left` room state for rooms the user has not joined. Be
144+
// more precise than that, and set it to `None` if we haven't joined
145+
// that room.
146+
let state = if client.get_room(room_id).is_none() {
147+
None
148+
} else {
149+
response.membership.map(|membership| RoomState::from(&membership))
150+
};
151+
152+
Ok(RoomPreview {
153+
canonical_alias: response.canonical_alias,
154+
name: response.name,
155+
topic: response.topic,
156+
avatar_url: response.avatar_url,
157+
num_joined_members: response.num_joined_members,
158+
room_type: response.room_type,
159+
join_rule: response.join_rule,
160+
is_world_readable: response.world_readable,
161+
state,
162+
})
163+
}
164+
165+
/// Get a [`RoomPreview`] using the room state endpoint.
166+
///
167+
/// This is always available on a remote server, but will only work if one
168+
/// of these two conditions is true:
169+
///
170+
/// - the user has joined the room at some point (i.e. they're still joined
171+
/// or they've joined
172+
/// it and left it later).
173+
/// - the room has an history visibility set to world-readable.
174+
///
175+
/// This method is exposed for testing purposes; clients should prefer
176+
/// `Client::get_room_preview` in general over this.
177+
pub async fn from_state_events(client: &Client, room_id: &RoomId) -> crate::Result<Self> {
106178
let state_request = get_state_events::v3::Request::new(room_id.to_owned());
107179
let joined_members_request = joined_members::v3::Request::new(room_id.to_owned());
108180

@@ -128,6 +200,8 @@ impl RoomPreview {
128200
room_info.handle_state_event(&ev.into());
129201
}
130202

131-
Ok(Self::from_room_info(room_info, num_joined_members, None))
203+
let state = client.get_room(room_id).map(|room| room.state());
204+
205+
Ok(Self::from_room_info(room_info, num_joined_members, state))
132206
}
133207
}

Diff for: testing/matrix-sdk-integration-testing/assets/ci-start.sh

+3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ rc_invites:
5656
per_issuer:
5757
per_second: 1000
5858
burst_count: 1000
59+
60+
experimental_features:
61+
msc3266_enabled: true
5962
""" >> /data/homeserver.yaml
6063

6164
echo " ====== Starting server with: ====== "

Diff for: testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs

+78-38
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use futures_util::{pin_mut, FutureExt, StreamExt as _};
1010
use matrix_sdk::{
1111
bytes::Bytes,
1212
config::SyncSettings,
13+
room_preview::RoomPreview,
1314
ruma::{
1415
api::client::{
1516
receipt::create_receipt::v3::ReceiptType,
@@ -29,6 +30,8 @@ use matrix_sdk::{
2930
AnySyncMessageLikeEvent, InitialStateEvent, Mentions, StateEventType,
3031
},
3132
mxc_uri,
33+
space::SpaceRoomJoinRule,
34+
RoomId,
3235
},
3336
Client, RoomInfo, RoomListEntry, RoomMemberships, RoomState, SlidingSyncList, SlidingSyncMode,
3437
};
@@ -1082,14 +1085,24 @@ async fn test_room_preview() -> Result<()> {
10821085
InitialStateEvent::new(RoomHistoryVisibilityEventContent::new(HistoryVisibility::WorldReadable)).to_raw_any(),
10831086
InitialStateEvent::new(RoomJoinRulesEventContent::new(JoinRule::Invite)).to_raw_any(),
10841087
],
1085-
//TODO: this doesn't allow preview => could be tested!
1086-
//preset: Some(RoomPreset::PrivateChat),
10871088
}))
10881089
.await?;
10891090

10901091
room.set_avatar_url(mxc_uri!("mxc://localhost/alice"), None).await?;
10911092

1093+
// Alice creates another room, and still doesn't invite Bob.
1094+
let private_room = alice
1095+
.create_room(assign!(CreateRoomRequest::new(), {
1096+
name: Some("Alice's Room 2".to_owned()),
1097+
initial_state: vec![
1098+
InitialStateEvent::new(RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared)).to_raw_any(),
1099+
InitialStateEvent::new(RoomJoinRulesEventContent::new(JoinRule::Public)).to_raw_any(),
1100+
],
1101+
}))
1102+
.await?;
1103+
10921104
let room_id = room.room_id();
1105+
let private_room_id = private_room.room_id();
10931106

10941107
// Wait for Alice's stream to stabilize (stop updating when we haven't received
10951108
// successful updates for more than 2 seconds).
@@ -1104,49 +1117,76 @@ async fn test_room_preview() -> Result<()> {
11041117
}
11051118
}
11061119

1107-
let preview = alice.get_room_preview(room_id).await?;
1108-
assert_eq!(preview.canonical_alias.unwrap().alias(), room_alias);
1109-
assert_eq!(preview.name.unwrap(), "Alice's Room");
1110-
assert_eq!(preview.topic.unwrap(), "Discussing Alice's Topic");
1111-
assert_eq!(preview.avatar_url.unwrap(), mxc_uri!("mxc://localhost/alice"));
1112-
assert_eq!(preview.num_joined_members, 1);
1113-
assert!(preview.room_type.is_none());
1114-
// Because of the preset:
1115-
assert_eq!(preview.join_rule, JoinRule::Invite);
1116-
assert_eq!(preview.history_visibility, HistoryVisibility::WorldReadable);
1117-
assert_eq!(preview.state, Some(RoomState::Joined));
1120+
get_room_preview_with_room_state(&alice, &bob, &room_alias, room_id, private_room_id).await;
1121+
get_room_preview_with_room_summary(&alice, &bob, &room_alias, room_id, private_room_id).await;
11181122

1119-
// Bob definitely doesn't know about the room, but they can get a preview of the
1120-
// room too.
1121-
let preview = bob.get_room_preview(room_id).await?;
1123+
{
1124+
// Dummy test for `Client::get_room_preview` which may call one or the other
1125+
// methods.
1126+
let preview = alice.get_room_preview(room_id).await.unwrap();
1127+
assert_room_preview(&preview, &room_alias);
1128+
assert_eq!(preview.state, Some(RoomState::Joined));
1129+
}
11221130

1123-
assert_eq!(preview.canonical_alias.unwrap().alias(), room_alias);
1124-
assert_eq!(preview.name.unwrap(), "Alice's Room");
1125-
assert_eq!(preview.topic.unwrap(), "Discussing Alice's Topic");
1126-
assert_eq!(preview.avatar_url.unwrap(), mxc_uri!("mxc://localhost/alice"));
1131+
Ok(())
1132+
}
1133+
1134+
fn assert_room_preview(preview: &RoomPreview, room_alias: &str) {
1135+
assert_eq!(preview.canonical_alias.as_ref().unwrap().alias(), room_alias);
1136+
assert_eq!(preview.name.as_ref().unwrap(), "Alice's Room");
1137+
assert_eq!(preview.topic.as_ref().unwrap(), "Discussing Alice's Topic");
1138+
assert_eq!(preview.avatar_url.as_ref().unwrap(), mxc_uri!("mxc://localhost/alice"));
11271139
assert_eq!(preview.num_joined_members, 1);
11281140
assert!(preview.room_type.is_none());
1129-
assert_eq!(preview.join_rule, JoinRule::Invite);
1130-
assert_eq!(preview.history_visibility, HistoryVisibility::WorldReadable);
1141+
assert_eq!(preview.join_rule, SpaceRoomJoinRule::Invite);
1142+
assert!(preview.is_world_readable);
1143+
}
11311144

1132-
// Only difference with Alice's room is here: since Bob hasn't joined the room,
1133-
// they don't have any associated room state.
1134-
assert_eq!(preview.state, None);
1145+
async fn get_room_preview_with_room_state(
1146+
alice: &Client,
1147+
bob: &Client,
1148+
room_alias: &str,
1149+
room_id: &RoomId,
1150+
public_no_history_room_id: &RoomId,
1151+
) {
1152+
// Alice has joined the room, so they get the full details.
1153+
let preview = RoomPreview::from_state_events(alice, room_id).await.unwrap();
1154+
assert_room_preview(&preview, room_alias);
1155+
assert_eq!(preview.state, Some(RoomState::Joined));
11351156

1136-
// Now Alice creates another room, with a private preset, and still doesn't
1137-
// invite Bob.
1138-
let room = alice
1139-
.create_room(assign!(CreateRoomRequest::new(), {
1140-
initial_state: vec![
1141-
InitialStateEvent::new(RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared)).to_raw_any(),
1142-
InitialStateEvent::new(RoomJoinRulesEventContent::new(JoinRule::Invite)).to_raw_any(),
1143-
],
1144-
}))
1145-
.await?;
1157+
// Bob definitely doesn't know about the room, but they can get a preview of the
1158+
// room too.
1159+
let preview = RoomPreview::from_state_events(bob, room_id).await.unwrap();
1160+
assert_room_preview(&preview, room_alias);
1161+
assert!(preview.state.is_none());
11461162

1147-
// So Bob can't preview it.
1148-
let preview_result = bob.get_room_preview(room.room_id()).await;
1163+
// Bob can't preview the second room, because its history visibility is neither
1164+
// world-readable, nor have they joined the room before.
1165+
let preview_result = RoomPreview::from_state_events(bob, public_no_history_room_id).await;
11491166
assert_eq!(preview_result.unwrap_err().as_client_api_error().unwrap().status_code, 403);
1167+
}
11501168

1151-
Ok(())
1169+
async fn get_room_preview_with_room_summary(
1170+
alice: &Client,
1171+
bob: &Client,
1172+
room_alias: &str,
1173+
room_id: &RoomId,
1174+
public_no_history_room_id: &RoomId,
1175+
) {
1176+
// Alice has joined the room, so they get the full details.
1177+
let preview = RoomPreview::from_room_summary(alice, room_id).await.unwrap();
1178+
assert_room_preview(&preview, room_alias);
1179+
assert_eq!(preview.state, Some(RoomState::Joined));
1180+
1181+
// Bob definitely doesn't know about the room, but they can get a preview of the
1182+
// room too.
1183+
let preview = RoomPreview::from_room_summary(bob, room_id).await.unwrap();
1184+
assert_room_preview(&preview, room_alias);
1185+
assert!(preview.state.is_none());
1186+
1187+
// Bob can preview the second room with the room summary (because its join rule
1188+
// is set to public, or because Alice is a member of that room).
1189+
let preview = RoomPreview::from_room_summary(bob, public_no_history_room_id).await.unwrap();
1190+
assert_eq!(preview.name.unwrap(), "Alice's Room 2");
1191+
assert!(preview.state.is_none());
11521192
}

0 commit comments

Comments
 (0)