Skip to content

Commit

Permalink
feat(beacons): unstable support for msc3489
Browse files Browse the repository at this point in the history
  • Loading branch information
torrybr committed Jul 2, 2024
1 parent 1c92633 commit 33816d2
Show file tree
Hide file tree
Showing 18 changed files with 684 additions and 24 deletions.
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,
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 @@ -159,6 +159,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(())
})
}

/// 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(),
};
};

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> {
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))) => {
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>>,
/// 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 @@ -197,6 +197,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.

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?")
}
}
}

/// 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.clone(),
value.beacon_info_event_content.live.clone(),
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

0 comments on commit 33816d2

Please sign in to comment.