ruma_events/room/
member.rs

1//! Types for the [`m.room.member`] event.
2//!
3//! [`m.room.member`]: https://spec.matrix.org/latest/client-server-api/#mroommember
4
5use js_int::Int;
6use ruma_common::{
7    OwnedMxcUri, OwnedTransactionId, OwnedUserId, ServerSignatures, UserId,
8    room_version_rules::RedactionRules,
9    serde::{CanBeEmpty, Raw, StringEnum},
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/// The content of an `m.room.member` event.
25///
26/// The current membership state of a user in the room.
27///
28/// Adjusts the membership state for a user in a room. It is preferable to use the membership
29/// APIs (`/rooms/<room id>/invite` etc) when performing membership actions rather than
30/// adjusting the state directly as there are a restricted set of valid transformations. For
31/// example, user A cannot force user B to join a room, and trying to force this state change
32/// directly will fail.
33///
34/// This event may also include an `invite_room_state` key inside the event's unsigned data, but
35/// Ruma doesn't currently expose this; see [#998](https://github.com/ruma/ruma/issues/998).
36///
37/// The user for which a membership applies is represented by the `state_key`. Under some
38/// conditions, the `sender` and `state_key` may not match - this may be interpreted as the
39/// `sender` affecting the membership state of the `state_key` user.
40///
41/// The membership for a given user can change over time. Previous membership can be retrieved
42/// from the `prev_content` object on an event. If not present, the user's previous membership
43/// must be assumed as leave.
44#[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    /// The avatar URL for this user, if any.
56    ///
57    /// This is added by the homeserver. If you activate the `compat-empty-string-null` feature,
58    /// this field being an empty string in JSON will result in `None` here during deserialization.
59    #[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    /// The display name for this user, if any.
67    ///
68    /// This is added by the homeserver.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub displayname: Option<String>,
71
72    /// Flag indicating whether the room containing this event was created with the intention of
73    /// being a direct chat.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub is_direct: Option<bool>,
76
77    /// The membership state of this user.
78    pub membership: MembershipState,
79
80    /// If this member event is the successor to a third party invitation, this field will
81    /// contain information about that invitation.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub third_party_invite: Option<ThirdPartyInvite>,
84
85    /// The [BlurHash](https://blurha.sh) for the avatar pointed to by `avatar_url`.
86    ///
87    /// This uses the unstable prefix in
88    /// [MSC2448](https://github.com/matrix-org/matrix-spec-proposals/pull/2448).
89    #[cfg(feature = "unstable-msc2448")]
90    #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
91    pub blurhash: Option<String>,
92
93    /// User-supplied text for why their membership has changed.
94    ///
95    /// For kicks and bans, this is typically the reason for the kick or ban. For other membership
96    /// changes, this is a way for the user to communicate their intent without having to send a
97    /// message to the room, such as in a case where Bob rejects an invite from Alice about an
98    /// upcoming concert, but can't make it that day.
99    ///
100    /// Clients are not recommended to show this reason to users when receiving an invite due to
101    /// the potential for spam and abuse. Hiding the reason behind a button or other component
102    /// is recommended.
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub reason: Option<String>,
105
106    /// Arbitrarily chosen `UserId` (MxID) of a local user who can send an invite.
107    #[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    /// Creates a new `RoomMemberEventContent` with the given membership state.
114    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    /// Obtain the details about this event that are required to calculate a membership change.
129    ///
130    /// This is required when you want to calculate the change a redacted `m.room.member` event
131    /// made.
132    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    /// Helper function for membership change.
141    ///
142    /// This requires data from the full event:
143    ///
144    /// * The previous details computed from `event.unsigned.prev_content`,
145    /// * The sender of the event,
146    /// * The state key of the event.
147    ///
148    /// Check [the specification][spec] for details.
149    ///
150    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
151    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
175/// The possibly redacted form of [`RoomMemberEventContent`].
176///
177/// This type is used when it's not obvious whether the content is redacted or not.
178#[derive(Clone, Debug, Deserialize, Serialize)]
179#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
180pub struct PossiblyRedactedRoomMemberEventContent {
181    /// The avatar URL for this user, if any.
182    ///
183    /// This is added by the homeserver. If you activate the `compat-empty-string-null` feature,
184    /// this field being an empty string in JSON will result in `None` here during deserialization.
185    #[serde(skip_serializing_if = "Option::is_none")]
186    #[cfg_attr(
187        feature = "compat-empty-string-null",
188        serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
189    )]
190    pub avatar_url: Option<OwnedMxcUri>,
191
192    /// The display name for this user, if any.
193    ///
194    /// This is added by the homeserver.
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub displayname: Option<String>,
197
198    /// Flag indicating whether the room containing this event was created with the intention of
199    /// being a direct chat.
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub is_direct: Option<bool>,
202
203    /// The membership state of this user.
204    pub membership: MembershipState,
205
206    /// If this member event is the successor to a third party invitation, this field will
207    /// contain information about that invitation.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub third_party_invite: Option<PossiblyRedactedThirdPartyInvite>,
210
211    /// The [BlurHash](https://blurha.sh) for the avatar pointed to by `avatar_url`.
212    ///
213    /// This uses the unstable prefix in
214    /// [MSC2448](https://github.com/matrix-org/matrix-spec-proposals/pull/2448).
215    #[cfg(feature = "unstable-msc2448")]
216    #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
217    pub blurhash: Option<String>,
218
219    /// User-supplied text for why their membership has changed.
220    ///
221    /// For kicks and bans, this is typically the reason for the kick or ban. For other membership
222    /// changes, this is a way for the user to communicate their intent without having to send a
223    /// message to the room, such as in a case where Bob rejects an invite from Alice about an
224    /// upcoming concert, but can't make it that day.
225    ///
226    /// Clients are not recommended to show this reason to users when receiving an invite due to
227    /// the potential for spam and abuse. Hiding the reason behind a button or other component
228    /// is recommended.
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub reason: Option<String>,
231
232    /// Arbitrarily chosen `UserId` (MxID) of a local user who can send an invite.
233    #[serde(rename = "join_authorised_via_users_server")]
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub join_authorized_via_users_server: Option<OwnedUserId>,
236}
237
238impl PossiblyRedactedRoomMemberEventContent {
239    /// Creates a new `PossiblyRedactedRoomMemberEventContent` with the given membership state.
240    pub fn new(membership: MembershipState) -> Self {
241        Self {
242            membership,
243            avatar_url: None,
244            displayname: None,
245            is_direct: None,
246            third_party_invite: None,
247            #[cfg(feature = "unstable-msc2448")]
248            blurhash: None,
249            reason: None,
250            join_authorized_via_users_server: None,
251        }
252    }
253
254    /// Obtain the details about this event that are required to calculate a membership change.
255    ///
256    /// This is required when you want to calculate the change a redacted `m.room.member` event
257    /// made.
258    pub fn details(&self) -> MembershipDetails<'_> {
259        MembershipDetails {
260            avatar_url: self.avatar_url.as_deref(),
261            displayname: self.displayname.as_deref(),
262            membership: &self.membership,
263        }
264    }
265
266    /// Helper function for membership change.
267    ///
268    /// This requires data from the full event:
269    ///
270    /// * The previous details computed from `event.unsigned.prev_content`,
271    /// * The sender of the event,
272    /// * The state key of the event.
273    ///
274    /// Check [the specification][spec] for details.
275    ///
276    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
277    pub fn membership_change<'a>(
278        &'a self,
279        prev_details: Option<MembershipDetails<'a>>,
280        sender: &UserId,
281        state_key: &UserId,
282    ) -> MembershipChange<'a> {
283        membership_change(self.details(), prev_details, sender, state_key)
284    }
285}
286
287impl PossiblyRedactedStateEventContent for PossiblyRedactedRoomMemberEventContent {
288    type StateKey = OwnedUserId;
289
290    fn event_type(&self) -> StateEventType {
291        StateEventType::RoomMember
292    }
293}
294
295impl StaticEventContent for PossiblyRedactedRoomMemberEventContent {
296    const TYPE: &'static str = RoomMemberEventContent::TYPE;
297    type IsPrefix = <RoomMemberEventContent as StaticEventContent>::IsPrefix;
298}
299
300impl RedactContent for PossiblyRedactedRoomMemberEventContent {
301    type Redacted = Self;
302
303    fn redact(self, rules: &RedactionRules) -> Self {
304        Self {
305            membership: self.membership,
306            third_party_invite: self.third_party_invite.and_then(|i| i.redact(rules)),
307            join_authorized_via_users_server: self
308                .join_authorized_via_users_server
309                .filter(|_| rules.keep_room_member_join_authorised_via_users_server),
310            avatar_url: None,
311            displayname: None,
312            is_direct: None,
313            #[cfg(feature = "unstable-msc2448")]
314            blurhash: None,
315            reason: None,
316        }
317    }
318}
319
320impl From<RoomMemberEventContent> for PossiblyRedactedRoomMemberEventContent {
321    fn from(value: RoomMemberEventContent) -> Self {
322        let RoomMemberEventContent {
323            avatar_url,
324            displayname,
325            is_direct,
326            membership,
327            third_party_invite,
328            #[cfg(feature = "unstable-msc2448")]
329            blurhash,
330            reason,
331            join_authorized_via_users_server,
332        } = value;
333
334        Self {
335            avatar_url,
336            displayname,
337            is_direct,
338            membership,
339            third_party_invite: third_party_invite.map(Into::into),
340            #[cfg(feature = "unstable-msc2448")]
341            blurhash,
342            reason,
343            join_authorized_via_users_server,
344        }
345    }
346}
347
348impl From<RedactedRoomMemberEventContent> for PossiblyRedactedRoomMemberEventContent {
349    fn from(value: RedactedRoomMemberEventContent) -> Self {
350        let RedactedRoomMemberEventContent {
351            membership,
352            third_party_invite,
353            join_authorized_via_users_server,
354        } = value;
355
356        Self {
357            avatar_url: None,
358            displayname: None,
359            is_direct: None,
360            membership,
361            third_party_invite: third_party_invite.map(Into::into),
362            #[cfg(feature = "unstable-msc2448")]
363            blurhash: None,
364            reason: None,
365            join_authorized_via_users_server,
366        }
367    }
368}
369
370/// A member event that has been redacted.
371#[derive(Clone, Debug, Deserialize, Serialize)]
372#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
373pub struct RedactedRoomMemberEventContent {
374    /// The membership state of this user.
375    pub membership: MembershipState,
376
377    /// If this member event is the successor to a third party invitation, this field will
378    /// contain information about that invitation.
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub third_party_invite: Option<RedactedThirdPartyInvite>,
381
382    /// An arbitrary user who has the power to issue invites.
383    ///
384    /// This is redacted in room versions 8 and below. It is used for validating
385    /// joins when the join rule is restricted.
386    #[serde(rename = "join_authorised_via_users_server", skip_serializing_if = "Option::is_none")]
387    pub join_authorized_via_users_server: Option<OwnedUserId>,
388}
389
390impl RedactedRoomMemberEventContent {
391    /// Create a `RedactedRoomMemberEventContent` with the given membership.
392    pub fn new(membership: MembershipState) -> Self {
393        Self { membership, third_party_invite: None, join_authorized_via_users_server: None }
394    }
395
396    /// Obtain the details about this event that are required to calculate a membership change.
397    ///
398    /// This is required when you want to calculate the change a redacted `m.room.member` event
399    /// made.
400    pub fn details(&self) -> MembershipDetails<'_> {
401        MembershipDetails { avatar_url: None, displayname: None, membership: &self.membership }
402    }
403
404    /// Helper function for membership change.
405    ///
406    /// Since redacted events don't have `unsigned.prev_content`, you have to pass the `.details()`
407    /// of the previous `m.room.member` event manually (if there is a previous `m.room.member`
408    /// event).
409    ///
410    /// This also requires data from the full event:
411    ///
412    /// * The sender of the event,
413    /// * The state key of the event.
414    ///
415    /// Check [the specification][spec] for details.
416    ///
417    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
418    pub fn membership_change<'a>(
419        &'a self,
420        prev_details: Option<MembershipDetails<'a>>,
421        sender: &UserId,
422        state_key: &UserId,
423    ) -> MembershipChange<'a> {
424        membership_change(self.details(), prev_details, sender, state_key)
425    }
426}
427
428impl RedactedStateEventContent for RedactedRoomMemberEventContent {
429    type StateKey = OwnedUserId;
430
431    fn event_type(&self) -> StateEventType {
432        StateEventType::RoomMember
433    }
434}
435
436impl StaticEventContent for RedactedRoomMemberEventContent {
437    const TYPE: &'static str = RoomMemberEventContent::TYPE;
438    type IsPrefix = <RoomMemberEventContent as StaticEventContent>::IsPrefix;
439}
440
441impl RoomMemberEvent {
442    /// Obtain the membership state, regardless of whether this event is redacted.
443    pub fn membership(&self) -> &MembershipState {
444        match self {
445            Self::Original(ev) => &ev.content.membership,
446            Self::Redacted(ev) => &ev.content.membership,
447        }
448    }
449}
450
451impl SyncRoomMemberEvent {
452    /// Obtain the membership state, regardless of whether this event is redacted.
453    pub fn membership(&self) -> &MembershipState {
454        match self {
455            Self::Original(ev) => &ev.content.membership,
456            Self::Redacted(ev) => &ev.content.membership,
457        }
458    }
459}
460
461/// The membership state of a user.
462#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
463#[derive(Clone, StringEnum)]
464#[ruma_enum(rename_all = "lowercase")]
465#[non_exhaustive]
466pub enum MembershipState {
467    /// The user is banned.
468    Ban,
469
470    /// The user has been invited.
471    Invite,
472
473    /// The user has joined.
474    Join,
475
476    /// The user has requested to join.
477    Knock,
478
479    /// The user has left.
480    Leave,
481
482    #[doc(hidden)]
483    _Custom(PrivOwnedStr),
484}
485
486/// Information about a third party invitation.
487#[derive(Clone, Debug, Deserialize, Serialize)]
488#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
489pub struct ThirdPartyInvite {
490    /// A name which can be displayed to represent the user instead of their third party
491    /// identifier.
492    pub display_name: String,
493
494    /// A block of content which has been signed, which servers can use to verify the event.
495    ///
496    /// Clients should ignore this.
497    pub signed: Raw<SignedContent>,
498}
499
500impl ThirdPartyInvite {
501    /// Creates a new `ThirdPartyInvite` with the given display name and signed content.
502    pub fn new(display_name: String, signed: Raw<SignedContent>) -> Self {
503        Self { display_name, signed }
504    }
505
506    /// Transform `self` into a redacted form (removing most or all fields) according to the spec.
507    ///
508    /// Returns `None` if the field for this object was redacted according to the given
509    /// [`RedactionRules`], otherwise returns the redacted form.
510    fn redact(self, rules: &RedactionRules) -> Option<RedactedThirdPartyInvite> {
511        rules
512            .keep_room_member_third_party_invite_signed
513            .then_some(RedactedThirdPartyInvite { signed: self.signed })
514    }
515}
516
517/// Redacted information about a third party invitation.
518#[derive(Clone, Debug, Deserialize, Serialize)]
519#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
520pub struct RedactedThirdPartyInvite {
521    /// A block of content which has been signed, which servers can use to verify the event.
522    ///
523    /// Clients should ignore this.
524    pub signed: Raw<SignedContent>,
525}
526
527/// Possibly redacted information about a third party invitation.
528#[derive(Clone, Debug, Deserialize, Serialize)]
529#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
530pub struct PossiblyRedactedThirdPartyInvite {
531    /// A name which can be displayed to represent the user instead of their third party
532    /// identifier.
533    #[serde(skip_serializing_if = "Option::is_none")]
534    pub display_name: Option<String>,
535
536    /// A block of content which has been signed, which servers can use to verify the event.
537    ///
538    /// Clients should ignore this.
539    pub signed: Raw<SignedContent>,
540}
541
542impl PossiblyRedactedThirdPartyInvite {
543    /// Creates a new `PossiblyRedactedThirdPartyInvite` with the given display name and signed
544    /// content.
545    pub fn new(display_name: String, signed: Raw<SignedContent>) -> Self {
546        Self { display_name: Some(display_name), signed }
547    }
548
549    /// Transform `self` into a redacted form (removing most or all fields) according to the spec.
550    ///
551    /// Returns `None` if the field for this object was redacted according to the given
552    /// [`RedactionRules`], otherwise returns the redacted form.
553    fn redact(self, rules: &RedactionRules) -> Option<Self> {
554        rules
555            .keep_room_member_third_party_invite_signed
556            .then_some(Self { display_name: None, signed: self.signed })
557    }
558}
559
560impl From<ThirdPartyInvite> for PossiblyRedactedThirdPartyInvite {
561    fn from(value: ThirdPartyInvite) -> Self {
562        let ThirdPartyInvite { display_name, signed } = value;
563        Self { display_name: Some(display_name), signed }
564    }
565}
566
567impl From<RedactedThirdPartyInvite> for PossiblyRedactedThirdPartyInvite {
568    fn from(value: RedactedThirdPartyInvite) -> Self {
569        let RedactedThirdPartyInvite { signed } = value;
570        Self { display_name: None, signed }
571    }
572}
573
574/// A block of content which has been signed, which servers can use to verify a third party
575/// invitation.
576#[derive(Clone, Debug, Deserialize, Serialize)]
577#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
578pub struct SignedContent {
579    /// The invited Matrix user ID.
580    ///
581    /// Must be equal to the user_id property of the event.
582    pub mxid: OwnedUserId,
583
584    /// A single signature from the verifying server, in the format specified by the Signing Events
585    /// section of the server-server API.
586    pub signatures: ServerSignatures,
587
588    /// The token property of the containing `third_party_invite` object.
589    pub token: String,
590}
591
592impl SignedContent {
593    /// Creates a new `SignedContent` with the given mxid, signature and token.
594    pub fn new(signatures: ServerSignatures, mxid: OwnedUserId, token: String) -> Self {
595        Self { mxid, signatures, token }
596    }
597}
598
599impl OriginalRoomMemberEvent {
600    /// Obtain the details about this event that are required to calculate a membership change.
601    ///
602    /// This is required when you want to calculate the change a redacted `m.room.member` event
603    /// made.
604    pub fn details(&self) -> MembershipDetails<'_> {
605        self.content.details()
606    }
607
608    /// Get a reference to the `prev_content` in unsigned, if it exists.
609    ///
610    /// Shorthand for `event.unsigned.prev_content.as_ref()`
611    pub fn prev_content(&self) -> Option<&PossiblyRedactedRoomMemberEventContent> {
612        self.unsigned.prev_content.as_ref()
613    }
614
615    fn prev_details(&self) -> Option<MembershipDetails<'_>> {
616        self.prev_content().map(|c| c.details())
617    }
618
619    /// Helper function for membership change.
620    ///
621    /// Check [the specification][spec] for details.
622    ///
623    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
624    pub fn membership_change(&self) -> MembershipChange<'_> {
625        membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
626    }
627}
628
629impl RedactedRoomMemberEvent {
630    /// Obtain the details about this event that are required to calculate a membership change.
631    ///
632    /// This is required when you want to calculate the change a redacted `m.room.member` event
633    /// made.
634    pub fn details(&self) -> MembershipDetails<'_> {
635        self.content.details()
636    }
637
638    /// Helper function for membership change.
639    ///
640    /// Since redacted events don't have `unsigned.prev_content`, you have to pass the `.details()`
641    /// of the previous `m.room.member` event manually (if there is a previous `m.room.member`
642    /// event).
643    ///
644    /// Check [the specification][spec] for details.
645    ///
646    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
647    pub fn membership_change<'a>(
648        &'a self,
649        prev_details: Option<MembershipDetails<'a>>,
650    ) -> MembershipChange<'a> {
651        membership_change(self.details(), prev_details, &self.sender, &self.state_key)
652    }
653}
654
655impl OriginalSyncRoomMemberEvent {
656    /// Obtain the details about this event that are required to calculate a membership change.
657    ///
658    /// This is required when you want to calculate the change a redacted `m.room.member` event
659    /// made.
660    pub fn details(&self) -> MembershipDetails<'_> {
661        self.content.details()
662    }
663
664    /// Get a reference to the `prev_content` in unsigned, if it exists.
665    ///
666    /// Shorthand for `event.unsigned.prev_content.as_ref()`
667    pub fn prev_content(&self) -> Option<&PossiblyRedactedRoomMemberEventContent> {
668        self.unsigned.prev_content.as_ref()
669    }
670
671    fn prev_details(&self) -> Option<MembershipDetails<'_>> {
672        self.prev_content().map(|c| c.details())
673    }
674
675    /// Helper function for membership change.
676    ///
677    /// Check [the specification][spec] for details.
678    ///
679    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
680    pub fn membership_change(&self) -> MembershipChange<'_> {
681        membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
682    }
683}
684
685impl RedactedSyncRoomMemberEvent {
686    /// Obtain the details about this event that are required to calculate a membership change.
687    ///
688    /// This is required when you want to calculate the change a redacted `m.room.member` event
689    /// made.
690    pub fn details(&self) -> MembershipDetails<'_> {
691        self.content.details()
692    }
693
694    /// Helper function for membership change.
695    ///
696    /// Since redacted events don't have `unsigned.prev_content`, you have to pass the `.details()`
697    /// of the previous `m.room.member` event manually (if there is a previous `m.room.member`
698    /// event).
699    ///
700    /// Check [the specification][spec] for details.
701    ///
702    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
703    pub fn membership_change<'a>(
704        &'a self,
705        prev_details: Option<MembershipDetails<'a>>,
706    ) -> MembershipChange<'a> {
707        membership_change(self.details(), prev_details, &self.sender, &self.state_key)
708    }
709}
710
711impl StrippedRoomMemberEvent {
712    /// Obtain the details about this event that are required to calculate a membership change.
713    ///
714    /// This is required when you want to calculate the change a redacted `m.room.member` event
715    /// made.
716    pub fn details(&self) -> MembershipDetails<'_> {
717        self.content.details()
718    }
719
720    /// Helper function for membership change.
721    ///
722    /// Since stripped events don't have `unsigned.prev_content`, you have to pass the `.details()`
723    /// of the previous `m.room.member` event manually (if there is a previous `m.room.member`
724    /// event).
725    ///
726    /// Check [the specification][spec] for details.
727    ///
728    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
729    pub fn membership_change<'a>(
730        &'a self,
731        prev_details: Option<MembershipDetails<'a>>,
732    ) -> MembershipChange<'a> {
733        membership_change(self.details(), prev_details, &self.sender, &self.state_key)
734    }
735}
736
737/// Extra information about a message event that is not incorporated into the event's hash.
738#[derive(Clone, Debug, Default, Deserialize)]
739#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
740pub struct RoomMemberUnsigned {
741    /// The time in milliseconds that has elapsed since the event was sent.
742    ///
743    /// This field is generated by the local homeserver, and may be incorrect if the local time on
744    /// at least one of the two servers is out of sync, which can cause the age to either be
745    /// negative or greater than it actually is.
746    pub age: Option<Int>,
747
748    /// The client-supplied transaction ID, if the client being given the event is the same one
749    /// which sent it.
750    pub transaction_id: Option<OwnedTransactionId>,
751
752    /// Optional previous content of the event.
753    pub prev_content: Option<PossiblyRedactedRoomMemberEventContent>,
754
755    /// Stripped state events to assist the receiver in identifying the room when receiving an
756    /// invite.
757    #[serde(default)]
758    pub invite_room_state: Vec<Raw<AnyStrippedStateEvent>>,
759
760    /// Stripped state events to assist the receiver in identifying the room after knocking.
761    #[serde(default)]
762    pub knock_room_state: Vec<Raw<AnyStrippedStateEvent>>,
763
764    /// [Bundled aggregations] of related child events.
765    ///
766    /// [Bundled aggregations]: https://spec.matrix.org/latest/client-server-api/#aggregations-of-child-events
767    #[serde(rename = "m.relations", default)]
768    pub relations: BundledStateRelations,
769}
770
771impl RoomMemberUnsigned {
772    /// Create a new `Unsigned` with fields set to `None`.
773    pub fn new() -> Self {
774        Self::default()
775    }
776}
777
778impl CanBeEmpty for RoomMemberUnsigned {
779    /// Whether this unsigned data is empty (all fields are `None`).
780    ///
781    /// This method is used to determine whether to skip serializing the `unsigned` field in room
782    /// events. Do not use it to determine whether an incoming `unsigned` field was present - it
783    /// could still have been present but contained none of the known fields.
784    fn is_empty(&self) -> bool {
785        self.age.is_none()
786            && self.transaction_id.is_none()
787            && self.prev_content.is_none()
788            && self.invite_room_state.is_empty()
789            && self.relations.is_empty()
790    }
791}
792
793#[cfg(test)]
794mod tests {
795    use assert_matches2::assert_matches;
796    use js_int::uint;
797    use maplit::btreemap;
798    use ruma_common::{
799        MilliSecondsSinceUnixEpoch, ServerSigningKeyId, SigningKeyAlgorithm, mxc_uri,
800        serde::CanBeEmpty, server_name, server_signing_key_version, user_id,
801    };
802    use serde_json::{from_value as from_json_value, json};
803
804    use super::{MembershipState, RoomMemberEventContent};
805    use crate::OriginalStateEvent;
806
807    #[test]
808    fn serde_with_no_prev_content() {
809        let json = json!({
810            "type": "m.room.member",
811            "content": {
812                "membership": "join"
813            },
814            "event_id": "$h29iv0s8:example.com",
815            "origin_server_ts": 1,
816            "room_id": "!n8f893n9:example.com",
817            "sender": "@carl:example.com",
818            "state_key": "@carl:example.com"
819        });
820
821        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
822        assert_eq!(ev.event_id, "$h29iv0s8:example.com");
823        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
824        assert_eq!(ev.room_id, "!n8f893n9:example.com");
825        assert_eq!(ev.sender, "@carl:example.com");
826        assert_eq!(ev.state_key, "@carl:example.com");
827        assert!(ev.unsigned.is_empty());
828
829        assert_eq!(ev.content.avatar_url, None);
830        assert_eq!(ev.content.displayname, None);
831        assert_eq!(ev.content.is_direct, None);
832        assert_eq!(ev.content.membership, MembershipState::Join);
833        assert_matches!(ev.content.third_party_invite, None);
834    }
835
836    #[test]
837    fn serde_with_prev_content() {
838        let json = json!({
839            "type": "m.room.member",
840            "content": {
841                "membership": "join"
842            },
843            "event_id": "$h29iv0s8:example.com",
844            "origin_server_ts": 1,
845            "room_id": "!n8f893n9:example.com",
846            "sender": "@carl:example.com",
847            "state_key": "@carl:example.com",
848            "unsigned": {
849                "prev_content": {
850                    "membership": "join"
851                },
852            },
853        });
854
855        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
856        assert_eq!(ev.event_id, "$h29iv0s8:example.com");
857        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
858        assert_eq!(ev.room_id, "!n8f893n9:example.com");
859        assert_eq!(ev.sender, "@carl:example.com");
860        assert_eq!(ev.state_key, "@carl:example.com");
861
862        assert_eq!(ev.content.avatar_url, None);
863        assert_eq!(ev.content.displayname, None);
864        assert_eq!(ev.content.is_direct, None);
865        assert_eq!(ev.content.membership, MembershipState::Join);
866        assert_matches!(ev.content.third_party_invite, None);
867
868        let prev_content = ev.unsigned.prev_content.unwrap();
869        assert_eq!(prev_content.avatar_url, None);
870        assert_eq!(prev_content.displayname, None);
871        assert_eq!(prev_content.is_direct, None);
872        assert_eq!(prev_content.membership, MembershipState::Join);
873        assert_matches!(prev_content.third_party_invite, None);
874    }
875
876    #[test]
877    fn serde_with_content_full() {
878        let json = json!({
879            "type": "m.room.member",
880            "content": {
881                "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
882                "displayname": "Alice Margatroid",
883                "is_direct": true,
884                "membership": "invite",
885                "third_party_invite": {
886                    "display_name": "alice",
887                    "signed": {
888                        "mxid": "@alice:example.org",
889                        "signatures": {
890                            "magic.forest": {
891                                "ed25519:3": "foobar"
892                            }
893                        },
894                        "token": "abc123"
895                    }
896                }
897            },
898            "event_id": "$143273582443PhrSn:example.org",
899            "origin_server_ts": 233,
900            "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
901            "sender": "@alice:example.org",
902            "state_key": "@alice:example.org"
903        });
904
905        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
906        assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
907        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
908        assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
909        assert_eq!(ev.sender, "@alice:example.org");
910        assert_eq!(ev.state_key, "@alice:example.org");
911        assert!(ev.unsigned.is_empty());
912
913        assert_eq!(
914            ev.content.avatar_url.as_deref(),
915            Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
916        );
917        assert_eq!(ev.content.displayname.as_deref(), Some("Alice Margatroid"));
918        assert_eq!(ev.content.is_direct, Some(true));
919        assert_eq!(ev.content.membership, MembershipState::Invite);
920
921        let third_party_invite = ev.content.third_party_invite.unwrap();
922        assert_eq!(third_party_invite.display_name, "alice");
923        let signed = third_party_invite.signed.deserialize().unwrap();
924        assert_eq!(signed.mxid, "@alice:example.org");
925        assert_eq!(signed.signatures.len(), 1);
926        let server_signatures = signed.signatures.get(server_name!("magic.forest")).unwrap();
927        assert_eq!(
928            *server_signatures,
929            btreemap! {
930                ServerSigningKeyId::from_parts(
931                    SigningKeyAlgorithm::Ed25519,
932                    server_signing_key_version!("3")
933                ) => "foobar".to_owned()
934            }
935        );
936        assert_eq!(signed.token, "abc123");
937    }
938
939    #[test]
940    fn serde_with_prev_content_full() {
941        let json = json!({
942            "type": "m.room.member",
943            "content": {
944                "membership": "join",
945            },
946            "event_id": "$143273582443PhrSn:example.org",
947            "origin_server_ts": 233,
948            "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
949            "sender": "@alice:example.org",
950            "state_key": "@alice:example.org",
951            "unsigned": {
952                "prev_content": {
953                    "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
954                    "displayname": "Alice Margatroid",
955                    "is_direct": true,
956                    "membership": "invite",
957                    "third_party_invite": {
958                        "display_name": "alice",
959                        "signed": {
960                            "mxid": "@alice:example.org",
961                            "signatures": {
962                                "magic.forest": {
963                                    "ed25519:3": "foobar",
964                                },
965                            },
966                            "token": "abc123"
967                        },
968                    },
969                },
970            },
971        });
972
973        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
974        assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
975        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
976        assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
977        assert_eq!(ev.sender, "@alice:example.org");
978        assert_eq!(ev.state_key, "@alice:example.org");
979
980        assert_eq!(ev.content.avatar_url, None);
981        assert_eq!(ev.content.displayname, None);
982        assert_eq!(ev.content.is_direct, None);
983        assert_eq!(ev.content.membership, MembershipState::Join);
984        assert_matches!(ev.content.third_party_invite, None);
985
986        let prev_content = ev.unsigned.prev_content.unwrap();
987        assert_eq!(
988            prev_content.avatar_url.as_deref(),
989            Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
990        );
991        assert_eq!(prev_content.displayname.as_deref(), Some("Alice Margatroid"));
992        assert_eq!(prev_content.is_direct, Some(true));
993        assert_eq!(prev_content.membership, MembershipState::Invite);
994
995        let third_party_invite = prev_content.third_party_invite.unwrap();
996        assert_eq!(third_party_invite.display_name.as_deref(), Some("alice"));
997        let signed = third_party_invite.signed.deserialize().unwrap();
998        assert_eq!(signed.mxid, "@alice:example.org");
999        assert_eq!(signed.signatures.len(), 1);
1000        let server_signatures = signed.signatures.get(server_name!("magic.forest")).unwrap();
1001        assert_eq!(
1002            *server_signatures,
1003            btreemap! {
1004                ServerSigningKeyId::from_parts(
1005                    SigningKeyAlgorithm::Ed25519,
1006                    server_signing_key_version!("3")
1007                ) => "foobar".to_owned()
1008            }
1009        );
1010        assert_eq!(signed.token, "abc123");
1011    }
1012
1013    #[test]
1014    fn serde_with_join_authorized() {
1015        let json = json!({
1016            "type": "m.room.member",
1017            "content": {
1018                "membership": "join",
1019                "join_authorised_via_users_server": "@notcarl:example.com"
1020            },
1021            "event_id": "$h29iv0s8:example.com",
1022            "origin_server_ts": 1,
1023            "room_id": "!n8f893n9:example.com",
1024            "sender": "@carl:example.com",
1025            "state_key": "@carl:example.com"
1026        });
1027
1028        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
1029        assert_eq!(ev.event_id, "$h29iv0s8:example.com");
1030        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
1031        assert_eq!(ev.room_id, "!n8f893n9:example.com");
1032        assert_eq!(ev.sender, "@carl:example.com");
1033        assert_eq!(ev.state_key, "@carl:example.com");
1034        assert!(ev.unsigned.is_empty());
1035
1036        assert_eq!(ev.content.avatar_url, None);
1037        assert_eq!(ev.content.displayname, None);
1038        assert_eq!(ev.content.is_direct, None);
1039        assert_eq!(ev.content.membership, MembershipState::Join);
1040        assert_matches!(ev.content.third_party_invite, None);
1041        assert_eq!(
1042            ev.content.join_authorized_via_users_server.as_deref(),
1043            Some(user_id!("@notcarl:example.com"))
1044        );
1045    }
1046}