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)]
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(), )
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, "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, "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, "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, "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 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 content_two_hours_ago.set_created_ts_if_none(one_second_ago);
809 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}