1use js_int::Int;
6#[cfg(feature = "unstable-msc4293")]
7use ruma_common::canonical_json::RedactionEvent;
8use ruma_common::{
9 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 prev_content: Option<PossiblyRedactedRoomMemberEventContent>,
882
883 #[serde(default)]
886 pub invite_room_state: Vec<Raw<AnyStrippedStateEvent>>,
887
888 #[serde(default)]
890 pub knock_room_state: Vec<Raw<AnyStrippedStateEvent>>,
891
892 #[serde(rename = "m.relations", default)]
896 pub relations: BundledStateRelations,
897}
898
899impl RoomMemberUnsigned {
900 pub fn new() -> Self {
902 Self::default()
903 }
904}
905
906impl CanBeEmpty for RoomMemberUnsigned {
907 fn is_empty(&self) -> bool {
913 self.age.is_none()
914 && self.transaction_id.is_none()
915 && self.prev_content.is_none()
916 && self.invite_room_state.is_empty()
917 && self.relations.is_empty()
918 }
919}
920
921#[cfg(feature = "unstable-msc4293")]
922impl RedactionEvent for OriginalRoomMemberEvent {}
923
924#[cfg(feature = "unstable-msc4293")]
925impl RedactionEvent for OriginalSyncRoomMemberEvent {}
926
927#[cfg(feature = "unstable-msc4293")]
928impl RedactionEvent for RoomMemberEvent {}
929
930#[cfg(feature = "unstable-msc4293")]
931impl RedactionEvent for SyncRoomMemberEvent {}
932
933#[cfg(test)]
934mod tests {
935 use assert_matches2::assert_matches;
936 use js_int::uint;
937 use maplit::btreemap;
938 use ruma_common::{
939 MilliSecondsSinceUnixEpoch, ServerSigningKeyId, SigningKeyAlgorithm, mxc_uri,
940 serde::CanBeEmpty, server_name, server_signing_key_version, user_id,
941 };
942 use serde_json::{from_value as from_json_value, json};
943
944 use super::{MembershipState, RoomMemberEventContent};
945 use crate::OriginalStateEvent;
946
947 #[test]
948 fn serde_with_no_prev_content() {
949 let json = json!({
950 "type": "m.room.member",
951 "content": {
952 "membership": "join"
953 },
954 "event_id": "$h29iv0s8:example.com",
955 "origin_server_ts": 1,
956 "room_id": "!n8f893n9:example.com",
957 "sender": "@carl:example.com",
958 "state_key": "@carl:example.com"
959 });
960
961 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
962 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
963 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
964 assert_eq!(ev.room_id, "!n8f893n9:example.com");
965 assert_eq!(ev.sender, "@carl:example.com");
966 assert_eq!(ev.state_key, "@carl:example.com");
967 assert!(ev.unsigned.is_empty());
968
969 assert_eq!(ev.content.avatar_url, None);
970 assert_eq!(ev.content.displayname, None);
971 assert_eq!(ev.content.is_direct, None);
972 assert_eq!(ev.content.membership, MembershipState::Join);
973 assert_matches!(ev.content.third_party_invite, None);
974 }
975
976 #[test]
977 fn serde_with_prev_content() {
978 let json = json!({
979 "type": "m.room.member",
980 "content": {
981 "membership": "join"
982 },
983 "event_id": "$h29iv0s8:example.com",
984 "origin_server_ts": 1,
985 "room_id": "!n8f893n9:example.com",
986 "sender": "@carl:example.com",
987 "state_key": "@carl:example.com",
988 "unsigned": {
989 "prev_content": {
990 "membership": "join"
991 },
992 },
993 });
994
995 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
996 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
997 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
998 assert_eq!(ev.room_id, "!n8f893n9:example.com");
999 assert_eq!(ev.sender, "@carl:example.com");
1000 assert_eq!(ev.state_key, "@carl:example.com");
1001
1002 assert_eq!(ev.content.avatar_url, None);
1003 assert_eq!(ev.content.displayname, None);
1004 assert_eq!(ev.content.is_direct, None);
1005 assert_eq!(ev.content.membership, MembershipState::Join);
1006 assert_matches!(ev.content.third_party_invite, None);
1007
1008 let prev_content = ev.unsigned.prev_content.unwrap();
1009 assert_eq!(prev_content.avatar_url, None);
1010 assert_eq!(prev_content.displayname, None);
1011 assert_eq!(prev_content.is_direct, None);
1012 assert_eq!(prev_content.membership, MembershipState::Join);
1013 assert_matches!(prev_content.third_party_invite, None);
1014 }
1015
1016 #[test]
1017 fn serde_with_content_full() {
1018 let json = json!({
1019 "type": "m.room.member",
1020 "content": {
1021 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
1022 "displayname": "Alice Margatroid",
1023 "is_direct": true,
1024 "membership": "invite",
1025 "third_party_invite": {
1026 "display_name": "alice",
1027 "signed": {
1028 "mxid": "@alice:example.org",
1029 "signatures": {
1030 "magic.forest": {
1031 "ed25519:3": "foobar"
1032 }
1033 },
1034 "token": "abc123"
1035 }
1036 }
1037 },
1038 "event_id": "$143273582443PhrSn:example.org",
1039 "origin_server_ts": 233,
1040 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
1041 "sender": "@alice:example.org",
1042 "state_key": "@alice:example.org"
1043 });
1044
1045 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
1046 assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
1047 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
1048 assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
1049 assert_eq!(ev.sender, "@alice:example.org");
1050 assert_eq!(ev.state_key, "@alice:example.org");
1051 assert!(ev.unsigned.is_empty());
1052
1053 assert_eq!(
1054 ev.content.avatar_url.as_deref(),
1055 Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
1056 );
1057 assert_eq!(ev.content.displayname.as_deref(), Some("Alice Margatroid"));
1058 assert_eq!(ev.content.is_direct, Some(true));
1059 assert_eq!(ev.content.membership, MembershipState::Invite);
1060
1061 let third_party_invite = ev.content.third_party_invite.unwrap();
1062 assert_eq!(third_party_invite.display_name, "alice");
1063 let signed = third_party_invite.signed.deserialize().unwrap();
1064 assert_eq!(signed.mxid, "@alice:example.org");
1065 assert_eq!(signed.signatures.len(), 1);
1066 let server_signatures = signed.signatures.get(server_name!("magic.forest")).unwrap();
1067 assert_eq!(
1068 *server_signatures,
1069 btreemap! {
1070 ServerSigningKeyId::from_parts(
1071 SigningKeyAlgorithm::Ed25519,
1072 server_signing_key_version!("3")
1073 ) => "foobar".to_owned()
1074 }
1075 );
1076 assert_eq!(signed.token, "abc123");
1077 }
1078
1079 #[test]
1080 fn serde_with_prev_content_full() {
1081 let json = json!({
1082 "type": "m.room.member",
1083 "content": {
1084 "membership": "join",
1085 },
1086 "event_id": "$143273582443PhrSn:example.org",
1087 "origin_server_ts": 233,
1088 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
1089 "sender": "@alice:example.org",
1090 "state_key": "@alice:example.org",
1091 "unsigned": {
1092 "prev_content": {
1093 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
1094 "displayname": "Alice Margatroid",
1095 "is_direct": true,
1096 "membership": "invite",
1097 "third_party_invite": {
1098 "display_name": "alice",
1099 "signed": {
1100 "mxid": "@alice:example.org",
1101 "signatures": {
1102 "magic.forest": {
1103 "ed25519:3": "foobar",
1104 },
1105 },
1106 "token": "abc123"
1107 },
1108 },
1109 },
1110 },
1111 });
1112
1113 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
1114 assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
1115 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
1116 assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
1117 assert_eq!(ev.sender, "@alice:example.org");
1118 assert_eq!(ev.state_key, "@alice:example.org");
1119
1120 assert_eq!(ev.content.avatar_url, None);
1121 assert_eq!(ev.content.displayname, None);
1122 assert_eq!(ev.content.is_direct, None);
1123 assert_eq!(ev.content.membership, MembershipState::Join);
1124 assert_matches!(ev.content.third_party_invite, None);
1125
1126 let prev_content = ev.unsigned.prev_content.unwrap();
1127 assert_eq!(
1128 prev_content.avatar_url.as_deref(),
1129 Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
1130 );
1131 assert_eq!(prev_content.displayname.as_deref(), Some("Alice Margatroid"));
1132 assert_eq!(prev_content.is_direct, Some(true));
1133 assert_eq!(prev_content.membership, MembershipState::Invite);
1134
1135 let third_party_invite = prev_content.third_party_invite.unwrap();
1136 assert_eq!(third_party_invite.display_name.as_deref(), Some("alice"));
1137 let signed = third_party_invite.signed.deserialize().unwrap();
1138 assert_eq!(signed.mxid, "@alice:example.org");
1139 assert_eq!(signed.signatures.len(), 1);
1140 let server_signatures = signed.signatures.get(server_name!("magic.forest")).unwrap();
1141 assert_eq!(
1142 *server_signatures,
1143 btreemap! {
1144 ServerSigningKeyId::from_parts(
1145 SigningKeyAlgorithm::Ed25519,
1146 server_signing_key_version!("3")
1147 ) => "foobar".to_owned()
1148 }
1149 );
1150 assert_eq!(signed.token, "abc123");
1151 }
1152
1153 #[test]
1154 fn serde_with_join_authorized() {
1155 let json = json!({
1156 "type": "m.room.member",
1157 "content": {
1158 "membership": "join",
1159 "join_authorised_via_users_server": "@notcarl:example.com"
1160 },
1161 "event_id": "$h29iv0s8:example.com",
1162 "origin_server_ts": 1,
1163 "room_id": "!n8f893n9:example.com",
1164 "sender": "@carl:example.com",
1165 "state_key": "@carl:example.com"
1166 });
1167
1168 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
1169 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
1170 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
1171 assert_eq!(ev.room_id, "!n8f893n9:example.com");
1172 assert_eq!(ev.sender, "@carl:example.com");
1173 assert_eq!(ev.state_key, "@carl:example.com");
1174 assert!(ev.unsigned.is_empty());
1175
1176 assert_eq!(ev.content.avatar_url, None);
1177 assert_eq!(ev.content.displayname, None);
1178 assert_eq!(ev.content.is_direct, None);
1179 assert_eq!(ev.content.membership, MembershipState::Join);
1180 assert_matches!(ev.content.third_party_invite, None);
1181 assert_eq!(
1182 ev.content.join_authorized_via_users_server.as_deref(),
1183 Some(user_id!("@notcarl:example.com"))
1184 );
1185 }
1186}