Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

room preview: allow passing through a list of servers to discover a room with MSC3266 #3407

Merged
merged 2 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 39 additions & 16 deletions bindings/matrix-sdk-ffi/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ use ruma::{
room::power_levels::RoomPowerLevelsEventContent, GlobalAccountDataEventType,
},
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
OwnedServerName, RoomAliasId, RoomOrAliasId,
OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
Expand Down Expand Up @@ -771,25 +771,48 @@ impl Client {
Ok(response.into())
}

/// Get the preview of a room, to interact with it.
pub async fn get_room_preview(
/// Given a room id, get the preview of a room, to interact with it.
///
/// The list of `via_servers` must be a list of servers that know
/// about the room and can resolve it, and that may appear as a `via`
/// parameter in e.g. a permalink URL. This list can be empty.
pub async fn get_room_preview_from_room_id(
&self,
room_id_or_alias: String,
room_id: String,
via_servers: Vec<String>,
) -> Result<RoomPreview, ClientError> {
let room_id = if let Ok(parsed_room_id) = RoomId::parse(&room_id_or_alias) {
parsed_room_id
} else {
// Try to resolve it as an alias.
let Ok(room_alias) = RoomAliasId::parse(&room_id_or_alias) else {
return Err(anyhow!("room_id_or_alias is neither a room id or an alias").into());
};
let response = self.inner.resolve_room_alias(&room_alias).await?;
response.room_id
};
let room_id = RoomId::parse(&room_id).context("room_id is not a valid room id")?;

let via_servers = via_servers
.into_iter()
.map(ServerName::parse)
.collect::<Result<Vec<_>, _>>()
.context("at least one `via` server name is invalid")?;

// The `into()` call below doesn't work if I do `(&room_id).into()`, so I let
// 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?;

Ok(RoomPreview::from_sdk(sdk_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<RoomPreview, ClientError> {
let room_alias =
RoomAliasId::parse(&room_alias).context("room_alias is not a valid room alias")?;

// The `into()` call below doesn't work if I do `(&room_id).into()`, so I let
// rustc win that one fight.
let room_alias: &RoomAliasId = &room_alias;

let sdk_room_preview = self.inner.get_room_preview(&room_id).await?;
let sdk_room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?;

Ok(RoomPreview::from_sdk(room_id, sdk_room_preview))
Ok(RoomPreview::from_sdk(sdk_room_preview))
}
}

Expand Down
6 changes: 3 additions & 3 deletions bindings/matrix-sdk-ffi/src/room_preview.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, RoomState};
use ruma::{space::SpaceRoomJoinRule, OwnedRoomId};
use ruma::space::SpaceRoomJoinRule;

/// The preview of a room, be it invited/joined/left, or not.
#[derive(uniffi::Record)]
Expand Down Expand Up @@ -31,9 +31,9 @@ pub struct RoomPreview {
}

impl RoomPreview {
pub(crate) fn from_sdk(room_id: OwnedRoomId, preview: SdkRoomPreview) -> Self {
pub(crate) fn from_sdk(preview: SdkRoomPreview) -> Self {
Self {
room_id: room_id.to_string(),
room_id: preview.room_id.to_string(),
canonical_alias: preview.canonical_alias.map(|alias| alias.to_string()),
name: preview.name,
topic: preview.topic,
Expand Down
16 changes: 13 additions & 3 deletions crates/matrix-sdk/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -944,11 +944,21 @@ impl Client {

/// Gets the preview of a room, whether the current user knows it (because
/// they've joined/left/been invited to it) or not.
pub async fn get_room_preview(&self, room_id: &RoomId) -> Result<RoomPreview> {
if let Some(room) = self.get_room(room_id) {
pub async fn get_room_preview(
&self,
room_or_alias_id: &RoomOrAliasId,
via: Vec<OwnedServerName>,
) -> Result<RoomPreview> {
let room_id = match <&RoomId>::try_from(room_or_alias_id) {
Ok(room_id) => room_id.to_owned(),
Err(alias) => self.resolve_room_alias(alias).await?.room_id,
};

if let Some(room) = self.get_room(&room_id) {
return Ok(RoomPreview::from_known(&room));
}
RoomPreview::from_unknown(self, room_id).await

RoomPreview::from_unknown(self, room_id, room_or_alias_id, via).await
}

/// Resolve a room alias to a room id and a list of servers which know
Expand Down
34 changes: 26 additions & 8 deletions crates/matrix-sdk/src/room_preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use ruma::{
events::room::{history_visibility::HistoryVisibility, join_rules::JoinRule},
room::RoomType,
space::SpaceRoomJoinRule,
OwnedMxcUri, OwnedRoomAliasId, RoomId,
OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedServerName, RoomId, RoomOrAliasId,
};
use tokio::try_join;
use tracing::{instrument, warn};
Expand All @@ -34,6 +34,12 @@ use crate::{Client, Room};
/// The preview of a room, be it invited/joined/left, or not.
#[derive(Debug)]
pub struct RoomPreview {
/// The actual room id for this room.
///
/// Remember the room preview can be fetched from a room alias id, so we
/// might not know ahead of time what the room id is.
pub room_id: OwnedRoomId,

/// The canonical alias for the room.
pub canonical_alias: Option<OwnedRoomAliasId>,

Expand Down Expand Up @@ -76,6 +82,7 @@ impl RoomPreview {
state: Option<RoomState>,
) -> Self {
RoomPreview {
room_id: room_info.room_id().to_owned(),
canonical_alias: room_info.canonical_alias().map(ToOwned::to_owned),
name: room_info.name().map(ToOwned::to_owned),
topic: room_info.topic().map(ToOwned::to_owned),
Expand Down Expand Up @@ -108,10 +115,15 @@ impl RoomPreview {
}

#[instrument(skip(client))]
pub(crate) async fn from_unknown(client: &Client, room_id: &RoomId) -> crate::Result<Self> {
pub(crate) async fn from_unknown(
client: &Client,
room_id: OwnedRoomId,
room_or_alias_id: &RoomOrAliasId,
via: Vec<OwnedServerName>,
) -> crate::Result<Self> {
// 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 {
match Self::from_room_summary(client, room_id.clone(), room_or_alias_id, via).await {
Ok(res) => return Ok(res),
Err(err) => {
warn!("error when previewing room from the room summary endpoint: {err}");
Expand All @@ -123,7 +135,7 @@ impl RoomPreview {
// - 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
Self::from_state_events(client, &room_id).await
}

/// Get a [`RoomPreview`] using MSC3266, if available on the remote server.
Expand All @@ -132,24 +144,30 @@ impl RoomPreview {
///
/// 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> {
pub async fn from_room_summary(
client: &Client,
room_id: OwnedRoomId,
room_or_alias_id: &RoomOrAliasId,
via: Vec<OwnedServerName>,
) -> crate::Result<Self> {
let request = ruma::api::client::room::get_summary::msc3266::Request::new(
room_id.to_owned().into(),
Vec::new(),
room_or_alias_id.to_owned(),
via,
);

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() {
let state = if client.get_room(&room_id).is_none() {
None
} else {
response.membership.map(|membership| RoomState::from(&membership))
};

Ok(RoomPreview {
room_id,
canonical_alias: response.canonical_alias,
name: response.name,
topic: response.topic,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1118,7 +1118,7 @@ async fn test_room_preview() -> Result<()> {
{
// 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();
let preview = alice.get_room_preview(room_id.into(), Vec::new()).await.unwrap();
assert_room_preview(&preview, &room_alias);
assert_eq!(preview.state, Some(RoomState::Joined));
}
Expand Down Expand Up @@ -1169,20 +1169,47 @@ async fn get_room_preview_with_room_summary(
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();
let preview =
RoomPreview::from_room_summary(alice, room_id.to_owned(), room_id.into(), Vec::new())
.await
.unwrap();

assert_room_preview(&preview, room_alias);
assert_eq!(preview.state, Some(RoomState::Joined));

// The preview also works when using the room alias parameter.
let full_alias = format!("#{room_alias}:{}", alice.user_id().unwrap().server_name());
let preview = RoomPreview::from_room_summary(
alice,
room_id.to_owned(),
<_>::try_from(full_alias.as_str()).unwrap(),
Vec::new(),
)
.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();
let preview =
RoomPreview::from_room_summary(bob, room_id.to_owned(), room_id.into(), Vec::new())
.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();
let preview = RoomPreview::from_room_summary(
bob,
public_no_history_room_id.to_owned(),
public_no_history_room_id.into(),
Vec::new(),
)
.await
.unwrap();
assert_eq!(preview.name.unwrap(), "Alice's Room 2");
assert!(preview.state.is_none());
}