From f0e0194ff20f40d8871af98b25a9c95ebac9daee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sun, 26 Jun 2022 13:32:44 +0200 Subject: [PATCH] feat(sdk): Add method to get a room permalink Include routing for room IDs --- crates/matrix-sdk/src/client/mod.rs | 321 ++++++++++++++++++++++++++- crates/matrix-sdk/src/room/common.rs | 65 +++++- 2 files changed, 383 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index b905252e0..c14303a9f 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2216,7 +2216,7 @@ pub(crate) mod tests { }, mxc_uri, room_id, thirdparty, uint, user_id, TransactionId, UserId, }; - use serde_json::json; + use serde_json::{json, Value as JsonValue}; use url::Url; use super::{Client, ClientBuilder, Session}; @@ -3960,4 +3960,323 @@ pub(crate) mod tests { mocked_messages.assert(); mocked_messages_2.assert(); } + + #[async_test] + async fn room_permalink() { + fn sync_response(index: u8, room_timeline_events: &[JsonValue]) -> JsonValue { + json!({ + "device_one_time_keys_count": {}, + "next_batch": format!("s526_47314_0_7_1_1_1_11444_{}", index + 1), + "device_lists": { + "changed": [], + "left": [] + }, + "account_data": { + "events": [] + }, + "rooms": { + "invite": {}, + "join": { + "!test_room:127.0.0.1": { + "summary": {}, + "account_data": { + "events": [] + }, + "ephemeral": { + "events": [] + }, + "state": { + "events": [] + }, + "timeline": { + "events": room_timeline_events, + "limited": false, + "prev_batch": format!("s526_47314_0_7_1_1_1_11444_{}", index - 1), + }, + "unread_notifications": { + "highlight_count": 0, + "notification_count": 0, + } + } + }, + "leave": {} + }, + "to_device": { + "events": [] + }, + "presence": { + "events": [] + } + }) + } + + fn room_member_events(nb: usize, server: &str) -> Vec { + let mut events = Vec::with_capacity(nb); + for i in 0..nb { + let id = format!("${server}{i}"); + let user = format!("@user{i}:{server}"); + events.push(json!({ + "content": { + "membership": "join", + }, + "event_id": id, + "origin_server_ts": 151800140, + "sender": user, + "state_key": user, + "type": "m.room.member", + })) + } + events + } + + let client = logged_in_client().await; + let sync_settings = SyncSettings::new(); + + // Without elligible server + let mut sync_index = 1; + let res = sync_response( + sync_index, + &[ + json!({ + "content": { + "creator": "@creator:127.0.0.1", + "room_version": "6", + }, + "event_id": "$151957878228ekrDs", + "origin_server_ts": 15195787, + "sender": "@creator:localhost", + "state_key": "", + "type": "m.room.create", + }), + json!({ + "content": { + "membership": "join", + }, + "event_id": "$151800140517rfvjc", + "origin_server_ts": 151800140, + "sender": "@creator:127.0.0.1", + "state_key": "@creator:127.0.0.1", + "type": "m.room.member", + }), + ], + ); + let _sync = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) + .with_status(200) + .with_body(res.to_string()) + .match_header("authorization", "Bearer 1234") + .create(); + client.sync_once(sync_settings.clone()).await.unwrap(); + let room = client.get_room(room_id!("!test_room:127.0.0.1")).unwrap(); + + assert_eq!(room.permalink().await.unwrap(), "https://matrix.to/#/%21test_room%3A127.0.0.1"); + + // With a single elligible server + sync_index += 1; + let res = sync_response( + sync_index, + &[json!({ + "content": { + "membership": "join", + }, + "event_id": "$151800140517rfvjc", + "origin_server_ts": 151800140, + "sender": "@example:localhost", + "state_key": "@example:localhost", + "type": "m.room.member", + })], + ); + let _sync = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) + .with_status(200) + .with_body(res.to_string()) + .match_header("authorization", "Bearer 1234") + .create(); + client.sync_once(sync_settings.clone()).await.unwrap(); + + assert_eq!( + room.permalink().await.unwrap(), + "https://matrix.to/#/%21test_room%3A127.0.0.1?via=localhost" + ); + + // With two elligible servers + sync_index += 1; + let res = sync_response(sync_index, &room_member_events(15, "notarealhs")); + let _sync = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) + .with_status(200) + .with_body(res.to_string()) + .match_header("authorization", "Bearer 1234") + .create(); + client.sync_once(sync_settings.clone()).await.unwrap(); + + assert_eq!( + room.permalink().await.unwrap(), + "https://matrix.to/#/%21test_room%3A127.0.0.1?via=notarealhs&via=localhost" + ); + + // With three elligible servers + sync_index += 1; + let res = sync_response(sync_index, &room_member_events(5, "mymatrix")); + let _sync = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) + .with_status(200) + .with_body(res.to_string()) + .match_header("authorization", "Bearer 1234") + .create(); + client.sync_once(sync_settings.clone()).await.unwrap(); + + assert_eq!( + room.permalink().await.unwrap(), + "https://matrix.to/#/%21test_room%3A127.0.0.1?via=notarealhs&via=mymatrix&via=localhost" + ); + + // With four elligible servers + sync_index += 1; + let res = sync_response(sync_index, &room_member_events(10, "yourmatrix")); + let _sync = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) + .with_status(200) + .with_body(res.to_string()) + .match_header("authorization", "Bearer 1234") + .create(); + client.sync_once(sync_settings.clone()).await.unwrap(); + + assert_eq!( + room.permalink().await.unwrap(), + "https://matrix.to/#/%21test_room%3A127.0.0.1?via=notarealhs&via=yourmatrix&via=mymatrix" + ); + + // With power levels + sync_index += 1; + let res = sync_response( + sync_index, + &[json!({ + "content": { + "users": { + "@example:localhost": 50, + }, + }, + "event_id": "$15139375512JaHAW", + "origin_server_ts": 151393755, + "sender": "@creator:127.0.0.1", + "state_key": "", + "type": "m.room.power_levels", + })], + ); + let _sync = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) + .with_status(200) + .with_body(res.to_string()) + .match_header("authorization", "Bearer 1234") + .create(); + client.sync_once(sync_settings.clone()).await.unwrap(); + + assert_eq!( + room.permalink().await.unwrap(), + "https://matrix.to/#/%21test_room%3A127.0.0.1?via=localhost&via=notarealhs&via=yourmatrix" + ); + + // With higher power levels + sync_index += 1; + let res = sync_response( + sync_index, + &[json!({ + "content": { + "users": { + "@example:localhost": 50, + "@user0:mymatrix": 70, + }, + }, + "event_id": "$15139375512JaHAZ", + "origin_server_ts": 151393755, + "sender": "@creator:127.0.0.1", + "state_key": "", + "type": "m.room.power_levels", + })], + ); + let _sync = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) + .with_status(200) + .with_body(res.to_string()) + .match_header("authorization", "Bearer 1234") + .create(); + client.sync_once(sync_settings.clone()).await.unwrap(); + + assert_eq!( + room.permalink().await.unwrap(), + "https://matrix.to/#/%21test_room%3A127.0.0.1?via=mymatrix&via=notarealhs&via=yourmatrix" + ); + + // With server ACLs + sync_index += 1; + let res = sync_response( + sync_index, + &[json!({ + "content": { + "allow": ["*"], + "allow_ip_literals": true, + "deny": ["notarealhs"], + }, + "event_id": "$143273582443PhrSn", + "origin_server_ts": 1432735824, + "sender": "@creator:127.0.0.1", + "state_key": "", + "type": "m.room.server_acl", + })], + ); + let _sync = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) + .with_status(200) + .with_body(res.to_string()) + .match_header("authorization", "Bearer 1234") + .create(); + client.sync_once(sync_settings.clone()).await.unwrap(); + + assert_eq!( + room.permalink().await.unwrap(), + "https://matrix.to/#/%21test_room%3A127.0.0.1?via=mymatrix&via=yourmatrix&via=localhost" + ); + + // With an alternative alias + sync_index += 1; + let res = sync_response( + sync_index, + &[json!({ + "content": { + "alt_aliases": ["#alias:localhost"], + }, + "event_id": "$15139375513VdeRF", + "origin_server_ts": 151393755, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.canonical_alias", + })], + ); + let _sync = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) + .with_status(200) + .with_body(res.to_string()) + .match_header("authorization", "Bearer 1234") + .create(); + client.sync_once(sync_settings.clone()).await.unwrap(); + + assert_eq!(room.permalink().await.unwrap(), "https://matrix.to/#/%23alias%3Alocalhost"); + + // With a canonical alias + sync_index += 1; + let res = sync_response( + sync_index, + &[json!({ + "content": { + "alias": "#canonical:localhost", + "alt_aliases": ["#alias:localhost"], + }, + "event_id": "$15139375513VdeRF", + "origin_server_ts": 151393755, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.canonical_alias", + })], + ); + let _sync = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) + .with_status(200) + .with_body(res.to_string()) + .match_header("authorization", "Bearer 1234") + .create(); + client.sync_once(sync_settings).await.unwrap(); + + assert_eq!(room.permalink().await.unwrap(), "https://matrix.to/#/%23canonical%3Alocalhost"); + } } diff --git a/crates/matrix-sdk/src/room/common.rs b/crates/matrix-sdk/src/room/common.rs index 31c56a3a3..d375710ff 100644 --- a/crates/matrix-sdk/src/room/common.rs +++ b/crates/matrix-sdk/src/room/common.rs @@ -28,7 +28,10 @@ use ruma::{ assign, events::{ direct::DirectEvent, - room::{history_visibility::HistoryVisibility, MediaSource}, + room::{ + history_visibility::HistoryVisibility, server_acl::RoomServerAclEventContent, + MediaSource, + }, tag::{TagInfo, TagName}, AnyRoomAccountDataEvent, AnyStateEvent, AnySyncStateEvent, GlobalAccountDataEventType, RedactContent, RedactedEventContent, RoomAccountDataEvent, RoomAccountDataEventContent, @@ -36,7 +39,7 @@ use ruma::{ SyncStateEvent, }, serde::Raw, - uint, EventId, RoomId, UInt, UserId, + uint, EventId, RoomId, ServerName, UInt, UserId, }; use crate::{ @@ -931,6 +934,64 @@ impl Common { Err(Error::NoOlmMachine) } } + + /// Get a permalink to this room. + /// + /// If this room has an alias, we use it. Otherwise, we try to use the + /// synced members in the room for [routing] the room ID. + /// + /// This currently returns a `matrix.to` URI but the format of the permalink + /// might change without notice so don't rely on it. + /// + /// [routing]: https://spec.matrix.org/v1.3/appendices/#routing + pub async fn permalink(&self) -> Result { + if let Some(alias) = self.canonical_alias().or_else(|| self.alt_aliases().pop()) { + return Ok(alias.matrix_to_uri().to_string()); + } + + let acl_ev = self + .get_state_event_static::("") + .await? + .and_then(|ev| ev.deserialize().ok()); + let acl = acl_ev.as_ref().and_then(|ev| ev.as_original()).map(|ev| &ev.content); + + // Filter out server names that: + // - Are blocked due to server ACLs + // - Are IP addresses + let members: Vec<_> = self + .joined_members_no_sync() + .await? + .into_iter() + .filter(|member| { + let server = member.user_id().server_name(); + acl.filter(|acl| !acl.is_allowed(server)).is_none() && !server.is_ip_literal() + }) + .collect(); + + // Get the server of the highest power level user in the room, provided + // they are at least power level 50. + let max = members + .iter() + .max_by_key(|member| member.power_level()) + .filter(|max| max.power_level() >= 50) + .map(|member| member.user_id().server_name()); + + // Sort the servers by population. + let servers = members + .iter() + .map(|member| member.user_id().server_name()) + .filter(|server| max.filter(|max| max == server).is_none()) + .fold(BTreeMap::<&ServerName, u32>::new(), |mut servers, server| { + *servers.entry(server).or_default() += 1; + servers + }); + let mut servers: Vec<_> = servers.into_iter().collect(); + servers.sort_unstable_by(|(_, count_a), (_, count_b)| count_b.cmp(count_a)); + + let via = max.into_iter().chain(servers.into_iter().map(|(name, _)| name)).take(3); + + Ok(self.room_id().matrix_to_uri(via).to_string()) + } } /// Options for [`messages`][Common::messages].