1mod focus;
8mod member_data;
9mod member_state_key;
10
11use std::time::Duration;
12
13pub use focus::*;
14pub use member_data::*;
15pub use member_state_key::*;
16use ruma_common::{MilliSecondsSinceUnixEpoch, OwnedDeviceId, room_version_rules::RedactionRules};
17use ruma_macros::{EventContent, StringEnum};
18use serde::{Deserialize, Serialize};
19
20use crate::{
21 PossiblyRedactedStateEventContent, PrivOwnedStr, RedactContent, RedactedStateEventContent,
22 StateEventType, StaticEventContent,
23};
24
25#[derive(Clone, Debug, Serialize, Deserialize, EventContent, PartialEq)]
36#[ruma_event(type = "org.matrix.msc3401.call.member", kind = State, state_key_type = CallMemberStateKey, custom_redacted, custom_possibly_redacted)]
37#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
38#[serde(untagged)]
39pub enum CallMemberEventContent {
40 LegacyContent(LegacyMembershipContent),
43 SessionContent(SessionMembershipData),
46 Empty(EmptyMembershipData),
48}
49
50impl CallMemberEventContent {
51 pub fn new_legacy(memberships: Vec<LegacyMembershipData>) -> Self {
53 Self::LegacyContent(LegacyMembershipContent {
54 memberships, })
56 }
57
58 pub fn new(
70 application: Application,
71 device_id: OwnedDeviceId,
72 focus_active: ActiveFocus,
73 foci_preferred: Vec<Focus>,
74 created_ts: Option<MilliSecondsSinceUnixEpoch>,
75 expires: Option<Duration>,
76 ) -> Self {
77 Self::SessionContent(SessionMembershipData {
78 application,
79 device_id,
80 focus_active,
81 foci_preferred,
82 created_ts,
83 expires: expires.unwrap_or(Duration::from_secs(14_400)), })
85 }
86
87 pub fn new_empty(leave_reason: Option<LeaveReason>) -> Self {
89 Self::Empty(EmptyMembershipData { leave_reason })
90 }
91
92 pub fn active_memberships(
104 &self,
105 origin_server_ts: Option<MilliSecondsSinceUnixEpoch>,
106 ) -> Vec<MembershipData<'_>> {
107 match self {
108 CallMemberEventContent::LegacyContent(content) => content
109 .memberships
110 .iter()
111 .map(MembershipData::Legacy)
112 .filter(|m| !m.is_expired(origin_server_ts))
113 .collect(),
114 CallMemberEventContent::SessionContent(content) => {
115 vec![MembershipData::Session(content)]
116 .into_iter()
117 .filter(|m| !m.is_expired(origin_server_ts))
118 .collect()
119 }
120
121 CallMemberEventContent::Empty(_) => Vec::new(),
122 }
123 }
124
125 pub fn memberships(&self) -> Vec<MembershipData<'_>> {
128 match self {
129 CallMemberEventContent::LegacyContent(content) => {
130 content.memberships.iter().map(MembershipData::Legacy).collect()
131 }
132 CallMemberEventContent::SessionContent(content) => {
133 [content].map(MembershipData::Session).to_vec()
134 }
135 CallMemberEventContent::Empty(_) => Vec::new(),
136 }
137 }
138
139 pub fn set_created_ts_if_none(&mut self, origin_server_ts: MilliSecondsSinceUnixEpoch) {
148 match self {
149 CallMemberEventContent::LegacyContent(content) => {
150 content.memberships.iter_mut().for_each(|m: &mut LegacyMembershipData| {
151 m.created_ts.get_or_insert(origin_server_ts);
152 });
153 }
154 CallMemberEventContent::SessionContent(m) => {
155 m.created_ts.get_or_insert(origin_server_ts);
156 }
157 _ => (),
158 }
159 }
160}
161
162#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
164#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
165pub struct EmptyMembershipData {
166 #[serde(skip_serializing_if = "Option::is_none")]
169 pub leave_reason: Option<LeaveReason>,
170}
171
172#[derive(Clone, StringEnum)]
178#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
179#[ruma_enum(rename_all(prefix = "m.", rule = "snake_case"))]
180pub enum LeaveReason {
181 LostConnection,
184 #[doc(hidden)]
185 _Custom(PrivOwnedStr),
186}
187
188impl RedactContent for CallMemberEventContent {
189 type Redacted = RedactedCallMemberEventContent;
190
191 fn redact(self, _rules: &RedactionRules) -> Self::Redacted {
192 RedactedCallMemberEventContent {}
193 }
194}
195
196pub type PossiblyRedactedCallMemberEventContent = CallMemberEventContent;
201
202impl PossiblyRedactedStateEventContent for PossiblyRedactedCallMemberEventContent {
203 type StateKey = CallMemberStateKey;
204
205 fn event_type(&self) -> StateEventType {
206 StateEventType::CallMember
207 }
208}
209
210#[derive(Clone, Debug, Deserialize, Serialize)]
212#[allow(clippy::exhaustive_structs)]
213pub struct RedactedCallMemberEventContent {}
214
215impl RedactedStateEventContent for RedactedCallMemberEventContent {
216 type StateKey = CallMemberStateKey;
217
218 fn event_type(&self) -> StateEventType {
219 StateEventType::CallMember
220 }
221}
222
223impl StaticEventContent for RedactedCallMemberEventContent {
224 const TYPE: &'static str = CallMemberEventContent::TYPE;
225 type IsPrefix = <CallMemberEventContent as StaticEventContent>::IsPrefix;
226}
227
228impl From<RedactedCallMemberEventContent> for PossiblyRedactedCallMemberEventContent {
229 fn from(_value: RedactedCallMemberEventContent) -> Self {
230 Self::new_empty(None)
231 }
232}
233
234#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
236#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
237pub struct LegacyMembershipContent {
238 memberships: Vec<LegacyMembershipData>,
249}
250
251#[cfg(test)]
252mod tests {
253 use std::time::Duration;
254
255 use assert_matches2::assert_matches;
256 use ruma_common::{
257 MilliSecondsSinceUnixEpoch as TS, OwnedEventId, OwnedRoomId, OwnedUserId, device_id,
258 owned_device_id, user_id,
259 };
260 use serde_json::{Value as JsonValue, from_value as from_json_value, json};
261
262 use super::{
263 CallMemberEventContent,
264 focus::{ActiveFocus, ActiveLivekitFocus, Focus, LivekitFocus},
265 member_data::{
266 Application, CallApplicationContent, CallScope, LegacyMembershipData, MembershipData,
267 },
268 };
269 use crate::{
270 AnyStateEvent, StateEvent,
271 call::member::{EmptyMembershipData, FocusSelection, SessionMembershipData},
272 rtc::notification::CallIntent,
273 };
274
275 fn create_call_member_legacy_event_content() -> CallMemberEventContent {
276 CallMemberEventContent::new_legacy(vec![LegacyMembershipData {
277 application: Application::Call(CallApplicationContent::new(
278 "123456".to_owned(),
279 CallScope::Room,
280 )),
281 device_id: owned_device_id!("ABCDE"),
282 expires: Duration::from_secs(3600),
283 foci_active: vec![Focus::Livekit(LivekitFocus {
284 alias: "1".to_owned(),
285 service_url: "https://livekit.com".to_owned(),
286 })],
287 membership_id: "0".to_owned(),
288 created_ts: None,
289 }])
290 }
291
292 fn create_call_member_event_content() -> CallMemberEventContent {
293 CallMemberEventContent::new(
294 Application::Call(CallApplicationContent::new("123456".to_owned(), CallScope::Room)),
295 owned_device_id!("ABCDE"),
296 ActiveFocus::Livekit(ActiveLivekitFocus {
297 focus_selection: FocusSelection::OldestMembership,
298 }),
299 vec![Focus::Livekit(LivekitFocus {
300 alias: "1".to_owned(),
301 service_url: "https://livekit.com".to_owned(),
302 })],
303 None,
304 Duration::from_secs(3600).into(), )
306 }
307
308 #[test]
309 fn serialize_call_member_event_content() {
310 let call_member_event = &json!({
311 "application": "m.call",
312 "call_id": "123456",
313 "scope": "m.room",
314 "device_id": "ABCDE",
315 "expires": 3_600_000, "foci_preferred": [
317 {
318 "livekit_alias": "1",
319 "livekit_service_url": "https://livekit.com",
320 "type": "livekit"
321 }
322 ],
323 "focus_active":{
324 "type":"livekit",
325 "focus_selection":"oldest_membership"
326 }
327 });
328 assert_eq!(
329 call_member_event,
330 &serde_json::to_value(create_call_member_event_content()).unwrap()
331 );
332
333 let empty_call_member_event = &json!({});
334 assert_eq!(
335 empty_call_member_event,
336 &serde_json::to_value(CallMemberEventContent::Empty(EmptyMembershipData {
337 leave_reason: None
338 }))
339 .unwrap()
340 );
341 }
342
343 #[test]
344 fn serialize_legacy_call_member_event_content() {
345 let call_member_event = &json!({
346 "memberships": [
347 {
348 "application": "m.call",
349 "call_id": "123456",
350 "scope": "m.room",
351 "device_id": "ABCDE",
352 "expires": 3_600_000,
353 "foci_active": [
354 {
355 "livekit_alias": "1",
356 "livekit_service_url": "https://livekit.com",
357 "type": "livekit"
358 }
359 ],
360 "membershipID": "0"
361 }
362 ]
363 });
364
365 assert_eq!(
366 call_member_event,
367 &serde_json::to_value(create_call_member_legacy_event_content()).unwrap()
368 );
369 }
370 #[test]
371 fn deserialize_call_member_event_content() {
372 let call_member_ev = CallMemberEventContent::new(
373 Application::Call(CallApplicationContent::new("123456".to_owned(), CallScope::Room)),
374 owned_device_id!("THIS_DEVICE"),
375 ActiveFocus::Livekit(ActiveLivekitFocus {
376 focus_selection: FocusSelection::OldestMembership,
377 }),
378 vec![Focus::Livekit(LivekitFocus {
379 alias: "room1".to_owned(),
380 service_url: "https://livekit1.com".to_owned(),
381 })],
382 None,
383 None,
384 );
385
386 let call_member_ev_json = json!({
387 "application": "m.call",
388 "call_id": "123456",
389 "scope": "m.room",
390 "expires": 14_400_000, "device_id": "THIS_DEVICE",
392 "focus_active":{
393 "type": "livekit",
394 "focus_selection": "oldest_membership"
395 },
396 "foci_preferred": [
397 {
398 "livekit_alias": "room1",
399 "livekit_service_url": "https://livekit1.com",
400 "type": "livekit"
401 }
402 ],
403 });
404
405 let ev_content: CallMemberEventContent =
406 serde_json::from_value(call_member_ev_json).unwrap();
407 assert_eq!(
408 serde_json::to_string(&ev_content).unwrap(),
409 serde_json::to_string(&call_member_ev).unwrap()
410 );
411 let empty = CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None });
412 assert_eq!(
413 serde_json::to_string(&json!({})).unwrap(),
414 serde_json::to_string(&empty).unwrap()
415 );
416 }
417
418 #[test]
419 #[cfg(feature = "unstable-msc4075")]
420 fn deserialize_event_with_call_intent() {
421 let call_member_ev = CallMemberEventContent::new(
422 Application::Call(CallApplicationContent {
423 call_id: "".to_owned(),
424 scope: CallScope::Room,
425 call_intent: Some(CallIntent::Audio),
426 }),
427 owned_device_id!("THIS_DEVICE"),
428 ActiveFocus::Livekit(ActiveLivekitFocus {
429 focus_selection: FocusSelection::OldestMembership,
430 }),
431 vec![Focus::Livekit(LivekitFocus {
432 alias: "room1".to_owned(),
433 service_url: "https://livekit1.com".to_owned(),
434 })],
435 None,
436 None,
437 );
438
439 let json = json!({
440 "application": "m.call",
441 "call_id": "",
442 "scope": "m.room",
443 "m.call.intent": "audio",
444 "device_id": "THIS_DEVICE",
445 "foci_preferred": [
446 {
447 "type": "livekit",
448 "livekit_alias": "room1",
449 "livekit_service_url": "https://livekit1.com"
450 }
451 ],
452 "focus_active": {
453 "type": "livekit",
454 "focus_selection": "oldest_membership"
455 },
456 "expires": 14_400_000
457 });
458
459 let ev_content: CallMemberEventContent = serde_json::from_value(json).unwrap();
460 assert_eq!(
461 serde_json::to_string(&ev_content).unwrap(),
462 serde_json::to_string(&call_member_ev).unwrap()
463 );
464 }
465
466 #[test]
467 #[cfg(feature = "unstable-msc4075")]
468 fn deserialize_application() {
469 let test_cases = vec![
470 (
471 Application::Call(CallApplicationContent {
472 call_id: "".to_owned(),
473 scope: CallScope::Room,
474 call_intent: None,
475 }),
476 json!({
477 "application": "m.call",
478 "call_id": "",
479 "scope": "m.room",
480 }),
481 ),
482 (
483 Application::Call(CallApplicationContent {
484 call_id: "".to_owned(),
485 scope: CallScope::Room,
486 call_intent: Some(CallIntent::Audio),
487 }),
488 json!({
489 "application": "m.call",
490 "call_id": "",
491 "scope": "m.room",
492 "m.call.intent": "audio"
493 }),
494 ),
495 (
496 Application::Call(CallApplicationContent {
497 call_id: "xxxx".to_owned(),
498 scope: CallScope::User,
499 call_intent: Some(CallIntent::Video),
500 }),
501 json!({
502 "application": "m.call",
503 "call_id": "xxxx",
504 "scope": "m.user",
505 "m.call.intent": "video"
506 }),
507 ),
508 ];
509
510 for (model, jon) in test_cases {
511 let app: Application = serde_json::from_value(jon).unwrap();
512 assert_eq!(
513 serde_json::to_string(&app).unwrap(),
514 serde_json::to_string(&model).unwrap()
515 );
516 }
517 }
518
519 #[test]
520 fn deserialize_legacy_call_member_event_content() {
521 let call_member_ev = CallMemberEventContent::new_legacy(vec![
522 LegacyMembershipData {
523 application: Application::Call(CallApplicationContent::new(
524 "123456".to_owned(),
525 CallScope::Room,
526 )),
527 device_id: owned_device_id!("THIS_DEVICE"),
528 expires: Duration::from_secs(3600),
529 foci_active: vec![Focus::Livekit(LivekitFocus {
530 alias: "room1".to_owned(),
531 service_url: "https://livekit1.com".to_owned(),
532 })],
533 membership_id: "0".to_owned(),
534 created_ts: None,
535 },
536 LegacyMembershipData {
537 application: Application::Call(CallApplicationContent::new(
538 "".to_owned(),
539 CallScope::Room,
540 )),
541 device_id: owned_device_id!("OTHER_DEVICE"),
542 expires: Duration::from_secs(3600),
543 foci_active: vec![Focus::Livekit(LivekitFocus {
544 alias: "room2".to_owned(),
545 service_url: "https://livekit2.com".to_owned(),
546 })],
547 membership_id: "0".to_owned(),
548 created_ts: None,
549 },
550 ]);
551
552 let call_member_ev_json = json!({
553 "memberships": [
554 {
555 "application": "m.call",
556 "call_id": "123456",
557 "scope": "m.room",
558 "device_id": "THIS_DEVICE",
559 "expires": 3_600_000,
560 "foci_active": [
561 {
562 "livekit_alias": "room1",
563 "livekit_service_url": "https://livekit1.com",
564 "type": "livekit"
565 }
566 ],
567 "membershipID": "0",
568 },
569 {
570 "application": "m.call",
571 "call_id": "",
572 "scope": "m.room",
573 "device_id": "OTHER_DEVICE",
574 "expires": 3_600_000,
575 "foci_active": [
576 {
577 "livekit_alias": "room2",
578 "livekit_service_url": "https://livekit2.com",
579 "type": "livekit"
580 }
581 ],
582 "membershipID": "0"
583 }
584 ]
585 });
586
587 let ev_content: CallMemberEventContent =
588 serde_json::from_value(call_member_ev_json).unwrap();
589 assert_eq!(
590 serde_json::to_string(&ev_content).unwrap(),
591 serde_json::to_string(&call_member_ev).unwrap()
592 );
593 }
594
595 fn member_event_json(state_key: &str) -> JsonValue {
596 json!({
597 "content":{
598 "expires": 3_600_000, "application": "m.call",
600 "call_id": "",
601 "scope": "m.room",
602 "device_id": "THIS_DEVICE",
603 "focus_active":{
604 "type": "livekit",
605 "focus_selection": "oldest_membership"
606 },
607 "foci_preferred": [
608 {
609 "livekit_alias": "room1",
610 "livekit_service_url": "https://livekit1.com",
611 "type": "livekit"
612 }
613 ],
614 },
615 "type": "m.call.member",
616 "origin_server_ts": 111,
617 "event_id": "$3qfxjGYSu4sL25FtR0ep6vePOc",
618 "room_id": "!1234:example.org",
619 "sender": "@user:example.org",
620 "state_key": state_key,
621 "unsigned":{
622 "age":10,
623 "prev_content": {},
624 "prev_sender":"@user:example.org",
625 }
626 })
627 }
628
629 fn deserialize_member_event_helper(state_key: &str) {
630 let ev = member_event_json(state_key);
631
632 assert_matches!(
633 from_json_value(ev),
634 Ok(AnyStateEvent::CallMember(StateEvent::Original(member_event)))
635 );
636
637 let event_id = OwnedEventId::try_from("$3qfxjGYSu4sL25FtR0ep6vePOc").unwrap();
638 let sender = OwnedUserId::try_from("@user:example.org").unwrap();
639 let room_id = OwnedRoomId::try_from("!1234:example.org").unwrap();
640 assert_eq!(member_event.state_key.as_ref(), state_key);
641 assert_eq!(member_event.event_id, event_id);
642 assert_eq!(member_event.sender, sender);
643 assert_eq!(member_event.room_id, room_id);
644 assert_eq!(member_event.origin_server_ts, TS(js_int::UInt::new(111).unwrap()));
645 let membership = SessionMembershipData {
646 application: Application::Call(CallApplicationContent::new(
647 "".to_owned(),
648 CallScope::Room,
649 )),
650 device_id: owned_device_id!("THIS_DEVICE"),
651 foci_preferred: [Focus::Livekit(LivekitFocus {
652 alias: "room1".to_owned(),
653 service_url: "https://livekit1.com".to_owned(),
654 })]
655 .to_vec(),
656 focus_active: ActiveFocus::Livekit(ActiveLivekitFocus {
657 focus_selection: FocusSelection::OldestMembership,
658 }),
659 created_ts: None,
660 expires: Duration::from_secs(3600),
661 };
662 assert_eq!(
663 member_event.content,
664 CallMemberEventContent::SessionContent(membership.clone())
665 );
666
667 assert_eq!(
669 member_event.content.active_memberships(None)[0],
670 vec![MembershipData::Session(&membership)][0]
671 );
672 assert_eq!(js_int::Int::new(10), member_event.unsigned.age);
673 assert_eq!(
674 CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None }),
675 member_event.unsigned.prev_content.unwrap()
676 );
677
678 }
681
682 #[test]
683 fn deserialize_member_event() {
684 deserialize_member_event_helper("@user:example.org");
685 }
686
687 #[test]
688 fn deserialize_member_event_with_scoped_state_key_prefixed() {
689 deserialize_member_event_helper("_@user:example.org_THIS_DEVICE_m.call");
690 }
691
692 #[test]
693 fn deserialize_member_event_with_scoped_state_key_unprefixed() {
694 deserialize_member_event_helper("@user:example.org_THIS_DEVICE_m.call");
695 }
696
697 fn timestamps() -> (TS, TS, TS) {
698 let now = TS::now();
699 let one_second_ago =
700 now.to_system_time().unwrap().checked_sub(Duration::from_secs(1)).unwrap();
701 let two_hours_ago =
702 now.to_system_time().unwrap().checked_sub(Duration::from_secs(60 * 60 * 2)).unwrap();
703 (
704 now,
705 TS::from_system_time(one_second_ago).unwrap(),
706 TS::from_system_time(two_hours_ago).unwrap(),
707 )
708 }
709
710 #[test]
711 fn legacy_memberships_do_expire() {
712 let content_legacy = create_call_member_legacy_event_content();
713 let (now, one_second_ago, two_hours_ago) = timestamps();
714
715 assert_eq!(
716 content_legacy.active_memberships(Some(one_second_ago)),
717 content_legacy.memberships()
718 );
719 assert_eq!(content_legacy.active_memberships(Some(now)), content_legacy.memberships());
720 assert_eq!(
721 content_legacy.active_memberships(Some(two_hours_ago)),
722 (vec![] as Vec<MembershipData<'_>>)
723 );
724 }
725
726 #[test]
727 fn session_membership_does_expire() {
728 let content = create_call_member_event_content();
729 let (now, one_second_ago, two_hours_ago) = timestamps();
730
731 assert_eq!(content.active_memberships(Some(now)), content.memberships());
732 assert_eq!(content.active_memberships(Some(one_second_ago)), content.memberships());
733 assert_eq!(
734 content.active_memberships(Some(two_hours_ago)),
735 (vec![] as Vec<MembershipData<'_>>)
736 );
737 }
738
739 #[test]
740 fn set_created_ts() {
741 let mut content_now = create_call_member_legacy_event_content();
742 let mut content_two_hours_ago = create_call_member_legacy_event_content();
743 let mut content_one_second_ago = create_call_member_legacy_event_content();
744 let (now, one_second_ago, two_hours_ago) = timestamps();
745
746 content_now.set_created_ts_if_none(now);
747 content_one_second_ago.set_created_ts_if_none(one_second_ago);
748 content_two_hours_ago.set_created_ts_if_none(two_hours_ago);
749 assert_eq!(content_now.active_memberships(None), content_now.memberships());
750
751 assert_eq!(
752 content_two_hours_ago.active_memberships(None),
753 vec![] as Vec<MembershipData<'_>>
754 );
755 assert_eq!(
756 content_one_second_ago.active_memberships(None),
757 content_one_second_ago.memberships()
758 );
759
760 content_two_hours_ago.set_created_ts_if_none(one_second_ago);
762 assert_eq!(
764 content_two_hours_ago.active_memberships(None),
765 vec![] as Vec<MembershipData<'_>>
766 );
767 }
768
769 #[test]
770 fn test_parse_rtc_member_event_key() {
771 assert!(from_json_value::<AnyStateEvent>(member_event_json("abc")).is_err());
772 assert!(from_json_value::<AnyStateEvent>(member_event_json("@nocolon")).is_err());
773 assert!(from_json_value::<AnyStateEvent>(member_event_json("@noserverpart:")).is_err());
774 assert!(
775 from_json_value::<AnyStateEvent>(member_event_json("@noserverpart:_suffix")).is_err()
776 );
777
778 let user_id = user_id!("@username:example.org").as_str();
779 let device_id = device_id!("VALID_DEVICE_ID").as_str();
780
781 let parse_result = from_json_value::<AnyStateEvent>(member_event_json(user_id));
782 assert_matches!(parse_result, Ok(_));
783 assert_matches!(
784 from_json_value::<AnyStateEvent>(member_event_json(&format!("{user_id}_{device_id}"))),
785 Ok(_)
786 );
787
788 assert_matches!(
789 from_json_value::<AnyStateEvent>(member_event_json(&format!(
790 "{user_id}:invalid_suffix"
791 ))),
792 Err(_)
793 );
794
795 assert_matches!(
796 from_json_value::<AnyStateEvent>(member_event_json(&format!("_{user_id}"))),
797 Err(_)
798 );
799
800 assert_matches!(
801 from_json_value::<AnyStateEvent>(member_event_json(&format!("_{user_id}_{device_id}"))),
802 Ok(_)
803 );
804
805 assert_matches!(
806 from_json_value::<AnyStateEvent>(member_event_json(&format!(
807 "_{user_id}:invalid_suffix"
808 ))),
809 Err(_)
810 );
811 assert_matches!(
812 from_json_value::<AnyStateEvent>(member_event_json(&format!("{user_id}_"))),
813 Err(_)
814 );
815 }
816}