Skip to content

Commit

Permalink
sdk: basic support for sending and stopping live location shares
Browse files Browse the repository at this point in the history
  • Loading branch information
torrybr committed Jul 22, 2024
1 parent 925c5b2 commit 5ddd6fd
Show file tree
Hide file tree
Showing 8 changed files with 499 additions and 4 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ ruma = { git = "https://github.com/matrix-org/ruma", rev = "f25b3220d0c3ece77200
"compat-encrypted-stickers",
"unstable-msc3401",
"unstable-msc3266",
"unstable-msc3488",
"unstable-msc3489",
"unstable-msc4075",
"unstable-msc4140",
] }
Expand Down
8 changes: 8 additions & 0 deletions crates/matrix-sdk-base/src/rooms/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub use normal::{
use ruma::{
assign,
events::{
beacon_info::BeaconInfoEventContent,
call::member::CallMemberEventContent,
macros::EventContent,
room::{
Expand Down Expand Up @@ -81,6 +82,9 @@ impl fmt::Display for DisplayName {
pub struct BaseRoomInfo {
/// The avatar URL of this room.
pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
/// All shared live location beacons of this room.
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub(crate) beacons: BTreeMap<OwnedUserId, MinimalStateEvent<BeaconInfoEventContent>>,
/// The canonical alias of this room.
pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
/// The `m.room.create` event content of this room.
Expand Down Expand Up @@ -141,6 +145,9 @@ impl BaseRoomInfo {
/// Returns true if the event modified the info, false otherwise.
pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool {
match ev {
AnySyncStateEvent::BeaconInfo(b) => {
self.beacons.insert(b.state_key().clone(), b.into());
}
// No redacted branch - enabling encryption cannot be undone.
AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => {
self.encryption = Some(encryption.content.clone());
Expand Down Expand Up @@ -335,6 +342,7 @@ impl Default for BaseRoomInfo {
fn default() -> Self {
Self {
avatar: None,
beacons: BTreeMap::new(),
canonical_alias: None,
create: None,
dm_targets: Default::default(),
Expand Down
58 changes: 57 additions & 1 deletion crates/matrix-sdk-base/src/store/integration_tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
//! Trait and macro of integration tests for StateStore implementations.
use std::collections::{BTreeMap, BTreeSet};
use std::{
collections::{BTreeMap, BTreeSet},
time::Duration,
};

use assert_matches::assert_matches;
use assert_matches2::assert_let;
Expand All @@ -11,6 +14,7 @@ use ruma::{
api::{client::media::get_content_thumbnail::v3::Method, MatrixVersion},
event_id,
events::{
beacon_info::BeaconInfoEventContent,
presence::PresenceEvent,
receipt::{ReceiptThread, ReceiptType},
room::{
Expand Down Expand Up @@ -91,6 +95,8 @@ pub trait StateStoreIntegrationTests {
async fn test_send_queue(&self);
/// Test saving/restoring server capabilities.
async fn test_server_capabilities_saving(&self);
/// Test saving live location share beacons
async fn test_beacon_saving(&self);
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
Expand Down Expand Up @@ -1481,6 +1487,30 @@ impl StateStoreIntegrationTests for DynStateStore {
assert!(outstanding_rooms.iter().any(|room| room == room_id));
assert!(outstanding_rooms.iter().any(|room| room == room_id2));
}

async fn test_beacon_saving(&self) {
let room_id = room_id!("!test_beacon_saving:localhost");

let raw_event = custom_beacon_info_event(user_id(), true, 1234567890);
let event = raw_event.deserialize().unwrap();

assert!(self
.get_state_event(room_id, StateEventType::BeaconInfo, user_id().as_str())
.await
.unwrap()
.is_none());

let mut changes = StateChanges::default();
changes.add_state_event(room_id, event, raw_event);

self.save_changes(&changes).await.unwrap();

assert!(self
.get_state_event(room_id, StateEventType::BeaconInfo, user_id().as_str())
.await
.unwrap()
.is_some());
}
}

/// Macro building to allow your StateStore implementation to run the entire
Expand Down Expand Up @@ -1649,6 +1679,12 @@ macro_rules! statestore_integration_tests {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_send_queue().await;
}

#[async_test]
async fn test_beacon_saving() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_beacon_saving().await;
}
};
}

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

Raw::new(&ev_json).unwrap().cast()
}

fn custom_beacon_info_event(
user_id: &UserId,
live: bool,
duration_millis: u64,
) -> Raw<AnySyncStateEvent> {
let content =
BeaconInfoEventContent::new(None, Duration::from_millis(duration_millis), live, None);

let event = json!({
"event_id": "$h29iv0s8:example.com",
"content": content,
"sender": user_id,
"type": "org.matrix.msc3672.beacon_info",
"origin_server_ts": 0u64,
"state_key": user_id,
});

serde_json::from_value(event).unwrap()
}
1 change: 1 addition & 0 deletions crates/matrix-sdk-base/src/store/migration_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ impl BaseRoomInfoV1 {

Box::new(BaseRoomInfo {
avatar,
beacons: BTreeMap::new(),
canonical_alias,
create,
dm_targets,
Expand Down
33 changes: 33 additions & 0 deletions crates/matrix-sdk/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,39 @@ pub enum ImageError {
ThumbnailBiggerThanOriginal,
}

/// Errors that can happen when interacting with the beacon API.
#[derive(Debug, Error)]
pub enum BeaconError {
// A network error occurred.
#[error("Network error: {0}")]
Network(#[from] HttpError),

// The beacon information is not found.
#[error("Existing beacon information not found.")]
NotFound,

// The redacted event is not an error, but it's not useful for the client.
#[error("Beacon event is redacted and cannot be processed.")]
Redacted,

// The client must join the room to access the beacon information.
#[error("Must join the room to access beacon information.")]
Stripped,

#[error("Deserialization error: {0}")]
Deserialization(#[from] serde_json::Error),

// Allow for other errors to be wrapped.
#[error("Other error: {0}")]
Other(Box<Error>),
}

impl From<Error> for BeaconError {
fn from(err: Error) -> Self {
BeaconError::Other(Box::new(err))
}
}

/// Errors that can happen when refreshing an access token.
///
/// This is usually only returned by [`Client::refresh_access_token()`], unless
Expand Down
66 changes: 65 additions & 1 deletion 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_info::BeaconInfoEventContent,
call::notify::{ApplicationType, CallNotifyEventContent, NotifyType},
direct::DirectEventContent,
marked_unread::MarkedUnreadEventContent,
Expand Down Expand Up @@ -97,7 +98,7 @@ use crate::{
attachment::AttachmentConfig,
client::WeakClient,
config::RequestConfig,
error::WrongRoomState,
error::{BeaconError, WrongRoomState},
event_cache::{self, EventCacheDropHandles, RoomEventCache},
event_handler::{EventHandler, EventHandlerDropGuard, EventHandlerHandle, SyncEvent},
media::{MediaFormat, MediaRequest},
Expand Down Expand Up @@ -2668,6 +2669,69 @@ impl Room {
Ok(())
}

/// Start sharing live location in the room.
///
/// # Arguments
///
/// * `duration_millis` - The duration for which the live location is
/// shared, in milliseconds.
/// * `description` - An optional description for the live location share.
///
/// # Errors
///
/// Returns an error if the room is not joined or if the state event could
/// not be sent.
pub async fn start_live_location_share(
&self,
duration_millis: u64,
description: Option<String>,
) -> Result<send_state_event::v3::Response> {
self.ensure_room_joined()?;

self.send_state_event_for_key(
self.own_user_id(),
BeaconInfoEventContent::new(
description,
Duration::from_millis(duration_millis),
true,
None,
),
)
.await
}

/// Stop sharing live location in the room.
///
/// # Errors
///
/// Returns an error if the room is not joined, if the beacon information
/// is redacted or stripped, or if the state event is not found.
pub async fn stop_live_location_share(
&self,
) -> 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)),
}
} else {
Err(BeaconError::NotFound)
}
}

/// Send a call notification event in the current room.
///
/// This is only supposed to be used in **custom** situations where the user
Expand Down
57 changes: 55 additions & 2 deletions crates/matrix-sdk/tests/integration/room/joined.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ use matrix_sdk::{
};
use matrix_sdk_base::RoomState;
use matrix_sdk_test::{
async_test, test_json, test_json::sync::CUSTOM_ROOM_POWER_LEVELS, EphemeralTestEvent,
GlobalAccountDataTestEvent, JoinedRoomBuilder, SyncResponseBuilder, DEFAULT_TEST_ROOM_ID,
async_test, test_json,
test_json::sync::{CUSTOM_ROOM_POWER_LEVELS, LIVE_LOCATION_SHARING_SYNC},
EphemeralTestEvent, GlobalAccountDataTestEvent, JoinedRoomBuilder, SyncResponseBuilder,
DEFAULT_TEST_ROOM_ID,
};
use ruma::events::StateEventType;
use ruma::{
api::client::{membership::Invite3pidInit, receipt::create_receipt::v3::ReceiptType},
assign, event_id,
Expand Down Expand Up @@ -768,3 +771,53 @@ async fn test_make_reply_event_doesnt_require_event_cache() {
// make_edit_event works, even if the event cache hasn't been enabled.
room.make_edit_event(resp_event_id, EditedContent::RoomMessage(new_content)).await.unwrap();
}

#[async_test]
async fn test_start_live_location_share_for_room() {
let (client, server) = logged_in_client_with_server().await;

Mock::given(method("PUT"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/org.matrix.msc3672.beacon_info/.*"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID))
.mount(&server)
.await;

mock_sync(&server, &*test_json::SYNC, None).await;

let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));

let _response = client.sync_once(sync_settings).await.unwrap();

let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap();

let response = room.start_live_location_share(3000, None).await.unwrap();
assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id);
}

#[async_test]
async fn test_stop_sharing_live_location() {
let (client, server) = logged_in_client_with_server().await;

Mock::given(method("PUT"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/org.matrix.msc3672.beacon_info/.*"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID))
.mount(&server)
.await;

mock_sync(&server, &*LIVE_LOCATION_SHARING_SYNC, None).await;

let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));

let _response = client.sync_once(sync_settings).await.unwrap();

let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap();

let response = room.stop_live_location_share().await.unwrap();

assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id);

let state_events = room.get_state_events(StateEventType::BeaconInfo).await.unwrap();
assert_eq!(state_events.len(), 1);
}
Loading

0 comments on commit 5ddd6fd

Please sign in to comment.