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 };
273
274 fn create_call_member_legacy_event_content() -> CallMemberEventContent {
275 CallMemberEventContent::new_legacy(vec![LegacyMembershipData {
276 application: Application::Call(CallApplicationContent {
277 call_id: "123456".to_owned(),
278 scope: CallScope::Room,
279 }),
280 device_id: owned_device_id!("ABCDE"),
281 expires: Duration::from_secs(3600),
282 foci_active: vec![Focus::Livekit(LivekitFocus {
283 alias: "1".to_owned(),
284 service_url: "https://livekit.com".to_owned(),
285 })],
286 membership_id: "0".to_owned(),
287 created_ts: None,
288 }])
289 }
290
291 fn create_call_member_event_content() -> CallMemberEventContent {
292 CallMemberEventContent::new(
293 Application::Call(CallApplicationContent {
294 call_id: "123456".to_owned(),
295 scope: CallScope::Room,
296 }),
297 owned_device_id!("ABCDE"),
298 ActiveFocus::Livekit(ActiveLivekitFocus {
299 focus_selection: FocusSelection::OldestMembership,
300 }),
301 vec![Focus::Livekit(LivekitFocus {
302 alias: "1".to_owned(),
303 service_url: "https://livekit.com".to_owned(),
304 })],
305 None,
306 Duration::from_secs(3600).into(), )
308 }
309
310 #[test]
311 fn serialize_call_member_event_content() {
312 let call_member_event = &json!({
313 "application": "m.call",
314 "call_id": "123456",
315 "scope": "m.room",
316 "device_id": "ABCDE",
317 "expires": 3_600_000, "foci_preferred": [
319 {
320 "livekit_alias": "1",
321 "livekit_service_url": "https://livekit.com",
322 "type": "livekit"
323 }
324 ],
325 "focus_active":{
326 "type":"livekit",
327 "focus_selection":"oldest_membership"
328 }
329 });
330 assert_eq!(
331 call_member_event,
332 &serde_json::to_value(create_call_member_event_content()).unwrap()
333 );
334
335 let empty_call_member_event = &json!({});
336 assert_eq!(
337 empty_call_member_event,
338 &serde_json::to_value(CallMemberEventContent::Empty(EmptyMembershipData {
339 leave_reason: None
340 }))
341 .unwrap()
342 );
343 }
344
345 #[test]
346 fn serialize_legacy_call_member_event_content() {
347 let call_member_event = &json!({
348 "memberships": [
349 {
350 "application": "m.call",
351 "call_id": "123456",
352 "scope": "m.room",
353 "device_id": "ABCDE",
354 "expires": 3_600_000,
355 "foci_active": [
356 {
357 "livekit_alias": "1",
358 "livekit_service_url": "https://livekit.com",
359 "type": "livekit"
360 }
361 ],
362 "membershipID": "0"
363 }
364 ]
365 });
366
367 assert_eq!(
368 call_member_event,
369 &serde_json::to_value(create_call_member_legacy_event_content()).unwrap()
370 );
371 }
372 #[test]
373 fn deserialize_call_member_event_content() {
374 let call_member_ev = CallMemberEventContent::new(
375 Application::Call(CallApplicationContent {
376 call_id: "123456".to_owned(),
377 scope: CallScope::Room,
378 }),
379 owned_device_id!("THIS_DEVICE"),
380 ActiveFocus::Livekit(ActiveLivekitFocus {
381 focus_selection: FocusSelection::OldestMembership,
382 }),
383 vec![Focus::Livekit(LivekitFocus {
384 alias: "room1".to_owned(),
385 service_url: "https://livekit1.com".to_owned(),
386 })],
387 None,
388 None,
389 );
390
391 let call_member_ev_json = json!({
392 "application": "m.call",
393 "call_id": "123456",
394 "scope": "m.room",
395 "expires": 14_400_000, "device_id": "THIS_DEVICE",
397 "focus_active":{
398 "type": "livekit",
399 "focus_selection": "oldest_membership"
400 },
401 "foci_preferred": [
402 {
403 "livekit_alias": "room1",
404 "livekit_service_url": "https://livekit1.com",
405 "type": "livekit"
406 }
407 ],
408 });
409
410 let ev_content: CallMemberEventContent =
411 serde_json::from_value(call_member_ev_json).unwrap();
412 assert_eq!(
413 serde_json::to_string(&ev_content).unwrap(),
414 serde_json::to_string(&call_member_ev).unwrap()
415 );
416 let empty = CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None });
417 assert_eq!(
418 serde_json::to_string(&json!({})).unwrap(),
419 serde_json::to_string(&empty).unwrap()
420 );
421 }
422
423 #[test]
424 fn deserialize_legacy_call_member_event_content() {
425 let call_member_ev = CallMemberEventContent::new_legacy(vec![
426 LegacyMembershipData {
427 application: Application::Call(CallApplicationContent {
428 call_id: "123456".to_owned(),
429 scope: CallScope::Room,
430 }),
431 device_id: owned_device_id!("THIS_DEVICE"),
432 expires: Duration::from_secs(3600),
433 foci_active: vec![Focus::Livekit(LivekitFocus {
434 alias: "room1".to_owned(),
435 service_url: "https://livekit1.com".to_owned(),
436 })],
437 membership_id: "0".to_owned(),
438 created_ts: None,
439 },
440 LegacyMembershipData {
441 application: Application::Call(CallApplicationContent {
442 call_id: "".to_owned(),
443 scope: CallScope::Room,
444 }),
445 device_id: owned_device_id!("OTHER_DEVICE"),
446 expires: Duration::from_secs(3600),
447 foci_active: vec![Focus::Livekit(LivekitFocus {
448 alias: "room2".to_owned(),
449 service_url: "https://livekit2.com".to_owned(),
450 })],
451 membership_id: "0".to_owned(),
452 created_ts: None,
453 },
454 ]);
455
456 let call_member_ev_json = json!({
457 "memberships": [
458 {
459 "application": "m.call",
460 "call_id": "123456",
461 "scope": "m.room",
462 "device_id": "THIS_DEVICE",
463 "expires": 3_600_000,
464 "foci_active": [
465 {
466 "livekit_alias": "room1",
467 "livekit_service_url": "https://livekit1.com",
468 "type": "livekit"
469 }
470 ],
471 "membershipID": "0",
472 },
473 {
474 "application": "m.call",
475 "call_id": "",
476 "scope": "m.room",
477 "device_id": "OTHER_DEVICE",
478 "expires": 3_600_000,
479 "foci_active": [
480 {
481 "livekit_alias": "room2",
482 "livekit_service_url": "https://livekit2.com",
483 "type": "livekit"
484 }
485 ],
486 "membershipID": "0"
487 }
488 ]
489 });
490
491 let ev_content: CallMemberEventContent =
492 serde_json::from_value(call_member_ev_json).unwrap();
493 assert_eq!(
494 serde_json::to_string(&ev_content).unwrap(),
495 serde_json::to_string(&call_member_ev).unwrap()
496 );
497 }
498
499 fn member_event_json(state_key: &str) -> JsonValue {
500 json!({
501 "content":{
502 "expires": 3_600_000, "application": "m.call",
504 "call_id": "",
505 "scope": "m.room",
506 "device_id": "THIS_DEVICE",
507 "focus_active":{
508 "type": "livekit",
509 "focus_selection": "oldest_membership"
510 },
511 "foci_preferred": [
512 {
513 "livekit_alias": "room1",
514 "livekit_service_url": "https://livekit1.com",
515 "type": "livekit"
516 }
517 ],
518 },
519 "type": "m.call.member",
520 "origin_server_ts": 111,
521 "event_id": "$3qfxjGYSu4sL25FtR0ep6vePOc",
522 "room_id": "!1234:example.org",
523 "sender": "@user:example.org",
524 "state_key": state_key,
525 "unsigned":{
526 "age":10,
527 "prev_content": {},
528 "prev_sender":"@user:example.org",
529 }
530 })
531 }
532
533 fn deserialize_member_event_helper(state_key: &str) {
534 let ev = member_event_json(state_key);
535
536 assert_matches!(
537 from_json_value(ev),
538 Ok(AnyStateEvent::CallMember(StateEvent::Original(member_event)))
539 );
540
541 let event_id = OwnedEventId::try_from("$3qfxjGYSu4sL25FtR0ep6vePOc").unwrap();
542 let sender = OwnedUserId::try_from("@user:example.org").unwrap();
543 let room_id = OwnedRoomId::try_from("!1234:example.org").unwrap();
544 assert_eq!(member_event.state_key.as_ref(), state_key);
545 assert_eq!(member_event.event_id, event_id);
546 assert_eq!(member_event.sender, sender);
547 assert_eq!(member_event.room_id, room_id);
548 assert_eq!(member_event.origin_server_ts, TS(js_int::UInt::new(111).unwrap()));
549 let membership = SessionMembershipData {
550 application: Application::Call(CallApplicationContent {
551 call_id: "".to_owned(),
552 scope: CallScope::Room,
553 }),
554 device_id: owned_device_id!("THIS_DEVICE"),
555 foci_preferred: [Focus::Livekit(LivekitFocus {
556 alias: "room1".to_owned(),
557 service_url: "https://livekit1.com".to_owned(),
558 })]
559 .to_vec(),
560 focus_active: ActiveFocus::Livekit(ActiveLivekitFocus {
561 focus_selection: FocusSelection::OldestMembership,
562 }),
563 created_ts: None,
564 expires: Duration::from_secs(3600),
565 };
566 assert_eq!(
567 member_event.content,
568 CallMemberEventContent::SessionContent(membership.clone())
569 );
570
571 assert_eq!(
573 member_event.content.active_memberships(None)[0],
574 vec![MembershipData::Session(&membership)][0]
575 );
576 assert_eq!(js_int::Int::new(10), member_event.unsigned.age);
577 assert_eq!(
578 CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None }),
579 member_event.unsigned.prev_content.unwrap()
580 );
581
582 }
585
586 #[test]
587 fn deserialize_member_event() {
588 deserialize_member_event_helper("@user:example.org");
589 }
590
591 #[test]
592 fn deserialize_member_event_with_scoped_state_key_prefixed() {
593 deserialize_member_event_helper("_@user:example.org_THIS_DEVICE_m.call");
594 }
595
596 #[test]
597 fn deserialize_member_event_with_scoped_state_key_unprefixed() {
598 deserialize_member_event_helper("@user:example.org_THIS_DEVICE_m.call");
599 }
600
601 fn timestamps() -> (TS, TS, TS) {
602 let now = TS::now();
603 let one_second_ago =
604 now.to_system_time().unwrap().checked_sub(Duration::from_secs(1)).unwrap();
605 let two_hours_ago =
606 now.to_system_time().unwrap().checked_sub(Duration::from_secs(60 * 60 * 2)).unwrap();
607 (
608 now,
609 TS::from_system_time(one_second_ago).unwrap(),
610 TS::from_system_time(two_hours_ago).unwrap(),
611 )
612 }
613
614 #[test]
615 fn legacy_memberships_do_expire() {
616 let content_legacy = create_call_member_legacy_event_content();
617 let (now, one_second_ago, two_hours_ago) = timestamps();
618
619 assert_eq!(
620 content_legacy.active_memberships(Some(one_second_ago)),
621 content_legacy.memberships()
622 );
623 assert_eq!(content_legacy.active_memberships(Some(now)), content_legacy.memberships());
624 assert_eq!(
625 content_legacy.active_memberships(Some(two_hours_ago)),
626 (vec![] as Vec<MembershipData<'_>>)
627 );
628 }
629
630 #[test]
631 fn session_membership_does_expire() {
632 let content = create_call_member_event_content();
633 let (now, one_second_ago, two_hours_ago) = timestamps();
634
635 assert_eq!(content.active_memberships(Some(now)), content.memberships());
636 assert_eq!(content.active_memberships(Some(one_second_ago)), content.memberships());
637 assert_eq!(
638 content.active_memberships(Some(two_hours_ago)),
639 (vec![] as Vec<MembershipData<'_>>)
640 );
641 }
642
643 #[test]
644 fn set_created_ts() {
645 let mut content_now = create_call_member_legacy_event_content();
646 let mut content_two_hours_ago = create_call_member_legacy_event_content();
647 let mut content_one_second_ago = create_call_member_legacy_event_content();
648 let (now, one_second_ago, two_hours_ago) = timestamps();
649
650 content_now.set_created_ts_if_none(now);
651 content_one_second_ago.set_created_ts_if_none(one_second_ago);
652 content_two_hours_ago.set_created_ts_if_none(two_hours_ago);
653 assert_eq!(content_now.active_memberships(None), content_now.memberships());
654
655 assert_eq!(
656 content_two_hours_ago.active_memberships(None),
657 vec![] as Vec<MembershipData<'_>>
658 );
659 assert_eq!(
660 content_one_second_ago.active_memberships(None),
661 content_one_second_ago.memberships()
662 );
663
664 content_two_hours_ago.set_created_ts_if_none(one_second_ago);
666 assert_eq!(
668 content_two_hours_ago.active_memberships(None),
669 vec![] as Vec<MembershipData<'_>>
670 );
671 }
672
673 #[test]
674 fn test_parse_rtc_member_event_key() {
675 assert!(from_json_value::<AnyStateEvent>(member_event_json("abc")).is_err());
676 assert!(from_json_value::<AnyStateEvent>(member_event_json("@nocolon")).is_err());
677 assert!(from_json_value::<AnyStateEvent>(member_event_json("@noserverpart:")).is_err());
678 assert!(
679 from_json_value::<AnyStateEvent>(member_event_json("@noserverpart:_suffix")).is_err()
680 );
681
682 let user_id = user_id!("@username:example.org").as_str();
683 let device_id = device_id!("VALID_DEVICE_ID").as_str();
684
685 let parse_result = from_json_value::<AnyStateEvent>(member_event_json(user_id));
686 assert_matches!(parse_result, Ok(_));
687 assert_matches!(
688 from_json_value::<AnyStateEvent>(member_event_json(&format!("{user_id}_{device_id}"))),
689 Ok(_)
690 );
691
692 assert_matches!(
693 from_json_value::<AnyStateEvent>(member_event_json(&format!(
694 "{user_id}:invalid_suffix"
695 ))),
696 Err(_)
697 );
698
699 assert_matches!(
700 from_json_value::<AnyStateEvent>(member_event_json(&format!("_{user_id}"))),
701 Err(_)
702 );
703
704 assert_matches!(
705 from_json_value::<AnyStateEvent>(member_event_json(&format!("_{user_id}_{device_id}"))),
706 Ok(_)
707 );
708
709 assert_matches!(
710 from_json_value::<AnyStateEvent>(member_event_json(&format!(
711 "_{user_id}:invalid_suffix"
712 ))),
713 Err(_)
714 );
715 assert_matches!(
716 from_json_value::<AnyStateEvent>(member_event_json(&format!("{user_id}_"))),
717 Err(_)
718 );
719 }
720}