1use js_int::Int;
6use ruma_common::{
7 room_version_rules::RedactionRules,
8 serde::{CanBeEmpty, Raw, StringEnum},
9 OwnedMxcUri, OwnedTransactionId, OwnedUserId, ServerSignatures, UserId,
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
175pub type PossiblyRedactedRoomMemberEventContent = RoomMemberEventContent;
179
180impl PossiblyRedactedStateEventContent for RoomMemberEventContent {
181 type StateKey = OwnedUserId;
182
183 fn event_type(&self) -> StateEventType {
184 StateEventType::RoomMember
185 }
186}
187
188#[derive(Clone, Debug, Deserialize, Serialize)]
190#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
191pub struct RedactedRoomMemberEventContent {
192 pub membership: MembershipState,
194
195 #[serde(skip_serializing_if = "Option::is_none")]
198 pub third_party_invite: Option<RedactedThirdPartyInvite>,
199
200 #[serde(rename = "join_authorised_via_users_server", skip_serializing_if = "Option::is_none")]
205 pub join_authorized_via_users_server: Option<OwnedUserId>,
206}
207
208impl RedactedRoomMemberEventContent {
209 pub fn new(membership: MembershipState) -> Self {
211 Self { membership, third_party_invite: None, join_authorized_via_users_server: None }
212 }
213
214 pub fn details(&self) -> MembershipDetails<'_> {
219 MembershipDetails { avatar_url: None, displayname: None, membership: &self.membership }
220 }
221
222 pub fn membership_change<'a>(
237 &'a self,
238 prev_details: Option<MembershipDetails<'a>>,
239 sender: &UserId,
240 state_key: &UserId,
241 ) -> MembershipChange<'a> {
242 membership_change(self.details(), prev_details, sender, state_key)
243 }
244}
245
246impl RedactedStateEventContent for RedactedRoomMemberEventContent {
247 type StateKey = OwnedUserId;
248
249 fn event_type(&self) -> StateEventType {
250 StateEventType::RoomMember
251 }
252}
253
254impl StaticEventContent for RedactedRoomMemberEventContent {
255 const TYPE: &'static str = RoomMemberEventContent::TYPE;
256 type IsPrefix = <RoomMemberEventContent as StaticEventContent>::IsPrefix;
257}
258
259impl RoomMemberEvent {
260 pub fn membership(&self) -> &MembershipState {
262 match self {
263 Self::Original(ev) => &ev.content.membership,
264 Self::Redacted(ev) => &ev.content.membership,
265 }
266 }
267}
268
269impl SyncRoomMemberEvent {
270 pub fn membership(&self) -> &MembershipState {
272 match self {
273 Self::Original(ev) => &ev.content.membership,
274 Self::Redacted(ev) => &ev.content.membership,
275 }
276 }
277}
278
279#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
281#[derive(Clone, StringEnum)]
282#[ruma_enum(rename_all = "lowercase")]
283#[non_exhaustive]
284pub enum MembershipState {
285 Ban,
287
288 Invite,
290
291 Join,
293
294 Knock,
296
297 Leave,
299
300 #[doc(hidden)]
301 _Custom(PrivOwnedStr),
302}
303
304#[derive(Clone, Debug, Deserialize, Serialize)]
306#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
307pub struct ThirdPartyInvite {
308 pub display_name: String,
311
312 pub signed: Raw<SignedContent>,
316}
317
318impl ThirdPartyInvite {
319 pub fn new(display_name: String, signed: Raw<SignedContent>) -> Self {
321 Self { display_name, signed }
322 }
323
324 fn redact(self, rules: &RedactionRules) -> Option<RedactedThirdPartyInvite> {
329 rules
330 .keep_room_member_third_party_invite_signed
331 .then_some(RedactedThirdPartyInvite { signed: self.signed })
332 }
333}
334
335#[derive(Clone, Debug, Deserialize, Serialize)]
337#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
338pub struct RedactedThirdPartyInvite {
339 pub signed: Raw<SignedContent>,
343}
344
345#[derive(Clone, Debug, Deserialize, Serialize)]
348#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
349pub struct SignedContent {
350 pub mxid: OwnedUserId,
354
355 pub signatures: ServerSignatures,
358
359 pub token: String,
361}
362
363impl SignedContent {
364 pub fn new(signatures: ServerSignatures, mxid: OwnedUserId, token: String) -> Self {
366 Self { mxid, signatures, token }
367 }
368}
369
370impl OriginalRoomMemberEvent {
371 pub fn details(&self) -> MembershipDetails<'_> {
376 self.content.details()
377 }
378
379 pub fn prev_content(&self) -> Option<&RoomMemberEventContent> {
383 self.unsigned.prev_content.as_ref()
384 }
385
386 fn prev_details(&self) -> Option<MembershipDetails<'_>> {
387 self.prev_content().map(|c| c.details())
388 }
389
390 pub fn membership_change(&self) -> MembershipChange<'_> {
396 membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
397 }
398}
399
400impl RedactedRoomMemberEvent {
401 pub fn details(&self) -> MembershipDetails<'_> {
406 self.content.details()
407 }
408
409 pub fn membership_change<'a>(
419 &'a self,
420 prev_details: Option<MembershipDetails<'a>>,
421 ) -> MembershipChange<'a> {
422 membership_change(self.details(), prev_details, &self.sender, &self.state_key)
423 }
424}
425
426impl OriginalSyncRoomMemberEvent {
427 pub fn details(&self) -> MembershipDetails<'_> {
432 self.content.details()
433 }
434
435 pub fn prev_content(&self) -> Option<&RoomMemberEventContent> {
439 self.unsigned.prev_content.as_ref()
440 }
441
442 fn prev_details(&self) -> Option<MembershipDetails<'_>> {
443 self.prev_content().map(|c| c.details())
444 }
445
446 pub fn membership_change(&self) -> MembershipChange<'_> {
452 membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
453 }
454}
455
456impl RedactedSyncRoomMemberEvent {
457 pub fn details(&self) -> MembershipDetails<'_> {
462 self.content.details()
463 }
464
465 pub fn membership_change<'a>(
475 &'a self,
476 prev_details: Option<MembershipDetails<'a>>,
477 ) -> MembershipChange<'a> {
478 membership_change(self.details(), prev_details, &self.sender, &self.state_key)
479 }
480}
481
482impl StrippedRoomMemberEvent {
483 pub fn details(&self) -> MembershipDetails<'_> {
488 self.content.details()
489 }
490
491 pub fn membership_change<'a>(
501 &'a self,
502 prev_details: Option<MembershipDetails<'a>>,
503 ) -> MembershipChange<'a> {
504 membership_change(self.details(), prev_details, &self.sender, &self.state_key)
505 }
506}
507
508#[derive(Clone, Debug, Default, Deserialize)]
510#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
511pub struct RoomMemberUnsigned {
512 pub age: Option<Int>,
518
519 pub transaction_id: Option<OwnedTransactionId>,
522
523 pub prev_content: Option<PossiblyRedactedRoomMemberEventContent>,
525
526 #[serde(default)]
529 pub invite_room_state: Vec<Raw<AnyStrippedStateEvent>>,
530
531 #[serde(default)]
533 pub knock_room_state: Vec<Raw<AnyStrippedStateEvent>>,
534
535 #[serde(rename = "m.relations", default)]
539 pub relations: BundledStateRelations,
540}
541
542impl RoomMemberUnsigned {
543 pub fn new() -> Self {
545 Self::default()
546 }
547}
548
549impl CanBeEmpty for RoomMemberUnsigned {
550 fn is_empty(&self) -> bool {
556 self.age.is_none()
557 && self.transaction_id.is_none()
558 && self.prev_content.is_none()
559 && self.invite_room_state.is_empty()
560 && self.relations.is_empty()
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 use assert_matches2::assert_matches;
567 use js_int::uint;
568 use maplit::btreemap;
569 use ruma_common::{
570 mxc_uri, serde::CanBeEmpty, server_name, server_signing_key_version, user_id,
571 MilliSecondsSinceUnixEpoch, ServerSigningKeyId, SigningKeyAlgorithm,
572 };
573 use serde_json::{from_value as from_json_value, json};
574
575 use super::{MembershipState, RoomMemberEventContent};
576 use crate::OriginalStateEvent;
577
578 #[test]
579 fn serde_with_no_prev_content() {
580 let json = json!({
581 "type": "m.room.member",
582 "content": {
583 "membership": "join"
584 },
585 "event_id": "$h29iv0s8:example.com",
586 "origin_server_ts": 1,
587 "room_id": "!n8f893n9:example.com",
588 "sender": "@carl:example.com",
589 "state_key": "@carl:example.com"
590 });
591
592 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
593 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
594 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
595 assert_eq!(ev.room_id, "!n8f893n9:example.com");
596 assert_eq!(ev.sender, "@carl:example.com");
597 assert_eq!(ev.state_key, "@carl:example.com");
598 assert!(ev.unsigned.is_empty());
599
600 assert_eq!(ev.content.avatar_url, None);
601 assert_eq!(ev.content.displayname, None);
602 assert_eq!(ev.content.is_direct, None);
603 assert_eq!(ev.content.membership, MembershipState::Join);
604 assert_matches!(ev.content.third_party_invite, None);
605 }
606
607 #[test]
608 fn serde_with_prev_content() {
609 let json = json!({
610 "type": "m.room.member",
611 "content": {
612 "membership": "join"
613 },
614 "event_id": "$h29iv0s8:example.com",
615 "origin_server_ts": 1,
616 "room_id": "!n8f893n9:example.com",
617 "sender": "@carl:example.com",
618 "state_key": "@carl:example.com",
619 "unsigned": {
620 "prev_content": {
621 "membership": "join"
622 },
623 },
624 });
625
626 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
627 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
628 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
629 assert_eq!(ev.room_id, "!n8f893n9:example.com");
630 assert_eq!(ev.sender, "@carl:example.com");
631 assert_eq!(ev.state_key, "@carl:example.com");
632
633 assert_eq!(ev.content.avatar_url, None);
634 assert_eq!(ev.content.displayname, None);
635 assert_eq!(ev.content.is_direct, None);
636 assert_eq!(ev.content.membership, MembershipState::Join);
637 assert_matches!(ev.content.third_party_invite, None);
638
639 let prev_content = ev.unsigned.prev_content.unwrap();
640 assert_eq!(prev_content.avatar_url, None);
641 assert_eq!(prev_content.displayname, None);
642 assert_eq!(prev_content.is_direct, None);
643 assert_eq!(prev_content.membership, MembershipState::Join);
644 assert_matches!(prev_content.third_party_invite, None);
645 }
646
647 #[test]
648 fn serde_with_content_full() {
649 let json = json!({
650 "type": "m.room.member",
651 "content": {
652 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
653 "displayname": "Alice Margatroid",
654 "is_direct": true,
655 "membership": "invite",
656 "third_party_invite": {
657 "display_name": "alice",
658 "signed": {
659 "mxid": "@alice:example.org",
660 "signatures": {
661 "magic.forest": {
662 "ed25519:3": "foobar"
663 }
664 },
665 "token": "abc123"
666 }
667 }
668 },
669 "event_id": "$143273582443PhrSn:example.org",
670 "origin_server_ts": 233,
671 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
672 "sender": "@alice:example.org",
673 "state_key": "@alice:example.org"
674 });
675
676 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
677 assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
678 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
679 assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
680 assert_eq!(ev.sender, "@alice:example.org");
681 assert_eq!(ev.state_key, "@alice:example.org");
682 assert!(ev.unsigned.is_empty());
683
684 assert_eq!(
685 ev.content.avatar_url.as_deref(),
686 Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
687 );
688 assert_eq!(ev.content.displayname.as_deref(), Some("Alice Margatroid"));
689 assert_eq!(ev.content.is_direct, Some(true));
690 assert_eq!(ev.content.membership, MembershipState::Invite);
691
692 let third_party_invite = ev.content.third_party_invite.unwrap();
693 assert_eq!(third_party_invite.display_name, "alice");
694 let signed = third_party_invite.signed.deserialize().unwrap();
695 assert_eq!(signed.mxid, "@alice:example.org");
696 assert_eq!(signed.signatures.len(), 1);
697 let server_signatures = signed.signatures.get(server_name!("magic.forest")).unwrap();
698 assert_eq!(
699 *server_signatures,
700 btreemap! {
701 ServerSigningKeyId::from_parts(
702 SigningKeyAlgorithm::Ed25519,
703 server_signing_key_version!("3")
704 ) => "foobar".to_owned()
705 }
706 );
707 assert_eq!(signed.token, "abc123");
708 }
709
710 #[test]
711 fn serde_with_prev_content_full() {
712 let json = json!({
713 "type": "m.room.member",
714 "content": {
715 "membership": "join",
716 },
717 "event_id": "$143273582443PhrSn:example.org",
718 "origin_server_ts": 233,
719 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
720 "sender": "@alice:example.org",
721 "state_key": "@alice:example.org",
722 "unsigned": {
723 "prev_content": {
724 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
725 "displayname": "Alice Margatroid",
726 "is_direct": true,
727 "membership": "invite",
728 "third_party_invite": {
729 "display_name": "alice",
730 "signed": {
731 "mxid": "@alice:example.org",
732 "signatures": {
733 "magic.forest": {
734 "ed25519:3": "foobar",
735 },
736 },
737 "token": "abc123"
738 },
739 },
740 },
741 },
742 });
743
744 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
745 assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
746 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
747 assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
748 assert_eq!(ev.sender, "@alice:example.org");
749 assert_eq!(ev.state_key, "@alice:example.org");
750
751 assert_eq!(ev.content.avatar_url, None);
752 assert_eq!(ev.content.displayname, None);
753 assert_eq!(ev.content.is_direct, None);
754 assert_eq!(ev.content.membership, MembershipState::Join);
755 assert_matches!(ev.content.third_party_invite, None);
756
757 let prev_content = ev.unsigned.prev_content.unwrap();
758 assert_eq!(
759 prev_content.avatar_url.as_deref(),
760 Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
761 );
762 assert_eq!(prev_content.displayname.as_deref(), Some("Alice Margatroid"));
763 assert_eq!(prev_content.is_direct, Some(true));
764 assert_eq!(prev_content.membership, MembershipState::Invite);
765
766 let third_party_invite = prev_content.third_party_invite.unwrap();
767 assert_eq!(third_party_invite.display_name, "alice");
768 let signed = third_party_invite.signed.deserialize().unwrap();
769 assert_eq!(signed.mxid, "@alice:example.org");
770 assert_eq!(signed.signatures.len(), 1);
771 let server_signatures = signed.signatures.get(server_name!("magic.forest")).unwrap();
772 assert_eq!(
773 *server_signatures,
774 btreemap! {
775 ServerSigningKeyId::from_parts(
776 SigningKeyAlgorithm::Ed25519,
777 server_signing_key_version!("3")
778 ) => "foobar".to_owned()
779 }
780 );
781 assert_eq!(signed.token, "abc123");
782 }
783
784 #[test]
785 fn serde_with_join_authorized() {
786 let json = json!({
787 "type": "m.room.member",
788 "content": {
789 "membership": "join",
790 "join_authorised_via_users_server": "@notcarl:example.com"
791 },
792 "event_id": "$h29iv0s8:example.com",
793 "origin_server_ts": 1,
794 "room_id": "!n8f893n9:example.com",
795 "sender": "@carl:example.com",
796 "state_key": "@carl:example.com"
797 });
798
799 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
800 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
801 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
802 assert_eq!(ev.room_id, "!n8f893n9:example.com");
803 assert_eq!(ev.sender, "@carl:example.com");
804 assert_eq!(ev.state_key, "@carl:example.com");
805 assert!(ev.unsigned.is_empty());
806
807 assert_eq!(ev.content.avatar_url, None);
808 assert_eq!(ev.content.displayname, None);
809 assert_eq!(ev.content.is_direct, None);
810 assert_eq!(ev.content.membership, MembershipState::Join);
811 assert_matches!(ev.content.third_party_invite, None);
812 assert_eq!(
813 ev.content.join_authorized_via_users_server.as_deref(),
814 Some(user_id!("@notcarl:example.com"))
815 );
816 }
817}