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