1use js_int::Int;
6use ruma_common::{
7 serde::{CanBeEmpty, Raw, StringEnum},
8 OwnedMxcUri, OwnedTransactionId, OwnedUserId, RoomVersionId, ServerSignatures, UserId,
9};
10use ruma_macros::EventContent;
11use serde::{Deserialize, Serialize};
12
13use crate::{
14 AnyStrippedStateEvent, BundledStateRelations, EventContent, PossiblyRedactedStateEventContent,
15 PrivOwnedStr, RedactContent, RedactedStateEventContent, StateEventType,
16};
17
18mod change;
19
20use self::change::membership_change;
21pub use self::change::{Change, MembershipChange, MembershipDetails};
22
23#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
44#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
45#[ruma_event(
46 type = "m.room.member",
47 kind = State,
48 state_key_type = OwnedUserId,
49 unsigned_type = RoomMemberUnsigned,
50 custom_redacted,
51 custom_possibly_redacted,
52)]
53pub struct RoomMemberEventContent {
54 #[serde(skip_serializing_if = "Option::is_none")]
59 #[cfg_attr(
60 feature = "compat-empty-string-null",
61 serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
62 )]
63 pub avatar_url: Option<OwnedMxcUri>,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
69 pub displayname: Option<String>,
70
71 #[serde(skip_serializing_if = "Option::is_none")]
74 pub is_direct: Option<bool>,
75
76 pub membership: MembershipState,
78
79 #[serde(skip_serializing_if = "Option::is_none")]
82 pub third_party_invite: Option<ThirdPartyInvite>,
83
84 #[cfg(feature = "unstable-msc2448")]
89 #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
90 pub blurhash: Option<String>,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
103 pub reason: Option<String>,
104
105 #[serde(rename = "join_authorised_via_users_server")]
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub join_authorized_via_users_server: Option<OwnedUserId>,
109}
110
111impl RoomMemberEventContent {
112 pub fn new(membership: MembershipState) -> Self {
114 Self {
115 membership,
116 avatar_url: None,
117 displayname: None,
118 is_direct: None,
119 third_party_invite: None,
120 #[cfg(feature = "unstable-msc2448")]
121 blurhash: None,
122 reason: None,
123 join_authorized_via_users_server: None,
124 }
125 }
126
127 pub fn details(&self) -> MembershipDetails<'_> {
132 MembershipDetails {
133 avatar_url: self.avatar_url.as_deref(),
134 displayname: self.displayname.as_deref(),
135 membership: &self.membership,
136 }
137 }
138
139 pub fn membership_change<'a>(
151 &'a self,
152 prev_details: Option<MembershipDetails<'a>>,
153 sender: &UserId,
154 state_key: &UserId,
155 ) -> MembershipChange<'a> {
156 membership_change(self.details(), prev_details, sender, state_key)
157 }
158}
159
160impl RedactContent for RoomMemberEventContent {
161 type Redacted = RedactedRoomMemberEventContent;
162
163 fn redact(self, version: &RoomVersionId) -> RedactedRoomMemberEventContent {
164 RedactedRoomMemberEventContent {
165 membership: self.membership,
166 third_party_invite: self.third_party_invite.and_then(|i| i.redact(version)),
167 join_authorized_via_users_server: match version {
168 RoomVersionId::V1
169 | RoomVersionId::V2
170 | RoomVersionId::V3
171 | RoomVersionId::V4
172 | RoomVersionId::V5
173 | RoomVersionId::V6
174 | RoomVersionId::V7
175 | RoomVersionId::V8 => None,
176 _ => self.join_authorized_via_users_server,
177 },
178 }
179 }
180}
181
182pub type PossiblyRedactedRoomMemberEventContent = RoomMemberEventContent;
186
187impl PossiblyRedactedStateEventContent for RoomMemberEventContent {
188 type StateKey = OwnedUserId;
189}
190
191#[derive(Clone, Debug, Deserialize, Serialize)]
193#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
194pub struct RedactedRoomMemberEventContent {
195 pub membership: MembershipState,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
201 pub third_party_invite: Option<RedactedThirdPartyInvite>,
202
203 #[serde(rename = "join_authorised_via_users_server", skip_serializing_if = "Option::is_none")]
208 pub join_authorized_via_users_server: Option<OwnedUserId>,
209}
210
211impl RedactedRoomMemberEventContent {
212 pub fn new(membership: MembershipState) -> Self {
214 Self { membership, third_party_invite: None, join_authorized_via_users_server: None }
215 }
216
217 pub fn details(&self) -> MembershipDetails<'_> {
222 MembershipDetails { avatar_url: None, displayname: None, membership: &self.membership }
223 }
224
225 pub fn membership_change<'a>(
240 &'a self,
241 prev_details: Option<MembershipDetails<'a>>,
242 sender: &UserId,
243 state_key: &UserId,
244 ) -> MembershipChange<'a> {
245 membership_change(self.details(), prev_details, sender, state_key)
246 }
247}
248
249impl EventContent for RedactedRoomMemberEventContent {
250 type EventType = StateEventType;
251
252 fn event_type(&self) -> StateEventType {
253 StateEventType::RoomMember
254 }
255}
256
257impl RedactedStateEventContent for RedactedRoomMemberEventContent {
258 type StateKey = OwnedUserId;
259}
260
261impl RoomMemberEvent {
262 pub fn membership(&self) -> &MembershipState {
264 match self {
265 Self::Original(ev) => &ev.content.membership,
266 Self::Redacted(ev) => &ev.content.membership,
267 }
268 }
269}
270
271impl SyncRoomMemberEvent {
272 pub fn membership(&self) -> &MembershipState {
274 match self {
275 Self::Original(ev) => &ev.content.membership,
276 Self::Redacted(ev) => &ev.content.membership,
277 }
278 }
279}
280
281#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
283#[derive(Clone, PartialEq, Eq, StringEnum)]
284#[ruma_enum(rename_all = "lowercase")]
285#[non_exhaustive]
286pub enum MembershipState {
287 Ban,
289
290 Invite,
292
293 Join,
295
296 Knock,
298
299 Leave,
301
302 #[doc(hidden)]
303 _Custom(PrivOwnedStr),
304}
305
306#[derive(Clone, Debug, Deserialize, Serialize)]
308#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
309pub struct ThirdPartyInvite {
310 pub display_name: String,
313
314 pub signed: SignedContent,
318}
319
320impl ThirdPartyInvite {
321 pub fn new(display_name: String, signed: SignedContent) -> Self {
323 Self { display_name, signed }
324 }
325
326 fn redact(self, version: &RoomVersionId) -> Option<RedactedThirdPartyInvite> {
331 match version {
332 RoomVersionId::V1
333 | RoomVersionId::V2
334 | RoomVersionId::V3
335 | RoomVersionId::V4
336 | RoomVersionId::V5
337 | RoomVersionId::V6
338 | RoomVersionId::V7
339 | RoomVersionId::V8
340 | RoomVersionId::V9
341 | RoomVersionId::V10 => None,
342 _ => Some(RedactedThirdPartyInvite { signed: self.signed }),
343 }
344 }
345}
346
347#[derive(Clone, Debug, Deserialize, Serialize)]
349#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
350pub struct RedactedThirdPartyInvite {
351 pub signed: SignedContent,
355}
356
357#[derive(Clone, Debug, Deserialize, Serialize)]
360#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
361pub struct SignedContent {
362 pub mxid: OwnedUserId,
366
367 pub signatures: ServerSignatures,
370
371 pub token: String,
373}
374
375impl SignedContent {
376 pub fn new(signatures: ServerSignatures, mxid: OwnedUserId, token: String) -> Self {
378 Self { mxid, signatures, token }
379 }
380}
381
382impl OriginalRoomMemberEvent {
383 pub fn details(&self) -> MembershipDetails<'_> {
388 self.content.details()
389 }
390
391 pub fn prev_content(&self) -> Option<&RoomMemberEventContent> {
395 self.unsigned.prev_content.as_ref()
396 }
397
398 fn prev_details(&self) -> Option<MembershipDetails<'_>> {
399 self.prev_content().map(|c| c.details())
400 }
401
402 pub fn membership_change(&self) -> MembershipChange<'_> {
408 membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
409 }
410}
411
412impl RedactedRoomMemberEvent {
413 pub fn details(&self) -> MembershipDetails<'_> {
418 self.content.details()
419 }
420
421 pub fn membership_change<'a>(
431 &'a self,
432 prev_details: Option<MembershipDetails<'a>>,
433 ) -> MembershipChange<'a> {
434 membership_change(self.details(), prev_details, &self.sender, &self.state_key)
435 }
436}
437
438impl OriginalSyncRoomMemberEvent {
439 pub fn details(&self) -> MembershipDetails<'_> {
444 self.content.details()
445 }
446
447 pub fn prev_content(&self) -> Option<&RoomMemberEventContent> {
451 self.unsigned.prev_content.as_ref()
452 }
453
454 fn prev_details(&self) -> Option<MembershipDetails<'_>> {
455 self.prev_content().map(|c| c.details())
456 }
457
458 pub fn membership_change(&self) -> MembershipChange<'_> {
464 membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
465 }
466}
467
468impl RedactedSyncRoomMemberEvent {
469 pub fn details(&self) -> MembershipDetails<'_> {
474 self.content.details()
475 }
476
477 pub fn membership_change<'a>(
487 &'a self,
488 prev_details: Option<MembershipDetails<'a>>,
489 ) -> MembershipChange<'a> {
490 membership_change(self.details(), prev_details, &self.sender, &self.state_key)
491 }
492}
493
494impl StrippedRoomMemberEvent {
495 pub fn details(&self) -> MembershipDetails<'_> {
500 self.content.details()
501 }
502
503 pub fn membership_change<'a>(
513 &'a self,
514 prev_details: Option<MembershipDetails<'a>>,
515 ) -> MembershipChange<'a> {
516 membership_change(self.details(), prev_details, &self.sender, &self.state_key)
517 }
518}
519
520#[derive(Clone, Debug, Default, Deserialize)]
522#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
523pub struct RoomMemberUnsigned {
524 pub age: Option<Int>,
530
531 pub transaction_id: Option<OwnedTransactionId>,
534
535 pub prev_content: Option<PossiblyRedactedRoomMemberEventContent>,
537
538 #[serde(default)]
540 pub invite_room_state: Vec<Raw<AnyStrippedStateEvent>>,
541
542 #[serde(rename = "m.relations", default)]
546 pub relations: BundledStateRelations,
547}
548
549impl RoomMemberUnsigned {
550 pub fn new() -> Self {
552 Self::default()
553 }
554}
555
556impl CanBeEmpty for RoomMemberUnsigned {
557 fn is_empty(&self) -> bool {
563 self.age.is_none()
564 && self.transaction_id.is_none()
565 && self.prev_content.is_none()
566 && self.invite_room_state.is_empty()
567 && self.relations.is_empty()
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use assert_matches2::assert_matches;
574 use js_int::uint;
575 use maplit::btreemap;
576 use ruma_common::{
577 mxc_uri, serde::CanBeEmpty, server_name, server_signing_key_version, user_id,
578 MilliSecondsSinceUnixEpoch, ServerSigningKeyId, SigningKeyAlgorithm,
579 };
580 use serde_json::{from_value as from_json_value, json};
581
582 use super::{MembershipState, RoomMemberEventContent};
583 use crate::OriginalStateEvent;
584
585 #[test]
586 fn serde_with_no_prev_content() {
587 let json = json!({
588 "type": "m.room.member",
589 "content": {
590 "membership": "join"
591 },
592 "event_id": "$h29iv0s8:example.com",
593 "origin_server_ts": 1,
594 "room_id": "!n8f893n9:example.com",
595 "sender": "@carl:example.com",
596 "state_key": "@carl:example.com"
597 });
598
599 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
600 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
601 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
602 assert_eq!(ev.room_id, "!n8f893n9:example.com");
603 assert_eq!(ev.sender, "@carl:example.com");
604 assert_eq!(ev.state_key, "@carl:example.com");
605 assert!(ev.unsigned.is_empty());
606
607 assert_eq!(ev.content.avatar_url, None);
608 assert_eq!(ev.content.displayname, None);
609 assert_eq!(ev.content.is_direct, None);
610 assert_eq!(ev.content.membership, MembershipState::Join);
611 assert_matches!(ev.content.third_party_invite, None);
612 }
613
614 #[test]
615 fn serde_with_prev_content() {
616 let json = json!({
617 "type": "m.room.member",
618 "content": {
619 "membership": "join"
620 },
621 "event_id": "$h29iv0s8:example.com",
622 "origin_server_ts": 1,
623 "room_id": "!n8f893n9:example.com",
624 "sender": "@carl:example.com",
625 "state_key": "@carl:example.com",
626 "unsigned": {
627 "prev_content": {
628 "membership": "join"
629 },
630 },
631 });
632
633 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
634 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
635 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
636 assert_eq!(ev.room_id, "!n8f893n9:example.com");
637 assert_eq!(ev.sender, "@carl:example.com");
638 assert_eq!(ev.state_key, "@carl:example.com");
639
640 assert_eq!(ev.content.avatar_url, None);
641 assert_eq!(ev.content.displayname, None);
642 assert_eq!(ev.content.is_direct, None);
643 assert_eq!(ev.content.membership, MembershipState::Join);
644 assert_matches!(ev.content.third_party_invite, None);
645
646 let prev_content = ev.unsigned.prev_content.unwrap();
647 assert_eq!(prev_content.avatar_url, None);
648 assert_eq!(prev_content.displayname, None);
649 assert_eq!(prev_content.is_direct, None);
650 assert_eq!(prev_content.membership, MembershipState::Join);
651 assert_matches!(prev_content.third_party_invite, None);
652 }
653
654 #[test]
655 fn serde_with_content_full() {
656 let json = json!({
657 "type": "m.room.member",
658 "content": {
659 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
660 "displayname": "Alice Margatroid",
661 "is_direct": true,
662 "membership": "invite",
663 "third_party_invite": {
664 "display_name": "alice",
665 "signed": {
666 "mxid": "@alice:example.org",
667 "signatures": {
668 "magic.forest": {
669 "ed25519:3": "foobar"
670 }
671 },
672 "token": "abc123"
673 }
674 }
675 },
676 "event_id": "$143273582443PhrSn:example.org",
677 "origin_server_ts": 233,
678 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
679 "sender": "@alice:example.org",
680 "state_key": "@alice:example.org"
681 });
682
683 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
684 assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
685 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
686 assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
687 assert_eq!(ev.sender, "@alice:example.org");
688 assert_eq!(ev.state_key, "@alice:example.org");
689 assert!(ev.unsigned.is_empty());
690
691 assert_eq!(
692 ev.content.avatar_url.as_deref(),
693 Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
694 );
695 assert_eq!(ev.content.displayname.as_deref(), Some("Alice Margatroid"));
696 assert_eq!(ev.content.is_direct, Some(true));
697 assert_eq!(ev.content.membership, MembershipState::Invite);
698
699 let third_party_invite = ev.content.third_party_invite.unwrap();
700 assert_eq!(third_party_invite.display_name, "alice");
701 assert_eq!(third_party_invite.signed.mxid, "@alice:example.org");
702 assert_eq!(third_party_invite.signed.signatures.len(), 1);
703 let server_signatures =
704 third_party_invite.signed.signatures.get(server_name!("magic.forest")).unwrap();
705 assert_eq!(
706 *server_signatures,
707 btreemap! {
708 ServerSigningKeyId::from_parts(
709 SigningKeyAlgorithm::Ed25519,
710 server_signing_key_version!("3")
711 ) => "foobar".to_owned()
712 }
713 );
714 assert_eq!(third_party_invite.signed.token, "abc123");
715 }
716
717 #[test]
718 fn serde_with_prev_content_full() {
719 let json = json!({
720 "type": "m.room.member",
721 "content": {
722 "membership": "join",
723 },
724 "event_id": "$143273582443PhrSn:example.org",
725 "origin_server_ts": 233,
726 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
727 "sender": "@alice:example.org",
728 "state_key": "@alice:example.org",
729 "unsigned": {
730 "prev_content": {
731 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
732 "displayname": "Alice Margatroid",
733 "is_direct": true,
734 "membership": "invite",
735 "third_party_invite": {
736 "display_name": "alice",
737 "signed": {
738 "mxid": "@alice:example.org",
739 "signatures": {
740 "magic.forest": {
741 "ed25519:3": "foobar",
742 },
743 },
744 "token": "abc123"
745 },
746 },
747 },
748 },
749 });
750
751 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
752 assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
753 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
754 assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
755 assert_eq!(ev.sender, "@alice:example.org");
756 assert_eq!(ev.state_key, "@alice:example.org");
757
758 assert_eq!(ev.content.avatar_url, None);
759 assert_eq!(ev.content.displayname, None);
760 assert_eq!(ev.content.is_direct, None);
761 assert_eq!(ev.content.membership, MembershipState::Join);
762 assert_matches!(ev.content.third_party_invite, None);
763
764 let prev_content = ev.unsigned.prev_content.unwrap();
765 assert_eq!(
766 prev_content.avatar_url.as_deref(),
767 Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
768 );
769 assert_eq!(prev_content.displayname.as_deref(), Some("Alice Margatroid"));
770 assert_eq!(prev_content.is_direct, Some(true));
771 assert_eq!(prev_content.membership, MembershipState::Invite);
772
773 let third_party_invite = prev_content.third_party_invite.unwrap();
774 assert_eq!(third_party_invite.display_name, "alice");
775 assert_eq!(third_party_invite.signed.mxid, "@alice:example.org");
776 assert_eq!(third_party_invite.signed.signatures.len(), 1);
777 let server_signatures =
778 third_party_invite.signed.signatures.get(server_name!("magic.forest")).unwrap();
779 assert_eq!(
780 *server_signatures,
781 btreemap! {
782 ServerSigningKeyId::from_parts(
783 SigningKeyAlgorithm::Ed25519,
784 server_signing_key_version!("3")
785 ) => "foobar".to_owned()
786 }
787 );
788 assert_eq!(third_party_invite.signed.token, "abc123");
789 }
790
791 #[test]
792 fn serde_with_join_authorized() {
793 let json = json!({
794 "type": "m.room.member",
795 "content": {
796 "membership": "join",
797 "join_authorised_via_users_server": "@notcarl:example.com"
798 },
799 "event_id": "$h29iv0s8:example.com",
800 "origin_server_ts": 1,
801 "room_id": "!n8f893n9:example.com",
802 "sender": "@carl:example.com",
803 "state_key": "@carl:example.com"
804 });
805
806 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
807 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
808 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
809 assert_eq!(ev.room_id, "!n8f893n9:example.com");
810 assert_eq!(ev.sender, "@carl:example.com");
811 assert_eq!(ev.state_key, "@carl:example.com");
812 assert!(ev.unsigned.is_empty());
813
814 assert_eq!(ev.content.avatar_url, None);
815 assert_eq!(ev.content.displayname, None);
816 assert_eq!(ev.content.is_direct, None);
817 assert_eq!(ev.content.membership, MembershipState::Join);
818 assert_matches!(ev.content.third_party_invite, None);
819 assert_eq!(
820 ev.content.join_authorized_via_users_server.as_deref(),
821 Some(user_id!("@notcarl:example.com"))
822 );
823 }
824}