From 05c11723a64758a2a406553a7c292d20c3cf0e50 Mon Sep 17 00:00:00 2001 From: torrybr <16907963+torrybr@users.noreply.github.com> Date: Thu, 14 Nov 2024 19:20:21 -0500 Subject: [PATCH] feat(sdk): support for listening to beacon events --- crates/matrix-sdk/src/lib.rs | 1 + crates/matrix-sdk/src/live_location_share.rs | 40 +++ crates/matrix-sdk/src/room/mod.rs | 53 +++- .../tests/integration/room/beacon/mod.rs | 292 +++++++++++++++++- 4 files changed, 379 insertions(+), 7 deletions(-) create mode 100644 crates/matrix-sdk/src/live_location_share.rs diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 370061fa8de..48b8638530c 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -94,6 +94,7 @@ pub use sliding_sync::{ #[cfg(feature = "uniffi")] uniffi::setup_scaffolding!(); +mod live_location_share; #[cfg(any(test, feature = "testing"))] pub mod test_utils; diff --git a/crates/matrix-sdk/src/live_location_share.rs b/crates/matrix-sdk/src/live_location_share.rs new file mode 100644 index 00000000000..ec541ad1f8b --- /dev/null +++ b/crates/matrix-sdk/src/live_location_share.rs @@ -0,0 +1,40 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Types for live location sharing. + +use ruma::{ + events::{beacon_info::BeaconInfoEventContent, location::LocationContent}, + MilliSecondsSinceUnixEpoch, OwnedUserId, +}; + +/// Details of the last known location beacon. +#[derive(Clone, Debug)] +pub struct LastLocation { + /// The most recent location content of the user. + pub location: LocationContent, + /// The timestamp of when the location was updated. + pub ts: MilliSecondsSinceUnixEpoch, +} + +/// Details of a users live location share. +#[derive(Clone, Debug)] +pub struct LiveLocationShare { + /// The user's last known location. + pub last_location: LastLocation, + /// Information about the associated beacon event. + pub beacon_info: BeaconInfoEventContent, + /// The user ID of the person sharing their live location. + pub user_id: OwnedUserId, +} diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 00484d31457..c507aaf10bc 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -75,7 +75,7 @@ use ruma::{ }, assign, events::{ - beacon::BeaconEventContent, + beacon::{BeaconEventContent, OriginalSyncBeaconEvent}, beacon_info::BeaconInfoEventContent, call::notify::{ApplicationType, CallNotifyEventContent, NotifyType}, direct::DirectEventContent, @@ -132,6 +132,7 @@ use crate::{ error::{BeaconError, WrongRoomState}, event_cache::{self, EventCacheDropHandles, RoomEventCache}, event_handler::{EventHandler, EventHandlerDropGuard, EventHandlerHandle, SyncEvent}, + live_location_share::{LastLocation, LiveLocationShare}, media::{MediaFormat, MediaRequestParameters}, notification_settings::{IsEncrypted, IsOneToOne, RoomNotificationMode}, room::power_levels::{RoomPowerLevelChanges, RoomPowerLevelsExt}, @@ -2990,7 +2991,7 @@ impl Room { Ok(()) } - /// Get the beacon information event in the room for the current user. + /// Get the beacon information event in the room for the `user_id`. /// /// # Errors /// @@ -2998,9 +2999,10 @@ impl Room { /// not be deserialized. async fn get_user_beacon_info( &self, + user_id: &UserId, ) -> Result, BeaconError> { let raw_event = self - .get_state_event_static_for_key::(self.own_user_id()) + .get_state_event_static_for_key::(user_id) .await? .ok_or(BeaconError::NotFound)?; @@ -3053,7 +3055,7 @@ impl Room { ) -> Result { self.ensure_room_joined()?; - let mut beacon_info_event = self.get_user_beacon_info().await?; + let mut beacon_info_event = self.get_user_beacon_info(self.own_user_id()).await?; beacon_info_event.content.stop(); Ok(self.send_state_event_for_key(self.own_user_id(), beacon_info_event.content).await?) } @@ -3075,7 +3077,7 @@ impl Room { ) -> Result { self.ensure_room_joined()?; - let beacon_info_event = self.get_user_beacon_info().await?; + let beacon_info_event = self.get_user_beacon_info(self.own_user_id()).await?; if beacon_info_event.content.is_live() { let content = BeaconEventContent::new(beacon_info_event.event_id, geo_uri, None); @@ -3166,6 +3168,47 @@ impl Room { }, } } + + /// Subscribe to live location sharing events for this room. + /// + /// The returned receiver will receive a new event for each sync response + /// that contains a `m.beacon` event. + pub fn subscribe_to_live_location_shares( + &self, + ) -> (EventHandlerDropGuard, broadcast::Receiver) { + let (sender, receiver) = broadcast::channel(128); + + let room_id = self.room_id().to_owned(); + let room = self.clone(); + + let beacon_event_handler_handle = self.client.add_room_event_handler(&room_id, { + move |event: OriginalSyncBeaconEvent| async move { + let user_id = event.sender; + + let beacon_info = match room.get_user_beacon_info(&user_id).await { + Ok(info) => info.content, + Err(e) => { + eprintln!("Failed to get beacon info: {:?}", e); + return; + } + }; + + let live_location_share = LiveLocationShare { + last_location: LastLocation { + location: event.content.location, + ts: event.content.ts, + }, + user_id, + beacon_info, + }; + + let _ = sender.send(live_location_share); + } + }); + + let drop_guard = self.client().event_handler_drop_guard(beacon_event_handler_handle); + (drop_guard, receiver) + } } #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] diff --git a/crates/matrix-sdk/tests/integration/room/beacon/mod.rs b/crates/matrix-sdk/tests/integration/room/beacon/mod.rs index d83bfda7c70..5f8c6b58f95 100644 --- a/crates/matrix-sdk/tests/integration/room/beacon/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/beacon/mod.rs @@ -1,8 +1,12 @@ use std::time::{Duration, UNIX_EPOCH}; +use js_int::uint; use matrix_sdk::config::SyncSettings; -use matrix_sdk_test::{async_test, mocks::mock_encryption_state, test_json, DEFAULT_TEST_ROOM_ID}; -use ruma::{event_id, time::SystemTime}; +use matrix_sdk_test::{ + async_test, mocks::mock_encryption_state, sync_timeline_event, test_json, JoinedRoomBuilder, + SyncResponseBuilder, DEFAULT_TEST_ROOM_ID, +}; +use ruma::{event_id, events::location::AssetType, time::SystemTime, MilliSecondsSinceUnixEpoch}; use serde_json::json; use wiremock::{ matchers::{body_partial_json, header, method, path_regex}, @@ -153,3 +157,287 @@ async fn test_send_location_beacon_with_expired_live_share() { assert!(response.is_err()); } + +#[async_test] +async fn test_subscribe_to_live_location_shares() { + let (client, server) = logged_in_client_with_server().await; + + let mut sync_builder = SyncResponseBuilder::new(); + + let current_time = MilliSecondsSinceUnixEpoch::now(); + let millis_time = current_time + .to_system_time() + .unwrap() + .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": millis_time, + "timeout": 3000, + "org.matrix.msc3488.asset": { "type": "m.self" } + }, + "event_id": "$15139375514XsgmR:localhost", + "origin_server_ts": millis_time, + "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)); + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let room = client.get_room(*DEFAULT_TEST_ROOM_ID).unwrap(); + + let (_drop_guard, mut receiver) = room.subscribe_to_live_location_shares(); + + let mut timeline_events = Vec::new(); + + for i in 0..75 { + timeline_events.push(sync_timeline_event!({ + "content": { + "m.relates_to": { + "event_id": "$TlS7h0NHzBdZIccsSspF5CMpQE8YMT0stRern0nXscI", + "rel_type": "m.reference" + }, + "org.matrix.msc3488.location": { + "uri": format!("geo:{}.9575274619722,12.494122581370175;u={}", i, i) + }, + "org.matrix.msc3488.ts": 1_636_829_458 + }, + "event_id": format!("$152037280074GZeOm:localhost{}", i), + "origin_server_ts": 1_636_829_458, + "sender": "@example:localhost", + "type": "org.matrix.msc3672.beacon", + "unsigned": { + "age": 598971 + } + })); + } + + sync_builder.add_joined_room( + JoinedRoomBuilder::new(*DEFAULT_TEST_ROOM_ID).add_timeline_bulk(timeline_events.clone()), + ); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + for i in 0..timeline_events.len() { + let live_location_share = + receiver.recv().await.expect("Failed to receive live location share"); + + assert_eq!(live_location_share.user_id.to_string(), "@example:localhost"); + + assert_eq!( + live_location_share.last_location.location.uri, + format!("geo:{}.9575274619722,12.494122581370175;u={}", i, i) + ); + + assert!(live_location_share.last_location.location.description.is_none()); + assert!(live_location_share.last_location.location.zoom_level.is_none()); + assert_eq!( + live_location_share.last_location.ts, + MilliSecondsSinceUnixEpoch(uint!(1_636_829_458)) + ); + + assert!(live_location_share.beacon_info.live); + assert!(live_location_share.beacon_info.is_live()); + assert_eq!(live_location_share.beacon_info.description, Some("Live Share".to_owned())); + assert_eq!(live_location_share.beacon_info.timeout, Duration::from_millis(3000)); + assert_eq!(live_location_share.beacon_info.ts, current_time); + assert_eq!(live_location_share.beacon_info.asset.type_, AssetType::Self_); + } +} + +#[async_test] +async fn test_subscribe_to_live_location_shares_with_multiple_users() { + let (client, server) = logged_in_client_with_server().await; + + let mut sync_builder = SyncResponseBuilder::new(); + + let current_time = MilliSecondsSinceUnixEpoch::now(); + let millis_time = current_time + .to_system_time() + .unwrap() + .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": millis_time, + "timeout": 3000, + "org.matrix.msc3488.asset": { "type": "m.self" } + }, + "event_id": "$15139375514XsgmR:localhost", + "origin_server_ts": millis_time, + "sender": "@user1:localhost", + "state_key": "@user1:localhost", + "type": "org.matrix.msc3672.beacon_info", + "unsigned": { + "age": 7034220 + } + }, + { + "content": { + "description": "Live Share", + "live": true, + "org.matrix.msc3488.ts": millis_time, + "timeout": 3000, + "org.matrix.msc3488.asset": { "type": "m.self" } + }, + "event_id": "$16139375514XsgmR:localhost", + "origin_server_ts": millis_time, + "sender": "@user2:localhost", + "state_key": "@user2:localhost", + "type": "org.matrix.msc3672.beacon_info", + "unsigned": { + "age": 7034220 + } + } + ] + } + } + } + } + + }), + None, + ) + .await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let room = client.get_room(*DEFAULT_TEST_ROOM_ID).unwrap(); + + let (_drop_guard, mut receiver) = room.subscribe_to_live_location_shares(); + + sync_builder.add_joined_room(JoinedRoomBuilder::new(*DEFAULT_TEST_ROOM_ID).add_timeline_bulk( + [ + sync_timeline_event!({ + "content": { + "m.relates_to": { + "event_id": "$TlS7h0NHzBdZIccsSspF5CMpQE8YMT0stRern0nXscI", + "rel_type": "m.reference" + }, + "org.matrix.msc3488.location": { + "uri": "geo:8.95752746197222,12.494122581370175;u=10" + }, + "org.matrix.msc3488.ts": 1_636_829_458 + }, + "event_id": "$152037280074GZeOm:localhost", + "origin_server_ts": 1_636_829_458, + "sender": "@user1:localhost", + "type": "org.matrix.msc3672.beacon", + "unsigned": { + "age": 598971 + } + }), + sync_timeline_event!({ + "content": { + "m.relates_to": { + "event_id": "$TlS7h0NHzBdZIccsSspF5CMpQE8YMT0stRern0nXscI", + "rel_type": "m.reference" + }, + "org.matrix.msc3488.location": { + "uri": "geo:9.95752746197222,13.494122581370175;u=10" + }, + "org.matrix.msc3488.ts": 1_636_829_458 + }, + "event_id": "$162037280074GZeOm:localhost", + "origin_server_ts": 1_636_829_458, + "sender": "@user2:localhost", + "type": "org.matrix.msc3672.beacon", + "unsigned": { + "age": 598971 + } + }), + ], + )); + + mock_sync(&server, sync_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let live_location_share = receiver.recv().await.expect("Failed to receive live location share"); + + assert_eq!(live_location_share.user_id.to_string(), "@user1:localhost"); + + assert_eq!( + live_location_share.last_location.location.uri, + "geo:8.95752746197222,12.494122581370175;u=10" + ); + assert!(live_location_share.last_location.location.description.is_none()); + assert!(live_location_share.last_location.location.zoom_level.is_none()); + assert_eq!( + live_location_share.last_location.ts, + MilliSecondsSinceUnixEpoch(uint!(1_636_829_458)) + ); + + assert!(live_location_share.beacon_info.live); + assert!(live_location_share.beacon_info.is_live()); + assert_eq!(live_location_share.beacon_info.description, Some("Live Share".to_owned())); + assert_eq!(live_location_share.beacon_info.timeout, Duration::from_millis(3000)); + assert_eq!(live_location_share.beacon_info.ts, current_time); + assert_eq!(live_location_share.beacon_info.asset.type_, AssetType::Self_); + + let live_location_share = receiver.recv().await.expect("Failed to receive live location share"); + + assert_eq!(live_location_share.user_id.to_string(), "@user2:localhost"); + + assert_eq!( + live_location_share.last_location.location.uri, + "geo:9.95752746197222,13.494122581370175;u=10" + ); + assert!(live_location_share.last_location.location.description.is_none()); + assert!(live_location_share.last_location.location.zoom_level.is_none()); + assert_eq!( + live_location_share.last_location.ts, + MilliSecondsSinceUnixEpoch(uint!(1_636_829_458)) + ); + + assert!(live_location_share.beacon_info.live); + assert!(live_location_share.beacon_info.is_live()); + assert_eq!(live_location_share.beacon_info.description, Some("Live Share".to_owned())); + assert_eq!(live_location_share.beacon_info.timeout, Duration::from_millis(3000)); + assert_eq!(live_location_share.beacon_info.ts, current_time); + assert_eq!(live_location_share.beacon_info.asset.type_, AssetType::Self_); +}