Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sdk: basic support for sending location beacons #3771

Merged
merged 3 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading