Skip to main content

ruma_events/call/
member.rs

1//! Types for MatrixRTC state events ([MSC3401]).
2//!
3//! This implements a newer/updated version of MSC3401.
4//!
5//! [MSC3401]: https://github.com/matrix-org/matrix-spec-proposals/pull/3401
6
7mod 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/// The member state event for a MatrixRTC session.
26///
27/// This is the object containing all the data related to a Matrix users participation in a
28/// MatrixRTC session.
29///
30/// This is a unit struct with the enum [`CallMemberEventContent`] because a Ruma state event cannot
31/// be an enum and we need this to be an untagged enum for parsing purposes. (see
32/// [`CallMemberEventContent`])
33///
34/// This struct also exposes allows to call the methods from [`CallMemberEventContent`].
35#[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    /// The legacy format for m.call.member events. (An array of memberships. The devices of one
41    /// user.)
42    LegacyContent(LegacyMembershipContent),
43    /// Normal membership events. One event per membership. Multiple state keys will
44    /// be used to describe multiple devices for one user.
45    SessionContent(SessionMembershipData),
46    /// An empty content means this user has been in a rtc session but is not anymore.
47    Empty(EmptyMembershipData),
48}
49
50impl CallMemberEventContent {
51    /// Creates a new [`CallMemberEventContent`] with [`LegacyMembershipData`].
52    pub fn new_legacy(memberships: Vec<LegacyMembershipData>) -> Self {
53        Self::LegacyContent(LegacyMembershipContent {
54            memberships, //: memberships.into_iter().map(MembershipData::Legacy).collect(),
55        })
56    }
57
58    /// Creates a new [`CallMemberEventContent`] with [`SessionMembershipData`].
59    ///
60    /// # Arguments
61    /// * `application` - The application that is creating the membership.
62    /// * `device_id` - The device ID of the member.
63    /// * `focus_active` - The active focus state of the member.
64    /// * `foci_preferred` - The preferred focus states of the member.
65    /// * `created_ts` - The timestamp when this state event chain for memberships was created. when
66    ///   updating the event the `created_ts` should be copied from the previous state. Set to
67    ///   `None` if this is the initial join event for the session.
68    /// * `expires` - The time after which the event is considered as expired. Defaults to 4 hours.
69    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)), // Default to 4 hours
84        })
85    }
86
87    /// Creates a new Empty [`CallMemberEventContent`] representing a left membership.
88    pub fn new_empty(leave_reason: Option<LeaveReason>) -> Self {
89        Self::Empty(EmptyMembershipData { leave_reason })
90    }
91
92    /// All non expired memberships in this member event.
93    ///
94    /// In most cases you want to use this method instead of the public memberships field.
95    /// The memberships field will also include expired events.
96    ///
97    /// This copies all the memberships and converts them
98    /// # Arguments
99    ///
100    /// * `origin_server_ts` - optionally the `origin_server_ts` can be passed as a fallback in the
101    ///   Membership does not contain [`MembershipData::created_ts`]. (`origin_server_ts` will be
102    ///   ignored if [`MembershipData::created_ts`] is `Some`)
103    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    /// All the memberships for this event. Can only contain multiple elements in the case of legacy
126    /// `m.call.member` state events.
127    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    /// Set the `created_ts` in this event.
140    ///
141    /// Each call member event contains the `origin_server_ts` and `content.create_ts`.
142    /// `content.create_ts` is undefined for the initial event of a session (because the
143    /// `origin_server_ts` is not known on the client).
144    /// In the rust sdk we want to copy over the `origin_server_ts` of the event into the content.
145    /// (This allows to use `MinimalStateEvents` and still be able to determine if a membership is
146    /// expired)
147    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/// This describes the CallMember event if the user is not part of the current session.
163#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
164#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
165pub struct EmptyMembershipData {
166    /// An empty call member state event can optionally contain a leave reason.
167    /// If it is `None` the user has left the call ordinarily. (Intentional hangup)
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub leave_reason: Option<LeaveReason>,
170}
171
172/// This is the optional value for an empty membership event content:
173/// [`CallMemberEventContent::Empty`].
174///
175/// It is used when the user disconnected and a Future ([MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140))
176/// was used to update the membership after the client was not reachable anymore.
177#[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    /// The user left the call by losing network connection or closing
182    /// the client before it was able to send the leave event.
183    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
196/// The PossiblyRedacted version of [`CallMemberEventContent`].
197///
198/// Since [`CallMemberEventContent`] has the [`CallMemberEventContent::Empty`] state it already is
199/// compatible with the redacted version of the state event content.
200pub 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/// The Redacted version of [`CallMemberEventContent`].
211#[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/// Legacy content with an array of memberships. See also: [`CallMemberEventContent`]
235#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
236#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
237pub struct LegacyMembershipContent {
238    /// A list of all the memberships that user currently has in this room.
239    ///
240    /// There can be multiple ones in case the user participates with multiple devices or there
241    /// are multiple RTC applications running.
242    ///
243    /// e.g. a call and a spacial experience.
244    ///
245    /// Important: This includes expired memberships.
246    /// To retrieve a list including only valid memberships,
247    /// see [`active_memberships`](CallMemberEventContent::active_memberships).
248    memberships: Vec<LegacyMembershipData>,
249}
250
251#[cfg(test)]
252#[allow(deprecated)]
253mod tests {
254    use std::time::Duration;
255
256    use assert_matches::assert_matches;
257    use js_int::{int, uint};
258    use ruma_common::{
259        MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, device_id,
260        owned_device_id, user_id,
261    };
262    use serde_json::{Value as JsonValue, from_value as from_json_value, json};
263    use strass::assert_let;
264
265    use super::{
266        CallMemberEventContent,
267        focus::{ActiveFocus, ActiveLivekitFocus, Focus, LivekitFocus},
268        member_data::{
269            Application, CallApplicationContent, CallScope, LegacyMembershipData, MembershipData,
270        },
271    };
272    use crate::{
273        AnyStateEvent, StateEvent,
274        call::member::{EmptyMembershipData, FocusSelection, SessionMembershipData},
275        rtc::notification::CallIntent,
276    };
277
278    fn create_call_member_legacy_event_content() -> CallMemberEventContent {
279        CallMemberEventContent::new_legacy(vec![LegacyMembershipData {
280            application: Application::Call(CallApplicationContent::new(
281                "123456".to_owned(),
282                CallScope::Room,
283            )),
284            device_id: owned_device_id!("ABCDE"),
285            expires: Duration::from_secs(3600),
286            foci_active: vec![Focus::Livekit(LivekitFocus {
287                alias: Some("1".to_owned()),
288                service_url: "https://livekit.com".to_owned(),
289            })],
290            membership_id: "0".to_owned(),
291            created_ts: None,
292        }])
293    }
294
295    fn create_call_member_event_content() -> CallMemberEventContent {
296        CallMemberEventContent::new(
297            Application::Call(CallApplicationContent::new("123456".to_owned(), CallScope::Room)),
298            owned_device_id!("ABCDE"),
299            ActiveFocus::Livekit(ActiveLivekitFocus {
300                focus_selection: FocusSelection::OldestMembership,
301            }),
302            vec![Focus::Livekit(LivekitFocus {
303                alias: Some("1".to_owned()),
304                service_url: "https://livekit.com".to_owned(),
305            })],
306            None,
307            Duration::from_secs(3600).into(), // Default to 1 hour
308        )
309    }
310
311    #[test]
312    fn serialize_call_member_event_content() {
313        let call_member_event = &json!({
314            "application": "m.call",
315            "call_id": "123456",
316            "scope": "m.room",
317            "device_id": "ABCDE",
318            "expires": 3_600_000, // Default to 1 hour
319            "foci_preferred": [
320                {
321                    "livekit_alias": "1",
322                    "livekit_service_url": "https://livekit.com",
323                    "type": "livekit"
324                }
325            ],
326            "focus_active":{
327                "type":"livekit",
328                "focus_selection":"oldest_membership"
329            }
330        });
331        assert_eq!(
332            call_member_event,
333            &serde_json::to_value(create_call_member_event_content()).unwrap()
334        );
335
336        let empty_call_member_event = &json!({});
337        assert_eq!(
338            empty_call_member_event,
339            &serde_json::to_value(CallMemberEventContent::Empty(EmptyMembershipData {
340                leave_reason: None
341            }))
342            .unwrap()
343        );
344    }
345
346    #[test]
347    fn serialize_legacy_call_member_event_content() {
348        let call_member_event = &json!({
349            "memberships": [
350                {
351                    "application": "m.call",
352                    "call_id": "123456",
353                    "scope": "m.room",
354                    "device_id": "ABCDE",
355                    "expires": 3_600_000,
356                    "foci_active": [
357                        {
358                            "livekit_alias": "1",
359                            "livekit_service_url": "https://livekit.com",
360                            "type": "livekit"
361                        }
362                    ],
363                    "membershipID": "0"
364                }
365            ]
366        });
367
368        assert_eq!(
369            call_member_event,
370            &serde_json::to_value(create_call_member_legacy_event_content()).unwrap()
371        );
372    }
373    #[test]
374    fn deserialize_call_member_event_content() {
375        let call_member_ev = CallMemberEventContent::new(
376            Application::Call(CallApplicationContent::new("123456".to_owned(), CallScope::Room)),
377            owned_device_id!("THIS_DEVICE"),
378            ActiveFocus::Livekit(ActiveLivekitFocus {
379                focus_selection: FocusSelection::OldestMembership,
380            }),
381            vec![Focus::Livekit(LivekitFocus {
382                alias: Some("room1".to_owned()),
383                service_url: "https://livekit1.com".to_owned(),
384            })],
385            None,
386            None,
387        );
388
389        let call_member_ev_json = json!({
390            "application": "m.call",
391            "call_id": "123456",
392            "scope": "m.room",
393            "expires": 14_400_000, // Default to 4 hours
394            "device_id": "THIS_DEVICE",
395            "focus_active":{
396                "type": "livekit",
397                "focus_selection": "oldest_membership"
398            },
399            "foci_preferred": [
400                {
401                    "livekit_alias": "room1",
402                    "livekit_service_url": "https://livekit1.com",
403                    "type": "livekit"
404                }
405            ],
406        });
407
408        let ev_content: CallMemberEventContent =
409            serde_json::from_value(call_member_ev_json).unwrap();
410        assert_eq!(
411            serde_json::to_string(&ev_content).unwrap(),
412            serde_json::to_string(&call_member_ev).unwrap()
413        );
414        let empty = CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None });
415        assert_eq!(
416            serde_json::to_string(&json!({})).unwrap(),
417            serde_json::to_string(&empty).unwrap()
418        );
419    }
420
421    #[test]
422    fn deserialize_call_member_event_content_without_livekit_alias() {
423        let call_member_ev = CallMemberEventContent::new(
424            Application::Call(CallApplicationContent::new("123456".to_owned(), CallScope::Room)),
425            owned_device_id!("THIS_DEVICE"),
426            ActiveFocus::Livekit(ActiveLivekitFocus {
427                focus_selection: FocusSelection::OldestMembership,
428            }),
429            vec![Focus::Livekit(LivekitFocus {
430                alias: None,
431                service_url: "https://livekit1.com".to_owned(),
432            })],
433            None,
434            None,
435        );
436
437        let call_member_ev_json = json!({
438            "application": "m.call",
439            "call_id": "123456",
440            "scope": "m.room",
441            "expires": 14_400_000, // Default to 4 hours
442            "device_id": "THIS_DEVICE",
443            "focus_active":{
444                "type": "livekit",
445                "focus_selection": "oldest_membership"
446            },
447            "foci_preferred": [
448                {
449                    "livekit_service_url": "https://livekit1.com",
450                    "type": "livekit"
451                }
452            ],
453        });
454
455        let ev_content: CallMemberEventContent =
456            serde_json::from_value(call_member_ev_json).unwrap();
457        assert_eq!(
458            serde_json::to_string(&ev_content).unwrap(),
459            serde_json::to_string(&call_member_ev).unwrap()
460        );
461        let empty = CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None });
462        assert_eq!(
463            serde_json::to_string(&json!({})).unwrap(),
464            serde_json::to_string(&empty).unwrap()
465        );
466    }
467
468    #[test]
469    #[cfg(feature = "unstable-msc4075")]
470    fn deserialize_event_with_call_intent() {
471        let call_member_ev = CallMemberEventContent::new(
472            Application::Call(CallApplicationContent {
473                call_id: "".to_owned(),
474                scope: CallScope::Room,
475                call_intent: Some(CallIntent::Audio),
476            }),
477            owned_device_id!("THIS_DEVICE"),
478            ActiveFocus::Livekit(ActiveLivekitFocus {
479                focus_selection: FocusSelection::OldestMembership,
480            }),
481            vec![Focus::Livekit(LivekitFocus {
482                alias: Some("room1".to_owned()),
483                service_url: "https://livekit1.com".to_owned(),
484            })],
485            None,
486            None,
487        );
488
489        let json = json!({
490              "application": "m.call",
491              "call_id": "",
492              "scope": "m.room",
493              "m.call.intent": "audio",
494              "device_id": "THIS_DEVICE",
495              "foci_preferred": [
496                {
497                  "type": "livekit",
498                  "livekit_alias": "room1",
499                  "livekit_service_url": "https://livekit1.com"
500                }
501              ],
502              "focus_active": {
503                "type": "livekit",
504                "focus_selection": "oldest_membership"
505              },
506              "expires": 14_400_000
507        });
508
509        let ev_content: CallMemberEventContent = serde_json::from_value(json).unwrap();
510        assert_eq!(
511            serde_json::to_string(&ev_content).unwrap(),
512            serde_json::to_string(&call_member_ev).unwrap()
513        );
514    }
515
516    #[test]
517    #[cfg(feature = "unstable-msc4075")]
518    fn deserialize_application() {
519        let test_cases = vec![
520            (
521                Application::Call(CallApplicationContent {
522                    call_id: "".to_owned(),
523                    scope: CallScope::Room,
524                    call_intent: None,
525                }),
526                json!({
527                  "application": "m.call",
528                  "call_id": "",
529                  "scope": "m.room",
530                }),
531            ),
532            (
533                Application::Call(CallApplicationContent {
534                    call_id: "".to_owned(),
535                    scope: CallScope::Room,
536                    call_intent: Some(CallIntent::Audio),
537                }),
538                json!({
539                  "application": "m.call",
540                  "call_id": "",
541                  "scope": "m.room",
542                  "m.call.intent": "audio"
543                }),
544            ),
545            (
546                Application::Call(CallApplicationContent {
547                    call_id: "xxxx".to_owned(),
548                    scope: CallScope::User,
549                    call_intent: Some(CallIntent::Video),
550                }),
551                json!({
552                  "application": "m.call",
553                  "call_id": "xxxx",
554                  "scope": "m.user",
555                  "m.call.intent": "video"
556                }),
557            ),
558        ];
559
560        for (model, jon) in test_cases {
561            let app: Application = serde_json::from_value(jon).unwrap();
562            assert_eq!(
563                serde_json::to_string(&app).unwrap(),
564                serde_json::to_string(&model).unwrap()
565            );
566        }
567    }
568
569    #[test]
570    fn deserialize_legacy_call_member_event_content() {
571        let call_member_ev = CallMemberEventContent::new_legacy(vec![
572            LegacyMembershipData {
573                application: Application::Call(CallApplicationContent::new(
574                    "123456".to_owned(),
575                    CallScope::Room,
576                )),
577                device_id: owned_device_id!("THIS_DEVICE"),
578                expires: Duration::from_secs(3600),
579                foci_active: vec![Focus::Livekit(LivekitFocus {
580                    alias: Some("room1".to_owned()),
581                    service_url: "https://livekit1.com".to_owned(),
582                })],
583                membership_id: "0".to_owned(),
584                created_ts: None,
585            },
586            LegacyMembershipData {
587                application: Application::Call(CallApplicationContent::new(
588                    "".to_owned(),
589                    CallScope::Room,
590                )),
591                device_id: owned_device_id!("OTHER_DEVICE"),
592                expires: Duration::from_secs(3600),
593                foci_active: vec![Focus::Livekit(LivekitFocus {
594                    alias: Some("room2".to_owned()),
595                    service_url: "https://livekit2.com".to_owned(),
596                })],
597                membership_id: "0".to_owned(),
598                created_ts: None,
599            },
600        ]);
601
602        let call_member_ev_json = json!({
603            "memberships": [
604                {
605                    "application": "m.call",
606                    "call_id": "123456",
607                    "scope": "m.room",
608                    "device_id": "THIS_DEVICE",
609                    "expires": 3_600_000,
610                    "foci_active": [
611                        {
612                            "livekit_alias": "room1",
613                            "livekit_service_url": "https://livekit1.com",
614                            "type": "livekit"
615                        }
616                    ],
617                    "membershipID": "0",
618                },
619                {
620                    "application": "m.call",
621                    "call_id": "",
622                    "scope": "m.room",
623                    "device_id": "OTHER_DEVICE",
624                    "expires": 3_600_000,
625                    "foci_active": [
626                        {
627                            "livekit_alias": "room2",
628                            "livekit_service_url": "https://livekit2.com",
629                            "type": "livekit"
630                        }
631                    ],
632                    "membershipID": "0"
633                }
634            ]
635        });
636
637        let ev_content: CallMemberEventContent =
638            serde_json::from_value(call_member_ev_json).unwrap();
639        assert_eq!(
640            serde_json::to_string(&ev_content).unwrap(),
641            serde_json::to_string(&call_member_ev).unwrap()
642        );
643    }
644
645    fn member_event_json(state_key: &str) -> JsonValue {
646        json!({
647            "content":{
648                "expires": 3_600_000, // Default to 4 hours
649                "application": "m.call",
650                "call_id": "",
651                "scope": "m.room",
652                "device_id": "THIS_DEVICE",
653                "focus_active":{
654                    "type": "livekit",
655                    "focus_selection": "oldest_membership"
656                },
657                "foci_preferred": [
658                    {
659                        "livekit_alias": "room1",
660                        "livekit_service_url": "https://livekit1.com",
661                        "type": "livekit"
662                    }
663                ],
664            },
665            "type": "m.call.member",
666            "origin_server_ts": 111,
667            "event_id": "$3qfxjGYSu4sL25FtR0ep6vePOc",
668            "room_id": "!1234:example.org",
669            "sender": "@user:example.org",
670            "state_key": state_key,
671            "unsigned":{
672                "age":10,
673                "prev_content": {},
674                "prev_sender":"@user:example.org",
675            }
676        })
677    }
678
679    fn deserialize_member_event_helper(state_key: &str) {
680        let ev = member_event_json(state_key);
681
682        assert_let!(
683            Ok(AnyStateEvent::CallMember(StateEvent::Original(member_event))) = from_json_value(ev)
684        );
685
686        let event_id = OwnedEventId::try_from("$3qfxjGYSu4sL25FtR0ep6vePOc").unwrap();
687        let sender = OwnedUserId::try_from("@user:example.org").unwrap();
688        let room_id = OwnedRoomId::try_from("!1234:example.org").unwrap();
689        assert_eq!(member_event.state_key.as_ref(), state_key);
690        assert_eq!(member_event.event_id, event_id);
691        assert_eq!(member_event.sender, sender);
692        assert_eq!(member_event.room_id, room_id);
693        assert_eq!(member_event.origin_server_ts.0, uint!(111));
694        let membership = SessionMembershipData {
695            application: Application::Call(CallApplicationContent::new(
696                "".to_owned(),
697                CallScope::Room,
698            )),
699            device_id: owned_device_id!("THIS_DEVICE"),
700            foci_preferred: [Focus::Livekit(LivekitFocus {
701                alias: Some("room1".to_owned()),
702                service_url: "https://livekit1.com".to_owned(),
703            })]
704            .to_vec(),
705            focus_active: ActiveFocus::Livekit(ActiveLivekitFocus {
706                focus_selection: FocusSelection::OldestMembership,
707            }),
708            created_ts: None,
709            expires: Duration::from_secs(3600),
710        };
711        assert_eq!(
712            member_event.content,
713            CallMemberEventContent::SessionContent(membership.clone())
714        );
715
716        // Correctly computes the active_memberships array.
717        assert_eq!(
718            member_event.content.active_memberships(None)[0],
719            MembershipData::Session(&membership)
720        );
721        assert_eq!(member_event.unsigned.age, Some(int!(10)));
722        assert_eq!(
723            member_event.unsigned.prev_content.unwrap(),
724            CallMemberEventContent::Empty(EmptyMembershipData { leave_reason: None }),
725        );
726    }
727
728    #[test]
729    fn deserialize_member_event() {
730        deserialize_member_event_helper("@user:example.org");
731    }
732
733    #[test]
734    fn deserialize_member_event_with_scoped_state_key_prefixed() {
735        deserialize_member_event_helper("_@user:example.org_THIS_DEVICE_m.call");
736    }
737
738    #[test]
739    fn deserialize_member_event_with_scoped_state_key_unprefixed() {
740        deserialize_member_event_helper("@user:example.org_THIS_DEVICE_m.call");
741    }
742
743    fn timestamps()
744    -> (MilliSecondsSinceUnixEpoch, MilliSecondsSinceUnixEpoch, MilliSecondsSinceUnixEpoch) {
745        let now = MilliSecondsSinceUnixEpoch::now();
746        let one_second_ago =
747            now.to_system_time().unwrap().checked_sub(Duration::from_secs(1)).unwrap();
748        let two_hours_ago =
749            now.to_system_time().unwrap().checked_sub(Duration::from_secs(60 * 60 * 2)).unwrap();
750        (
751            now,
752            MilliSecondsSinceUnixEpoch::from_system_time(one_second_ago).unwrap(),
753            MilliSecondsSinceUnixEpoch::from_system_time(two_hours_ago).unwrap(),
754        )
755    }
756
757    #[test]
758    fn legacy_memberships_do_expire() {
759        let content_legacy = create_call_member_legacy_event_content();
760        let (now, one_second_ago, two_hours_ago) = timestamps();
761
762        assert_eq!(
763            content_legacy.active_memberships(Some(one_second_ago)),
764            content_legacy.memberships()
765        );
766        assert_eq!(content_legacy.active_memberships(Some(now)), content_legacy.memberships());
767        assert_eq!(
768            content_legacy.active_memberships(Some(two_hours_ago)),
769            vec![] as Vec<MembershipData<'_>>
770        );
771    }
772
773    #[test]
774    fn session_membership_does_expire() {
775        let content = create_call_member_event_content();
776        let (now, one_second_ago, two_hours_ago) = timestamps();
777
778        assert_eq!(content.active_memberships(Some(now)), content.memberships());
779        assert_eq!(content.active_memberships(Some(one_second_ago)), content.memberships());
780        assert_eq!(
781            content.active_memberships(Some(two_hours_ago)),
782            vec![] as Vec<MembershipData<'_>>
783        );
784    }
785
786    #[test]
787    fn set_created_ts() {
788        let mut content_now = create_call_member_legacy_event_content();
789        let mut content_two_hours_ago = create_call_member_legacy_event_content();
790        let mut content_one_second_ago = create_call_member_legacy_event_content();
791        let (now, one_second_ago, two_hours_ago) = timestamps();
792
793        content_now.set_created_ts_if_none(now);
794        content_one_second_ago.set_created_ts_if_none(one_second_ago);
795        content_two_hours_ago.set_created_ts_if_none(two_hours_ago);
796        assert_eq!(content_now.active_memberships(None), content_now.memberships());
797
798        assert_eq!(
799            content_two_hours_ago.active_memberships(None),
800            vec![] as Vec<MembershipData<'_>>
801        );
802        assert_eq!(
803            content_one_second_ago.active_memberships(None),
804            content_one_second_ago.memberships()
805        );
806
807        // created_ts should not be overwritten.
808        content_two_hours_ago.set_created_ts_if_none(one_second_ago);
809        // There still should be no active membership.
810        assert_eq!(
811            content_two_hours_ago.active_memberships(None),
812            vec![] as Vec<MembershipData<'_>>
813        );
814    }
815
816    #[test]
817    fn test_parse_rtc_member_event_key() {
818        assert!(from_json_value::<AnyStateEvent>(member_event_json("abc")).is_err());
819        assert!(from_json_value::<AnyStateEvent>(member_event_json("@nocolon")).is_err());
820        assert!(from_json_value::<AnyStateEvent>(member_event_json("@noserverpart:")).is_err());
821        assert!(
822            from_json_value::<AnyStateEvent>(member_event_json("@noserverpart:_suffix")).is_err()
823        );
824
825        let user_id = user_id!("@username:example.org").as_str();
826        let device_id = device_id!("VALID_DEVICE_ID").as_str();
827
828        let parse_result = from_json_value::<AnyStateEvent>(member_event_json(user_id));
829        assert_matches!(parse_result, Ok(_));
830        assert_matches!(
831            from_json_value::<AnyStateEvent>(member_event_json(&format!("{user_id}_{device_id}"))),
832            Ok(_)
833        );
834
835        assert_matches!(
836            from_json_value::<AnyStateEvent>(member_event_json(&format!(
837                "{user_id}:invalid_suffix"
838            ))),
839            Err(_)
840        );
841
842        assert_matches!(
843            from_json_value::<AnyStateEvent>(member_event_json(&format!("_{user_id}"))),
844            Err(_)
845        );
846
847        assert_matches!(
848            from_json_value::<AnyStateEvent>(member_event_json(&format!("_{user_id}_{device_id}"))),
849            Ok(_)
850        );
851
852        assert_matches!(
853            from_json_value::<AnyStateEvent>(member_event_json(&format!(
854                "_{user_id}:invalid_suffix"
855            ))),
856            Err(_)
857        );
858        assert_matches!(
859            from_json_value::<AnyStateEvent>(member_event_json(&format!("{user_id}_"))),
860            Err(_)
861        );
862    }
863}