Skip to content

Commit

Permalink
Merge pull request #3771 from torrybr/feat/send-beacon
Browse files Browse the repository at this point in the history
sdk: basic support for sending location beacons
  • Loading branch information
jmartinesp authored Jul 29, 2024
2 parents 6fca1e8 + 3756eeb commit e083311
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 22 deletions.
5 changes: 5 additions & 0 deletions crates/matrix-sdk/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Error>),
Expand Down
75 changes: 54 additions & 21 deletions crates/matrix-sdk/src/room/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ use ruma::{
},
assign,
events::{
beacon::BeaconEventContent,
beacon_info::BeaconInfoEventContent,
call::notify::{ApplicationType, CallNotifyEventContent, NotifyType},
direct::DirectEventContent,
Expand All @@ -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,
Expand Down Expand Up @@ -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<OriginalSyncStateEvent<BeaconInfoEventContent>, BeaconError> {
let raw_event = self
.get_state_event_static_for_key::<BeaconInfoEventContent, _>(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
Expand Down Expand Up @@ -2795,24 +2817,35 @@ impl Room {
) -> Result<send_state_event::v3::Response, BeaconError> {
self.ensure_room_joined()?;

if let Some(raw_event) = self
.get_state_event_static_for_key::<BeaconInfoEventContent, _>(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<send_message_event::v3::Response, BeaconError> {
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)
}
}

Expand Down
155 changes: 155 additions & 0 deletions crates/matrix-sdk/tests/integration/room/beacon/mod.rs
Original file line number Diff line number Diff line change
@@ -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());
}
1 change: 0 additions & 1 deletion crates/matrix-sdk/tests/integration/room/joined.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions crates/matrix-sdk/tests/integration/room/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod attachment;
mod beacon;
mod common;
mod joined;
mod left;
Expand Down

0 comments on commit e083311

Please sign in to comment.