mod focus;
mod member_data;
mod member_state_key;
pub use focus::*;
pub use member_data::*;
pub use member_state_key::*;
use ruma_common::{MilliSecondsSinceUnixEpoch, OwnedDeviceId};
use ruma_macros::{EventContent, StringEnum};
use serde::{Deserialize, Serialize};
use crate::{
PossiblyRedactedStateEventContent, PrivOwnedStr, RedactContent, RedactedStateEventContent,
StateEventType,
};
#[derive(Clone, Debug, Serialize, Deserialize, EventContent, PartialEq)]
#[ruma_event(type = "org.matrix.msc3401.call.member", kind = State, state_key_type = CallMemberStateKey, custom_redacted, custom_possibly_redacted)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(untagged)]
pub enum CallMemberEventContent {
LegacyContent(LegacyMembershipContent),
SessionContent(SessionMembershipData),
Empty(EmptyMembershipData),
}
impl CallMemberEventContent {
pub fn new_legacy(memberships: Vec<LegacyMembershipData>) -> Self {
Self::LegacyContent(LegacyMembershipContent {
memberships, })
}
pub fn new(
application: Application,
device_id: OwnedDeviceId,
focus_active: ActiveFocus,
foci_preferred: Vec<Focus>,
created_ts: Option<MilliSecondsSinceUnixEpoch>,
) -> Self {
Self::SessionContent(SessionMembershipData {
application,
device_id,
focus_active,
foci_preferred,
created_ts,
})
}
pub fn new_empty(leave_reason: Option<LeaveReason>) -> Self {
Self::Empty(EmptyMembershipData { leave_reason })
}
pub fn active_memberships(
&self,
origin_server_ts: Option<MilliSecondsSinceUnixEpoch>,
) -> Vec<MembershipData<'_>> {
match self {
CallMemberEventContent::LegacyContent(content) => {
content.active_memberships(origin_server_ts)
}
CallMemberEventContent::SessionContent(content) => {
[content].map(MembershipData::Session).to_vec()
}
CallMemberEventContent::Empty(_) => Vec::new(),
}
}
pub fn memberships(&self) -> Vec<MembershipData<'_>> {
match self {
CallMemberEventContent::LegacyContent(content) => {
content.memberships.iter().map(MembershipData::Legacy).collect()
}
CallMemberEventContent::SessionContent(content) => {
[content].map(MembershipData::Session).to_vec()
}
CallMemberEventContent::Empty(_) => Vec::new(),
}
}
pub fn set_created_ts_if_none(&mut self, origin_server_ts: MilliSecondsSinceUnixEpoch) {
match self {
CallMemberEventContent::LegacyContent(content) => {
content.memberships.iter_mut().for_each(|m: &mut LegacyMembershipData| {
m.created_ts.get_or_insert(origin_server_ts);
});
}
CallMemberEventContent::SessionContent(m) => {
m.created_ts.get_or_insert(origin_server_ts);
}
_ => (),
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct EmptyMembershipData {
#[serde(skip_serializing_if = "Option::is_none")]
pub leave_reason: Option<LeaveReason>,
}
#[derive(Clone, PartialEq, StringEnum)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_enum(rename_all = "m.snake_case")]
pub enum LeaveReason {
LostConnection,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
impl RedactContent for CallMemberEventContent {
type Redacted = RedactedCallMemberEventContent;
fn redact(self, _version: &ruma_common::RoomVersionId) -> Self::Redacted {
RedactedCallMemberEventContent {}
}
}
pub type PossiblyRedactedCallMemberEventContent = CallMemberEventContent;
impl PossiblyRedactedStateEventContent for PossiblyRedactedCallMemberEventContent {
type StateKey = CallMemberStateKey;
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[allow(clippy::exhaustive_structs)]
pub struct RedactedCallMemberEventContent {}
impl ruma_events::content::EventContent for RedactedCallMemberEventContent {
type EventType = StateEventType;
fn event_type(&self) -> Self::EventType {
StateEventType::CallMember
}
}
impl RedactedStateEventContent for RedactedCallMemberEventContent {
type StateKey = CallMemberStateKey;
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct LegacyMembershipContent {
memberships: Vec<LegacyMembershipData>,
}
impl LegacyMembershipContent {
fn active_memberships(
&self,
origin_server_ts: Option<MilliSecondsSinceUnixEpoch>,
) -> Vec<MembershipData<'_>> {
self.memberships
.iter()
.filter(|m| !m.is_expired(origin_server_ts))
.map(MembershipData::Legacy)
.collect()
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use assert_matches2::assert_matches;
use ruma_common::{
device_id, owned_device_id, user_id, MilliSecondsSinceUnixEpoch as TS, OwnedEventId,
OwnedRoomId, OwnedUserId,
};
use serde_json::{from_value as from_json_value, json, Value as JsonValue};
use super::{
focus::{ActiveFocus, ActiveLivekitFocus, Focus, LivekitFocus},
member_data::{
Application, CallApplicationContent, CallScope, LegacyMembershipData, MembershipData,
},
CallMemberEventContent,
};
use crate::{
call::member::{EmptyMembershipData, FocusSelection, SessionMembershipData},
AnyStateEvent, StateEvent,
};
fn create_call_member_legacy_event_content() -> CallMemberEventContent {
CallMemberEventContent::new_legacy(vec![LegacyMembershipData {
application: Application::Call(CallApplicationContent {
call_id: "123456".to_owned(),
scope: CallScope::Room,
}),
device_id: owned_device_id!("ABCDE"),
expires: Duration::from_secs(3600),
foci_active: vec![Focus::Livekit(LivekitFocus {
alias: "1".to_owned(),
service_url: "https://livekit.com".to_owned(),
})],
membership_id: "0".to_owned(),
created_ts: None,
}])
}
fn create_call_member_event_content() -> CallMemberEventContent {
CallMemberEventContent::new(
Application::Call(CallApplicationContent {
call_id: "123456".to_owned(),
scope: CallScope::Room,
}),
owned_device_id!("ABCDE"),
ActiveFocus::Livekit(ActiveLivekitFocus {
focus_selection: FocusSelection::OldestMembership,
}),
vec![Focus::Livekit(LivekitFocus {
alias: "1".to_owned(),
service_url: "https://livekit.com".to_owned(),
})],
None,
)
}
#[test]
fn serialize_call_member_event_content() {
let call_member_event = &json!({
"application": "m.call",
"call_id": "123456",
"scope": "m.room",
"device_id": "ABCDE",
"foci_preferred": [
{
"livekit_alias": "1",
"livekit_service_url": "https://livekit.com",
"type": "livekit"
}
],
"focus_active":{
"type":"livekit",
"focus_selection":"oldest_membership"
}
});
assert_eq!(
call_member_event,
&serde_json::to_value(create_call_member_event_content()).unwrap()
);
let empty_call_member_event = &json!({});
assert_eq!(
empty_call_member_event,
&serde_json::to_value(CallMemberEventContent::Empty(EmptyMembershipData {
leave_reason: None
}))
.unwrap()
);
}
#[test]
fn serialize_legacy_call_member_event_content() {
let call_member_event = &json!({
"memberships": [
{
"application": "m.call",
"call_id": "123456",
"scope": "m.room",
"device_id": "ABCDE",
"expires": 3_600_000,
"foci_active": [
{
"livekit_alias": "1",
"livekit_service_url": "https://livekit.com",
"type": "livekit"
}
],
"membershipID": "0"
}
]
});
assert_eq!(
call_member_event,
&serde_json::to_value(create_call_member_legacy_event_content()).unwrap()
);
}
#[test]
fn deserialize_call_member_event_content() {
let call_member_ev = CallMemberEventContent::new(
Application::Call(CallApplicationContent {
call_id: "123456".to_owned(),
scope: CallScope::Room,
}),
owned_device_id!("THIS_DEVICE"),
ActiveFocus::Livekit(ActiveLivekitFocus {
focus_selection: FocusSelection::OldestMembership,
}),
vec![Focus::Livekit(LivekitFocus {
alias: "room1".to_owned(),
service_url: "https://livekit1.com".to_owned(),
})],
None,
);
let call_member_ev_json = json!({
"application": "m.call",
"call_id": "123456",
"scope": "m.room",
"device_id": "THIS_DEVICE",
"focus_active":{
"type": "livekit",
"focus_selection": "oldest_membership"
},
"foci_preferred": [
{
"livekit_alias": "room1",
"livekit_service_url": "https://livekit1.com",
"type": "livekit"
}
],
});
let ev_content: CallMemberEventContent =
serde_json::from_value(call_member_ev_json).unwrap();
assert_eq!(
serde_json::to_string(&ev_content).unwrap(),
serde_json::to_string(&call_member_ev).unwrap()
);
let empty = CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None });
assert_eq!(
serde_json::to_string(&json!({})).unwrap(),
serde_json::to_string(&empty).unwrap()
);
}
#[test]
fn deserialize_legacy_call_member_event_content() {
let call_member_ev = CallMemberEventContent::new_legacy(vec![
LegacyMembershipData {
application: Application::Call(CallApplicationContent {
call_id: "123456".to_owned(),
scope: CallScope::Room,
}),
device_id: owned_device_id!("THIS_DEVICE"),
expires: Duration::from_secs(3600),
foci_active: vec![Focus::Livekit(LivekitFocus {
alias: "room1".to_owned(),
service_url: "https://livekit1.com".to_owned(),
})],
membership_id: "0".to_owned(),
created_ts: None,
},
LegacyMembershipData {
application: Application::Call(CallApplicationContent {
call_id: "".to_owned(),
scope: CallScope::Room,
}),
device_id: owned_device_id!("OTHER_DEVICE"),
expires: Duration::from_secs(3600),
foci_active: vec![Focus::Livekit(LivekitFocus {
alias: "room2".to_owned(),
service_url: "https://livekit2.com".to_owned(),
})],
membership_id: "0".to_owned(),
created_ts: None,
},
]);
let call_member_ev_json = json!({
"memberships": [
{
"application": "m.call",
"call_id": "123456",
"scope": "m.room",
"device_id": "THIS_DEVICE",
"expires": 3_600_000,
"foci_active": [
{
"livekit_alias": "room1",
"livekit_service_url": "https://livekit1.com",
"type": "livekit"
}
],
"membershipID": "0",
},
{
"application": "m.call",
"call_id": "",
"scope": "m.room",
"device_id": "OTHER_DEVICE",
"expires": 3_600_000,
"foci_active": [
{
"livekit_alias": "room2",
"livekit_service_url": "https://livekit2.com",
"type": "livekit"
}
],
"membershipID": "0"
}
]
});
let ev_content: CallMemberEventContent =
serde_json::from_value(call_member_ev_json).unwrap();
assert_eq!(
serde_json::to_string(&ev_content).unwrap(),
serde_json::to_string(&call_member_ev).unwrap()
);
}
fn member_event_json(state_key: &str) -> JsonValue {
json!({
"content":{
"application": "m.call",
"call_id": "",
"scope": "m.room",
"device_id": "THIS_DEVICE",
"focus_active":{
"type": "livekit",
"focus_selection": "oldest_membership"
},
"foci_preferred": [
{
"livekit_alias": "room1",
"livekit_service_url": "https://livekit1.com",
"type": "livekit"
}
],
},
"type": "m.call.member",
"origin_server_ts": 111,
"event_id": "$3qfxjGYSu4sL25FtR0ep6vePOc",
"room_id": "!1234:example.org",
"sender": "@user:example.org",
"state_key": state_key,
"unsigned":{
"age":10,
"prev_content": {},
"prev_sender":"@user:example.org",
}
})
}
fn deserialize_member_event_helper(state_key: &str) {
let ev = member_event_json(state_key);
assert_matches!(
from_json_value(ev),
Ok(AnyStateEvent::CallMember(StateEvent::Original(member_event)))
);
let event_id = OwnedEventId::try_from("$3qfxjGYSu4sL25FtR0ep6vePOc").unwrap();
let sender = OwnedUserId::try_from("@user:example.org").unwrap();
let room_id = OwnedRoomId::try_from("!1234:example.org").unwrap();
assert_eq!(member_event.state_key.as_ref(), state_key);
assert_eq!(member_event.event_id, event_id);
assert_eq!(member_event.sender, sender);
assert_eq!(member_event.room_id, room_id);
assert_eq!(member_event.origin_server_ts, TS(js_int::UInt::new(111).unwrap()));
let membership = SessionMembershipData {
application: Application::Call(CallApplicationContent {
call_id: "".to_owned(),
scope: CallScope::Room,
}),
device_id: owned_device_id!("THIS_DEVICE"),
foci_preferred: [Focus::Livekit(LivekitFocus {
alias: "room1".to_owned(),
service_url: "https://livekit1.com".to_owned(),
})]
.to_vec(),
focus_active: ActiveFocus::Livekit(ActiveLivekitFocus {
focus_selection: FocusSelection::OldestMembership,
}),
created_ts: None,
};
assert_eq!(
member_event.content,
CallMemberEventContent::SessionContent(membership.clone())
);
assert_eq!(
member_event.content.active_memberships(None)[0],
vec![MembershipData::Session(&membership)][0]
);
assert_eq!(js_int::Int::new(10), member_event.unsigned.age);
assert_eq!(
CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None }),
member_event.unsigned.prev_content.unwrap()
);
}
#[test]
fn deserialize_member_event() {
deserialize_member_event_helper("@user:example.org");
}
#[test]
fn deserialize_member_event_with_scoped_state_key_prefixed() {
deserialize_member_event_helper("_@user:example.org_THIS_DEVICE");
}
#[test]
fn deserialize_member_event_with_scoped_state_key_unprefixed() {
deserialize_member_event_helper("@user:example.org_THIS_DEVICE");
}
fn timestamps() -> (TS, TS, TS) {
let now = TS::now();
let one_second_ago =
now.to_system_time().unwrap().checked_sub(Duration::from_secs(1)).unwrap();
let two_hours_ago =
now.to_system_time().unwrap().checked_sub(Duration::from_secs(60 * 60 * 2)).unwrap();
(
now,
TS::from_system_time(one_second_ago).unwrap(),
TS::from_system_time(two_hours_ago).unwrap(),
)
}
#[test]
fn legacy_memberships_do_expire() {
let content_legacy = create_call_member_legacy_event_content();
let (now, one_second_ago, two_hours_ago) = timestamps();
assert_eq!(
content_legacy.active_memberships(Some(one_second_ago)),
content_legacy.memberships()
);
assert_eq!(content_legacy.active_memberships(Some(now)), content_legacy.memberships());
assert_eq!(
content_legacy.active_memberships(Some(two_hours_ago)),
(vec![] as Vec<MembershipData<'_>>)
);
let content_session = create_call_member_event_content();
assert_eq!(
content_session.active_memberships(Some(one_second_ago)),
content_session.memberships()
);
assert_eq!(content_session.active_memberships(Some(now)), content_session.memberships());
assert_eq!(
content_session.active_memberships(Some(two_hours_ago)),
content_session.memberships()
);
}
#[test]
fn set_created_ts() {
let mut content_now = create_call_member_legacy_event_content();
let mut content_two_hours_ago = create_call_member_legacy_event_content();
let mut content_one_second_ago = create_call_member_legacy_event_content();
let (now, one_second_ago, two_hours_ago) = timestamps();
content_now.set_created_ts_if_none(now);
content_one_second_ago.set_created_ts_if_none(one_second_ago);
content_two_hours_ago.set_created_ts_if_none(two_hours_ago);
assert_eq!(content_now.active_memberships(None), content_now.memberships());
assert_eq!(
content_two_hours_ago.active_memberships(None),
vec![] as Vec<MembershipData<'_>>
);
assert_eq!(
content_one_second_ago.active_memberships(None),
content_one_second_ago.memberships()
);
content_two_hours_ago.set_created_ts_if_none(one_second_ago);
assert_eq!(
content_two_hours_ago.active_memberships(None),
vec![] as Vec<MembershipData<'_>>
);
}
#[test]
fn test_parse_rtc_member_event_key() {
assert!(from_json_value::<AnyStateEvent>(member_event_json("abc")).is_err());
assert!(from_json_value::<AnyStateEvent>(member_event_json("@nocolon")).is_err());
assert!(from_json_value::<AnyStateEvent>(member_event_json("@noserverpart:")).is_err());
assert!(
from_json_value::<AnyStateEvent>(member_event_json("@noserverpart:_suffix")).is_err()
);
let user_id = user_id!("@username:example.org").as_str();
let device_id = device_id!("VALID_DEVICE_ID").as_str();
let parse_result = from_json_value::<AnyStateEvent>(member_event_json(user_id));
assert_matches!(parse_result, Ok(_));
assert_matches!(
from_json_value::<AnyStateEvent>(member_event_json(&format!("{user_id}_{device_id}"))),
Ok(_)
);
assert_matches!(
from_json_value::<AnyStateEvent>(member_event_json(&format!(
"{user_id}:invalid_suffix"
))),
Err(_)
);
assert_matches!(
from_json_value::<AnyStateEvent>(member_event_json(&format!("_{user_id}"))),
Err(_)
);
assert_matches!(
from_json_value::<AnyStateEvent>(member_event_json(&format!("_{user_id}_{device_id}"))),
Ok(_)
);
assert_matches!(
from_json_value::<AnyStateEvent>(member_event_json(&format!(
"_{user_id}:invalid_suffix"
))),
Err(_)
);
assert_matches!(
from_json_value::<AnyStateEvent>(member_event_json(&format!("{user_id}_"))),
Err(_)
);
}
}