1use js_int::Int;
6use ruma_common::{
7 OwnedMxcUri, OwnedTransactionId, OwnedUserId, ServerSignatures, UserId,
8 room_version_rules::RedactionRules,
9 serde::{CanBeEmpty, Raw, StringEnum},
10};
11use ruma_macros::EventContent;
12use serde::{Deserialize, Serialize};
13
14use crate::{
15 AnyStrippedStateEvent, BundledStateRelations, PossiblyRedactedStateEventContent, PrivOwnedStr,
16 RedactContent, RedactedStateEventContent, StateEventType, StaticEventContent,
17};
18
19mod change;
20
21use self::change::membership_change;
22pub use self::change::{Change, MembershipChange, MembershipDetails};
23
24#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
45#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
46#[ruma_event(
47 type = "m.room.member",
48 kind = State,
49 state_key_type = OwnedUserId,
50 unsigned_type = RoomMemberUnsigned,
51 custom_redacted,
52 custom_possibly_redacted,
53)]
54pub struct RoomMemberEventContent {
55 #[serde(skip_serializing_if = "Option::is_none")]
60 #[cfg_attr(
61 feature = "compat-empty-string-null",
62 serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
63 )]
64 pub avatar_url: Option<OwnedMxcUri>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
70 pub displayname: Option<String>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
75 pub is_direct: Option<bool>,
76
77 pub membership: MembershipState,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
83 pub third_party_invite: Option<ThirdPartyInvite>,
84
85 #[cfg(feature = "unstable-msc2448")]
90 #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
91 pub blurhash: Option<String>,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
104 pub reason: Option<String>,
105
106 #[serde(rename = "join_authorised_via_users_server")]
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub join_authorized_via_users_server: Option<OwnedUserId>,
110}
111
112impl RoomMemberEventContent {
113 pub fn new(membership: MembershipState) -> Self {
115 Self {
116 membership,
117 avatar_url: None,
118 displayname: None,
119 is_direct: None,
120 third_party_invite: None,
121 #[cfg(feature = "unstable-msc2448")]
122 blurhash: None,
123 reason: None,
124 join_authorized_via_users_server: None,
125 }
126 }
127
128 pub fn details(&self) -> MembershipDetails<'_> {
133 MembershipDetails {
134 avatar_url: self.avatar_url.as_deref(),
135 displayname: self.displayname.as_deref(),
136 membership: &self.membership,
137 }
138 }
139
140 pub fn membership_change<'a>(
152 &'a self,
153 prev_details: Option<MembershipDetails<'a>>,
154 sender: &UserId,
155 state_key: &UserId,
156 ) -> MembershipChange<'a> {
157 membership_change(self.details(), prev_details, sender, state_key)
158 }
159}
160
161impl RedactContent for RoomMemberEventContent {
162 type Redacted = RedactedRoomMemberEventContent;
163
164 fn redact(self, rules: &RedactionRules) -> RedactedRoomMemberEventContent {
165 RedactedRoomMemberEventContent {
166 membership: self.membership,
167 third_party_invite: self.third_party_invite.and_then(|i| i.redact(rules)),
168 join_authorized_via_users_server: self
169 .join_authorized_via_users_server
170 .filter(|_| rules.keep_room_member_join_authorised_via_users_server),
171 }
172 }
173}
174
175#[derive(Clone, Debug, Deserialize, Serialize)]
179#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
180pub struct PossiblyRedactedRoomMemberEventContent {
181 #[serde(skip_serializing_if = "Option::is_none")]
186 #[cfg_attr(
187 feature = "compat-empty-string-null",
188 serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
189 )]
190 pub avatar_url: Option<OwnedMxcUri>,
191
192 #[serde(skip_serializing_if = "Option::is_none")]
196 pub displayname: Option<String>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
201 pub is_direct: Option<bool>,
202
203 pub membership: MembershipState,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
209 pub third_party_invite: Option<PossiblyRedactedThirdPartyInvite>,
210
211 #[cfg(feature = "unstable-msc2448")]
216 #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
217 pub blurhash: Option<String>,
218
219 #[serde(skip_serializing_if = "Option::is_none")]
230 pub reason: Option<String>,
231
232 #[serde(rename = "join_authorised_via_users_server")]
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub join_authorized_via_users_server: Option<OwnedUserId>,
236}
237
238impl PossiblyRedactedRoomMemberEventContent {
239 pub fn new(membership: MembershipState) -> Self {
241 Self {
242 membership,
243 avatar_url: None,
244 displayname: None,
245 is_direct: None,
246 third_party_invite: None,
247 #[cfg(feature = "unstable-msc2448")]
248 blurhash: None,
249 reason: None,
250 join_authorized_via_users_server: None,
251 }
252 }
253
254 pub fn details(&self) -> MembershipDetails<'_> {
259 MembershipDetails {
260 avatar_url: self.avatar_url.as_deref(),
261 displayname: self.displayname.as_deref(),
262 membership: &self.membership,
263 }
264 }
265
266 pub fn membership_change<'a>(
278 &'a self,
279 prev_details: Option<MembershipDetails<'a>>,
280 sender: &UserId,
281 state_key: &UserId,
282 ) -> MembershipChange<'a> {
283 membership_change(self.details(), prev_details, sender, state_key)
284 }
285}
286
287impl PossiblyRedactedStateEventContent for PossiblyRedactedRoomMemberEventContent {
288 type StateKey = OwnedUserId;
289
290 fn event_type(&self) -> StateEventType {
291 StateEventType::RoomMember
292 }
293}
294
295impl StaticEventContent for PossiblyRedactedRoomMemberEventContent {
296 const TYPE: &'static str = RoomMemberEventContent::TYPE;
297 type IsPrefix = <RoomMemberEventContent as StaticEventContent>::IsPrefix;
298}
299
300impl RedactContent for PossiblyRedactedRoomMemberEventContent {
301 type Redacted = Self;
302
303 fn redact(self, rules: &RedactionRules) -> Self {
304 Self {
305 membership: self.membership,
306 third_party_invite: self.third_party_invite.and_then(|i| i.redact(rules)),
307 join_authorized_via_users_server: self
308 .join_authorized_via_users_server
309 .filter(|_| rules.keep_room_member_join_authorised_via_users_server),
310 avatar_url: None,
311 displayname: None,
312 is_direct: None,
313 #[cfg(feature = "unstable-msc2448")]
314 blurhash: None,
315 reason: None,
316 }
317 }
318}
319
320impl From<RoomMemberEventContent> for PossiblyRedactedRoomMemberEventContent {
321 fn from(value: RoomMemberEventContent) -> Self {
322 let RoomMemberEventContent {
323 avatar_url,
324 displayname,
325 is_direct,
326 membership,
327 third_party_invite,
328 #[cfg(feature = "unstable-msc2448")]
329 blurhash,
330 reason,
331 join_authorized_via_users_server,
332 } = value;
333
334 Self {
335 avatar_url,
336 displayname,
337 is_direct,
338 membership,
339 third_party_invite: third_party_invite.map(Into::into),
340 #[cfg(feature = "unstable-msc2448")]
341 blurhash,
342 reason,
343 join_authorized_via_users_server,
344 }
345 }
346}
347
348impl From<RedactedRoomMemberEventContent> for PossiblyRedactedRoomMemberEventContent {
349 fn from(value: RedactedRoomMemberEventContent) -> Self {
350 let RedactedRoomMemberEventContent {
351 membership,
352 third_party_invite,
353 join_authorized_via_users_server,
354 } = value;
355
356 Self {
357 avatar_url: None,
358 displayname: None,
359 is_direct: None,
360 membership,
361 third_party_invite: third_party_invite.map(Into::into),
362 #[cfg(feature = "unstable-msc2448")]
363 blurhash: None,
364 reason: None,
365 join_authorized_via_users_server,
366 }
367 }
368}
369
370#[derive(Clone, Debug, Deserialize, Serialize)]
372#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
373pub struct RedactedRoomMemberEventContent {
374 pub membership: MembershipState,
376
377 #[serde(skip_serializing_if = "Option::is_none")]
380 pub third_party_invite: Option<RedactedThirdPartyInvite>,
381
382 #[serde(rename = "join_authorised_via_users_server", skip_serializing_if = "Option::is_none")]
387 pub join_authorized_via_users_server: Option<OwnedUserId>,
388}
389
390impl RedactedRoomMemberEventContent {
391 pub fn new(membership: MembershipState) -> Self {
393 Self { membership, third_party_invite: None, join_authorized_via_users_server: None }
394 }
395
396 pub fn details(&self) -> MembershipDetails<'_> {
401 MembershipDetails { avatar_url: None, displayname: None, membership: &self.membership }
402 }
403
404 pub fn membership_change<'a>(
419 &'a self,
420 prev_details: Option<MembershipDetails<'a>>,
421 sender: &UserId,
422 state_key: &UserId,
423 ) -> MembershipChange<'a> {
424 membership_change(self.details(), prev_details, sender, state_key)
425 }
426}
427
428impl RedactedStateEventContent for RedactedRoomMemberEventContent {
429 type StateKey = OwnedUserId;
430
431 fn event_type(&self) -> StateEventType {
432 StateEventType::RoomMember
433 }
434}
435
436impl StaticEventContent for RedactedRoomMemberEventContent {
437 const TYPE: &'static str = RoomMemberEventContent::TYPE;
438 type IsPrefix = <RoomMemberEventContent as StaticEventContent>::IsPrefix;
439}
440
441impl RoomMemberEvent {
442 pub fn membership(&self) -> &MembershipState {
444 match self {
445 Self::Original(ev) => &ev.content.membership,
446 Self::Redacted(ev) => &ev.content.membership,
447 }
448 }
449}
450
451impl SyncRoomMemberEvent {
452 pub fn membership(&self) -> &MembershipState {
454 match self {
455 Self::Original(ev) => &ev.content.membership,
456 Self::Redacted(ev) => &ev.content.membership,
457 }
458 }
459}
460
461#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
463#[derive(Clone, StringEnum)]
464#[ruma_enum(rename_all = "lowercase")]
465#[non_exhaustive]
466pub enum MembershipState {
467 Ban,
469
470 Invite,
472
473 Join,
475
476 Knock,
478
479 Leave,
481
482 #[doc(hidden)]
483 _Custom(PrivOwnedStr),
484}
485
486#[derive(Clone, Debug, Deserialize, Serialize)]
488#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
489pub struct ThirdPartyInvite {
490 pub display_name: String,
493
494 pub signed: Raw<SignedContent>,
498}
499
500impl ThirdPartyInvite {
501 pub fn new(display_name: String, signed: Raw<SignedContent>) -> Self {
503 Self { display_name, signed }
504 }
505
506 fn redact(self, rules: &RedactionRules) -> Option<RedactedThirdPartyInvite> {
511 rules
512 .keep_room_member_third_party_invite_signed
513 .then_some(RedactedThirdPartyInvite { signed: self.signed })
514 }
515}
516
517#[derive(Clone, Debug, Deserialize, Serialize)]
519#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
520pub struct RedactedThirdPartyInvite {
521 pub signed: Raw<SignedContent>,
525}
526
527#[derive(Clone, Debug, Deserialize, Serialize)]
529#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
530pub struct PossiblyRedactedThirdPartyInvite {
531 #[serde(skip_serializing_if = "Option::is_none")]
534 pub display_name: Option<String>,
535
536 pub signed: Raw<SignedContent>,
540}
541
542impl PossiblyRedactedThirdPartyInvite {
543 pub fn new(display_name: String, signed: Raw<SignedContent>) -> Self {
546 Self { display_name: Some(display_name), signed }
547 }
548
549 fn redact(self, rules: &RedactionRules) -> Option<Self> {
554 rules
555 .keep_room_member_third_party_invite_signed
556 .then_some(Self { display_name: None, signed: self.signed })
557 }
558}
559
560impl From<ThirdPartyInvite> for PossiblyRedactedThirdPartyInvite {
561 fn from(value: ThirdPartyInvite) -> Self {
562 let ThirdPartyInvite { display_name, signed } = value;
563 Self { display_name: Some(display_name), signed }
564 }
565}
566
567impl From<RedactedThirdPartyInvite> for PossiblyRedactedThirdPartyInvite {
568 fn from(value: RedactedThirdPartyInvite) -> Self {
569 let RedactedThirdPartyInvite { signed } = value;
570 Self { display_name: None, signed }
571 }
572}
573
574#[derive(Clone, Debug, Deserialize, Serialize)]
577#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
578pub struct SignedContent {
579 pub mxid: OwnedUserId,
583
584 pub signatures: ServerSignatures,
587
588 pub token: String,
590}
591
592impl SignedContent {
593 pub fn new(signatures: ServerSignatures, mxid: OwnedUserId, token: String) -> Self {
595 Self { mxid, signatures, token }
596 }
597}
598
599impl OriginalRoomMemberEvent {
600 pub fn details(&self) -> MembershipDetails<'_> {
605 self.content.details()
606 }
607
608 pub fn prev_content(&self) -> Option<&PossiblyRedactedRoomMemberEventContent> {
612 self.unsigned.prev_content.as_ref()
613 }
614
615 fn prev_details(&self) -> Option<MembershipDetails<'_>> {
616 self.prev_content().map(|c| c.details())
617 }
618
619 pub fn membership_change(&self) -> MembershipChange<'_> {
625 membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
626 }
627}
628
629impl RedactedRoomMemberEvent {
630 pub fn details(&self) -> MembershipDetails<'_> {
635 self.content.details()
636 }
637
638 pub fn membership_change<'a>(
648 &'a self,
649 prev_details: Option<MembershipDetails<'a>>,
650 ) -> MembershipChange<'a> {
651 membership_change(self.details(), prev_details, &self.sender, &self.state_key)
652 }
653}
654
655impl OriginalSyncRoomMemberEvent {
656 pub fn details(&self) -> MembershipDetails<'_> {
661 self.content.details()
662 }
663
664 pub fn prev_content(&self) -> Option<&PossiblyRedactedRoomMemberEventContent> {
668 self.unsigned.prev_content.as_ref()
669 }
670
671 fn prev_details(&self) -> Option<MembershipDetails<'_>> {
672 self.prev_content().map(|c| c.details())
673 }
674
675 pub fn membership_change(&self) -> MembershipChange<'_> {
681 membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
682 }
683}
684
685impl RedactedSyncRoomMemberEvent {
686 pub fn details(&self) -> MembershipDetails<'_> {
691 self.content.details()
692 }
693
694 pub fn membership_change<'a>(
704 &'a self,
705 prev_details: Option<MembershipDetails<'a>>,
706 ) -> MembershipChange<'a> {
707 membership_change(self.details(), prev_details, &self.sender, &self.state_key)
708 }
709}
710
711impl StrippedRoomMemberEvent {
712 pub fn details(&self) -> MembershipDetails<'_> {
717 self.content.details()
718 }
719
720 pub fn membership_change<'a>(
730 &'a self,
731 prev_details: Option<MembershipDetails<'a>>,
732 ) -> MembershipChange<'a> {
733 membership_change(self.details(), prev_details, &self.sender, &self.state_key)
734 }
735}
736
737#[derive(Clone, Debug, Default, Deserialize)]
739#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
740pub struct RoomMemberUnsigned {
741 pub age: Option<Int>,
747
748 pub transaction_id: Option<OwnedTransactionId>,
751
752 pub prev_content: Option<PossiblyRedactedRoomMemberEventContent>,
754
755 #[serde(default)]
758 pub invite_room_state: Vec<Raw<AnyStrippedStateEvent>>,
759
760 #[serde(default)]
762 pub knock_room_state: Vec<Raw<AnyStrippedStateEvent>>,
763
764 #[serde(rename = "m.relations", default)]
768 pub relations: BundledStateRelations,
769}
770
771impl RoomMemberUnsigned {
772 pub fn new() -> Self {
774 Self::default()
775 }
776}
777
778impl CanBeEmpty for RoomMemberUnsigned {
779 fn is_empty(&self) -> bool {
785 self.age.is_none()
786 && self.transaction_id.is_none()
787 && self.prev_content.is_none()
788 && self.invite_room_state.is_empty()
789 && self.relations.is_empty()
790 }
791}
792
793#[cfg(test)]
794mod tests {
795 use assert_matches2::assert_matches;
796 use js_int::uint;
797 use maplit::btreemap;
798 use ruma_common::{
799 MilliSecondsSinceUnixEpoch, ServerSigningKeyId, SigningKeyAlgorithm, mxc_uri,
800 serde::CanBeEmpty, server_name, server_signing_key_version, user_id,
801 };
802 use serde_json::{from_value as from_json_value, json};
803
804 use super::{MembershipState, RoomMemberEventContent};
805 use crate::OriginalStateEvent;
806
807 #[test]
808 fn serde_with_no_prev_content() {
809 let json = json!({
810 "type": "m.room.member",
811 "content": {
812 "membership": "join"
813 },
814 "event_id": "$h29iv0s8:example.com",
815 "origin_server_ts": 1,
816 "room_id": "!n8f893n9:example.com",
817 "sender": "@carl:example.com",
818 "state_key": "@carl:example.com"
819 });
820
821 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
822 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
823 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
824 assert_eq!(ev.room_id, "!n8f893n9:example.com");
825 assert_eq!(ev.sender, "@carl:example.com");
826 assert_eq!(ev.state_key, "@carl:example.com");
827 assert!(ev.unsigned.is_empty());
828
829 assert_eq!(ev.content.avatar_url, None);
830 assert_eq!(ev.content.displayname, None);
831 assert_eq!(ev.content.is_direct, None);
832 assert_eq!(ev.content.membership, MembershipState::Join);
833 assert_matches!(ev.content.third_party_invite, None);
834 }
835
836 #[test]
837 fn serde_with_prev_content() {
838 let json = json!({
839 "type": "m.room.member",
840 "content": {
841 "membership": "join"
842 },
843 "event_id": "$h29iv0s8:example.com",
844 "origin_server_ts": 1,
845 "room_id": "!n8f893n9:example.com",
846 "sender": "@carl:example.com",
847 "state_key": "@carl:example.com",
848 "unsigned": {
849 "prev_content": {
850 "membership": "join"
851 },
852 },
853 });
854
855 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
856 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
857 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
858 assert_eq!(ev.room_id, "!n8f893n9:example.com");
859 assert_eq!(ev.sender, "@carl:example.com");
860 assert_eq!(ev.state_key, "@carl:example.com");
861
862 assert_eq!(ev.content.avatar_url, None);
863 assert_eq!(ev.content.displayname, None);
864 assert_eq!(ev.content.is_direct, None);
865 assert_eq!(ev.content.membership, MembershipState::Join);
866 assert_matches!(ev.content.third_party_invite, None);
867
868 let prev_content = ev.unsigned.prev_content.unwrap();
869 assert_eq!(prev_content.avatar_url, None);
870 assert_eq!(prev_content.displayname, None);
871 assert_eq!(prev_content.is_direct, None);
872 assert_eq!(prev_content.membership, MembershipState::Join);
873 assert_matches!(prev_content.third_party_invite, None);
874 }
875
876 #[test]
877 fn serde_with_content_full() {
878 let json = json!({
879 "type": "m.room.member",
880 "content": {
881 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
882 "displayname": "Alice Margatroid",
883 "is_direct": true,
884 "membership": "invite",
885 "third_party_invite": {
886 "display_name": "alice",
887 "signed": {
888 "mxid": "@alice:example.org",
889 "signatures": {
890 "magic.forest": {
891 "ed25519:3": "foobar"
892 }
893 },
894 "token": "abc123"
895 }
896 }
897 },
898 "event_id": "$143273582443PhrSn:example.org",
899 "origin_server_ts": 233,
900 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
901 "sender": "@alice:example.org",
902 "state_key": "@alice:example.org"
903 });
904
905 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
906 assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
907 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
908 assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
909 assert_eq!(ev.sender, "@alice:example.org");
910 assert_eq!(ev.state_key, "@alice:example.org");
911 assert!(ev.unsigned.is_empty());
912
913 assert_eq!(
914 ev.content.avatar_url.as_deref(),
915 Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
916 );
917 assert_eq!(ev.content.displayname.as_deref(), Some("Alice Margatroid"));
918 assert_eq!(ev.content.is_direct, Some(true));
919 assert_eq!(ev.content.membership, MembershipState::Invite);
920
921 let third_party_invite = ev.content.third_party_invite.unwrap();
922 assert_eq!(third_party_invite.display_name, "alice");
923 let signed = third_party_invite.signed.deserialize().unwrap();
924 assert_eq!(signed.mxid, "@alice:example.org");
925 assert_eq!(signed.signatures.len(), 1);
926 let server_signatures = signed.signatures.get(server_name!("magic.forest")).unwrap();
927 assert_eq!(
928 *server_signatures,
929 btreemap! {
930 ServerSigningKeyId::from_parts(
931 SigningKeyAlgorithm::Ed25519,
932 server_signing_key_version!("3")
933 ) => "foobar".to_owned()
934 }
935 );
936 assert_eq!(signed.token, "abc123");
937 }
938
939 #[test]
940 fn serde_with_prev_content_full() {
941 let json = json!({
942 "type": "m.room.member",
943 "content": {
944 "membership": "join",
945 },
946 "event_id": "$143273582443PhrSn:example.org",
947 "origin_server_ts": 233,
948 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
949 "sender": "@alice:example.org",
950 "state_key": "@alice:example.org",
951 "unsigned": {
952 "prev_content": {
953 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
954 "displayname": "Alice Margatroid",
955 "is_direct": true,
956 "membership": "invite",
957 "third_party_invite": {
958 "display_name": "alice",
959 "signed": {
960 "mxid": "@alice:example.org",
961 "signatures": {
962 "magic.forest": {
963 "ed25519:3": "foobar",
964 },
965 },
966 "token": "abc123"
967 },
968 },
969 },
970 },
971 });
972
973 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
974 assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
975 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
976 assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
977 assert_eq!(ev.sender, "@alice:example.org");
978 assert_eq!(ev.state_key, "@alice:example.org");
979
980 assert_eq!(ev.content.avatar_url, None);
981 assert_eq!(ev.content.displayname, None);
982 assert_eq!(ev.content.is_direct, None);
983 assert_eq!(ev.content.membership, MembershipState::Join);
984 assert_matches!(ev.content.third_party_invite, None);
985
986 let prev_content = ev.unsigned.prev_content.unwrap();
987 assert_eq!(
988 prev_content.avatar_url.as_deref(),
989 Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
990 );
991 assert_eq!(prev_content.displayname.as_deref(), Some("Alice Margatroid"));
992 assert_eq!(prev_content.is_direct, Some(true));
993 assert_eq!(prev_content.membership, MembershipState::Invite);
994
995 let third_party_invite = prev_content.third_party_invite.unwrap();
996 assert_eq!(third_party_invite.display_name.as_deref(), Some("alice"));
997 let signed = third_party_invite.signed.deserialize().unwrap();
998 assert_eq!(signed.mxid, "@alice:example.org");
999 assert_eq!(signed.signatures.len(), 1);
1000 let server_signatures = signed.signatures.get(server_name!("magic.forest")).unwrap();
1001 assert_eq!(
1002 *server_signatures,
1003 btreemap! {
1004 ServerSigningKeyId::from_parts(
1005 SigningKeyAlgorithm::Ed25519,
1006 server_signing_key_version!("3")
1007 ) => "foobar".to_owned()
1008 }
1009 );
1010 assert_eq!(signed.token, "abc123");
1011 }
1012
1013 #[test]
1014 fn serde_with_join_authorized() {
1015 let json = json!({
1016 "type": "m.room.member",
1017 "content": {
1018 "membership": "join",
1019 "join_authorised_via_users_server": "@notcarl:example.com"
1020 },
1021 "event_id": "$h29iv0s8:example.com",
1022 "origin_server_ts": 1,
1023 "room_id": "!n8f893n9:example.com",
1024 "sender": "@carl:example.com",
1025 "state_key": "@carl:example.com"
1026 });
1027
1028 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
1029 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
1030 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
1031 assert_eq!(ev.room_id, "!n8f893n9:example.com");
1032 assert_eq!(ev.sender, "@carl:example.com");
1033 assert_eq!(ev.state_key, "@carl:example.com");
1034 assert!(ev.unsigned.is_empty());
1035
1036 assert_eq!(ev.content.avatar_url, None);
1037 assert_eq!(ev.content.displayname, None);
1038 assert_eq!(ev.content.is_direct, None);
1039 assert_eq!(ev.content.membership, MembershipState::Join);
1040 assert_matches!(ev.content.third_party_invite, None);
1041 assert_eq!(
1042 ev.content.join_authorized_via_users_server.as_deref(),
1043 Some(user_id!("@notcarl:example.com"))
1044 );
1045 }
1046}