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    room_version_rules::RedactionRules,
8    serde::{CanBeEmpty, Raw, StringEnum},
9    OwnedMxcUri, OwnedTransactionId, OwnedUserId, ServerSignatures, UserId,
10};
11use ruma_macros::EventContent;
12use serde::{Deserialize, Serialize};
13
14use crate::{
15    AnyStrippedStateEvent, BundledStateRelations, PossiblyRedactedStateEventContent, PrivOwnedStr,
16    RedactContent, RedactedStateEventContent, StateEventType, StaticEventContent,
17};
18
19mod change;
20
21use self::change::membership_change;
22pub use self::change::{Change, MembershipChange, MembershipDetails};
23
24/// 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.
178pub type PossiblyRedactedRoomMemberEventContent = RoomMemberEventContent;
179
180impl PossiblyRedactedStateEventContent for RoomMemberEventContent {
181    type StateKey = OwnedUserId;
182
183    fn event_type(&self) -> StateEventType {
184        StateEventType::RoomMember
185    }
186}
187
188/// A member event that has been redacted.
189#[derive(Clone, Debug, Deserialize, Serialize)]
190#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
191pub struct RedactedRoomMemberEventContent {
192    /// The membership state of this user.
193    pub membership: MembershipState,
194
195    /// If this member event is the successor to a third party invitation, this field will
196    /// contain information about that invitation.
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub third_party_invite: Option<RedactedThirdPartyInvite>,
199
200    /// An arbitrary user who has the power to issue invites.
201    ///
202    /// This is redacted in room versions 8 and below. It is used for validating
203    /// joins when the join rule is restricted.
204    #[serde(rename = "join_authorised_via_users_server", skip_serializing_if = "Option::is_none")]
205    pub join_authorized_via_users_server: Option<OwnedUserId>,
206}
207
208impl RedactedRoomMemberEventContent {
209    /// Create a `RedactedRoomMemberEventContent` with the given membership.
210    pub fn new(membership: MembershipState) -> Self {
211        Self { membership, third_party_invite: None, join_authorized_via_users_server: None }
212    }
213
214    /// Obtain the details about this event that are required to calculate a membership change.
215    ///
216    /// This is required when you want to calculate the change a redacted `m.room.member` event
217    /// made.
218    pub fn details(&self) -> MembershipDetails<'_> {
219        MembershipDetails { avatar_url: None, displayname: None, membership: &self.membership }
220    }
221
222    /// Helper function for membership change.
223    ///
224    /// Since redacted events don't have `unsigned.prev_content`, you have to pass the `.details()`
225    /// of the previous `m.room.member` event manually (if there is a previous `m.room.member`
226    /// event).
227    ///
228    /// This also requires data from the full event:
229    ///
230    /// * The sender of the event,
231    /// * The state key of the event.
232    ///
233    /// Check [the specification][spec] for details.
234    ///
235    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
236    pub fn membership_change<'a>(
237        &'a self,
238        prev_details: Option<MembershipDetails<'a>>,
239        sender: &UserId,
240        state_key: &UserId,
241    ) -> MembershipChange<'a> {
242        membership_change(self.details(), prev_details, sender, state_key)
243    }
244}
245
246impl RedactedStateEventContent for RedactedRoomMemberEventContent {
247    type StateKey = OwnedUserId;
248
249    fn event_type(&self) -> StateEventType {
250        StateEventType::RoomMember
251    }
252}
253
254impl StaticEventContent for RedactedRoomMemberEventContent {
255    const TYPE: &'static str = RoomMemberEventContent::TYPE;
256    type IsPrefix = <RoomMemberEventContent as StaticEventContent>::IsPrefix;
257}
258
259impl RoomMemberEvent {
260    /// Obtain the membership state, regardless of whether this event is redacted.
261    pub fn membership(&self) -> &MembershipState {
262        match self {
263            Self::Original(ev) => &ev.content.membership,
264            Self::Redacted(ev) => &ev.content.membership,
265        }
266    }
267}
268
269impl SyncRoomMemberEvent {
270    /// Obtain the membership state, regardless of whether this event is redacted.
271    pub fn membership(&self) -> &MembershipState {
272        match self {
273            Self::Original(ev) => &ev.content.membership,
274            Self::Redacted(ev) => &ev.content.membership,
275        }
276    }
277}
278
279/// The membership state of a user.
280#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
281#[derive(Clone, StringEnum)]
282#[ruma_enum(rename_all = "lowercase")]
283#[non_exhaustive]
284pub enum MembershipState {
285    /// The user is banned.
286    Ban,
287
288    /// The user has been invited.
289    Invite,
290
291    /// The user has joined.
292    Join,
293
294    /// The user has requested to join.
295    Knock,
296
297    /// The user has left.
298    Leave,
299
300    #[doc(hidden)]
301    _Custom(PrivOwnedStr),
302}
303
304/// Information about a third party invitation.
305#[derive(Clone, Debug, Deserialize, Serialize)]
306#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
307pub struct ThirdPartyInvite {
308    /// A name which can be displayed to represent the user instead of their third party
309    /// identifier.
310    pub display_name: String,
311
312    /// A block of content which has been signed, which servers can use to verify the event.
313    ///
314    /// Clients should ignore this.
315    pub signed: Raw<SignedContent>,
316}
317
318impl ThirdPartyInvite {
319    /// Creates a new `ThirdPartyInvite` with the given display name and signed content.
320    pub fn new(display_name: String, signed: Raw<SignedContent>) -> Self {
321        Self { display_name, signed }
322    }
323
324    /// Transform `self` into a redacted form (removing most or all fields) according to the spec.
325    ///
326    /// Returns `None` if the field for this object was redacted according to the given
327    /// [`RedactionRules`], otherwise returns the redacted form.
328    fn redact(self, rules: &RedactionRules) -> Option<RedactedThirdPartyInvite> {
329        rules
330            .keep_room_member_third_party_invite_signed
331            .then_some(RedactedThirdPartyInvite { signed: self.signed })
332    }
333}
334
335/// Redacted information about a third party invitation.
336#[derive(Clone, Debug, Deserialize, Serialize)]
337#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
338pub struct RedactedThirdPartyInvite {
339    /// A block of content which has been signed, which servers can use to verify the event.
340    ///
341    /// Clients should ignore this.
342    pub signed: Raw<SignedContent>,
343}
344
345/// A block of content which has been signed, which servers can use to verify a third party
346/// invitation.
347#[derive(Clone, Debug, Deserialize, Serialize)]
348#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
349pub struct SignedContent {
350    /// The invited Matrix user ID.
351    ///
352    /// Must be equal to the user_id property of the event.
353    pub mxid: OwnedUserId,
354
355    /// A single signature from the verifying server, in the format specified by the Signing Events
356    /// section of the server-server API.
357    pub signatures: ServerSignatures,
358
359    /// The token property of the containing `third_party_invite` object.
360    pub token: String,
361}
362
363impl SignedContent {
364    /// Creates a new `SignedContent` with the given mxid, signature and token.
365    pub fn new(signatures: ServerSignatures, mxid: OwnedUserId, token: String) -> Self {
366        Self { mxid, signatures, token }
367    }
368}
369
370impl OriginalRoomMemberEvent {
371    /// Obtain the details about this event that are required to calculate a membership change.
372    ///
373    /// This is required when you want to calculate the change a redacted `m.room.member` event
374    /// made.
375    pub fn details(&self) -> MembershipDetails<'_> {
376        self.content.details()
377    }
378
379    /// Get a reference to the `prev_content` in unsigned, if it exists.
380    ///
381    /// Shorthand for `event.unsigned.prev_content.as_ref()`
382    pub fn prev_content(&self) -> Option<&RoomMemberEventContent> {
383        self.unsigned.prev_content.as_ref()
384    }
385
386    fn prev_details(&self) -> Option<MembershipDetails<'_>> {
387        self.prev_content().map(|c| c.details())
388    }
389
390    /// Helper function for membership change.
391    ///
392    /// Check [the specification][spec] for details.
393    ///
394    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
395    pub fn membership_change(&self) -> MembershipChange<'_> {
396        membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
397    }
398}
399
400impl RedactedRoomMemberEvent {
401    /// Obtain the details about this event that are required to calculate a membership change.
402    ///
403    /// This is required when you want to calculate the change a redacted `m.room.member` event
404    /// made.
405    pub fn details(&self) -> MembershipDetails<'_> {
406        self.content.details()
407    }
408
409    /// Helper function for membership change.
410    ///
411    /// Since redacted events don't have `unsigned.prev_content`, you have to pass the `.details()`
412    /// of the previous `m.room.member` event manually (if there is a previous `m.room.member`
413    /// 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    ) -> MembershipChange<'a> {
422        membership_change(self.details(), prev_details, &self.sender, &self.state_key)
423    }
424}
425
426impl OriginalSyncRoomMemberEvent {
427    /// Obtain the details about this event that are required to calculate a membership change.
428    ///
429    /// This is required when you want to calculate the change a redacted `m.room.member` event
430    /// made.
431    pub fn details(&self) -> MembershipDetails<'_> {
432        self.content.details()
433    }
434
435    /// Get a reference to the `prev_content` in unsigned, if it exists.
436    ///
437    /// Shorthand for `event.unsigned.prev_content.as_ref()`
438    pub fn prev_content(&self) -> Option<&RoomMemberEventContent> {
439        self.unsigned.prev_content.as_ref()
440    }
441
442    fn prev_details(&self) -> Option<MembershipDetails<'_>> {
443        self.prev_content().map(|c| c.details())
444    }
445
446    /// Helper function for membership change.
447    ///
448    /// Check [the specification][spec] for details.
449    ///
450    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
451    pub fn membership_change(&self) -> MembershipChange<'_> {
452        membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
453    }
454}
455
456impl RedactedSyncRoomMemberEvent {
457    /// Obtain the details about this event that are required to calculate a membership change.
458    ///
459    /// This is required when you want to calculate the change a redacted `m.room.member` event
460    /// made.
461    pub fn details(&self) -> MembershipDetails<'_> {
462        self.content.details()
463    }
464
465    /// Helper function for membership change.
466    ///
467    /// Since redacted events don't have `unsigned.prev_content`, you have to pass the `.details()`
468    /// of the previous `m.room.member` event manually (if there is a previous `m.room.member`
469    /// event).
470    ///
471    /// Check [the specification][spec] for details.
472    ///
473    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
474    pub fn membership_change<'a>(
475        &'a self,
476        prev_details: Option<MembershipDetails<'a>>,
477    ) -> MembershipChange<'a> {
478        membership_change(self.details(), prev_details, &self.sender, &self.state_key)
479    }
480}
481
482impl StrippedRoomMemberEvent {
483    /// Obtain the details about this event that are required to calculate a membership change.
484    ///
485    /// This is required when you want to calculate the change a redacted `m.room.member` event
486    /// made.
487    pub fn details(&self) -> MembershipDetails<'_> {
488        self.content.details()
489    }
490
491    /// Helper function for membership change.
492    ///
493    /// Since stripped events don't have `unsigned.prev_content`, you have to pass the `.details()`
494    /// of the previous `m.room.member` event manually (if there is a previous `m.room.member`
495    /// event).
496    ///
497    /// Check [the specification][spec] for details.
498    ///
499    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
500    pub fn membership_change<'a>(
501        &'a self,
502        prev_details: Option<MembershipDetails<'a>>,
503    ) -> MembershipChange<'a> {
504        membership_change(self.details(), prev_details, &self.sender, &self.state_key)
505    }
506}
507
508/// Extra information about a message event that is not incorporated into the event's hash.
509#[derive(Clone, Debug, Default, Deserialize)]
510#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
511pub struct RoomMemberUnsigned {
512    /// The time in milliseconds that has elapsed since the event was sent.
513    ///
514    /// This field is generated by the local homeserver, and may be incorrect if the local time on
515    /// at least one of the two servers is out of sync, which can cause the age to either be
516    /// negative or greater than it actually is.
517    pub age: Option<Int>,
518
519    /// The client-supplied transaction ID, if the client being given the event is the same one
520    /// which sent it.
521    pub transaction_id: Option<OwnedTransactionId>,
522
523    /// Optional previous content of the event.
524    pub prev_content: Option<PossiblyRedactedRoomMemberEventContent>,
525
526    /// Stripped state events to assist the receiver in identifying the room when receiving an
527    /// invite.
528    #[serde(default)]
529    pub invite_room_state: Vec<Raw<AnyStrippedStateEvent>>,
530
531    /// Stripped state events to assist the receiver in identifying the room after knocking.
532    #[serde(default)]
533    pub knock_room_state: Vec<Raw<AnyStrippedStateEvent>>,
534
535    /// [Bundled aggregations] of related child events.
536    ///
537    /// [Bundled aggregations]: https://spec.matrix.org/latest/client-server-api/#aggregations-of-child-events
538    #[serde(rename = "m.relations", default)]
539    pub relations: BundledStateRelations,
540}
541
542impl RoomMemberUnsigned {
543    /// Create a new `Unsigned` with fields set to `None`.
544    pub fn new() -> Self {
545        Self::default()
546    }
547}
548
549impl CanBeEmpty for RoomMemberUnsigned {
550    /// Whether this unsigned data is empty (all fields are `None`).
551    ///
552    /// This method is used to determine whether to skip serializing the `unsigned` field in room
553    /// events. Do not use it to determine whether an incoming `unsigned` field was present - it
554    /// could still have been present but contained none of the known fields.
555    fn is_empty(&self) -> bool {
556        self.age.is_none()
557            && self.transaction_id.is_none()
558            && self.prev_content.is_none()
559            && self.invite_room_state.is_empty()
560            && self.relations.is_empty()
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use assert_matches2::assert_matches;
567    use js_int::uint;
568    use maplit::btreemap;
569    use ruma_common::{
570        mxc_uri, serde::CanBeEmpty, server_name, server_signing_key_version, user_id,
571        MilliSecondsSinceUnixEpoch, ServerSigningKeyId, SigningKeyAlgorithm,
572    };
573    use serde_json::{from_value as from_json_value, json};
574
575    use super::{MembershipState, RoomMemberEventContent};
576    use crate::OriginalStateEvent;
577
578    #[test]
579    fn serde_with_no_prev_content() {
580        let json = json!({
581            "type": "m.room.member",
582            "content": {
583                "membership": "join"
584            },
585            "event_id": "$h29iv0s8:example.com",
586            "origin_server_ts": 1,
587            "room_id": "!n8f893n9:example.com",
588            "sender": "@carl:example.com",
589            "state_key": "@carl:example.com"
590        });
591
592        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
593        assert_eq!(ev.event_id, "$h29iv0s8:example.com");
594        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
595        assert_eq!(ev.room_id, "!n8f893n9:example.com");
596        assert_eq!(ev.sender, "@carl:example.com");
597        assert_eq!(ev.state_key, "@carl:example.com");
598        assert!(ev.unsigned.is_empty());
599
600        assert_eq!(ev.content.avatar_url, None);
601        assert_eq!(ev.content.displayname, None);
602        assert_eq!(ev.content.is_direct, None);
603        assert_eq!(ev.content.membership, MembershipState::Join);
604        assert_matches!(ev.content.third_party_invite, None);
605    }
606
607    #[test]
608    fn serde_with_prev_content() {
609        let json = json!({
610            "type": "m.room.member",
611            "content": {
612                "membership": "join"
613            },
614            "event_id": "$h29iv0s8:example.com",
615            "origin_server_ts": 1,
616            "room_id": "!n8f893n9:example.com",
617            "sender": "@carl:example.com",
618            "state_key": "@carl:example.com",
619            "unsigned": {
620                "prev_content": {
621                    "membership": "join"
622                },
623            },
624        });
625
626        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
627        assert_eq!(ev.event_id, "$h29iv0s8:example.com");
628        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
629        assert_eq!(ev.room_id, "!n8f893n9:example.com");
630        assert_eq!(ev.sender, "@carl:example.com");
631        assert_eq!(ev.state_key, "@carl:example.com");
632
633        assert_eq!(ev.content.avatar_url, None);
634        assert_eq!(ev.content.displayname, None);
635        assert_eq!(ev.content.is_direct, None);
636        assert_eq!(ev.content.membership, MembershipState::Join);
637        assert_matches!(ev.content.third_party_invite, None);
638
639        let prev_content = ev.unsigned.prev_content.unwrap();
640        assert_eq!(prev_content.avatar_url, None);
641        assert_eq!(prev_content.displayname, None);
642        assert_eq!(prev_content.is_direct, None);
643        assert_eq!(prev_content.membership, MembershipState::Join);
644        assert_matches!(prev_content.third_party_invite, None);
645    }
646
647    #[test]
648    fn serde_with_content_full() {
649        let json = json!({
650            "type": "m.room.member",
651            "content": {
652                "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
653                "displayname": "Alice Margatroid",
654                "is_direct": true,
655                "membership": "invite",
656                "third_party_invite": {
657                    "display_name": "alice",
658                    "signed": {
659                        "mxid": "@alice:example.org",
660                        "signatures": {
661                            "magic.forest": {
662                                "ed25519:3": "foobar"
663                            }
664                        },
665                        "token": "abc123"
666                    }
667                }
668            },
669            "event_id": "$143273582443PhrSn:example.org",
670            "origin_server_ts": 233,
671            "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
672            "sender": "@alice:example.org",
673            "state_key": "@alice:example.org"
674        });
675
676        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
677        assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
678        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
679        assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
680        assert_eq!(ev.sender, "@alice:example.org");
681        assert_eq!(ev.state_key, "@alice:example.org");
682        assert!(ev.unsigned.is_empty());
683
684        assert_eq!(
685            ev.content.avatar_url.as_deref(),
686            Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
687        );
688        assert_eq!(ev.content.displayname.as_deref(), Some("Alice Margatroid"));
689        assert_eq!(ev.content.is_direct, Some(true));
690        assert_eq!(ev.content.membership, MembershipState::Invite);
691
692        let third_party_invite = ev.content.third_party_invite.unwrap();
693        assert_eq!(third_party_invite.display_name, "alice");
694        let signed = third_party_invite.signed.deserialize().unwrap();
695        assert_eq!(signed.mxid, "@alice:example.org");
696        assert_eq!(signed.signatures.len(), 1);
697        let server_signatures = signed.signatures.get(server_name!("magic.forest")).unwrap();
698        assert_eq!(
699            *server_signatures,
700            btreemap! {
701                ServerSigningKeyId::from_parts(
702                    SigningKeyAlgorithm::Ed25519,
703                    server_signing_key_version!("3")
704                ) => "foobar".to_owned()
705            }
706        );
707        assert_eq!(signed.token, "abc123");
708    }
709
710    #[test]
711    fn serde_with_prev_content_full() {
712        let json = json!({
713            "type": "m.room.member",
714            "content": {
715                "membership": "join",
716            },
717            "event_id": "$143273582443PhrSn:example.org",
718            "origin_server_ts": 233,
719            "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
720            "sender": "@alice:example.org",
721            "state_key": "@alice:example.org",
722            "unsigned": {
723                "prev_content": {
724                    "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
725                    "displayname": "Alice Margatroid",
726                    "is_direct": true,
727                    "membership": "invite",
728                    "third_party_invite": {
729                        "display_name": "alice",
730                        "signed": {
731                            "mxid": "@alice:example.org",
732                            "signatures": {
733                                "magic.forest": {
734                                    "ed25519:3": "foobar",
735                                },
736                            },
737                            "token": "abc123"
738                        },
739                    },
740                },
741            },
742        });
743
744        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
745        assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
746        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
747        assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
748        assert_eq!(ev.sender, "@alice:example.org");
749        assert_eq!(ev.state_key, "@alice:example.org");
750
751        assert_eq!(ev.content.avatar_url, None);
752        assert_eq!(ev.content.displayname, None);
753        assert_eq!(ev.content.is_direct, None);
754        assert_eq!(ev.content.membership, MembershipState::Join);
755        assert_matches!(ev.content.third_party_invite, None);
756
757        let prev_content = ev.unsigned.prev_content.unwrap();
758        assert_eq!(
759            prev_content.avatar_url.as_deref(),
760            Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
761        );
762        assert_eq!(prev_content.displayname.as_deref(), Some("Alice Margatroid"));
763        assert_eq!(prev_content.is_direct, Some(true));
764        assert_eq!(prev_content.membership, MembershipState::Invite);
765
766        let third_party_invite = prev_content.third_party_invite.unwrap();
767        assert_eq!(third_party_invite.display_name, "alice");
768        let signed = third_party_invite.signed.deserialize().unwrap();
769        assert_eq!(signed.mxid, "@alice:example.org");
770        assert_eq!(signed.signatures.len(), 1);
771        let server_signatures = signed.signatures.get(server_name!("magic.forest")).unwrap();
772        assert_eq!(
773            *server_signatures,
774            btreemap! {
775                ServerSigningKeyId::from_parts(
776                    SigningKeyAlgorithm::Ed25519,
777                    server_signing_key_version!("3")
778                ) => "foobar".to_owned()
779            }
780        );
781        assert_eq!(signed.token, "abc123");
782    }
783
784    #[test]
785    fn serde_with_join_authorized() {
786        let json = json!({
787            "type": "m.room.member",
788            "content": {
789                "membership": "join",
790                "join_authorised_via_users_server": "@notcarl:example.com"
791            },
792            "event_id": "$h29iv0s8:example.com",
793            "origin_server_ts": 1,
794            "room_id": "!n8f893n9:example.com",
795            "sender": "@carl:example.com",
796            "state_key": "@carl:example.com"
797        });
798
799        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
800        assert_eq!(ev.event_id, "$h29iv0s8:example.com");
801        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
802        assert_eq!(ev.room_id, "!n8f893n9:example.com");
803        assert_eq!(ev.sender, "@carl:example.com");
804        assert_eq!(ev.state_key, "@carl:example.com");
805        assert!(ev.unsigned.is_empty());
806
807        assert_eq!(ev.content.avatar_url, None);
808        assert_eq!(ev.content.displayname, None);
809        assert_eq!(ev.content.is_direct, None);
810        assert_eq!(ev.content.membership, MembershipState::Join);
811        assert_matches!(ev.content.third_party_invite, None);
812        assert_eq!(
813            ev.content.join_authorized_via_users_server.as_deref(),
814            Some(user_id!("@notcarl:example.com"))
815        );
816    }
817}