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, EventContent, PossiblyRedactedStateEventContent,
16 PrivOwnedStr, RedactContent, RedactedStateEventContent, StateEventType,
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
184#[derive(Clone, Debug, Deserialize, Serialize)]
186#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
187pub struct RedactedRoomMemberEventContent {
188 pub membership: MembershipState,
190
191 #[serde(skip_serializing_if = "Option::is_none")]
194 pub third_party_invite: Option<RedactedThirdPartyInvite>,
195
196 #[serde(rename = "join_authorised_via_users_server", skip_serializing_if = "Option::is_none")]
201 pub join_authorized_via_users_server: Option<OwnedUserId>,
202}
203
204impl RedactedRoomMemberEventContent {
205 pub fn new(membership: MembershipState) -> Self {
207 Self { membership, third_party_invite: None, join_authorized_via_users_server: None }
208 }
209
210 pub fn details(&self) -> MembershipDetails<'_> {
215 MembershipDetails { avatar_url: None, displayname: None, membership: &self.membership }
216 }
217
218 pub fn membership_change<'a>(
233 &'a self,
234 prev_details: Option<MembershipDetails<'a>>,
235 sender: &UserId,
236 state_key: &UserId,
237 ) -> MembershipChange<'a> {
238 membership_change(self.details(), prev_details, sender, state_key)
239 }
240}
241
242impl EventContent for RedactedRoomMemberEventContent {
243 type EventType = StateEventType;
244
245 fn event_type(&self) -> StateEventType {
246 StateEventType::RoomMember
247 }
248}
249
250impl RedactedStateEventContent for RedactedRoomMemberEventContent {
251 type StateKey = OwnedUserId;
252}
253
254impl RoomMemberEvent {
255 pub fn membership(&self) -> &MembershipState {
257 match self {
258 Self::Original(ev) => &ev.content.membership,
259 Self::Redacted(ev) => &ev.content.membership,
260 }
261 }
262}
263
264impl SyncRoomMemberEvent {
265 pub fn membership(&self) -> &MembershipState {
267 match self {
268 Self::Original(ev) => &ev.content.membership,
269 Self::Redacted(ev) => &ev.content.membership,
270 }
271 }
272}
273
274#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
276#[derive(Clone, PartialEq, Eq, StringEnum)]
277#[ruma_enum(rename_all = "lowercase")]
278#[non_exhaustive]
279pub enum MembershipState {
280 Ban,
282
283 Invite,
285
286 Join,
288
289 Knock,
291
292 Leave,
294
295 #[doc(hidden)]
296 _Custom(PrivOwnedStr),
297}
298
299#[derive(Clone, Debug, Deserialize, Serialize)]
301#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
302pub struct ThirdPartyInvite {
303 pub display_name: String,
306
307 pub signed: SignedContent,
311}
312
313impl ThirdPartyInvite {
314 pub fn new(display_name: String, signed: SignedContent) -> Self {
316 Self { display_name, signed }
317 }
318
319 fn redact(self, rules: &RedactionRules) -> Option<RedactedThirdPartyInvite> {
324 rules
325 .keep_room_member_third_party_invite_signed
326 .then_some(RedactedThirdPartyInvite { signed: self.signed })
327 }
328}
329
330#[derive(Clone, Debug, Deserialize, Serialize)]
332#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
333pub struct RedactedThirdPartyInvite {
334 pub signed: SignedContent,
338}
339
340#[derive(Clone, Debug, Deserialize, Serialize)]
343#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
344pub struct SignedContent {
345 pub mxid: OwnedUserId,
349
350 pub signatures: ServerSignatures,
353
354 pub token: String,
356}
357
358impl SignedContent {
359 pub fn new(signatures: ServerSignatures, mxid: OwnedUserId, token: String) -> Self {
361 Self { mxid, signatures, token }
362 }
363}
364
365impl OriginalRoomMemberEvent {
366 pub fn details(&self) -> MembershipDetails<'_> {
371 self.content.details()
372 }
373
374 pub fn prev_content(&self) -> Option<&RoomMemberEventContent> {
378 self.unsigned.prev_content.as_ref()
379 }
380
381 fn prev_details(&self) -> Option<MembershipDetails<'_>> {
382 self.prev_content().map(|c| c.details())
383 }
384
385 pub fn membership_change(&self) -> MembershipChange<'_> {
391 membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
392 }
393}
394
395impl RedactedRoomMemberEvent {
396 pub fn details(&self) -> MembershipDetails<'_> {
401 self.content.details()
402 }
403
404 pub fn membership_change<'a>(
414 &'a self,
415 prev_details: Option<MembershipDetails<'a>>,
416 ) -> MembershipChange<'a> {
417 membership_change(self.details(), prev_details, &self.sender, &self.state_key)
418 }
419}
420
421impl OriginalSyncRoomMemberEvent {
422 pub fn details(&self) -> MembershipDetails<'_> {
427 self.content.details()
428 }
429
430 pub fn prev_content(&self) -> Option<&RoomMemberEventContent> {
434 self.unsigned.prev_content.as_ref()
435 }
436
437 fn prev_details(&self) -> Option<MembershipDetails<'_>> {
438 self.prev_content().map(|c| c.details())
439 }
440
441 pub fn membership_change(&self) -> MembershipChange<'_> {
447 membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
448 }
449}
450
451impl RedactedSyncRoomMemberEvent {
452 pub fn details(&self) -> MembershipDetails<'_> {
457 self.content.details()
458 }
459
460 pub fn membership_change<'a>(
470 &'a self,
471 prev_details: Option<MembershipDetails<'a>>,
472 ) -> MembershipChange<'a> {
473 membership_change(self.details(), prev_details, &self.sender, &self.state_key)
474 }
475}
476
477impl StrippedRoomMemberEvent {
478 pub fn details(&self) -> MembershipDetails<'_> {
483 self.content.details()
484 }
485
486 pub fn membership_change<'a>(
496 &'a self,
497 prev_details: Option<MembershipDetails<'a>>,
498 ) -> MembershipChange<'a> {
499 membership_change(self.details(), prev_details, &self.sender, &self.state_key)
500 }
501}
502
503#[derive(Clone, Debug, Default, Deserialize)]
505#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
506pub struct RoomMemberUnsigned {
507 pub age: Option<Int>,
513
514 pub transaction_id: Option<OwnedTransactionId>,
517
518 pub prev_content: Option<PossiblyRedactedRoomMemberEventContent>,
520
521 #[serde(default)]
523 pub invite_room_state: Vec<Raw<AnyStrippedStateEvent>>,
524
525 #[serde(rename = "m.relations", default)]
529 pub relations: BundledStateRelations,
530}
531
532impl RoomMemberUnsigned {
533 pub fn new() -> Self {
535 Self::default()
536 }
537}
538
539impl CanBeEmpty for RoomMemberUnsigned {
540 fn is_empty(&self) -> bool {
546 self.age.is_none()
547 && self.transaction_id.is_none()
548 && self.prev_content.is_none()
549 && self.invite_room_state.is_empty()
550 && self.relations.is_empty()
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use assert_matches2::assert_matches;
557 use js_int::uint;
558 use maplit::btreemap;
559 use ruma_common::{
560 mxc_uri, serde::CanBeEmpty, server_name, server_signing_key_version, user_id,
561 MilliSecondsSinceUnixEpoch, ServerSigningKeyId, SigningKeyAlgorithm,
562 };
563 use serde_json::{from_value as from_json_value, json};
564
565 use super::{MembershipState, RoomMemberEventContent};
566 use crate::OriginalStateEvent;
567
568 #[test]
569 fn serde_with_no_prev_content() {
570 let json = json!({
571 "type": "m.room.member",
572 "content": {
573 "membership": "join"
574 },
575 "event_id": "$h29iv0s8:example.com",
576 "origin_server_ts": 1,
577 "room_id": "!n8f893n9:example.com",
578 "sender": "@carl:example.com",
579 "state_key": "@carl:example.com"
580 });
581
582 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
583 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
584 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
585 assert_eq!(ev.room_id, "!n8f893n9:example.com");
586 assert_eq!(ev.sender, "@carl:example.com");
587 assert_eq!(ev.state_key, "@carl:example.com");
588 assert!(ev.unsigned.is_empty());
589
590 assert_eq!(ev.content.avatar_url, None);
591 assert_eq!(ev.content.displayname, None);
592 assert_eq!(ev.content.is_direct, None);
593 assert_eq!(ev.content.membership, MembershipState::Join);
594 assert_matches!(ev.content.third_party_invite, None);
595 }
596
597 #[test]
598 fn serde_with_prev_content() {
599 let json = json!({
600 "type": "m.room.member",
601 "content": {
602 "membership": "join"
603 },
604 "event_id": "$h29iv0s8:example.com",
605 "origin_server_ts": 1,
606 "room_id": "!n8f893n9:example.com",
607 "sender": "@carl:example.com",
608 "state_key": "@carl:example.com",
609 "unsigned": {
610 "prev_content": {
611 "membership": "join"
612 },
613 },
614 });
615
616 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
617 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
618 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
619 assert_eq!(ev.room_id, "!n8f893n9:example.com");
620 assert_eq!(ev.sender, "@carl:example.com");
621 assert_eq!(ev.state_key, "@carl:example.com");
622
623 assert_eq!(ev.content.avatar_url, None);
624 assert_eq!(ev.content.displayname, None);
625 assert_eq!(ev.content.is_direct, None);
626 assert_eq!(ev.content.membership, MembershipState::Join);
627 assert_matches!(ev.content.third_party_invite, None);
628
629 let prev_content = ev.unsigned.prev_content.unwrap();
630 assert_eq!(prev_content.avatar_url, None);
631 assert_eq!(prev_content.displayname, None);
632 assert_eq!(prev_content.is_direct, None);
633 assert_eq!(prev_content.membership, MembershipState::Join);
634 assert_matches!(prev_content.third_party_invite, None);
635 }
636
637 #[test]
638 fn serde_with_content_full() {
639 let json = json!({
640 "type": "m.room.member",
641 "content": {
642 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
643 "displayname": "Alice Margatroid",
644 "is_direct": true,
645 "membership": "invite",
646 "third_party_invite": {
647 "display_name": "alice",
648 "signed": {
649 "mxid": "@alice:example.org",
650 "signatures": {
651 "magic.forest": {
652 "ed25519:3": "foobar"
653 }
654 },
655 "token": "abc123"
656 }
657 }
658 },
659 "event_id": "$143273582443PhrSn:example.org",
660 "origin_server_ts": 233,
661 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
662 "sender": "@alice:example.org",
663 "state_key": "@alice:example.org"
664 });
665
666 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
667 assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
668 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
669 assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
670 assert_eq!(ev.sender, "@alice:example.org");
671 assert_eq!(ev.state_key, "@alice:example.org");
672 assert!(ev.unsigned.is_empty());
673
674 assert_eq!(
675 ev.content.avatar_url.as_deref(),
676 Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
677 );
678 assert_eq!(ev.content.displayname.as_deref(), Some("Alice Margatroid"));
679 assert_eq!(ev.content.is_direct, Some(true));
680 assert_eq!(ev.content.membership, MembershipState::Invite);
681
682 let third_party_invite = ev.content.third_party_invite.unwrap();
683 assert_eq!(third_party_invite.display_name, "alice");
684 assert_eq!(third_party_invite.signed.mxid, "@alice:example.org");
685 assert_eq!(third_party_invite.signed.signatures.len(), 1);
686 let server_signatures =
687 third_party_invite.signed.signatures.get(server_name!("magic.forest")).unwrap();
688 assert_eq!(
689 *server_signatures,
690 btreemap! {
691 ServerSigningKeyId::from_parts(
692 SigningKeyAlgorithm::Ed25519,
693 server_signing_key_version!("3")
694 ) => "foobar".to_owned()
695 }
696 );
697 assert_eq!(third_party_invite.signed.token, "abc123");
698 }
699
700 #[test]
701 fn serde_with_prev_content_full() {
702 let json = json!({
703 "type": "m.room.member",
704 "content": {
705 "membership": "join",
706 },
707 "event_id": "$143273582443PhrSn:example.org",
708 "origin_server_ts": 233,
709 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
710 "sender": "@alice:example.org",
711 "state_key": "@alice:example.org",
712 "unsigned": {
713 "prev_content": {
714 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
715 "displayname": "Alice Margatroid",
716 "is_direct": true,
717 "membership": "invite",
718 "third_party_invite": {
719 "display_name": "alice",
720 "signed": {
721 "mxid": "@alice:example.org",
722 "signatures": {
723 "magic.forest": {
724 "ed25519:3": "foobar",
725 },
726 },
727 "token": "abc123"
728 },
729 },
730 },
731 },
732 });
733
734 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
735 assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
736 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
737 assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
738 assert_eq!(ev.sender, "@alice:example.org");
739 assert_eq!(ev.state_key, "@alice:example.org");
740
741 assert_eq!(ev.content.avatar_url, None);
742 assert_eq!(ev.content.displayname, None);
743 assert_eq!(ev.content.is_direct, None);
744 assert_eq!(ev.content.membership, MembershipState::Join);
745 assert_matches!(ev.content.third_party_invite, None);
746
747 let prev_content = ev.unsigned.prev_content.unwrap();
748 assert_eq!(
749 prev_content.avatar_url.as_deref(),
750 Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
751 );
752 assert_eq!(prev_content.displayname.as_deref(), Some("Alice Margatroid"));
753 assert_eq!(prev_content.is_direct, Some(true));
754 assert_eq!(prev_content.membership, MembershipState::Invite);
755
756 let third_party_invite = prev_content.third_party_invite.unwrap();
757 assert_eq!(third_party_invite.display_name, "alice");
758 assert_eq!(third_party_invite.signed.mxid, "@alice:example.org");
759 assert_eq!(third_party_invite.signed.signatures.len(), 1);
760 let server_signatures =
761 third_party_invite.signed.signatures.get(server_name!("magic.forest")).unwrap();
762 assert_eq!(
763 *server_signatures,
764 btreemap! {
765 ServerSigningKeyId::from_parts(
766 SigningKeyAlgorithm::Ed25519,
767 server_signing_key_version!("3")
768 ) => "foobar".to_owned()
769 }
770 );
771 assert_eq!(third_party_invite.signed.token, "abc123");
772 }
773
774 #[test]
775 fn serde_with_join_authorized() {
776 let json = json!({
777 "type": "m.room.member",
778 "content": {
779 "membership": "join",
780 "join_authorised_via_users_server": "@notcarl:example.com"
781 },
782 "event_id": "$h29iv0s8:example.com",
783 "origin_server_ts": 1,
784 "room_id": "!n8f893n9:example.com",
785 "sender": "@carl:example.com",
786 "state_key": "@carl:example.com"
787 });
788
789 let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
790 assert_eq!(ev.event_id, "$h29iv0s8:example.com");
791 assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
792 assert_eq!(ev.room_id, "!n8f893n9:example.com");
793 assert_eq!(ev.sender, "@carl:example.com");
794 assert_eq!(ev.state_key, "@carl:example.com");
795 assert!(ev.unsigned.is_empty());
796
797 assert_eq!(ev.content.avatar_url, None);
798 assert_eq!(ev.content.displayname, None);
799 assert_eq!(ev.content.is_direct, None);
800 assert_eq!(ev.content.membership, MembershipState::Join);
801 assert_matches!(ev.content.third_party_invite, None);
802 assert_eq!(
803 ev.content.join_authorized_via_users_server.as_deref(),
804 Some(user_id!("@notcarl:example.com"))
805 );
806 }
807}