Skip to main content

ruma_events/room/
member.rs

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