Skip to content

Commit 5ddd6fd

Browse files
committed
sdk: basic support for sending and stopping live location shares
1 parent 925c5b2 commit 5ddd6fd

File tree

8 files changed

+499
-4
lines changed

8 files changed

+499
-4
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ ruma = { git = "https://github.com/matrix-org/ruma", rev = "f25b3220d0c3ece77200
5656
"compat-encrypted-stickers",
5757
"unstable-msc3401",
5858
"unstable-msc3266",
59+
"unstable-msc3488",
60+
"unstable-msc3489",
5961
"unstable-msc4075",
6062
"unstable-msc4140",
6163
] }

crates/matrix-sdk-base/src/rooms/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub use normal::{
1818
use ruma::{
1919
assign,
2020
events::{
21+
beacon_info::BeaconInfoEventContent,
2122
call::member::CallMemberEventContent,
2223
macros::EventContent,
2324
room::{
@@ -81,6 +82,9 @@ impl fmt::Display for DisplayName {
8182
pub struct BaseRoomInfo {
8283
/// The avatar URL of this room.
8384
pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
85+
/// All shared live location beacons of this room.
86+
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
87+
pub(crate) beacons: BTreeMap<OwnedUserId, MinimalStateEvent<BeaconInfoEventContent>>,
8488
/// The canonical alias of this room.
8589
pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
8690
/// The `m.room.create` event content of this room.
@@ -141,6 +145,9 @@ impl BaseRoomInfo {
141145
/// Returns true if the event modified the info, false otherwise.
142146
pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool {
143147
match ev {
148+
AnySyncStateEvent::BeaconInfo(b) => {
149+
self.beacons.insert(b.state_key().clone(), b.into());
150+
}
144151
// No redacted branch - enabling encryption cannot be undone.
145152
AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => {
146153
self.encryption = Some(encryption.content.clone());
@@ -335,6 +342,7 @@ impl Default for BaseRoomInfo {
335342
fn default() -> Self {
336343
Self {
337344
avatar: None,
345+
beacons: BTreeMap::new(),
338346
canonical_alias: None,
339347
create: None,
340348
dm_targets: Default::default(),

crates/matrix-sdk-base/src/store/integration_tests.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
//! Trait and macro of integration tests for StateStore implementations.
22
3-
use std::collections::{BTreeMap, BTreeSet};
3+
use std::{
4+
collections::{BTreeMap, BTreeSet},
5+
time::Duration,
6+
};
47

58
use assert_matches::assert_matches;
69
use assert_matches2::assert_let;
@@ -11,6 +14,7 @@ use ruma::{
1114
api::{client::media::get_content_thumbnail::v3::Method, MatrixVersion},
1215
event_id,
1316
events::{
17+
beacon_info::BeaconInfoEventContent,
1418
presence::PresenceEvent,
1519
receipt::{ReceiptThread, ReceiptType},
1620
room::{
@@ -91,6 +95,8 @@ pub trait StateStoreIntegrationTests {
9195
async fn test_send_queue(&self);
9296
/// Test saving/restoring server capabilities.
9397
async fn test_server_capabilities_saving(&self);
98+
/// Test saving live location share beacons
99+
async fn test_beacon_saving(&self);
94100
}
95101

96102
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -1481,6 +1487,30 @@ impl StateStoreIntegrationTests for DynStateStore {
14811487
assert!(outstanding_rooms.iter().any(|room| room == room_id));
14821488
assert!(outstanding_rooms.iter().any(|room| room == room_id2));
14831489
}
1490+
1491+
async fn test_beacon_saving(&self) {
1492+
let room_id = room_id!("!test_beacon_saving:localhost");
1493+
1494+
let raw_event = custom_beacon_info_event(user_id(), true, 1234567890);
1495+
let event = raw_event.deserialize().unwrap();
1496+
1497+
assert!(self
1498+
.get_state_event(room_id, StateEventType::BeaconInfo, user_id().as_str())
1499+
.await
1500+
.unwrap()
1501+
.is_none());
1502+
1503+
let mut changes = StateChanges::default();
1504+
changes.add_state_event(room_id, event, raw_event);
1505+
1506+
self.save_changes(&changes).await.unwrap();
1507+
1508+
assert!(self
1509+
.get_state_event(room_id, StateEventType::BeaconInfo, user_id().as_str())
1510+
.await
1511+
.unwrap()
1512+
.is_some());
1513+
}
14841514
}
14851515

14861516
/// Macro building to allow your StateStore implementation to run the entire
@@ -1649,6 +1679,12 @@ macro_rules! statestore_integration_tests {
16491679
let store = get_store().await.expect("creating store failed").into_state_store();
16501680
store.test_send_queue().await;
16511681
}
1682+
1683+
#[async_test]
1684+
async fn test_beacon_saving() {
1685+
let store = get_store().await.expect("creating store failed").into_state_store();
1686+
store.test_beacon_saving().await;
1687+
}
16521688
};
16531689
}
16541690

@@ -1729,3 +1765,23 @@ fn custom_presence_event(user_id: &UserId) -> Raw<PresenceEvent> {
17291765

17301766
Raw::new(&ev_json).unwrap().cast()
17311767
}
1768+
1769+
fn custom_beacon_info_event(
1770+
user_id: &UserId,
1771+
live: bool,
1772+
duration_millis: u64,
1773+
) -> Raw<AnySyncStateEvent> {
1774+
let content =
1775+
BeaconInfoEventContent::new(None, Duration::from_millis(duration_millis), live, None);
1776+
1777+
let event = json!({
1778+
"event_id": "$h29iv0s8:example.com",
1779+
"content": content,
1780+
"sender": user_id,
1781+
"type": "org.matrix.msc3672.beacon_info",
1782+
"origin_server_ts": 0u64,
1783+
"state_key": user_id,
1784+
});
1785+
1786+
serde_json::from_value(event).unwrap()
1787+
}

crates/matrix-sdk-base/src/store/migration_helpers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ impl BaseRoomInfoV1 {
199199

200200
Box::new(BaseRoomInfo {
201201
avatar,
202+
beacons: BTreeMap::new(),
202203
canonical_alias,
203204
create,
204205
dm_targets,

crates/matrix-sdk/src/error.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,39 @@ pub enum ImageError {
467467
ThumbnailBiggerThanOriginal,
468468
}
469469

470+
/// Errors that can happen when interacting with the beacon API.
471+
#[derive(Debug, Error)]
472+
pub enum BeaconError {
473+
// A network error occurred.
474+
#[error("Network error: {0}")]
475+
Network(#[from] HttpError),
476+
477+
// The beacon information is not found.
478+
#[error("Existing beacon information not found.")]
479+
NotFound,
480+
481+
// The redacted event is not an error, but it's not useful for the client.
482+
#[error("Beacon event is redacted and cannot be processed.")]
483+
Redacted,
484+
485+
// The client must join the room to access the beacon information.
486+
#[error("Must join the room to access beacon information.")]
487+
Stripped,
488+
489+
#[error("Deserialization error: {0}")]
490+
Deserialization(#[from] serde_json::Error),
491+
492+
// Allow for other errors to be wrapped.
493+
#[error("Other error: {0}")]
494+
Other(Box<Error>),
495+
}
496+
497+
impl From<Error> for BeaconError {
498+
fn from(err: Error) -> Self {
499+
BeaconError::Other(Box::new(err))
500+
}
501+
}
502+
470503
/// Errors that can happen when refreshing an access token.
471504
///
472505
/// This is usually only returned by [`Client::refresh_access_token()`], unless

crates/matrix-sdk/src/room/mod.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ use ruma::{
5252
},
5353
assign,
5454
events::{
55+
beacon_info::BeaconInfoEventContent,
5556
call::notify::{ApplicationType, CallNotifyEventContent, NotifyType},
5657
direct::DirectEventContent,
5758
marked_unread::MarkedUnreadEventContent,
@@ -97,7 +98,7 @@ use crate::{
9798
attachment::AttachmentConfig,
9899
client::WeakClient,
99100
config::RequestConfig,
100-
error::WrongRoomState,
101+
error::{BeaconError, WrongRoomState},
101102
event_cache::{self, EventCacheDropHandles, RoomEventCache},
102103
event_handler::{EventHandler, EventHandlerDropGuard, EventHandlerHandle, SyncEvent},
103104
media::{MediaFormat, MediaRequest},
@@ -2668,6 +2669,69 @@ impl Room {
26682669
Ok(())
26692670
}
26702671

2672+
/// Start sharing live location in the room.
2673+
///
2674+
/// # Arguments
2675+
///
2676+
/// * `duration_millis` - The duration for which the live location is
2677+
/// shared, in milliseconds.
2678+
/// * `description` - An optional description for the live location share.
2679+
///
2680+
/// # Errors
2681+
///
2682+
/// Returns an error if the room is not joined or if the state event could
2683+
/// not be sent.
2684+
pub async fn start_live_location_share(
2685+
&self,
2686+
duration_millis: u64,
2687+
description: Option<String>,
2688+
) -> Result<send_state_event::v3::Response> {
2689+
self.ensure_room_joined()?;
2690+
2691+
self.send_state_event_for_key(
2692+
self.own_user_id(),
2693+
BeaconInfoEventContent::new(
2694+
description,
2695+
Duration::from_millis(duration_millis),
2696+
true,
2697+
None,
2698+
),
2699+
)
2700+
.await
2701+
}
2702+
2703+
/// Stop sharing live location in the room.
2704+
///
2705+
/// # Errors
2706+
///
2707+
/// Returns an error if the room is not joined, if the beacon information
2708+
/// is redacted or stripped, or if the state event is not found.
2709+
pub async fn stop_live_location_share(
2710+
&self,
2711+
) -> Result<send_state_event::v3::Response, BeaconError> {
2712+
self.ensure_room_joined()?;
2713+
2714+
if let Some(raw_event) = self
2715+
.get_state_event_static_for_key::<BeaconInfoEventContent, _>(self.own_user_id())
2716+
.await?
2717+
{
2718+
match raw_event.deserialize() {
2719+
Ok(SyncOrStrippedState::Sync(SyncStateEvent::Original(beacon_info))) => {
2720+
let mut content = beacon_info.content.clone();
2721+
content.stop();
2722+
Ok(self.send_state_event_for_key(self.own_user_id(), content).await?)
2723+
}
2724+
Ok(SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_))) => {
2725+
Err(BeaconError::Redacted)
2726+
}
2727+
Ok(SyncOrStrippedState::Stripped(_)) => Err(BeaconError::Stripped),
2728+
Err(e) => Err(BeaconError::Deserialization(e)),
2729+
}
2730+
} else {
2731+
Err(BeaconError::NotFound)
2732+
}
2733+
}
2734+
26712735
/// Send a call notification event in the current room.
26722736
///
26732737
/// This is only supposed to be used in **custom** situations where the user

crates/matrix-sdk/tests/integration/room/joined.rs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ use matrix_sdk::{
1111
};
1212
use matrix_sdk_base::RoomState;
1313
use matrix_sdk_test::{
14-
async_test, test_json, test_json::sync::CUSTOM_ROOM_POWER_LEVELS, EphemeralTestEvent,
15-
GlobalAccountDataTestEvent, JoinedRoomBuilder, SyncResponseBuilder, DEFAULT_TEST_ROOM_ID,
14+
async_test, test_json,
15+
test_json::sync::{CUSTOM_ROOM_POWER_LEVELS, LIVE_LOCATION_SHARING_SYNC},
16+
EphemeralTestEvent, GlobalAccountDataTestEvent, JoinedRoomBuilder, SyncResponseBuilder,
17+
DEFAULT_TEST_ROOM_ID,
1618
};
19+
use ruma::events::StateEventType;
1720
use ruma::{
1821
api::client::{membership::Invite3pidInit, receipt::create_receipt::v3::ReceiptType},
1922
assign, event_id,
@@ -768,3 +771,53 @@ async fn test_make_reply_event_doesnt_require_event_cache() {
768771
// make_edit_event works, even if the event cache hasn't been enabled.
769772
room.make_edit_event(resp_event_id, EditedContent::RoomMessage(new_content)).await.unwrap();
770773
}
774+
775+
#[async_test]
776+
async fn test_start_live_location_share_for_room() {
777+
let (client, server) = logged_in_client_with_server().await;
778+
779+
Mock::given(method("PUT"))
780+
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/org.matrix.msc3672.beacon_info/.*"))
781+
.and(header("authorization", "Bearer 1234"))
782+
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID))
783+
.mount(&server)
784+
.await;
785+
786+
mock_sync(&server, &*test_json::SYNC, None).await;
787+
788+
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
789+
790+
let _response = client.sync_once(sync_settings).await.unwrap();
791+
792+
let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap();
793+
794+
let response = room.start_live_location_share(3000, None).await.unwrap();
795+
assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id);
796+
}
797+
798+
#[async_test]
799+
async fn test_stop_sharing_live_location() {
800+
let (client, server) = logged_in_client_with_server().await;
801+
802+
Mock::given(method("PUT"))
803+
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/org.matrix.msc3672.beacon_info/.*"))
804+
.and(header("authorization", "Bearer 1234"))
805+
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID))
806+
.mount(&server)
807+
.await;
808+
809+
mock_sync(&server, &*LIVE_LOCATION_SHARING_SYNC, None).await;
810+
811+
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
812+
813+
let _response = client.sync_once(sync_settings).await.unwrap();
814+
815+
let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap();
816+
817+
let response = room.stop_live_location_share().await.unwrap();
818+
819+
assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id);
820+
821+
let state_events = room.get_state_events(StateEventType::BeaconInfo).await.unwrap();
822+
assert_eq!(state_events.len(), 1);
823+
}

0 commit comments

Comments
 (0)