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: implement MSC3489 live location sharing #3621

Closed
wants to merge 3 commits into from
Closed
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
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "e5a370f7e5fcebb0da6e4945e5
"compat-encrypted-stickers",
"unstable-msc3401",
"unstable-msc3266",
"unstable-msc4075"
"unstable-msc3488",
"unstable-msc3489",
"unstable-msc4075",
] }
ruma-common = { git = "https://github.com/ruma/ruma", rev = "e5a370f7e5fcebb0da6e4945e51c5fafba9aa5f0" }
serde = "1.0.151"
Expand Down
6 changes: 6 additions & 0 deletions bindings/matrix-sdk-ffi/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {

#[derive(uniffi::Enum)]
pub enum MessageLikeEventContent {
Beacon,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this contain the location too?

CallAnswer,
CallInvite,
CallNotify { notify_type: NotifyType },
Expand All @@ -144,6 +145,7 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {

fn try_from(value: AnySyncMessageLikeEvent) -> anyhow::Result<Self> {
let content = match value {
AnySyncMessageLikeEvent::Beacon(_) => MessageLikeEventContent::Beacon,
AnySyncMessageLikeEvent::CallAnswer(_) => MessageLikeEventContent::CallAnswer,
AnySyncMessageLikeEvent::CallInvite(_) => MessageLikeEventContent::CallInvite,
AnySyncMessageLikeEvent::CallNotify(content) => {
Expand Down Expand Up @@ -230,6 +232,7 @@ where

#[derive(Clone, uniffi::Enum)]
pub enum StateEventType {
BeaconInfo,
CallMember,
PolicyRuleRoom,
PolicyRuleServer,
Expand Down Expand Up @@ -257,6 +260,7 @@ pub enum StateEventType {
impl From<StateEventType> for ruma::events::StateEventType {
fn from(val: StateEventType) -> Self {
match val {
StateEventType::BeaconInfo => Self::BeaconInfo,
StateEventType::CallMember => Self::CallMember,
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
Expand Down Expand Up @@ -285,6 +289,7 @@ impl From<StateEventType> for ruma::events::StateEventType {

#[derive(Clone, uniffi::Enum)]
pub enum MessageLikeEventType {
Beacon,
CallAnswer,
CallCandidates,
CallHangup,
Expand Down Expand Up @@ -313,6 +318,7 @@ pub enum MessageLikeEventType {
impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
fn from(val: MessageLikeEventType) -> Self {
match val {
MessageLikeEventType::Beacon => Self::Beacon,
MessageLikeEventType::CallAnswer => Self::CallAnswer,
MessageLikeEventType::CallInvite => Self::CallInvite,
MessageLikeEventType::CallNotify => Self::CallNotify,
Expand Down
14 changes: 14 additions & 0 deletions bindings/matrix-sdk-ffi/src/room.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,20 @@ impl Room {
}
}

pub async fn send_beacon_info(&self, duration_millis: u64) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
self.inner.send_beacon_info(duration_millis).await?;
Ok(())
})
}

pub async fn stop_beacon_info(&self) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
self.inner.stop_beacon_info().await?;
Ok(())
})
Comment on lines +164 to +174
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In both cases: no need for RUNTIME.block_on since the methods are async.

}

/// Forces the currently active room key, which is used to encrypt messages,
/// to be rotated.
///
Expand Down
29 changes: 28 additions & 1 deletion bindings/matrix-sdk-ffi/src/timeline/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use ruma::events::{
use tracing::warn;

use super::ProfileDetails;
use crate::ruma::{ImageInfo, MessageType, PollKind};
use crate::ruma::{ImageInfo, LocationContent, MessageType, PollKind};

#[derive(Clone, uniffi::Object)]
pub struct TimelineItemContent(pub(crate) matrix_sdk_ui::timeline::TimelineItemContent);
Expand All @@ -44,6 +44,29 @@ impl TimelineItemContent {
source: Arc::new(MediaSource::from(content.source.clone())),
}
}
Content::BeaconInfoState(beacon_state) => {
let Some(location) = beacon_state.last_location() else {
return TimelineItemContentKind::FailedToParseMessageLike {
event_type: "org.matrix.msc3672.beacon".to_string(),
error: "Could not find beacon last location content".to_string(),
};
};
Comment on lines +48 to +53
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the last location was not set, then I think the TimelineItemContent should not exist in the first place.


let body = location.description.unwrap_or_else(|| "Location".to_string());

let location = LocationContent {
body,
geo_uri: location.uri,
description: None,
zoom_level: None,
asset: None,
};

TimelineItemContentKind::BeaconInfoState {
location,
user_id: String::from(beacon_state.user_id()),
}
}
Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()),
Content::CallInvite => TimelineItemContentKind::CallInvite,
Content::CallNotify => TimelineItemContentKind::CallNotify,
Expand Down Expand Up @@ -110,6 +133,10 @@ impl TimelineItemContent {

#[derive(uniffi::Enum)]
pub enum TimelineItemContentKind {
BeaconInfoState {
location: LocationContent,
user_id: String,
},
Message,
RedactedMessage,
Sticker {
Expand Down
60 changes: 55 additions & 5 deletions bindings/matrix-sdk-ffi/src/timeline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,21 @@ use as_variant::as_variant;
use content::{InReplyToDetails, RepliedToEventDetails};
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt as _};
use matrix_sdk::attachment::{
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
BaseThumbnailInfo, BaseVideoInfo, Thumbnail,
use matrix_sdk::{
attachment::{
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
BaseThumbnailInfo, BaseVideoInfo, Thumbnail,
},
deserialized_responses::SyncOrStrippedState,
};
use matrix_sdk_ui::timeline::{
EventItemOrigin, LiveBackPaginationStatus, Profile, RepliedToEvent, TimelineDetails,
};
use mime::Mime;
use ruma::{
events::{
beacon::BeaconEventContent,
beacon_info::BeaconInfoEventContent,
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
poll::{
unstable_end::UnstablePollEndEventContent,
Expand All @@ -44,15 +49,15 @@ use ruma::{
ForwardThread, LocationMessageEventContent, MessageType,
RoomMessageEventContentWithoutRelation,
},
AnyMessageLikeEventContent,
AnyMessageLikeEventContent, SyncStateEvent,
},
EventId, OwnedTransactionId,
};
use tokio::{
sync::Mutex,
task::{AbortHandle, JoinHandle},
};
use tracing::{error, warn};
use tracing::{error, info, warn};
use uuid::Uuid;

use self::content::{Reaction, ReactionSenderData, TimelineItemContent};
Expand Down Expand Up @@ -521,6 +526,51 @@ impl Timeline {
Ok(())
}

/// Sends a user's location as a beacon based on their last beacon_info
/// event.
///
/// Retrieves the last beacon_info from the room state and sends a beacon
/// with the geo_uri. Since only one active beacon_info per user per
/// room is allowed, we can grab the currently live beacon_info event
/// from the room state.
///
/// TODO: Does the logic belong in self.inner.room() or here?
pub async fn send_user_location_beacon(
self: Arc<Self>,
geo_uri: String,
) -> Result<(), ClientError> {
Comment on lines +538 to +541
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's way too much logic in this function; it should not live at the FFI layer, otherwise it's ~impossible to test.

let Some(raw_event) = self
.inner
.room()
.get_state_event_static_for_key::<BeaconInfoEventContent, _>(
self.inner.room().own_user_id(),
)
.await?
else {
todo!("How to handle case of missing beacon event for state key?")
};

match raw_event.deserialize() {
Ok(SyncOrStrippedState::Sync(SyncStateEvent::Original(beacon_info))) => {
torrybr marked this conversation as resolved.
Show resolved Hide resolved
if beacon_info.content.is_live() {
let beacon_event =
BeaconEventContent::new(beacon_info.event_id, geo_uri.clone(), None);
let message_content = AnyMessageLikeEventContent::Beacon(beacon_event.clone());

RUNTIME.spawn(async move {
self.inner.send(message_content).await;
});
}
}
Ok(SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_))) => {}
Ok(SyncOrStrippedState::Stripped(_)) => {}
Err(e) => {
info!(room_id = ?self.inner.room().room_id(), "Could not deserialize m.beacon_info: {e}");
}
}
Ok(())
}

pub async fn send_location(
self: Arc<Self>,
body: String,
Expand Down
7 changes: 7 additions & 0 deletions crates/matrix-sdk-base/src/rooms/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub use normal::{Room, RoomHero, RoomInfo, RoomInfoUpdate, RoomState, RoomStateF
use ruma::{
assign,
events::{
beacon_info::BeaconInfoEventContent,
call::member::CallMemberEventContent,
macros::EventContent,
room::{
Expand Down Expand Up @@ -78,6 +79,8 @@ impl fmt::Display for DisplayName {
pub struct BaseRoomInfo {
/// The avatar URL of this room.
pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub(crate) beacons: BTreeMap<OwnedUserId, MinimalStateEvent<BeaconInfoEventContent>>,
torrybr marked this conversation as resolved.
Show resolved Hide resolved
/// 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 @@ -191,6 +194,9 @@ impl BaseRoomInfo {
ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty())
});
}
AnySyncStateEvent::BeaconInfo(b) => {
self.beacons.insert(b.state_key().clone(), b.into());
}
_ => return false,
}

Expand Down Expand Up @@ -332,6 +338,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
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
97 changes: 97 additions & 0 deletions crates/matrix-sdk-ui/src/timeline/beacons.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//! This module handles rendering of MSC3489 live location sharing in the
//! timeline.
Comment on lines +1 to +2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think having this in the timeline is the proper cutting point. Instead, this would be more useful to have at the SDK layer, and maybe even with a slightly different API.


use std::collections::HashMap;

use ruma::{
events::{
beacon::BeaconEventContent, beacon_info::BeaconInfoEventContent, location::LocationContent,
FullStateEventContent,
},
EventId, OwnedEventId, OwnedUserId,
};

/// Holds the state of a beacon_info.
///
/// This struct should be created for each beacon_info event handled and then
/// updated whenever handling any beacon event that relates to the same
/// beacon_info event.
#[derive(Clone, Debug)]
pub struct BeaconState {
pub(super) beacon_info_event_content: BeaconInfoEventContent,
pub(super) last_location: Option<LocationContent>,
pub(super) user_id: OwnedUserId,
}

impl BeaconState {
pub(super) fn new(
content: FullStateEventContent<BeaconInfoEventContent>,
user_id: OwnedUserId,
) -> Self {
match &content {
FullStateEventContent::Original { content, .. } => BeaconState {
beacon_info_event_content: content.clone(),
last_location: None,
user_id,
},
FullStateEventContent::Redacted(_) => {
todo!("How should this be handled?")
torrybr marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

/// Update the state with the last known associated beacon info.
///
/// Used when a new beacon_info event is sent with the live field set
/// to false.
pub(super) fn update_beacon_info(&self, content: &BeaconInfoEventContent) -> Self {
let mut clone = self.clone();
clone.beacon_info_event_content = content.clone();
clone
}

/// Update the state with the last known associated beacon location.
pub(super) fn update_beacon(&self, content: &BeaconEventContent) -> Self {
let mut clone = self.clone();
clone.last_location = Some(content.location.clone());
clone
}

/// Get the last known beacon location.
pub fn last_location(&self) -> Option<LocationContent> {
self.last_location.clone()
}

/// Get the user_id of the user who sent the beacon_info event.
pub fn user_id(&self) -> OwnedUserId {
self.user_id.clone()
}
}

impl From<BeaconState> for BeaconInfoEventContent {
fn from(value: BeaconState) -> Self {
BeaconInfoEventContent::new(
value.beacon_info_event_content.description.clone(),
value.beacon_info_event_content.timeout,
value.beacon_info_event_content.live,
None,
)
}
}

/// Acts as a cache for beacons before their beacon_infos have been handled.
#[derive(Clone, Debug, Default)]
pub(super) struct BeaconPendingEvents {
pub(super) pending_beacons: HashMap<OwnedEventId, BeaconEventContent>,
}

impl BeaconPendingEvents {
pub(super) fn add_beacon(&mut self, start_id: &EventId, content: &BeaconEventContent) {
self.pending_beacons.insert(start_id.to_owned(), content.clone());
}
pub(super) fn apply(&mut self, beacon_info_event_id: &EventId, beacon_state: &mut BeaconState) {
if let Some(newest_beacon) = self.pending_beacons.get_mut(beacon_info_event_id) {
beacon_state.last_location = Some(newest_beacon.location.clone());
}
}
}
Loading
Loading