From ab6d039369ca24af4228807c3b193c193978bce2 Mon Sep 17 00:00:00 2001 From: torrybr <16907963+torrybr@users.noreply.github.com> Date: Fri, 26 Jul 2024 22:57:16 -0400 Subject: [PATCH 1/3] sdk: basic support for sending live location beacons --- crates/matrix-sdk/src/error.rs | 5 + crates/matrix-sdk/src/room/mod.rs | 75 +++++++++++---- .../tests/integration/room/joined.rs | 96 ++++++++++++++++++- 3 files changed, 153 insertions(+), 23 deletions(-) diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index bb3a7c45c23..bff3a06e4bf 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -521,9 +521,14 @@ pub enum BeaconError { #[error("Must join the room to access beacon information.")] Stripped, + // The beacon event could not be deserialized. #[error("Deserialization error: {0}")] Deserialization(#[from] serde_json::Error), + // The beacon event is expired. + #[error("The beacon event has expired.")] + NotLive, + // Allow for other errors to be wrapped. #[error("Other error: {0}")] Other(Box), diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 4de645c6197..151b346f381 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -52,6 +52,7 @@ use ruma::{ }, assign, events::{ + beacon::BeaconEventContent, beacon_info::BeaconInfoEventContent, call::notify::{ApplicationType, CallNotifyEventContent, NotifyType}, direct::DirectEventContent, @@ -72,10 +73,10 @@ use ruma::{ tag::{TagInfo, TagName}, typing::SyncTypingEvent, AnyRoomAccountDataEvent, AnyRoomAccountDataEventContent, AnyTimelineEvent, EmptyStateKey, - Mentions, MessageLikeEventContent, MessageLikeEventType, RedactContent, - RedactedStateEventContent, RoomAccountDataEvent, RoomAccountDataEventContent, - RoomAccountDataEventType, StateEventContent, StateEventType, StaticEventContent, - StaticStateEventContent, SyncStateEvent, + Mentions, MessageLikeEventContent, MessageLikeEventType, OriginalSyncStateEvent, + RedactContent, RedactedStateEventContent, RoomAccountDataEvent, + RoomAccountDataEventContent, RoomAccountDataEventType, StateEventContent, StateEventType, + StaticEventContent, StaticStateEventContent, SyncStateEvent, }, push::{Action, PushConditionRoomCtx}, serde::Raw, @@ -2753,6 +2754,27 @@ impl Room { Ok(()) } + /// Get the beacon information event in the room for the current user. + /// + /// # Errors + /// + /// Returns an error if the event is redacted, stripped, not found or could + /// not be deserialized. + async fn get_user_beacon_info( + &self, + ) -> Result, BeaconError> { + let raw_event = self + .get_state_event_static_for_key::(self.own_user_id()) + .await? + .ok_or(BeaconError::NotFound)?; + + match raw_event.deserialize()? { + SyncOrStrippedState::Sync(SyncStateEvent::Original(beacon_info)) => Ok(beacon_info), + SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_)) => Err(BeaconError::Redacted), + SyncOrStrippedState::Stripped(_) => Err(BeaconError::Stripped), + } + } + /// Start sharing live location in the room. /// /// # Arguments @@ -2795,24 +2817,35 @@ impl Room { ) -> Result { self.ensure_room_joined()?; - if let Some(raw_event) = self - .get_state_event_static_for_key::(self.own_user_id()) - .await? - { - match raw_event.deserialize() { - Ok(SyncOrStrippedState::Sync(SyncStateEvent::Original(beacon_info))) => { - let mut content = beacon_info.content.clone(); - content.stop(); - Ok(self.send_state_event_for_key(self.own_user_id(), content).await?) - } - Ok(SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_))) => { - Err(BeaconError::Redacted) - } - Ok(SyncOrStrippedState::Stripped(_)) => Err(BeaconError::Stripped), - Err(e) => Err(BeaconError::Deserialization(e)), - } + let mut beacon_info_event = self.get_user_beacon_info().await?; + beacon_info_event.content.stop(); + Ok(self.send_state_event_for_key(self.own_user_id(), beacon_info_event.content).await?) + } + + /// Send a location beacon event in the current room. + /// + /// # Arguments + /// + /// * `geo_uri` - The geo URI of the location beacon. + /// + /// # Errors + /// + /// Returns an error if the room is not joined, if the beacon information + /// is redacted or stripped, if the location share is no longer live, + /// or if the state event is not found. + pub async fn send_location_beacon( + &self, + geo_uri: String, + ) -> Result { + self.ensure_room_joined()?; + + let beacon_info_event = self.get_user_beacon_info().await?; + + if beacon_info_event.content.is_live() { + let content = BeaconEventContent::new(beacon_info_event.event_id, geo_uri, None); + Ok(self.send(content).await?) } else { - Err(BeaconError::NotFound) + Err(BeaconError::NotLive) } } diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 71cb1d3b73b..93b6f7017dd 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -1,12 +1,13 @@ use std::{ sync::{Arc, Mutex}, - time::Duration, + time::{Duration, UNIX_EPOCH}, }; use futures_util::future::join_all; use js_int::uint; use matrix_sdk::{ config::SyncSettings, + instant::SystemTime, room::{edit::EditedContent, Receipts, ReportedContentScore, RoomMemberRole}, test_utils::events::EventFactory, }; @@ -37,7 +38,6 @@ use crate::{ logged_in_client_with_server, mock_encryption_state, mock_sync, mock_sync_with_new_room, synced_client, }; - #[async_test] async fn test_invite_user_by_id() { let (client, server) = logged_in_client_with_server().await; @@ -1003,3 +1003,95 @@ async fn test_stop_sharing_live_location() { assert!(!content.live); } + +#[async_test] +async fn test_send_location_beacon() { + let (client, server) = logged_in_client_with_server().await; + + // Validate request body and response, partial body matching due to + // auto-generated `org.matrix.msc3488.ts`. + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/org.matrix.msc3672.beacon/.*")) + .and(header("authorization", "Bearer 1234")) + .and(body_partial_json(json!({ + "m.relates_to": { + "event_id": "$15139375514XsgmR:localhost", + "rel_type": "m.reference" + }, + "org.matrix.msc3488.location": { + "uri": "geo:48.8588448,2.2943506" + } + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + let current_timestamp = + SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_millis() + as u64; + + mock_sync( + &server, + json!({ + "next_batch": "s526_47314_0_7_1_1_1_1_1", + "rooms": { + "join": { + *DEFAULT_TEST_ROOM_ID: { + "state": { + "events": [ + { + "content": { + "description": "Live Share", + "live": true, + "org.matrix.msc3488.ts": current_timestamp, + "timeout": 600_000, + "org.matrix.msc3488.asset": { "type": "m.self" } + }, + "event_id": "$15139375514XsgmR:localhost", + "origin_server_ts": 1_636_829_458, + "sender": "@example:localhost", + "state_key": "@example:localhost", + "type": "org.matrix.msc3672.beacon_info", + "unsigned": { + "age": 7034220 + } + }, + ] + } + } + } + } + + }), + None, + ) + .await; + + mock_encryption_state(&server, false).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + + let response = room.send_location_beacon("geo:48.8588448,2.2943506".to_owned()).await.unwrap(); + + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) +} + +#[async_test] +async fn test_send_location_beacon_fails_without_starting_live_share() { + let (client, server) = logged_in_client_with_server().await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + + let result = room.send_location_beacon("geo:48.8588448,2.2943506".to_owned()).await; + + assert!(result.is_err()); +} From dd252937c101ca16b33db329c7c7295d3570826a Mon Sep 17 00:00:00 2001 From: torrybr <16907963+torrybr@users.noreply.github.com> Date: Fri, 26 Jul 2024 23:18:12 -0400 Subject: [PATCH 2/3] test: verify test_send_location_beacon_with_expired_live_share --- .../tests/integration/room/joined.rs | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 93b6f7017dd..6a8ba193b40 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -1091,7 +1091,61 @@ async fn test_send_location_beacon_fails_without_starting_live_share() { let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); - let result = room.send_location_beacon("geo:48.8588448,2.2943506".to_owned()).await; + let response = room.send_location_beacon("geo:48.8588448,2.2943506".to_owned()).await; - assert!(result.is_err()); + assert!(response.is_err()); +} + +#[async_test] +async fn test_send_location_beacon_with_expired_live_share() { + let (client, server) = logged_in_client_with_server().await; + + mock_sync( + &server, + json!({ + "next_batch": "s526_47314_0_7_1_1_1_1_1", + "rooms": { + "join": { + *DEFAULT_TEST_ROOM_ID: { + "state": { + "events": [ + { + "content": { + "description": "Live Share", + "live": false, + "org.matrix.msc3488.ts": 1_636_829_458, + "timeout": 3000, + "org.matrix.msc3488.asset": { "type": "m.self" } + }, + "event_id": "$15139375514XsgmR:localhost", + "origin_server_ts": 1_636_829_458, + "sender": "@example:localhost", + "state_key": "@example:localhost", + "type": "org.matrix.msc3672.beacon_info", + "unsigned": { + "age": 7034220 + } + }, + ] + } + } + } + } + + }), + None, + ) + .await; + + mock_encryption_state(&server, false).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + + let response = room.send_location_beacon("geo:48.8588448,2.2943506".to_owned()).await; + + assert!(response.is_err()); } From 3756eeb38526de0b07e8afd65f0ad1d0154bbb2f Mon Sep 17 00:00:00 2001 From: torrybr <16907963+torrybr@users.noreply.github.com> Date: Fri, 26 Jul 2024 23:55:19 -0400 Subject: [PATCH 3/3] test: move beacon tests into own file --- .../tests/integration/room/beacon/mod.rs | 155 ++++++++++++++++++ .../tests/integration/room/joined.rs | 149 +---------------- .../matrix-sdk/tests/integration/room/mod.rs | 1 + 3 files changed, 157 insertions(+), 148 deletions(-) create mode 100644 crates/matrix-sdk/tests/integration/room/beacon/mod.rs diff --git a/crates/matrix-sdk/tests/integration/room/beacon/mod.rs b/crates/matrix-sdk/tests/integration/room/beacon/mod.rs new file mode 100644 index 00000000000..c3315138d20 --- /dev/null +++ b/crates/matrix-sdk/tests/integration/room/beacon/mod.rs @@ -0,0 +1,155 @@ +use std::time::{Duration, UNIX_EPOCH}; + +use matrix_sdk::{config::SyncSettings, instant::SystemTime}; +use matrix_sdk_test::{async_test, test_json, DEFAULT_TEST_ROOM_ID}; +use ruma::event_id; +use serde_json::json; +use wiremock::{ + matchers::{body_partial_json, header, method, path_regex}, + Mock, ResponseTemplate, +}; + +use crate::{logged_in_client_with_server, mock_encryption_state, mock_sync}; +#[async_test] +async fn test_send_location_beacon() { + let (client, server) = logged_in_client_with_server().await; + + // Validate request body and response, partial body matching due to + // auto-generated `org.matrix.msc3488.ts`. + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/org.matrix.msc3672.beacon/.*")) + .and(header("authorization", "Bearer 1234")) + .and(body_partial_json(json!({ + "m.relates_to": { + "event_id": "$15139375514XsgmR:localhost", + "rel_type": "m.reference" + }, + "org.matrix.msc3488.location": { + "uri": "geo:48.8588448,2.2943506" + } + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) + .mount(&server) + .await; + + let current_timestamp = + SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_millis() + as u64; + + mock_sync( + &server, + json!({ + "next_batch": "s526_47314_0_7_1_1_1_1_1", + "rooms": { + "join": { + *DEFAULT_TEST_ROOM_ID: { + "state": { + "events": [ + { + "content": { + "description": "Live Share", + "live": true, + "org.matrix.msc3488.ts": current_timestamp, + "timeout": 600_000, + "org.matrix.msc3488.asset": { "type": "m.self" } + }, + "event_id": "$15139375514XsgmR:localhost", + "origin_server_ts": 1_636_829_458, + "sender": "@example:localhost", + "state_key": "@example:localhost", + "type": "org.matrix.msc3672.beacon_info", + "unsigned": { + "age": 7034220 + } + }, + ] + } + } + } + } + + }), + None, + ) + .await; + + mock_encryption_state(&server, false).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + + let response = room.send_location_beacon("geo:48.8588448,2.2943506".to_owned()).await.unwrap(); + + assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) +} + +#[async_test] +async fn test_send_location_beacon_fails_without_starting_live_share() { + let (client, server) = logged_in_client_with_server().await; + + mock_sync(&server, &*test_json::SYNC, None).await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + + let response = room.send_location_beacon("geo:48.8588448,2.2943506".to_owned()).await; + + assert!(response.is_err()); +} + +#[async_test] +async fn test_send_location_beacon_with_expired_live_share() { + let (client, server) = logged_in_client_with_server().await; + + mock_sync( + &server, + json!({ + "next_batch": "s526_47314_0_7_1_1_1_1_1", + "rooms": { + "join": { + *DEFAULT_TEST_ROOM_ID: { + "state": { + "events": [ + { + "content": { + "description": "Live Share", + "live": false, + "org.matrix.msc3488.ts": 1_636_829_458, + "timeout": 3000, + "org.matrix.msc3488.asset": { "type": "m.self" } + }, + "event_id": "$15139375514XsgmR:localhost", + "origin_server_ts": 1_636_829_458, + "sender": "@example:localhost", + "state_key": "@example:localhost", + "type": "org.matrix.msc3672.beacon_info", + "unsigned": { + "age": 7034220 + } + }, + ] + } + } + } + } + + }), + None, + ) + .await; + + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + client.sync_once(sync_settings).await.unwrap(); + + let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); + + let response = room.send_location_beacon("geo:48.8588448,2.2943506".to_owned()).await; + + assert!(response.is_err()); +} diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 6a8ba193b40..2f993549061 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -1,13 +1,12 @@ use std::{ sync::{Arc, Mutex}, - time::{Duration, UNIX_EPOCH}, + time::Duration, }; use futures_util::future::join_all; use js_int::uint; use matrix_sdk::{ config::SyncSettings, - instant::SystemTime, room::{edit::EditedContent, Receipts, ReportedContentScore, RoomMemberRole}, test_utils::events::EventFactory, }; @@ -1003,149 +1002,3 @@ async fn test_stop_sharing_live_location() { assert!(!content.live); } - -#[async_test] -async fn test_send_location_beacon() { - let (client, server) = logged_in_client_with_server().await; - - // Validate request body and response, partial body matching due to - // auto-generated `org.matrix.msc3488.ts`. - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/org.matrix.msc3672.beacon/.*")) - .and(header("authorization", "Bearer 1234")) - .and(body_partial_json(json!({ - "m.relates_to": { - "event_id": "$15139375514XsgmR:localhost", - "rel_type": "m.reference" - }, - "org.matrix.msc3488.location": { - "uri": "geo:48.8588448,2.2943506" - } - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID)) - .mount(&server) - .await; - - let current_timestamp = - SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_millis() - as u64; - - mock_sync( - &server, - json!({ - "next_batch": "s526_47314_0_7_1_1_1_1_1", - "rooms": { - "join": { - *DEFAULT_TEST_ROOM_ID: { - "state": { - "events": [ - { - "content": { - "description": "Live Share", - "live": true, - "org.matrix.msc3488.ts": current_timestamp, - "timeout": 600_000, - "org.matrix.msc3488.asset": { "type": "m.self" } - }, - "event_id": "$15139375514XsgmR:localhost", - "origin_server_ts": 1_636_829_458, - "sender": "@example:localhost", - "state_key": "@example:localhost", - "type": "org.matrix.msc3672.beacon_info", - "unsigned": { - "age": 7034220 - } - }, - ] - } - } - } - } - - }), - None, - ) - .await; - - mock_encryption_state(&server, false).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); - - let response = room.send_location_beacon("geo:48.8588448,2.2943506".to_owned()).await.unwrap(); - - assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id) -} - -#[async_test] -async fn test_send_location_beacon_fails_without_starting_live_share() { - let (client, server) = logged_in_client_with_server().await; - - mock_sync(&server, &*test_json::SYNC, None).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); - - let response = room.send_location_beacon("geo:48.8588448,2.2943506".to_owned()).await; - - assert!(response.is_err()); -} - -#[async_test] -async fn test_send_location_beacon_with_expired_live_share() { - let (client, server) = logged_in_client_with_server().await; - - mock_sync( - &server, - json!({ - "next_batch": "s526_47314_0_7_1_1_1_1_1", - "rooms": { - "join": { - *DEFAULT_TEST_ROOM_ID: { - "state": { - "events": [ - { - "content": { - "description": "Live Share", - "live": false, - "org.matrix.msc3488.ts": 1_636_829_458, - "timeout": 3000, - "org.matrix.msc3488.asset": { "type": "m.self" } - }, - "event_id": "$15139375514XsgmR:localhost", - "origin_server_ts": 1_636_829_458, - "sender": "@example:localhost", - "state_key": "@example:localhost", - "type": "org.matrix.msc3672.beacon_info", - "unsigned": { - "age": 7034220 - } - }, - ] - } - } - } - } - - }), - None, - ) - .await; - - mock_encryption_state(&server, false).await; - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - client.sync_once(sync_settings).await.unwrap(); - - let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap(); - - let response = room.send_location_beacon("geo:48.8588448,2.2943506".to_owned()).await; - - assert!(response.is_err()); -} diff --git a/crates/matrix-sdk/tests/integration/room/mod.rs b/crates/matrix-sdk/tests/integration/room/mod.rs index 1dea955bc1c..5587f6d2724 100644 --- a/crates/matrix-sdk/tests/integration/room/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/mod.rs @@ -1,4 +1,5 @@ mod attachment; +mod beacon; mod common; mod joined; mod left;