Add active calls to RoomInfo

This commit is contained in:
Timo
2023-10-23 15:05:12 +02:00
committed by GitHub
parent a052f26748
commit 4933a50496
7 changed files with 269 additions and 11 deletions
Generated
+4 -4
View File
@@ -4886,9 +4886,9 @@ dependencies = [
[[package]]
name = "ruma"
version = "0.9.0"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85c63fe7f06396db1480c40cf44a57ad4359847cf6c6b3cdaa67507a00a79bb9"
checksum = "cc39664df66d707506b1dd318c30e600f25ebc1e7feadf4804ae45db67922c53"
dependencies = [
"assign",
"js_int",
@@ -4953,9 +4953,9 @@ dependencies = [
[[package]]
name = "ruma-events"
version = "0.27.4"
version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1784ded2966b990151d7d30a4533b737e1100ca9856178a999f3763a615a1efe"
checksum = "9135a0b84495fabbe46c477a1fcdef181001a215ec4b1f9215320cf980397636"
dependencies = [
"as_variant",
"indexmap 2.0.2",
+1 -1
View File
@@ -36,7 +36,7 @@ futures-executor = "0.3.21"
futures-util = { version = "0.3.26", default-features = false, features = ["alloc"] }
http = "0.2.6"
itertools = "0.11.0"
ruma = { version = "0.9.0", features = ["client-api-c", "compat-upload-signatures", "compat-user-id", "compat-arbitrary-length-ids"] }
ruma = { version = "0.9.2", features = ["client-api-c", "compat-upload-signatures", "compat-user-id", "compat-arbitrary-length-ids", "unstable-msc3401"] }
ruma-common = "0.12.0"
once_cell = "1.16.0"
serde = "1.0.151"
+18
View File
@@ -145,6 +145,24 @@ impl Room {
self.inner.state().into()
}
/// Is there a non expired membership with application "m.call" and scope
/// "m.room" in this room.
pub fn has_active_room_call(&self) -> bool {
self.inner.has_active_room_call()
}
/// Returns a Vec of userId's that participate in the room call.
///
/// matrix_rtc memberships with application "m.call" and scope "m.room" are
/// considered. A user can occur twice if they join with two devices.
/// convert to a set depending if the different users are required or the
/// amount of sessions.
///
/// The vector is ordered by oldest membership user to newest.
pub fn active_room_call_participants(&self) -> Vec<String> {
self.inner.active_room_call_participants().iter().map(|u| u.to_string()).collect()
}
pub fn inviter(&self) -> Option<Arc<RoomMember>> {
if self.inner.state() == RoomState::Invited {
RUNTIME.block_on(async move {
+8
View File
@@ -29,6 +29,8 @@ pub struct RoomInfo {
highlight_count: u64,
notification_count: u64,
user_defined_notification_mode: Option<RoomNotificationMode>,
has_room_call: bool,
active_room_call_participants: Vec<String>,
}
impl RoomInfo {
@@ -67,6 +69,12 @@ impl RoomInfo {
.user_defined_notification_mode()
.await
.map(Into::into),
has_room_call: room.has_active_room_call(),
active_room_call_participants: room
.active_room_call_participants()
.iter()
.map(|u| u.to_string())
.collect(),
})
}
}
+37 -1
View File
@@ -3,7 +3,11 @@
mod members;
pub(crate) mod normal;
use std::{collections::HashSet, fmt};
use std::{
collections::{BTreeMap, HashSet},
fmt,
hash::Hash,
};
use bitflags::bitflags;
pub use members::RoomMember;
@@ -11,6 +15,7 @@ pub use normal::{Room, RoomInfo, RoomState, RoomStateFilter};
use ruma::{
assign,
events::{
call::member::CallMemberEventContent,
macros::EventContent,
room::{
avatar::RoomAvatarEventContent,
@@ -95,6 +100,10 @@ pub struct BaseRoomInfo {
pub(crate) tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
/// The topic of this room.
pub(crate) topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
/// All Minimal state events that containing one or more running matrixRTC
/// memberships.
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub(crate) rtc_member: BTreeMap<OwnedUserId, MinimalStateEvent<CallMemberEventContent>>,
}
impl BaseRoomInfo {
@@ -166,6 +175,25 @@ impl BaseRoomInfo {
AnySyncStateEvent::RoomPowerLevels(p) => {
self.max_power_level = p.power_levels().max().into();
}
AnySyncStateEvent::CallMember(m) => {
let Some(o_ev) = m.as_original() else {
return false;
};
// we modify the event so that `origin_sever_ts` gets copied into
// `content.created_ts`
let mut o_ev = o_ev.clone();
o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts);
// add the new event.
self.rtc_member
.insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into());
// Remove all events that don't contain any memberships anymore.
self.rtc_member.retain(|_, ev| {
ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty())
});
}
_ => return false,
}
@@ -220,6 +248,11 @@ impl BaseRoomInfo {
AnyStrippedStateEvent::RoomPowerLevels(p) => {
self.max_power_level = p.power_levels().max().into();
}
AnyStrippedStateEvent::CallMember(_) => {
// Ignore stripped call state events. Rooms that are not in Joined or Left state
// wont have call information.
return false;
}
_ => return false,
}
@@ -248,6 +281,8 @@ impl BaseRoomInfo {
self.tombstone.as_mut().unwrap().redact(&room_version);
} else if self.topic.has_event_id(redacts) {
self.topic.as_mut().unwrap().redact(&room_version);
} else {
self.rtc_member.retain(|_, member_event| member_event.event_id() != Some(redacts));
}
}
}
@@ -281,6 +316,7 @@ impl Default for BaseRoomInfo {
name: None,
tombstone: None,
topic: None,
rtc_member: BTreeMap::new(),
}
}
}
+199 -4
View File
@@ -30,6 +30,7 @@ use ruma::events::AnySyncTimelineEvent;
use ruma::{
api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
events::{
call::member::Membership,
ignored_user_list::IgnoredUserListEventContent,
receipt::{Receipt, ReceiptThread, ReceiptType},
room::{
@@ -369,6 +370,24 @@ impl Room {
self.inner.read().topic().map(ToOwned::to_owned)
}
/// Is there a non expired membership with application "m.call" and scope
/// "m.room" in this room
pub fn has_active_room_call(&self) -> bool {
self.inner.read().has_active_room_call()
}
/// Returns a Vec of userId's that participate in the room call.
///
/// matrix_rtc memberships with application "m.call" and scope "m.room" are
/// considered. A user can occur twice if they join with two devices.
/// convert to a set depending if the different users are required or the
/// amount of sessions.
///
/// The vector is ordered by oldest membership user to newest.
pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
self.inner.read().active_room_call_participants()
}
/// Return the cached display name of the room if it was provided via sync,
/// or otherwise calculate it, taking into account its name, aliases and
/// members.
@@ -1016,6 +1035,59 @@ impl RoomInfo {
fn topic(&self) -> Option<&str> {
Some(&self.base_info.topic.as_ref()?.as_original()?.content.topic)
}
/// Get a list of all the valid (non expired) matrixRTC memberships and
/// associated UserId's in this room.
///
/// The vector is ordered by oldest membership to newest.
fn active_matrix_rtc_memberships(&self) -> Vec<(OwnedUserId, &Membership)> {
let mut v = self
.base_info
.rtc_member
.iter()
.filter_map(|(user_id, ev)| {
ev.as_original().map(|ev| {
ev.content
.active_memberships(None)
.into_iter()
.map(move |m| (user_id.clone(), m))
})
})
.flatten()
.collect::<Vec<_>>();
v.sort_by_key(|(_, m)| m.created_ts);
v
}
/// Similar to
/// [`matrix_rtc_memberships`](Self::active_matrix_rtc_memberships) but only
/// returns Memberships with application "m.call" and scope "m.room".
///
/// The vector is ordered by oldest membership user to newest.
fn active_room_call_memberships(&self) -> Vec<(OwnedUserId, &Membership)> {
self.active_matrix_rtc_memberships()
.into_iter()
.filter(|(_user_id, m)| m.is_room_call())
.collect()
}
/// Is there a non expired membership with application "m.call" and scope
/// "m.room" in this room.
pub fn has_active_room_call(&self) -> bool {
!self.active_room_call_memberships().is_empty()
}
/// Returns a Vec of userId's that participate in the room call.
///
/// matrix_rtc memberships with application "m.call" and scope "m.room" are
/// considered. A user can occur twice if they join with two devices.
/// convert to a set depending if the different users are required or the
/// amount of sessions.
///
/// The vector is ordered by oldest membership user to newest.
pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
self.active_room_call_memberships().iter().map(|(user_id, _)| user_id.clone()).collect()
}
}
#[cfg(feature = "experimental-sliding-sync")]
@@ -1105,15 +1177,24 @@ impl RoomStateFilter {
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::{
ops::Sub,
str::FromStr,
sync::Arc,
time::{Duration, SystemTime},
};
use assign::assign;
#[cfg(feature = "experimental-sliding-sync")]
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
use matrix_sdk_test::async_test;
use matrix_sdk_test::{async_test, ALICE, BOB, CAROL};
use ruma::{
api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
events::{
call::member::{
Application, CallApplicationContent, CallMemberEventContent, Focus, LivekitFocus,
Membership, MembershipInit, OriginalSyncCallMemberEvent,
},
room::{
canonical_alias::RoomCanonicalAliasEventContent,
member::{
@@ -1122,11 +1203,11 @@ mod tests {
},
name::RoomNameEventContent,
},
StateEventType,
AnySyncStateEvent, StateEventType, StateUnsigned, SyncStateEvent,
},
room_alias_id, room_id,
serde::Raw,
user_id, UserId,
user_id, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, UserId,
};
use serde_json::json;
@@ -1622,4 +1703,118 @@ mod tests {
Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(),
))
}
fn timestamp(minutes_ago: u32) -> MilliSecondsSinceUnixEpoch {
MilliSecondsSinceUnixEpoch::from_system_time(
SystemTime::now().sub(Duration::from_secs((60 * minutes_ago).into())),
)
.expect("date out of range")
}
fn call_member_state_event(
memberships: Vec<Membership>,
ev_id: &str,
user_id: &UserId,
) -> AnySyncStateEvent {
let content = CallMemberEventContent::new(memberships);
AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
content,
event_id: OwnedEventId::from_str(ev_id).unwrap(),
sender: user_id.to_owned(),
// we can simply use now here since this will be dropped when using a MinimalStateEvent
// in the roomInfo
origin_server_ts: timestamp(0),
state_key: user_id.to_owned(),
unsigned: StateUnsigned::new(),
}))
}
fn membership_for_my_call(
device_id: &str,
membership_id: &str,
minutes_ago: u32,
) -> Membership {
let application = Application::Call(CallApplicationContent::new(
"my_call_id_1".to_owned(),
ruma::events::call::member::CallScope::Room,
));
let foci_active = vec![Focus::Livekit(LivekitFocus::new(
"my_call_foci_alias".to_owned(),
"https://lk.org".to_owned(),
))];
assign!(
Membership::from(MembershipInit {
application,
device_id: device_id.to_owned(),
expires: Duration::from_millis(3_600_000),
foci_active,
membership_id: membership_id.to_owned(),
}),
{ created_ts: Some(timestamp(minutes_ago)) }
)
}
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
room.inner.update_if(|info| {
let mut res = false;
for ev in events {
res |= info.handle_state_event(ev);
}
res
});
}
/// `user_a`: empty memberships
/// `user_b`: one membership
/// `user_c`: two memberships (two devices)
fn create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
let (_, room) = make_room(RoomState::Joined);
let a_empty = call_member_state_event(Vec::new(), "$1234", a);
// make b 10min old
let m_init_b = membership_for_my_call("0", "0", 1);
let b_one = call_member_state_event(vec![m_init_b], "$12345", b);
// c1 1min old
let m_init_c1 = membership_for_my_call("0", "0", 10);
// c2 20min old
let m_init_c2 = membership_for_my_call("1", "0", 20);
let c_two = call_member_state_event(vec![m_init_c1, m_init_c2], "$123456", c);
// Intentionally use a non time sorted receive order.
receive_state_events(&room, vec![&c_two, &a_empty, &b_one]);
room
}
#[test]
fn show_correct_active_call_state() {
let room = create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
// This check also tests the ordering.
// We want older events to be in the front.
// user_b (Bob) is 1min old, c1 (CAROL) 10min old, c2 (CAROL) 20min old
assert_eq!(
vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
room.active_room_call_participants()
);
assert!(room.has_active_room_call());
}
#[test]
fn active_call_is_false_when_everyone_left() {
let room = create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
let b_empty_membership = call_member_state_event(Vec::new(), "$1234_1", &BOB);
let c_empty_membership = call_member_state_event(Vec::new(), "$12345_1", &CAROL);
receive_state_events(&room, vec![&b_empty_membership, &c_empty_membership]);
// We have no active call anymore after emptying the memberships
assert_eq!(Vec::<OwnedUserId>::new(), room.active_room_call_participants());
assert!(!room.has_active_room_call());
}
}
@@ -14,7 +14,7 @@
//! Data migration helpers for StateStore implementations.
use std::collections::HashSet;
use std::collections::{BTreeMap, HashSet};
#[cfg(feature = "experimental-sliding-sync")]
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
@@ -199,6 +199,7 @@ impl BaseRoomInfoV1 {
name,
tombstone,
topic,
rtc_member: BTreeMap::new(),
}
}
}