1mod focus;
8mod member_data;
9mod member_state_key;
10
11pub use focus::*;
12pub use member_data::*;
13pub use member_state_key::*;
14use ruma_common::{MilliSecondsSinceUnixEpoch, OwnedDeviceId};
15use ruma_macros::{EventContent, StringEnum};
16use serde::{Deserialize, Serialize};
17
18use crate::{
19 PossiblyRedactedStateEventContent, PrivOwnedStr, RedactContent, RedactedStateEventContent,
20 StateEventType,
21};
22
23#[derive(Clone, Debug, Serialize, Deserialize, EventContent, PartialEq)]
34#[ruma_event(type = "org.matrix.msc3401.call.member", kind = State, state_key_type = CallMemberStateKey, custom_redacted, custom_possibly_redacted)]
35#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
36#[serde(untagged)]
37pub enum CallMemberEventContent {
38 LegacyContent(LegacyMembershipContent),
41 SessionContent(SessionMembershipData),
44 Empty(EmptyMembershipData),
46}
47
48impl CallMemberEventContent {
49 pub fn new_legacy(memberships: Vec<LegacyMembershipData>) -> Self {
51 Self::LegacyContent(LegacyMembershipContent {
52 memberships, })
54 }
55
56 pub fn new(
58 application: Application,
59 device_id: OwnedDeviceId,
60 focus_active: ActiveFocus,
61 foci_preferred: Vec<Focus>,
62 created_ts: Option<MilliSecondsSinceUnixEpoch>,
63 ) -> Self {
64 Self::SessionContent(SessionMembershipData {
65 application,
66 device_id,
67 focus_active,
68 foci_preferred,
69 created_ts,
70 })
71 }
72
73 pub fn new_empty(leave_reason: Option<LeaveReason>) -> Self {
75 Self::Empty(EmptyMembershipData { leave_reason })
76 }
77
78 pub fn active_memberships(
90 &self,
91 origin_server_ts: Option<MilliSecondsSinceUnixEpoch>,
92 ) -> Vec<MembershipData<'_>> {
93 match self {
94 CallMemberEventContent::LegacyContent(content) => {
95 content.active_memberships(origin_server_ts)
96 }
97 CallMemberEventContent::SessionContent(content) => {
98 [content].map(MembershipData::Session).to_vec()
99 }
100 CallMemberEventContent::Empty(_) => Vec::new(),
101 }
102 }
103
104 pub fn memberships(&self) -> Vec<MembershipData<'_>> {
107 match self {
108 CallMemberEventContent::LegacyContent(content) => {
109 content.memberships.iter().map(MembershipData::Legacy).collect()
110 }
111 CallMemberEventContent::SessionContent(content) => {
112 [content].map(MembershipData::Session).to_vec()
113 }
114 CallMemberEventContent::Empty(_) => Vec::new(),
115 }
116 }
117
118 pub fn set_created_ts_if_none(&mut self, origin_server_ts: MilliSecondsSinceUnixEpoch) {
127 match self {
128 CallMemberEventContent::LegacyContent(content) => {
129 content.memberships.iter_mut().for_each(|m: &mut LegacyMembershipData| {
130 m.created_ts.get_or_insert(origin_server_ts);
131 });
132 }
133 CallMemberEventContent::SessionContent(m) => {
134 m.created_ts.get_or_insert(origin_server_ts);
135 }
136 _ => (),
137 }
138 }
139}
140
141#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
143#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
144pub struct EmptyMembershipData {
145 #[serde(skip_serializing_if = "Option::is_none")]
148 pub leave_reason: Option<LeaveReason>,
149}
150
151#[derive(Clone, PartialEq, StringEnum)]
157#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
158#[ruma_enum(rename_all = "m.snake_case")]
159pub enum LeaveReason {
160 LostConnection,
163 #[doc(hidden)]
164 _Custom(PrivOwnedStr),
165}
166
167impl RedactContent for CallMemberEventContent {
168 type Redacted = RedactedCallMemberEventContent;
169
170 fn redact(self, _version: &ruma_common::RoomVersionId) -> Self::Redacted {
171 RedactedCallMemberEventContent {}
172 }
173}
174
175pub type PossiblyRedactedCallMemberEventContent = CallMemberEventContent;
180
181impl PossiblyRedactedStateEventContent for PossiblyRedactedCallMemberEventContent {
182 type StateKey = CallMemberStateKey;
183}
184
185#[derive(Clone, Debug, Deserialize, Serialize)]
187#[allow(clippy::exhaustive_structs)]
188pub struct RedactedCallMemberEventContent {}
189
190impl ruma_events::content::EventContent for RedactedCallMemberEventContent {
191 type EventType = StateEventType;
192 fn event_type(&self) -> Self::EventType {
193 StateEventType::CallMember
194 }
195}
196
197impl RedactedStateEventContent for RedactedCallMemberEventContent {
198 type StateKey = CallMemberStateKey;
199}
200
201#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
203#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
204pub struct LegacyMembershipContent {
205 memberships: Vec<LegacyMembershipData>,
216}
217
218impl LegacyMembershipContent {
219 fn active_memberships(
220 &self,
221 origin_server_ts: Option<MilliSecondsSinceUnixEpoch>,
222 ) -> Vec<MembershipData<'_>> {
223 self.memberships
224 .iter()
225 .filter(|m| !m.is_expired(origin_server_ts))
226 .map(MembershipData::Legacy)
227 .collect()
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use std::time::Duration;
234
235 use assert_matches2::assert_matches;
236 use ruma_common::{
237 device_id, owned_device_id, user_id, MilliSecondsSinceUnixEpoch as TS, OwnedEventId,
238 OwnedRoomId, OwnedUserId,
239 };
240 use serde_json::{from_value as from_json_value, json, Value as JsonValue};
241
242 use super::{
243 focus::{ActiveFocus, ActiveLivekitFocus, Focus, LivekitFocus},
244 member_data::{
245 Application, CallApplicationContent, CallScope, LegacyMembershipData, MembershipData,
246 },
247 CallMemberEventContent,
248 };
249 use crate::{
250 call::member::{EmptyMembershipData, FocusSelection, SessionMembershipData},
251 AnyStateEvent, StateEvent,
252 };
253
254 fn create_call_member_legacy_event_content() -> CallMemberEventContent {
255 CallMemberEventContent::new_legacy(vec![LegacyMembershipData {
256 application: Application::Call(CallApplicationContent {
257 call_id: "123456".to_owned(),
258 scope: CallScope::Room,
259 }),
260 device_id: owned_device_id!("ABCDE"),
261 expires: Duration::from_secs(3600),
262 foci_active: vec![Focus::Livekit(LivekitFocus {
263 alias: "1".to_owned(),
264 service_url: "https://livekit.com".to_owned(),
265 })],
266 membership_id: "0".to_owned(),
267 created_ts: None,
268 }])
269 }
270
271 fn create_call_member_event_content() -> CallMemberEventContent {
272 CallMemberEventContent::new(
273 Application::Call(CallApplicationContent {
274 call_id: "123456".to_owned(),
275 scope: CallScope::Room,
276 }),
277 owned_device_id!("ABCDE"),
278 ActiveFocus::Livekit(ActiveLivekitFocus {
279 focus_selection: FocusSelection::OldestMembership,
280 }),
281 vec![Focus::Livekit(LivekitFocus {
282 alias: "1".to_owned(),
283 service_url: "https://livekit.com".to_owned(),
284 })],
285 None,
286 )
287 }
288
289 #[test]
290 fn serialize_call_member_event_content() {
291 let call_member_event = &json!({
292 "application": "m.call",
293 "call_id": "123456",
294 "scope": "m.room",
295 "device_id": "ABCDE",
296 "foci_preferred": [
297 {
298 "livekit_alias": "1",
299 "livekit_service_url": "https://livekit.com",
300 "type": "livekit"
301 }
302 ],
303 "focus_active":{
304 "type":"livekit",
305 "focus_selection":"oldest_membership"
306 }
307 });
308 assert_eq!(
309 call_member_event,
310 &serde_json::to_value(create_call_member_event_content()).unwrap()
311 );
312
313 let empty_call_member_event = &json!({});
314 assert_eq!(
315 empty_call_member_event,
316 &serde_json::to_value(CallMemberEventContent::Empty(EmptyMembershipData {
317 leave_reason: None
318 }))
319 .unwrap()
320 );
321 }
322
323 #[test]
324 fn serialize_legacy_call_member_event_content() {
325 let call_member_event = &json!({
326 "memberships": [
327 {
328 "application": "m.call",
329 "call_id": "123456",
330 "scope": "m.room",
331 "device_id": "ABCDE",
332 "expires": 3_600_000,
333 "foci_active": [
334 {
335 "livekit_alias": "1",
336 "livekit_service_url": "https://livekit.com",
337 "type": "livekit"
338 }
339 ],
340 "membershipID": "0"
341 }
342 ]
343 });
344
345 assert_eq!(
346 call_member_event,
347 &serde_json::to_value(create_call_member_legacy_event_content()).unwrap()
348 );
349 }
350 #[test]
351 fn deserialize_call_member_event_content() {
352 let call_member_ev = CallMemberEventContent::new(
353 Application::Call(CallApplicationContent {
354 call_id: "123456".to_owned(),
355 scope: CallScope::Room,
356 }),
357 owned_device_id!("THIS_DEVICE"),
358 ActiveFocus::Livekit(ActiveLivekitFocus {
359 focus_selection: FocusSelection::OldestMembership,
360 }),
361 vec![Focus::Livekit(LivekitFocus {
362 alias: "room1".to_owned(),
363 service_url: "https://livekit1.com".to_owned(),
364 })],
365 None,
366 );
367
368 let call_member_ev_json = json!({
369 "application": "m.call",
370 "call_id": "123456",
371 "scope": "m.room",
372 "device_id": "THIS_DEVICE",
373 "focus_active":{
374 "type": "livekit",
375 "focus_selection": "oldest_membership"
376 },
377 "foci_preferred": [
378 {
379 "livekit_alias": "room1",
380 "livekit_service_url": "https://livekit1.com",
381 "type": "livekit"
382 }
383 ],
384 });
385
386 let ev_content: CallMemberEventContent =
387 serde_json::from_value(call_member_ev_json).unwrap();
388 assert_eq!(
389 serde_json::to_string(&ev_content).unwrap(),
390 serde_json::to_string(&call_member_ev).unwrap()
391 );
392 let empty = CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None });
393 assert_eq!(
394 serde_json::to_string(&json!({})).unwrap(),
395 serde_json::to_string(&empty).unwrap()
396 );
397 }
398
399 #[test]
400 fn deserialize_legacy_call_member_event_content() {
401 let call_member_ev = CallMemberEventContent::new_legacy(vec![
402 LegacyMembershipData {
403 application: Application::Call(CallApplicationContent {
404 call_id: "123456".to_owned(),
405 scope: CallScope::Room,
406 }),
407 device_id: owned_device_id!("THIS_DEVICE"),
408 expires: Duration::from_secs(3600),
409 foci_active: vec![Focus::Livekit(LivekitFocus {
410 alias: "room1".to_owned(),
411 service_url: "https://livekit1.com".to_owned(),
412 })],
413 membership_id: "0".to_owned(),
414 created_ts: None,
415 },
416 LegacyMembershipData {
417 application: Application::Call(CallApplicationContent {
418 call_id: "".to_owned(),
419 scope: CallScope::Room,
420 }),
421 device_id: owned_device_id!("OTHER_DEVICE"),
422 expires: Duration::from_secs(3600),
423 foci_active: vec![Focus::Livekit(LivekitFocus {
424 alias: "room2".to_owned(),
425 service_url: "https://livekit2.com".to_owned(),
426 })],
427 membership_id: "0".to_owned(),
428 created_ts: None,
429 },
430 ]);
431
432 let call_member_ev_json = json!({
433 "memberships": [
434 {
435 "application": "m.call",
436 "call_id": "123456",
437 "scope": "m.room",
438 "device_id": "THIS_DEVICE",
439 "expires": 3_600_000,
440 "foci_active": [
441 {
442 "livekit_alias": "room1",
443 "livekit_service_url": "https://livekit1.com",
444 "type": "livekit"
445 }
446 ],
447 "membershipID": "0",
448 },
449 {
450 "application": "m.call",
451 "call_id": "",
452 "scope": "m.room",
453 "device_id": "OTHER_DEVICE",
454 "expires": 3_600_000,
455 "foci_active": [
456 {
457 "livekit_alias": "room2",
458 "livekit_service_url": "https://livekit2.com",
459 "type": "livekit"
460 }
461 ],
462 "membershipID": "0"
463 }
464 ]
465 });
466
467 let ev_content: CallMemberEventContent =
468 serde_json::from_value(call_member_ev_json).unwrap();
469 assert_eq!(
470 serde_json::to_string(&ev_content).unwrap(),
471 serde_json::to_string(&call_member_ev).unwrap()
472 );
473 }
474
475 fn member_event_json(state_key: &str) -> JsonValue {
476 json!({
477 "content":{
478 "application": "m.call",
479 "call_id": "",
480 "scope": "m.room",
481 "device_id": "THIS_DEVICE",
482 "focus_active":{
483 "type": "livekit",
484 "focus_selection": "oldest_membership"
485 },
486 "foci_preferred": [
487 {
488 "livekit_alias": "room1",
489 "livekit_service_url": "https://livekit1.com",
490 "type": "livekit"
491 }
492 ],
493 },
494 "type": "m.call.member",
495 "origin_server_ts": 111,
496 "event_id": "$3qfxjGYSu4sL25FtR0ep6vePOc",
497 "room_id": "!1234:example.org",
498 "sender": "@user:example.org",
499 "state_key": state_key,
500 "unsigned":{
501 "age":10,
502 "prev_content": {},
503 "prev_sender":"@user:example.org",
504 }
505 })
506 }
507
508 fn deserialize_member_event_helper(state_key: &str) {
509 let ev = member_event_json(state_key);
510
511 assert_matches!(
512 from_json_value(ev),
513 Ok(AnyStateEvent::CallMember(StateEvent::Original(member_event)))
514 );
515
516 let event_id = OwnedEventId::try_from("$3qfxjGYSu4sL25FtR0ep6vePOc").unwrap();
517 let sender = OwnedUserId::try_from("@user:example.org").unwrap();
518 let room_id = OwnedRoomId::try_from("!1234:example.org").unwrap();
519 assert_eq!(member_event.state_key.as_ref(), state_key);
520 assert_eq!(member_event.event_id, event_id);
521 assert_eq!(member_event.sender, sender);
522 assert_eq!(member_event.room_id, room_id);
523 assert_eq!(member_event.origin_server_ts, TS(js_int::UInt::new(111).unwrap()));
524 let membership = SessionMembershipData {
525 application: Application::Call(CallApplicationContent {
526 call_id: "".to_owned(),
527 scope: CallScope::Room,
528 }),
529 device_id: owned_device_id!("THIS_DEVICE"),
530 foci_preferred: [Focus::Livekit(LivekitFocus {
531 alias: "room1".to_owned(),
532 service_url: "https://livekit1.com".to_owned(),
533 })]
534 .to_vec(),
535 focus_active: ActiveFocus::Livekit(ActiveLivekitFocus {
536 focus_selection: FocusSelection::OldestMembership,
537 }),
538 created_ts: None,
539 };
540 assert_eq!(
541 member_event.content,
542 CallMemberEventContent::SessionContent(membership.clone())
543 );
544
545 assert_eq!(
547 member_event.content.active_memberships(None)[0],
548 vec![MembershipData::Session(&membership)][0]
549 );
550 assert_eq!(js_int::Int::new(10), member_event.unsigned.age);
551 assert_eq!(
552 CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None }),
553 member_event.unsigned.prev_content.unwrap()
554 );
555
556 }
559
560 #[test]
561 fn deserialize_member_event() {
562 deserialize_member_event_helper("@user:example.org");
563 }
564
565 #[test]
566 fn deserialize_member_event_with_scoped_state_key_prefixed() {
567 deserialize_member_event_helper("_@user:example.org_THIS_DEVICE");
568 }
569
570 #[test]
571 fn deserialize_member_event_with_scoped_state_key_unprefixed() {
572 deserialize_member_event_helper("@user:example.org_THIS_DEVICE");
573 }
574
575 fn timestamps() -> (TS, TS, TS) {
576 let now = TS::now();
577 let one_second_ago =
578 now.to_system_time().unwrap().checked_sub(Duration::from_secs(1)).unwrap();
579 let two_hours_ago =
580 now.to_system_time().unwrap().checked_sub(Duration::from_secs(60 * 60 * 2)).unwrap();
581 (
582 now,
583 TS::from_system_time(one_second_ago).unwrap(),
584 TS::from_system_time(two_hours_ago).unwrap(),
585 )
586 }
587
588 #[test]
589 fn legacy_memberships_do_expire() {
590 let content_legacy = create_call_member_legacy_event_content();
591 let (now, one_second_ago, two_hours_ago) = timestamps();
592
593 assert_eq!(
594 content_legacy.active_memberships(Some(one_second_ago)),
595 content_legacy.memberships()
596 );
597 assert_eq!(content_legacy.active_memberships(Some(now)), content_legacy.memberships());
598 assert_eq!(
599 content_legacy.active_memberships(Some(two_hours_ago)),
600 (vec![] as Vec<MembershipData<'_>>)
601 );
602 let content_session = create_call_member_event_content();
604 assert_eq!(
605 content_session.active_memberships(Some(one_second_ago)),
606 content_session.memberships()
607 );
608 assert_eq!(content_session.active_memberships(Some(now)), content_session.memberships());
609 assert_eq!(
610 content_session.active_memberships(Some(two_hours_ago)),
611 content_session.memberships()
612 );
613 }
614
615 #[test]
616 fn set_created_ts() {
617 let mut content_now = create_call_member_legacy_event_content();
618 let mut content_two_hours_ago = create_call_member_legacy_event_content();
619 let mut content_one_second_ago = create_call_member_legacy_event_content();
620 let (now, one_second_ago, two_hours_ago) = timestamps();
621
622 content_now.set_created_ts_if_none(now);
623 content_one_second_ago.set_created_ts_if_none(one_second_ago);
624 content_two_hours_ago.set_created_ts_if_none(two_hours_ago);
625 assert_eq!(content_now.active_memberships(None), content_now.memberships());
626
627 assert_eq!(
628 content_two_hours_ago.active_memberships(None),
629 vec![] as Vec<MembershipData<'_>>
630 );
631 assert_eq!(
632 content_one_second_ago.active_memberships(None),
633 content_one_second_ago.memberships()
634 );
635
636 content_two_hours_ago.set_created_ts_if_none(one_second_ago);
638 assert_eq!(
640 content_two_hours_ago.active_memberships(None),
641 vec![] as Vec<MembershipData<'_>>
642 );
643 }
644
645 #[test]
646 fn test_parse_rtc_member_event_key() {
647 assert!(from_json_value::<AnyStateEvent>(member_event_json("abc")).is_err());
648 assert!(from_json_value::<AnyStateEvent>(member_event_json("@nocolon")).is_err());
649 assert!(from_json_value::<AnyStateEvent>(member_event_json("@noserverpart:")).is_err());
650 assert!(
651 from_json_value::<AnyStateEvent>(member_event_json("@noserverpart:_suffix")).is_err()
652 );
653
654 let user_id = user_id!("@username:example.org").as_str();
655 let device_id = device_id!("VALID_DEVICE_ID").as_str();
656
657 let parse_result = from_json_value::<AnyStateEvent>(member_event_json(user_id));
658 assert_matches!(parse_result, Ok(_));
659 assert_matches!(
660 from_json_value::<AnyStateEvent>(member_event_json(&format!("{user_id}_{device_id}"))),
661 Ok(_)
662 );
663
664 assert_matches!(
665 from_json_value::<AnyStateEvent>(member_event_json(&format!(
666 "{user_id}:invalid_suffix"
667 ))),
668 Err(_)
669 );
670
671 assert_matches!(
672 from_json_value::<AnyStateEvent>(member_event_json(&format!("_{user_id}"))),
673 Err(_)
674 );
675
676 assert_matches!(
677 from_json_value::<AnyStateEvent>(member_event_json(&format!("_{user_id}_{device_id}"))),
678 Ok(_)
679 );
680
681 assert_matches!(
682 from_json_value::<AnyStateEvent>(member_event_json(&format!(
683 "_{user_id}:invalid_suffix"
684 ))),
685 Err(_)
686 );
687 assert_matches!(
688 from_json_value::<AnyStateEvent>(member_event_json(&format!("{user_id}_"))),
689 Err(_)
690 );
691 }
692}