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