1use js_int::Int;
6#[cfg(feature = "unstable-msc4293")]
7use ruma_common::canonical_json::RedactionEvent;
8use ruma_common::{
9 OwnedEventId, OwnedMxcUri, OwnedTransactionId, OwnedUserId, ServerSignatures, UserId,
10 room_version_rules::RedactionRules,
11 serde::{CanBeEmpty, Raw, StringEnum},
12};
13use ruma_macros::EventContent;
14use serde::{Deserialize, Serialize};
15
16use crate::{
17 AnyStrippedStateEvent, BundledStateRelations, PossiblyRedactedStateEventContent, PrivOwnedStr,
18 RedactContent, RedactedStateEventContent, StateEventType, StaticEventContent,
19};
20
21mod change;
22
23use self::change::membership_change;
24pub use self::change::{Change, MembershipChange, MembershipDetails};
25
26#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
47#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
48#[ruma_event(
49 type = "m.room.member",
50 kind = State,
51 state_key_type = OwnedUserId,
52 unsigned_type = RoomMemberUnsigned,
53 custom_redacted,
54 custom_possibly_redacted,
55)]
56pub struct RoomMemberEventContent {
57 #[serde(skip_serializing_if = "Option::is_none")]
62 #[cfg_attr(
63 feature = "compat-empty-string-null",
64 serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
65 )]
66 pub avatar_url: Option<OwnedMxcUri>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
72 pub displayname: Option<String>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
77 pub is_direct: Option<bool>,
78
79 pub membership: MembershipState,
81
82 #[serde(skip_serializing_if = "Option::is_none")]
85 pub third_party_invite: Option<ThirdPartyInvite>,
86
87 #[cfg(feature = "unstable-msc2448")]
92 #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
93 pub blurhash: Option<String>,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
106 pub reason: Option<String>,
107
108 #[serde(rename = "join_authorised_via_users_server")]
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub join_authorized_via_users_server: Option<OwnedUserId>,
112
113 #[cfg(feature = "unstable-msc4293")]
115 #[serde(
116 default,
117 rename = "org.matrix.msc4293.redact_events",
118 skip_serializing_if = "ruma_common::serde::is_default"
119 )]
120 pub redact_events: bool,
121}
122
123impl RoomMemberEventContent {
124 pub fn new(membership: MembershipState) -> Self {
126 Self {
127 membership,
128 avatar_url: None,
129 displayname: None,
130 is_direct: None,
131 third_party_invite: None,
132 #[cfg(feature = "unstable-msc2448")]
133 blurhash: None,
134 reason: None,
135 join_authorized_via_users_server: None,
136 #[cfg(feature = "unstable-msc4293")]
137 redact_events: false,
138 }
139 }
140
141 pub fn details(&self) -> MembershipDetails<'_> {
146 MembershipDetails {
147 avatar_url: self.avatar_url.as_deref(),
148 displayname: self.displayname.as_deref(),
149 membership: &self.membership,
150 }
151 }
152
153 pub fn membership_change<'a>(
165 &'a self,
166 prev_details: Option<MembershipDetails<'a>>,
167 sender: &UserId,
168 state_key: &UserId,
169 ) -> MembershipChange<'a> {
170 membership_change(self.details(), prev_details, sender, state_key)
171 }
172}
173
174impl RedactContent for RoomMemberEventContent {
175 type Redacted = RedactedRoomMemberEventContent;
176
177 fn redact(self, rules: &RedactionRules) -> RedactedRoomMemberEventContent {
178 RedactedRoomMemberEventContent {
179 membership: self.membership,
180 third_party_invite: self.third_party_invite.and_then(|i| i.redact(rules)),
181 join_authorized_via_users_server: self
182 .join_authorized_via_users_server
183 .filter(|_| rules.keep_room_member_join_authorised_via_users_server),
184 }
185 }
186}
187
188#[derive(Clone, Debug, Deserialize, Serialize)]
192#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
193pub struct PossiblyRedactedRoomMemberEventContent {
194 #[serde(skip_serializing_if = "Option::is_none")]
199 #[cfg_attr(
200 feature = "compat-empty-string-null",
201 serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
202 )]
203 pub avatar_url: Option<OwnedMxcUri>,
204
205 #[serde(skip_serializing_if = "Option::is_none")]
209 pub displayname: Option<String>,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
214 pub is_direct: Option<bool>,
215
216 pub membership: MembershipState,
218
219 #[serde(skip_serializing_if = "Option::is_none")]
222 pub third_party_invite: Option<PossiblyRedactedThirdPartyInvite>,
223
224 #[cfg(feature = "unstable-msc2448")]
229 #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
230 pub blurhash: Option<String>,
231
232 #[serde(skip_serializing_if = "Option::is_none")]
243 pub reason: Option<String>,
244
245 #[serde(rename = "join_authorised_via_users_server")]
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub join_authorized_via_users_server: Option<OwnedUserId>,
249
250 #[cfg(feature = "unstable-msc4293")]
256 #[serde(
257 default,
258 rename = "org.matrix.msc4293.redact_events",
259 skip_serializing_if = "ruma_common::serde::is_default"
260 )]
261 pub redact_events: bool,
262}
263
264impl PossiblyRedactedRoomMemberEventContent {
265 pub fn new(membership: MembershipState) -> Self {
267 Self {
268 membership,
269 avatar_url: None,
270 displayname: None,
271 is_direct: None,
272 third_party_invite: None,
273 #[cfg(feature = "unstable-msc2448")]
274 blurhash: None,
275 reason: None,
276 join_authorized_via_users_server: None,
277 #[cfg(feature = "unstable-msc4293")]
278 redact_events: false,
279 }
280 }
281
282 pub fn details(&self) -> MembershipDetails<'_> {
287 MembershipDetails {
288 avatar_url: self.avatar_url.as_deref(),
289 displayname: self.displayname.as_deref(),
290 membership: &self.membership,
291 }
292 }
293
294 pub fn membership_change<'a>(
306 &'a self,
307 prev_details: Option<MembershipDetails<'a>>,
308 sender: &UserId,
309 state_key: &UserId,
310 ) -> MembershipChange<'a> {
311 membership_change(self.details(), prev_details, sender, state_key)
312 }
313}
314
315impl PossiblyRedactedStateEventContent for PossiblyRedactedRoomMemberEventContent {
316 type StateKey = OwnedUserId;
317
318 fn event_type(&self) -> StateEventType {
319 StateEventType::RoomMember
320 }
321}
322
323impl StaticEventContent for PossiblyRedactedRoomMemberEventContent {
324 const TYPE: &'static str = RoomMemberEventContent::TYPE;
325 type IsPrefix = <RoomMemberEventContent as StaticEventContent>::IsPrefix;
326}
327
328impl RedactContent for PossiblyRedactedRoomMemberEventContent {
329 type Redacted = Self;
330
331 fn redact(self, rules: &RedactionRules) -> Self {
332 Self {
333 membership: self.membership,
334 third_party_invite: self.third_party_invite.and_then(|i| i.redact(rules)),
335 join_authorized_via_users_server: self
336 .join_authorized_via_users_server
337 .filter(|_| rules.keep_room_member_join_authorised_via_users_server),
338 avatar_url: None,
339 displayname: None,
340 is_direct: None,
341 #[cfg(feature = "unstable-msc2448")]
342 blurhash: None,
343 reason: None,
344 #[cfg(feature = "unstable-msc4293")]
345 redact_events: false,
346 }
347 }
348}
349
350impl From<RoomMemberEventContent> for PossiblyRedactedRoomMemberEventContent {
351 fn from(value: RoomMemberEventContent) -> Self {
352 let RoomMemberEventContent {
353 avatar_url,
354 displayname,
355 is_direct,
356 membership,
357 third_party_invite,
358 #[cfg(feature = "unstable-msc2448")]
359 blurhash,
360 reason,
361 join_authorized_via_users_server,
362 #[cfg(feature = "unstable-msc4293")]
363 redact_events,
364 } = value;
365
366 Self {
367 avatar_url,
368 displayname,
369 is_direct,
370 membership,
371 third_party_invite: third_party_invite.map(Into::into),
372 #[cfg(feature = "unstable-msc2448")]
373 blurhash,
374 reason,
375 join_authorized_via_users_server,
376 #[cfg(feature = "unstable-msc4293")]
377 redact_events,
378 }
379 }
380}
381
382impl From<RedactedRoomMemberEventContent> for PossiblyRedactedRoomMemberEventContent {
383 fn from(value: RedactedRoomMemberEventContent) -> Self {
384 let RedactedRoomMemberEventContent {
385 membership,
386 third_party_invite,
387 join_authorized_via_users_server,
388 } = value;
389
390 Self {
391 avatar_url: None,
392 displayname: None,
393 is_direct: None,
394 membership,
395 third_party_invite: third_party_invite.map(Into::into),
396 #[cfg(feature = "unstable-msc2448")]
397 blurhash: None,
398 reason: None,
399 join_authorized_via_users_server,
400 #[cfg(feature = "unstable-msc4293")]
401 redact_events: false,
402 }
403 }
404}
405
406#[derive(Clone, Debug, Deserialize, Serialize)]
408#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
409pub struct RedactedRoomMemberEventContent {
410 pub membership: MembershipState,
412
413 #[serde(skip_serializing_if = "Option::is_none")]
416 pub third_party_invite: Option<RedactedThirdPartyInvite>,
417
418 #[serde(rename = "join_authorised_via_users_server", skip_serializing_if = "Option::is_none")]
423 pub join_authorized_via_users_server: Option<OwnedUserId>,
424}
425
426impl RedactedRoomMemberEventContent {
427 pub fn new(membership: MembershipState) -> Self {
429 Self { membership, third_party_invite: None, join_authorized_via_users_server: None }
430 }
431
432 pub fn details(&self) -> MembershipDetails<'_> {
437 MembershipDetails { avatar_url: None, displayname: None, membership: &self.membership }
438 }
439
440 pub fn membership_change<'a>(
455 &'a self,
456 prev_details: Option<MembershipDetails<'a>>,
457 sender: &UserId,
458 state_key: &UserId,
459 ) -> MembershipChange<'a> {
460 membership_change(self.details(), prev_details, sender, state_key)
461 }
462}
463
464impl RedactedStateEventContent for RedactedRoomMemberEventContent {
465 type StateKey = OwnedUserId;
466
467 fn event_type(&self) -> StateEventType {
468 StateEventType::RoomMember
469 }
470}
471
472impl StaticEventContent for RedactedRoomMemberEventContent {
473 const TYPE: &'static str = RoomMemberEventContent::TYPE;
474 type IsPrefix = <RoomMemberEventContent as StaticEventContent>::IsPrefix;
475}
476
477impl RoomMemberEvent {
478 pub fn membership(&self) -> &MembershipState {
480 match self {
481 Self::Original(ev) => &ev.content.membership,
482 Self::Redacted(ev) => &ev.content.membership,
483 }
484 }
485
486 #[cfg(feature = "unstable-msc4293")]
494 pub fn should_redact_events(&self) -> bool {
495 if let Self::Original(ev) = self { ev.should_redact_events() } else { false }
496 }
497}
498
499impl SyncRoomMemberEvent {
500 pub fn membership(&self) -> &MembershipState {
502 match self {
503 Self::Original(ev) => &ev.content.membership,
504 Self::Redacted(ev) => &ev.content.membership,
505 }
506 }
507
508 #[cfg(feature = "unstable-msc4293")]
516 pub fn should_redact_events(&self) -> bool {
517 if let Self::Original(ev) = self { ev.should_redact_events() } else { false }
518 }
519}
520
521#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
523#[derive(Clone, StringEnum)]
524#[ruma_enum(rename_all = "lowercase")]
525#[non_exhaustive]
526pub enum MembershipState {
527 Ban,
529
530 Invite,
532
533 Join,
535
536 Knock,
538
539 Leave,
541
542 #[doc(hidden)]
543 _Custom(PrivOwnedStr),
544}
545
546#[derive(Clone, Debug, Deserialize, Serialize)]
548#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
549pub struct ThirdPartyInvite {
550 pub display_name: String,
553
554 pub signed: Raw<SignedContent>,
558}
559
560impl ThirdPartyInvite {
561 pub fn new(display_name: String, signed: Raw<SignedContent>) -> Self {
563 Self { display_name, signed }
564 }
565
566 fn redact(self, rules: &RedactionRules) -> Option<RedactedThirdPartyInvite> {
571 rules
572 .keep_room_member_third_party_invite_signed
573 .then_some(RedactedThirdPartyInvite { signed: self.signed })
574 }
575}
576
577#[derive(Clone, Debug, Deserialize, Serialize)]
579#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
580pub struct RedactedThirdPartyInvite {
581 pub signed: Raw<SignedContent>,
585}
586
587#[derive(Clone, Debug, Deserialize, Serialize)]
589#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
590pub struct PossiblyRedactedThirdPartyInvite {
591 #[serde(skip_serializing_if = "Option::is_none")]
594 pub display_name: Option<String>,
595
596 pub signed: Raw<SignedContent>,
600}
601
602impl PossiblyRedactedThirdPartyInvite {
603 pub fn new(display_name: String, signed: Raw<SignedContent>) -> Self {
606 Self { display_name: Some(display_name), signed }
607 }
608
609 fn redact(self, rules: &RedactionRules) -> Option<Self> {
614 rules
615 .keep_room_member_third_party_invite_signed
616 .then_some(Self { display_name: None, signed: self.signed })
617 }
618}
619
620impl From<ThirdPartyInvite> for PossiblyRedactedThirdPartyInvite {
621 fn from(value: ThirdPartyInvite) -> Self {
622 let ThirdPartyInvite { display_name, signed } = value;
623 Self { display_name: Some(display_name), signed }
624 }
625}
626
627impl From<RedactedThirdPartyInvite> for PossiblyRedactedThirdPartyInvite {
628 fn from(value: RedactedThirdPartyInvite) -> Self {
629 let RedactedThirdPartyInvite { signed } = value;
630 Self { display_name: None, signed }
631 }
632}
633
634#[derive(Clone, Debug, Deserialize, Serialize)]
637#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
638pub struct SignedContent {
639 pub mxid: OwnedUserId,
643
644 pub signatures: ServerSignatures,
647
648 pub token: String,
650}
651
652impl SignedContent {
653 pub fn new(signatures: ServerSignatures, mxid: OwnedUserId, token: String) -> Self {
655 Self { mxid, signatures, token }
656 }
657}
658
659impl OriginalRoomMemberEvent {
660 pub fn details(&self) -> MembershipDetails<'_> {
665 self.content.details()
666 }
667
668 pub fn prev_content(&self) -> Option<&PossiblyRedactedRoomMemberEventContent> {
672 self.unsigned.prev_content.as_ref()
673 }
674
675 fn prev_details(&self) -> Option<MembershipDetails<'_>> {
676 self.prev_content().map(|c| c.details())
677 }
678
679 pub fn membership_change(&self) -> MembershipChange<'_> {
685 membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
686 }
687
688 #[cfg(feature = "unstable-msc4293")]
696 pub fn should_redact_events(&self) -> bool {
697 self.content.redact_events
698 && self.state_key != self.sender
699 && matches!(self.content.membership, MembershipState::Ban | MembershipState::Leave)
700 }
701}
702
703impl RedactedRoomMemberEvent {
704 pub fn details(&self) -> MembershipDetails<'_> {
709 self.content.details()
710 }
711
712 pub fn membership_change<'a>(
722 &'a self,
723 prev_details: Option<MembershipDetails<'a>>,
724 ) -> MembershipChange<'a> {
725 membership_change(self.details(), prev_details, &self.sender, &self.state_key)
726 }
727
728 #[cfg(feature = "unstable-msc4293")]
736 pub fn should_redact_events(&self) -> bool {
737 false
739 }
740}
741
742impl OriginalSyncRoomMemberEvent {
743 pub fn details(&self) -> MembershipDetails<'_> {
748 self.content.details()
749 }
750
751 pub fn prev_content(&self) -> Option<&PossiblyRedactedRoomMemberEventContent> {
755 self.unsigned.prev_content.as_ref()
756 }
757
758 fn prev_details(&self) -> Option<MembershipDetails<'_>> {
759 self.prev_content().map(|c| c.details())
760 }
761
762 pub fn membership_change(&self) -> MembershipChange<'_> {
768 membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
769 }
770
771 #[cfg(feature = "unstable-msc4293")]
779 pub fn should_redact_events(&self) -> bool {
780 self.content.redact_events
781 && self.state_key != self.sender
782 && matches!(self.content.membership, MembershipState::Ban | MembershipState::Leave)
783 }
784}
785
786impl RedactedSyncRoomMemberEvent {
787 pub fn details(&self) -> MembershipDetails<'_> {
792 self.content.details()
793 }
794
795 pub fn membership_change<'a>(
805 &'a self,
806 prev_details: Option<MembershipDetails<'a>>,
807 ) -> MembershipChange<'a> {
808 membership_change(self.details(), prev_details, &self.sender, &self.state_key)
809 }
810
811 #[cfg(feature = "unstable-msc4293")]
819 pub fn should_redact_events(&self) -> bool {
820 false
822 }
823}
824
825impl StrippedRoomMemberEvent {
826 pub fn details(&self) -> MembershipDetails<'_> {
831 self.content.details()
832 }
833
834 pub fn membership_change<'a>(
844 &'a self,
845 prev_details: Option<MembershipDetails<'a>>,
846 ) -> MembershipChange<'a> {
847 membership_change(self.details(), prev_details, &self.sender, &self.state_key)
848 }
849
850 #[cfg(feature = "unstable-msc4293")]
858 pub fn should_redact_events(&self) -> bool {
859 self.content.redact_events
860 && self.state_key != self.sender
861 && matches!(self.content.membership, MembershipState::Ban | MembershipState::Leave)
862 }
863}
864
865#[derive(Clone, Debug, Default, Deserialize)]
867#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
868pub struct RoomMemberUnsigned {
869 pub age: Option<Int>,
875
876 pub transaction_id: Option<OwnedTransactionId>,
879
880 pub replaces_state: Option<OwnedEventId>,
885
886 pub prev_content: Option<PossiblyRedactedRoomMemberEventContent>,
888
889 #[serde(default)]
892 pub invite_room_state: Vec<Raw<AnyStrippedStateEvent>>,
893
894 #[serde(default)]
896 pub knock_room_state: Vec<Raw<AnyStrippedStateEvent>>,
897
898 #[serde(rename = "m.relations", default)]
902 pub relations: BundledStateRelations,
903}
904
905impl RoomMemberUnsigned {
906 pub fn new() -> Self {
908 Self::default()
909 }
910}
911
912impl CanBeEmpty for RoomMemberUnsigned {
913 fn is_empty(&self) -> bool {
919 let Self {
920 age,
921 transaction_id,
922 replaces_state,
923 prev_content,
924 invite_room_state,
925 knock_room_state,
926 relations,
927 } = self;
928
929 age.is_none()
930 && transaction_id.is_none()
931 && replaces_state.is_none()
932 && prev_content.is_none()
933 && invite_room_state.is_empty()
934 && knock_room_state.is_empty()
935 && relations.is_empty()
936 }
937}
938
939#[cfg(feature = "unstable-msc4293")]
940impl RedactionEvent for OriginalRoomMemberEvent {}
941
942#[cfg(feature = "unstable-msc4293")]
943impl RedactionEvent for OriginalSyncRoomMemberEvent {}
944
945#[cfg(feature = "unstable-msc4293")]
946impl RedactionEvent for RoomMemberEvent {}
947
948#[cfg(feature = "unstable-msc4293")]
949impl RedactionEvent for SyncRoomMemberEvent {}
950
951#[cfg(test)]
952mod tests {
953 use assert_matches::assert_matches;
954 use js_int::uint;
955 use maplit::btreemap;
956 use ruma_common::{
957 MilliSecondsSinceUnixEpoch, ServerSigningKeyId, SigningKeyAlgorithm, serde::CanBeEmpty,
958 server_signing_key_version,
959 };
960 use serde_json::{from_value as from_json_value, json};
961 use strass::assert_variant_eq;
962
963 use super::{MembershipState, RoomMemberEventContent};
964 use crate::OriginalStateEvent;
965
966 #[test]
967 fn serde_with_no_prev_content() {
968 let json = json!({
969 "type": "m.room.member",
970 "content": {
971 "membership": "join"
972 },
973 "event_id": "$h29iv0s8:example.com",
974 "origin_server_ts": 1,
975 "room_id": "!n8f893n9:example.com",
976 "sender": "@carl:example.com",
977 "state_key": "@carl:example.com"
978 });
979
980 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
981 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
982 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
983 assert_eq!(ev.room_id, "!n8f893n9:example.com");
984 assert_eq!(ev.sender, "@carl:example.com");
985 assert_eq!(ev.state_key, "@carl:example.com");
986 assert!(ev.unsigned.is_empty());
987
988 assert_eq!(ev.content.avatar_url, None);
989 assert_eq!(ev.content.displayname, None);
990 assert_eq!(ev.content.is_direct, None);
991 assert_eq!(ev.content.membership, MembershipState::Join);
992 assert_matches!(ev.content.third_party_invite, None);
993 }
994
995 #[test]
996 fn serde_with_prev_content() {
997 let json = json!({
998 "type": "m.room.member",
999 "content": {
1000 "membership": "join"
1001 },
1002 "event_id": "$h29iv0s8:example.com",
1003 "origin_server_ts": 1,
1004 "room_id": "!n8f893n9:example.com",
1005 "sender": "@carl:example.com",
1006 "state_key": "@carl:example.com",
1007 "unsigned": {
1008 "prev_content": {
1009 "membership": "join"
1010 },
1011 },
1012 });
1013
1014 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
1015 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
1016 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
1017 assert_eq!(ev.room_id, "!n8f893n9:example.com");
1018 assert_eq!(ev.sender, "@carl:example.com");
1019 assert_eq!(ev.state_key, "@carl:example.com");
1020
1021 assert_eq!(ev.content.avatar_url, None);
1022 assert_eq!(ev.content.displayname, None);
1023 assert_eq!(ev.content.is_direct, None);
1024 assert_eq!(ev.content.membership, MembershipState::Join);
1025 assert_matches!(ev.content.third_party_invite, None);
1026
1027 let prev_content = ev.unsigned.prev_content.unwrap();
1028 assert_eq!(prev_content.avatar_url, None);
1029 assert_eq!(prev_content.displayname, None);
1030 assert_eq!(prev_content.is_direct, None);
1031 assert_eq!(prev_content.membership, MembershipState::Join);
1032 assert_matches!(prev_content.third_party_invite, None);
1033 }
1034
1035 #[test]
1036 fn serde_with_replaces_state() {
1037 let json = json!({
1038 "type": "m.room.member",
1039 "content": {
1040 "membership": "join"
1041 },
1042 "event_id": "$h29iv0s8:example.com",
1043 "origin_server_ts": 1,
1044 "room_id": "!n8f893n9:example.com",
1045 "sender": "@carl:example.com",
1046 "state_key": "@carl:example.com",
1047 "unsigned": {
1048 "replaces_state": "$replaced:example.com",
1049 },
1050 });
1051
1052 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
1053 assert_variant_eq!(ev.unsigned.replaces_state, Some("$replaced:example.com"));
1054 assert!(!ev.unsigned.is_empty());
1055 }
1056
1057 #[test]
1058 fn serde_with_content_full() {
1059 let json = json!({
1060 "type": "m.room.member",
1061 "content": {
1062 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
1063 "displayname": "Alice Margatroid",
1064 "is_direct": true,
1065 "membership": "invite",
1066 "third_party_invite": {
1067 "display_name": "alice",
1068 "signed": {
1069 "mxid": "@alice:example.org",
1070 "signatures": {
1071 "magic.forest": {
1072 "ed25519:3": "foobar"
1073 }
1074 },
1075 "token": "abc123"
1076 }
1077 }
1078 },
1079 "event_id": "$143273582443PhrSn:example.org",
1080 "origin_server_ts": 233,
1081 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
1082 "sender": "@alice:example.org",
1083 "state_key": "@alice:example.org"
1084 });
1085
1086 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
1087 assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
1088 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
1089 assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
1090 assert_eq!(ev.sender, "@alice:example.org");
1091 assert_eq!(ev.state_key, "@alice:example.org");
1092 assert!(ev.unsigned.is_empty());
1093
1094 assert_variant_eq!(ev.content.avatar_url, Some("mxc://example.org/SEsfnsuifSDFSSEF"));
1095 assert_eq!(ev.content.displayname.as_deref(), Some("Alice Margatroid"));
1096 assert_eq!(ev.content.is_direct, Some(true));
1097 assert_eq!(ev.content.membership, MembershipState::Invite);
1098
1099 let third_party_invite = ev.content.third_party_invite.unwrap();
1100 assert_eq!(third_party_invite.display_name, "alice");
1101 let signed = third_party_invite.signed.deserialize().unwrap();
1102 assert_eq!(signed.mxid, "@alice:example.org");
1103 assert_eq!(signed.signatures.len(), 1);
1104 let server_signatures = signed.signatures.get("magic.forest").unwrap();
1105 assert_eq!(
1106 *server_signatures,
1107 btreemap! {
1108 ServerSigningKeyId::from_parts(
1109 SigningKeyAlgorithm::Ed25519,
1110 server_signing_key_version!("3")
1111 ) => "foobar".to_owned()
1112 }
1113 );
1114 assert_eq!(signed.token, "abc123");
1115 }
1116
1117 #[test]
1118 fn serde_with_prev_content_full() {
1119 let json = json!({
1120 "type": "m.room.member",
1121 "content": {
1122 "membership": "join",
1123 },
1124 "event_id": "$143273582443PhrSn:example.org",
1125 "origin_server_ts": 233,
1126 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
1127 "sender": "@alice:example.org",
1128 "state_key": "@alice:example.org",
1129 "unsigned": {
1130 "prev_content": {
1131 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
1132 "displayname": "Alice Margatroid",
1133 "is_direct": true,
1134 "membership": "invite",
1135 "third_party_invite": {
1136 "display_name": "alice",
1137 "signed": {
1138 "mxid": "@alice:example.org",
1139 "signatures": {
1140 "magic.forest": {
1141 "ed25519:3": "foobar",
1142 },
1143 },
1144 "token": "abc123"
1145 },
1146 },
1147 },
1148 },
1149 });
1150
1151 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
1152 assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
1153 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
1154 assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
1155 assert_eq!(ev.sender, "@alice:example.org");
1156 assert_eq!(ev.state_key, "@alice:example.org");
1157
1158 assert_eq!(ev.content.avatar_url, None);
1159 assert_eq!(ev.content.displayname, None);
1160 assert_eq!(ev.content.is_direct, None);
1161 assert_eq!(ev.content.membership, MembershipState::Join);
1162 assert_matches!(ev.content.third_party_invite, None);
1163
1164 let prev_content = ev.unsigned.prev_content.unwrap();
1165 assert_variant_eq!(prev_content.avatar_url, Some("mxc://example.org/SEsfnsuifSDFSSEF"));
1166 assert_eq!(prev_content.displayname.as_deref(), Some("Alice Margatroid"));
1167 assert_eq!(prev_content.is_direct, Some(true));
1168 assert_eq!(prev_content.membership, MembershipState::Invite);
1169
1170 let third_party_invite = prev_content.third_party_invite.unwrap();
1171 assert_eq!(third_party_invite.display_name.as_deref(), Some("alice"));
1172 let signed = third_party_invite.signed.deserialize().unwrap();
1173 assert_eq!(signed.mxid, "@alice:example.org");
1174 assert_eq!(signed.signatures.len(), 1);
1175 let server_signatures = signed.signatures.get("magic.forest").unwrap();
1176 assert_eq!(
1177 *server_signatures,
1178 btreemap! {
1179 ServerSigningKeyId::from_parts(
1180 SigningKeyAlgorithm::Ed25519,
1181 server_signing_key_version!("3")
1182 ) => "foobar".to_owned()
1183 }
1184 );
1185 assert_eq!(signed.token, "abc123");
1186 }
1187
1188 #[test]
1189 fn serde_with_join_authorized() {
1190 let json = json!({
1191 "type": "m.room.member",
1192 "content": {
1193 "membership": "join",
1194 "join_authorised_via_users_server": "@notcarl:example.com"
1195 },
1196 "event_id": "$h29iv0s8:example.com",
1197 "origin_server_ts": 1,
1198 "room_id": "!n8f893n9:example.com",
1199 "sender": "@carl:example.com",
1200 "state_key": "@carl:example.com"
1201 });
1202
1203 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
1204 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
1205 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
1206 assert_eq!(ev.room_id, "!n8f893n9:example.com");
1207 assert_eq!(ev.sender, "@carl:example.com");
1208 assert_eq!(ev.state_key, "@carl:example.com");
1209 assert!(ev.unsigned.is_empty());
1210
1211 assert_eq!(ev.content.avatar_url, None);
1212 assert_eq!(ev.content.displayname, None);
1213 assert_eq!(ev.content.is_direct, None);
1214 assert_eq!(ev.content.membership, MembershipState::Join);
1215 assert_matches!(ev.content.third_party_invite, None);
1216 assert_variant_eq!(
1217 ev.content.join_authorized_via_users_server,
1218 Some("@notcarl:example.com")
1219 );
1220 }
1221}