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, PartialEq, Eq, 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)]
528 pub invite_room_state: Vec<Raw<AnyStrippedStateEvent>>,
529
530 #[serde(rename = "m.relations", default)]
534 pub relations: BundledStateRelations,
535}
536
537impl RoomMemberUnsigned {
538 pub fn new() -> Self {
540 Self::default()
541 }
542}
543
544impl CanBeEmpty for RoomMemberUnsigned {
545 fn is_empty(&self) -> bool {
551 self.age.is_none()
552 && self.transaction_id.is_none()
553 && self.prev_content.is_none()
554 && self.invite_room_state.is_empty()
555 && self.relations.is_empty()
556 }
557}
558
559#[cfg(test)]
560mod tests {
561 use assert_matches2::assert_matches;
562 use js_int::uint;
563 use maplit::btreemap;
564 use ruma_common::{
565 mxc_uri, serde::CanBeEmpty, server_name, server_signing_key_version, user_id,
566 MilliSecondsSinceUnixEpoch, ServerSigningKeyId, SigningKeyAlgorithm,
567 };
568 use serde_json::{from_value as from_json_value, json};
569
570 use super::{MembershipState, RoomMemberEventContent};
571 use crate::OriginalStateEvent;
572
573 #[test]
574 fn serde_with_no_prev_content() {
575 let json = json!({
576 "type": "m.room.member",
577 "content": {
578 "membership": "join"
579 },
580 "event_id": "$h29iv0s8:example.com",
581 "origin_server_ts": 1,
582 "room_id": "!n8f893n9:example.com",
583 "sender": "@carl:example.com",
584 "state_key": "@carl:example.com"
585 });
586
587 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
588 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
589 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
590 assert_eq!(ev.room_id, "!n8f893n9:example.com");
591 assert_eq!(ev.sender, "@carl:example.com");
592 assert_eq!(ev.state_key, "@carl:example.com");
593 assert!(ev.unsigned.is_empty());
594
595 assert_eq!(ev.content.avatar_url, None);
596 assert_eq!(ev.content.displayname, None);
597 assert_eq!(ev.content.is_direct, None);
598 assert_eq!(ev.content.membership, MembershipState::Join);
599 assert_matches!(ev.content.third_party_invite, None);
600 }
601
602 #[test]
603 fn serde_with_prev_content() {
604 let json = json!({
605 "type": "m.room.member",
606 "content": {
607 "membership": "join"
608 },
609 "event_id": "$h29iv0s8:example.com",
610 "origin_server_ts": 1,
611 "room_id": "!n8f893n9:example.com",
612 "sender": "@carl:example.com",
613 "state_key": "@carl:example.com",
614 "unsigned": {
615 "prev_content": {
616 "membership": "join"
617 },
618 },
619 });
620
621 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
622 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
623 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
624 assert_eq!(ev.room_id, "!n8f893n9:example.com");
625 assert_eq!(ev.sender, "@carl:example.com");
626 assert_eq!(ev.state_key, "@carl:example.com");
627
628 assert_eq!(ev.content.avatar_url, None);
629 assert_eq!(ev.content.displayname, None);
630 assert_eq!(ev.content.is_direct, None);
631 assert_eq!(ev.content.membership, MembershipState::Join);
632 assert_matches!(ev.content.third_party_invite, None);
633
634 let prev_content = ev.unsigned.prev_content.unwrap();
635 assert_eq!(prev_content.avatar_url, None);
636 assert_eq!(prev_content.displayname, None);
637 assert_eq!(prev_content.is_direct, None);
638 assert_eq!(prev_content.membership, MembershipState::Join);
639 assert_matches!(prev_content.third_party_invite, None);
640 }
641
642 #[test]
643 fn serde_with_content_full() {
644 let json = json!({
645 "type": "m.room.member",
646 "content": {
647 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
648 "displayname": "Alice Margatroid",
649 "is_direct": true,
650 "membership": "invite",
651 "third_party_invite": {
652 "display_name": "alice",
653 "signed": {
654 "mxid": "@alice:example.org",
655 "signatures": {
656 "magic.forest": {
657 "ed25519:3": "foobar"
658 }
659 },
660 "token": "abc123"
661 }
662 }
663 },
664 "event_id": "$143273582443PhrSn:example.org",
665 "origin_server_ts": 233,
666 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
667 "sender": "@alice:example.org",
668 "state_key": "@alice:example.org"
669 });
670
671 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
672 assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
673 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
674 assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
675 assert_eq!(ev.sender, "@alice:example.org");
676 assert_eq!(ev.state_key, "@alice:example.org");
677 assert!(ev.unsigned.is_empty());
678
679 assert_eq!(
680 ev.content.avatar_url.as_deref(),
681 Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
682 );
683 assert_eq!(ev.content.displayname.as_deref(), Some("Alice Margatroid"));
684 assert_eq!(ev.content.is_direct, Some(true));
685 assert_eq!(ev.content.membership, MembershipState::Invite);
686
687 let third_party_invite = ev.content.third_party_invite.unwrap();
688 assert_eq!(third_party_invite.display_name, "alice");
689 let signed = third_party_invite.signed.deserialize().unwrap();
690 assert_eq!(signed.mxid, "@alice:example.org");
691 assert_eq!(signed.signatures.len(), 1);
692 let server_signatures = signed.signatures.get(server_name!("magic.forest")).unwrap();
693 assert_eq!(
694 *server_signatures,
695 btreemap! {
696 ServerSigningKeyId::from_parts(
697 SigningKeyAlgorithm::Ed25519,
698 server_signing_key_version!("3")
699 ) => "foobar".to_owned()
700 }
701 );
702 assert_eq!(signed.token, "abc123");
703 }
704
705 #[test]
706 fn serde_with_prev_content_full() {
707 let json = json!({
708 "type": "m.room.member",
709 "content": {
710 "membership": "join",
711 },
712 "event_id": "$143273582443PhrSn:example.org",
713 "origin_server_ts": 233,
714 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
715 "sender": "@alice:example.org",
716 "state_key": "@alice:example.org",
717 "unsigned": {
718 "prev_content": {
719 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
720 "displayname": "Alice Margatroid",
721 "is_direct": true,
722 "membership": "invite",
723 "third_party_invite": {
724 "display_name": "alice",
725 "signed": {
726 "mxid": "@alice:example.org",
727 "signatures": {
728 "magic.forest": {
729 "ed25519:3": "foobar",
730 },
731 },
732 "token": "abc123"
733 },
734 },
735 },
736 },
737 });
738
739 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
740 assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
741 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
742 assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
743 assert_eq!(ev.sender, "@alice:example.org");
744 assert_eq!(ev.state_key, "@alice:example.org");
745
746 assert_eq!(ev.content.avatar_url, None);
747 assert_eq!(ev.content.displayname, None);
748 assert_eq!(ev.content.is_direct, None);
749 assert_eq!(ev.content.membership, MembershipState::Join);
750 assert_matches!(ev.content.third_party_invite, None);
751
752 let prev_content = ev.unsigned.prev_content.unwrap();
753 assert_eq!(
754 prev_content.avatar_url.as_deref(),
755 Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
756 );
757 assert_eq!(prev_content.displayname.as_deref(), Some("Alice Margatroid"));
758 assert_eq!(prev_content.is_direct, Some(true));
759 assert_eq!(prev_content.membership, MembershipState::Invite);
760
761 let third_party_invite = prev_content.third_party_invite.unwrap();
762 assert_eq!(third_party_invite.display_name, "alice");
763 let signed = third_party_invite.signed.deserialize().unwrap();
764 assert_eq!(signed.mxid, "@alice:example.org");
765 assert_eq!(signed.signatures.len(), 1);
766 let server_signatures = signed.signatures.get(server_name!("magic.forest")).unwrap();
767 assert_eq!(
768 *server_signatures,
769 btreemap! {
770 ServerSigningKeyId::from_parts(
771 SigningKeyAlgorithm::Ed25519,
772 server_signing_key_version!("3")
773 ) => "foobar".to_owned()
774 }
775 );
776 assert_eq!(signed.token, "abc123");
777 }
778
779 #[test]
780 fn serde_with_join_authorized() {
781 let json = json!({
782 "type": "m.room.member",
783 "content": {
784 "membership": "join",
785 "join_authorised_via_users_server": "@notcarl:example.com"
786 },
787 "event_id": "$h29iv0s8:example.com",
788 "origin_server_ts": 1,
789 "room_id": "!n8f893n9:example.com",
790 "sender": "@carl:example.com",
791 "state_key": "@carl:example.com"
792 });
793
794 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
795 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
796 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
797 assert_eq!(ev.room_id, "!n8f893n9:example.com");
798 assert_eq!(ev.sender, "@carl:example.com");
799 assert_eq!(ev.state_key, "@carl:example.com");
800 assert!(ev.unsigned.is_empty());
801
802 assert_eq!(ev.content.avatar_url, None);
803 assert_eq!(ev.content.displayname, None);
804 assert_eq!(ev.content.is_direct, None);
805 assert_eq!(ev.content.membership, MembershipState::Join);
806 assert_matches!(ev.content.third_party_invite, None);
807 assert_eq!(
808 ev.content.join_authorized_via_users_server.as_deref(),
809 Some(user_id!("@notcarl:example.com"))
810 );
811 }
812}