Skip to content

Commit 4b8b741

Browse files
committed
sdk: basic support for sending and stopping live location shares
1 parent 81d388a commit 4b8b741

File tree

8 files changed

+492
-6
lines changed

8 files changed

+492
-6
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/ruma/ruma", rev = "c37843e9be619ffac8c4d33ad3
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: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
//! Trait and macro of integration tests for StateStore implementations.
22
3-
use std::collections::{BTreeMap, BTreeSet};
4-
53
use assert_matches::assert_matches;
64
use assert_matches2::assert_let;
75
use async_trait::async_trait;
@@ -11,6 +9,7 @@ use ruma::{
119
api::{client::media::get_content_thumbnail::v3::Method, MatrixVersion},
1210
event_id,
1311
events::{
12+
beacon_info::BeaconInfoEventContent,
1413
presence::PresenceEvent,
1514
receipt::{ReceiptThread, ReceiptType},
1615
room::{
@@ -33,6 +32,8 @@ use ruma::{
3332
uint, user_id, EventId, OwnedEventId, OwnedUserId, RoomId, TransactionId, UserId,
3433
};
3534
use serde_json::{json, value::Value as JsonValue};
35+
use std::collections::{BTreeMap, BTreeSet};
36+
use std::time::Duration;
3637

3738
use super::{DynStateStore, ServerCapabilities};
3839
use crate::{
@@ -91,6 +92,8 @@ pub trait StateStoreIntegrationTests {
9192
async fn test_send_queue(&self);
9293
/// Test saving/restoring server capabilities.
9394
async fn test_server_capabilities_saving(&self);
95+
/// Test saving live location share beacons
96+
async fn test_beacon_saving(&self);
9497
}
9598

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

14861513
/// Macro building to allow your StateStore implementation to run the entire
@@ -1649,6 +1676,12 @@ macro_rules! statestore_integration_tests {
16491676
let store = get_store().await.expect("creating store failed").into_state_store();
16501677
store.test_send_queue().await;
16511678
}
1679+
1680+
#[async_test]
1681+
async fn test_beacon_saving() {
1682+
let store = get_store().await.expect("creating store failed").into_state_store();
1683+
store.test_beacon_saving().await;
1684+
}
16521685
};
16531686
}
16541687

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

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

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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,36 @@ 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+
// Allow for other errors to be wrapped.
490+
#[error("Other error: {0}")]
491+
Other(Box<Error>),
492+
}
493+
494+
impl From<Error> for BeaconError {
495+
fn from(err: Error) -> Self {
496+
BeaconError::Other(Box::new(err))
497+
}
498+
}
499+
470500
/// Errors that can happen when refreshing an access token.
471501
///
472502
/// This is usually only returned by [`Client::refresh_access_token()`], unless

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

Lines changed: 65 additions & 0 deletions
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,
@@ -91,6 +92,7 @@ pub use self::{
9192
member::{RoomMember, RoomMemberRole},
9293
messages::{EventWithContextResponse, Messages, MessagesOptions},
9394
};
95+
use crate::error::BeaconError;
9496
#[cfg(doc)]
9597
use crate::event_cache::EventCache;
9698
use crate::{
@@ -2668,6 +2670,69 @@ impl Room {
26682670
Ok(())
26692671
}
26702672

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

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

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ use std::{
33
time::Duration,
44
};
55

6+
use crate::{logged_in_client_with_server, mock_encryption_state, mock_sync, synced_client};
67
use futures_util::future::join_all;
78
use matrix_sdk::{
89
config::SyncSettings,
910
room::{Receipts, ReportedContentScore, RoomMemberRole},
1011
};
1112
use matrix_sdk_base::RoomState;
1213
use matrix_sdk_test::{
13-
async_test, test_json, test_json::sync::CUSTOM_ROOM_POWER_LEVELS, EphemeralTestEvent,
14-
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,
1518
};
1619
use ruma::{
1720
api::client::{membership::Invite3pidInit, receipt::create_receipt::v3::ReceiptType},
@@ -25,8 +28,6 @@ use wiremock::{
2528
Mock, ResponseTemplate,
2629
};
2730

28-
use crate::{logged_in_client_with_server, mock_encryption_state, mock_sync, synced_client};
29-
3031
#[async_test]
3132
async fn test_invite_user_by_id() {
3233
let (client, server) = logged_in_client_with_server().await;
@@ -713,3 +714,51 @@ async fn test_call_notifications_notify_for_rooms() {
713714

714715
room.send_call_notification_if_needed().await.unwrap();
715716
}
717+
718+
#[async_test]
719+
async fn test_start_live_location_share_for_room() {
720+
let (client, server) = logged_in_client_with_server().await;
721+
722+
Mock::given(method("PUT"))
723+
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/org.matrix.msc3672.beacon_info/.*"))
724+
.and(header("authorization", "Bearer 1234"))
725+
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID))
726+
.mount(&server)
727+
.await;
728+
729+
mock_sync(&server, &*test_json::SYNC, None).await;
730+
731+
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
732+
733+
let _response = client.sync_once(sync_settings).await.unwrap();
734+
735+
let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap();
736+
737+
let response = room.start_live_location_share(3000, None).await.unwrap();
738+
739+
assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id);
740+
}
741+
742+
#[async_test]
743+
async fn test_stop_sharing_live_location() {
744+
let (client, server) = logged_in_client_with_server().await;
745+
746+
Mock::given(method("PUT"))
747+
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/org.matrix.msc3672.beacon_info/.*"))
748+
.and(header("authorization", "Bearer 1234"))
749+
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID))
750+
.mount(&server)
751+
.await;
752+
753+
mock_sync(&server, &*LIVE_LOCATION_SHARING_SYNC, None).await;
754+
755+
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
756+
757+
let _response = client.sync_once(sync_settings).await.unwrap();
758+
759+
let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap();
760+
761+
let response = room.stop_live_location_share().await.unwrap();
762+
763+
assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id);
764+
}

0 commit comments

Comments
 (0)