Skip to content

Commit

Permalink
feat(calls): add support for sending Matrix RTC call notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanceriu committed May 23, 2024
1 parent d7a8877 commit f5c8276
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 8 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ ruma = { version = "0.10.1", features = [
"compat-tag-info",
"unstable-msc3401",
"unstable-msc3266",
"unstable-msc4075"
] }
ruma-common = { version = "0.13.0" }
once_cell = "1.16.0"
Expand Down
13 changes: 12 additions & 1 deletion bindings/matrix-sdk-ffi/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ use ruma::events::{
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
};

use crate::{room_member::MembershipState, ruma::MessageType, ClientError};
use crate::{
room_member::MembershipState,
ruma::{MessageType, NotifyType},
ClientError,
};

#[derive(uniffi::Object)]
pub struct TimelineEvent(pub(crate) AnySyncTimelineEvent);
Expand Down Expand Up @@ -119,6 +123,7 @@ pub enum MessageLikeEventContent {
CallInvite,
CallHangup,
CallCandidates,
CallNotify { notify_type: NotifyType },
KeyVerificationReady,
KeyVerificationStart,
KeyVerificationCancel,
Expand All @@ -143,6 +148,12 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
AnySyncMessageLikeEvent::CallInvite(_) => MessageLikeEventContent::CallInvite,
AnySyncMessageLikeEvent::CallHangup(_) => MessageLikeEventContent::CallHangup,
AnySyncMessageLikeEvent::CallCandidates(_) => MessageLikeEventContent::CallCandidates,
AnySyncMessageLikeEvent::CallNotify(content) => {
let original_content = get_message_like_event_original_content(content)?;
MessageLikeEventContent::CallNotify {
notify_type: original_content.notify_type.into(),
}
}
AnySyncMessageLikeEvent::KeyVerificationReady(_) => {
MessageLikeEventContent::KeyVerificationReady
}
Expand Down
57 changes: 56 additions & 1 deletion bindings/matrix-sdk-ffi/src/room.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use ruma::{
api::client::room::report_content,
assign,
events::{
call::notify,
room::{
avatar::ImageInfo as RumaAvatarImageInfo,
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
Expand All @@ -30,7 +31,7 @@ use crate::{
event::{MessageLikeEventType, StateEventType},
room_info::RoomInfo,
room_member::RoomMember,
ruma::ImageInfo,
ruma::{ImageInfo, Mentions, NotifyType},
timeline::{EventTimelineItem, FocusEventError, ReceiptType, Timeline},
utils::u64_to_uint,
TaskHandle,
Expand Down Expand Up @@ -644,6 +645,48 @@ impl Room {
let event_id = EventId::parse(event_id)?;
Ok(self.inner.matrix_to_event_permalink(event_id).await?.to_string())
}

/// This will only send a call notification event if appropriate.
///
/// This function is supposed to be called whenever the user creates a room
/// call. It will send a `m.call.notify` event if:
/// - there is not yet a running call.
/// It will configure the notify type: ring or notify based on:
/// - is this a DM room -> ring
/// - is this a group with more than one other member -> notify
pub async fn send_call_notification_if_needed(&self) -> Result<(), ClientError> {
self.inner.send_call_notification_if_needed().await?;
Ok(())
}

/// Send a call notification event in the current room.
///
/// This is only supposed to be used in **custom** situations where the user
/// explicitly chooses to send a `m.call.notify` event to invite/notify
/// someone explicitly in unusual conditions. The default should be to
/// use `send_call_notification_if_necessary` just before a new room call is
/// created/joined.
///
/// One example could be that the UI allows to start a call with a subset of
/// users of the room members first. And then later on the user can
/// invite more users to the call.
pub async fn send_call_notification(
&self,
call_id: String,
application: RtcApplicationType,
notify_type: NotifyType,
mentions: Mentions,
) -> Result<(), ClientError> {
self.inner
.send_call_notification(
call_id,
application.into(),
notify_type.into(),
mentions.into(),
)
.await?;
Ok(())
}
}

/// Generates a `matrix.to` permalink to the given room alias.
Expand Down Expand Up @@ -770,3 +813,15 @@ impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
}))
}
}

#[derive(uniffi::Enum)]
pub enum RtcApplicationType {
Call,
}
impl From<RtcApplicationType> for notify::ApplicationType {
fn from(value: RtcApplicationType) -> Self {
match value {
RtcApplicationType::Call => notify::ApplicationType::Call,
}
}
}
25 changes: 25 additions & 0 deletions bindings/matrix-sdk-ffi/src/ruma.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use matrix_sdk::attachment::{
use ruma::{
assign,
events::{
call::notify::NotifyType as RumaNotifyType,
location::AssetType as RumaAssetType,
poll::start::PollKind as RumaPollKind,
room::{
Expand Down Expand Up @@ -375,6 +376,30 @@ impl From<RumaMessageType> for MessageType {
}
}

#[derive(Clone, uniffi::Enum)]
pub enum NotifyType {
Ring,
Notify,
}

impl From<RumaNotifyType> for NotifyType {
fn from(val: RumaNotifyType) -> Self {
match val {
RumaNotifyType::Ring => Self::Ring,
_ => Self::Notify,
}
}
}

impl From<NotifyType> for RumaNotifyType {
fn from(value: NotifyType) -> Self {
match value {
NotifyType::Ring => RumaNotifyType::Ring,
NotifyType::Notify => RumaNotifyType::Notify,
}
}
}

#[derive(Clone, uniffi::Record)]
pub struct EmoteMessageContent {
pub body: String,
Expand Down
1 change: 1 addition & 0 deletions crates/matrix-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Additions:
outbound session for that room. Can be used by clients as a dev tool like the `/discardsession` command.
- Add a new `LinkedChunk` data structure to represents all events per room ([#3166](https://github.com/matrix-org/matrix-rust-sdk/pull/3166)).
- Add new methods for tracking (on device only) the user's recently visited rooms called `Account::track_recently_visited_room(roomId)` and `Account::get_recently_visited_rooms()`
- Add `send_call_notification` and `send_call_notification_if_needed` methods. This allows to implement sending ring events on call start.

# 0.7.0

Expand Down
62 changes: 58 additions & 4 deletions crates/matrix-sdk/src/room/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ use ruma::{
},
assign,
events::{
call::notify::{ApplicationType, CallNotifyEventContent, NotifyType},
direct::DirectEventContent,
marked_unread::MarkedUnreadEventContent,
receipt::{Receipt, ReceiptThread, ReceiptType},
Expand All @@ -68,10 +69,11 @@ use ruma::{
space::{child::SpaceChildEventContent, parent::SpaceParentEventContent},
tag::{TagInfo, TagName},
typing::SyncTypingEvent,
AnyRoomAccountDataEvent, AnyTimelineEvent, EmptyStateKey, MessageLikeEventContent,
MessageLikeEventType, RedactContent, RedactedStateEventContent, RoomAccountDataEvent,
RoomAccountDataEventContent, RoomAccountDataEventType, StateEventContent, StateEventType,
StaticEventContent, StaticStateEventContent, SyncStateEvent,
AnyRoomAccountDataEvent, AnyTimelineEvent, EmptyStateKey, Mentions,
MessageLikeEventContent, MessageLikeEventType, RedactContent, RedactedStateEventContent,
RoomAccountDataEvent, RoomAccountDataEventContent, RoomAccountDataEventType,
StateEventContent, StateEventType, StaticEventContent, StaticStateEventContent,
SyncStateEvent,
},
push::{Action, PushConditionRoomCtx},
serde::Raw,
Expand Down Expand Up @@ -2627,6 +2629,58 @@ impl Room {
(maybe_room.unwrap(), drop_handles)
})
}

/// This will only send a call notification event if appropriate.
///
/// This function is supposed to be called whenever the user creates a room
/// call. It will send a `m.call.notify` event if:
/// - there is not yet a running call.
/// It will configure the notify type: ring or notify based on:
/// - is this a DM room -> ring
/// - is this a group with more than one other member -> notify
pub async fn send_call_notification_if_needed(&self) -> Result<()> {
if self.has_active_room_call() {
return Ok(());
}

self.send_call_notification(
self.room_id().to_string().to_owned(),
ApplicationType::Call,
if self.is_direct().await.unwrap_or(false) {
NotifyType::Ring
} else {
NotifyType::Notify
},
Mentions::with_room_mention(),
)
.await?;

Ok(())
}

/// Send a call notification event in the current room.
///
/// This is only supposed to be used in **custom** situations where the user
/// explicitly chooses to send a `m.call.notify` event to invite/notify
/// someone explicitly in unusual conditions. The default should be to
/// use `send_call_notification_if_needed` just before a new room call is
/// created/joined.
///
/// One example could be that the UI allows to start a call with a subset of
/// users of the room members first. And then later on the user can
/// invite more users to the call.
pub async fn send_call_notification(
&self,
call_id: String,
application: ApplicationType,
notify_type: NotifyType,
mentions: Mentions,
) -> Result<()> {
let call_notify_event_content =
CallNotifyEventContent::new(call_id, application, notify_type, mentions);
self.send(call_notify_event_content).await?;
Ok(())
}
}

/// Details of the (latest) invite.
Expand Down
89 changes: 87 additions & 2 deletions crates/matrix-sdk/tests/integration/room/joined.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ use matrix_sdk::{
use matrix_sdk_base::RoomState;
use matrix_sdk_test::{
async_test, test_json, test_json::sync::CUSTOM_ROOM_POWER_LEVELS, EphemeralTestEvent,
JoinedRoomBuilder, SyncResponseBuilder, DEFAULT_TEST_ROOM_ID,
GlobalAccountDataTestEvent, JoinedRoomBuilder, SyncResponseBuilder, DEFAULT_TEST_ROOM_ID,
};
use ruma::{
api::client::{membership::Invite3pidInit, receipt::create_receipt::v3::ReceiptType},
assign, event_id,
events::{receipt::ReceiptThread, room::message::RoomMessageEventContent, TimelineEventType},
int, mxc_uri, owned_event_id, room_id, thirdparty, user_id, OwnedUserId, TransactionId,
};
use serde_json::json;
use serde_json::{json, Value};
use wiremock::{
matchers::{body_json, body_partial_json, header, method, path_regex},
Mock, ResponseTemplate,
Expand Down Expand Up @@ -628,3 +628,88 @@ async fn test_reset_power_levels() {

room.reset_power_levels().await.unwrap();
}

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

let mut sync_builder = SyncResponseBuilder::new();
sync_builder.add_joined_room(JoinedRoomBuilder::default());
sync_builder.add_global_account_data_event(GlobalAccountDataTestEvent::Direct);

mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
mock_encryption_state(&server, false).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();
assert!(room.is_direct().await.unwrap());
assert!(!room.has_active_room_call());

Mock::given(method("PUT"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*"))
.and({
move |request: &wiremock::Request| {
let content: Value = request.body_json().expect("The body should be a JSON body");
assert_eq!(
content,
json!({
"application": "m.call",
"call_id": DEFAULT_TEST_ROOM_ID.to_string(),
"m.mentions": {"room" :true},
"notify_type": "ring"
}),
);
true
}
})
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"event_id": "$event_id"})))
.expect(1)
.mount(&server)
.await;

room.send_call_notification_if_needed().await.unwrap();
}

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

let mut sync_builder = SyncResponseBuilder::new();
sync_builder.add_joined_room(JoinedRoomBuilder::default());

mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
mock_encryption_state(&server, false).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();
assert!(!room.is_direct().await.unwrap());
assert!(!room.has_active_room_call());

Mock::given(method("PUT"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*"))
.and({
move |request: &wiremock::Request| {
let content: Value = request.body_json().expect("The body should be a JSON body");
assert_eq!(
content,
json!({
"application": "m.call",
"call_id": DEFAULT_TEST_ROOM_ID.to_string(),
"m.mentions": {"room" :true},
"notify_type": "notify"
}),
);
true
}
})
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"event_id": "$event_id"})))
.expect(1)
.mount(&server)
.await;

room.send_call_notification_if_needed().await.unwrap();
}

0 comments on commit f5c8276

Please sign in to comment.