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/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 71cb1d3b73b..2f993549061 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -37,7 +37,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; 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;