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    serde::{CanBeEmpty, Raw, StringEnum},
8    OwnedMxcUri, OwnedTransactionId, OwnedUserId, RoomVersionId, ServerSignatures, UserId,
9};
10use ruma_macros::EventContent;
11use serde::{Deserialize, Serialize};
12
13use crate::{
14    AnyStrippedStateEvent, BundledStateRelations, EventContent, PossiblyRedactedStateEventContent,
15    PrivOwnedStr, RedactContent, RedactedStateEventContent, StateEventType,
16};
17
18mod change;
19
20use self::change::membership_change;
21pub use self::change::{Change, MembershipChange, MembershipDetails};
22
23/// The content of an `m.room.member` event.
24///
25/// The current membership state of a user in the room.
26///
27/// Adjusts the membership state for a user in a room. It is preferable to use the membership
28/// APIs (`/rooms/<room id>/invite` etc) when performing membership actions rather than
29/// adjusting the state directly as there are a restricted set of valid transformations. For
30/// example, user A cannot force user B to join a room, and trying to force this state change
31/// directly will fail.
32///
33/// This event may also include an `invite_room_state` key inside the event's unsigned data, but
34/// Ruma doesn't currently expose this; see [#998](https://github.com/ruma/ruma/issues/998).
35///
36/// The user for which a membership applies is represented by the `state_key`. Under some
37/// conditions, the `sender` and `state_key` may not match - this may be interpreted as the
38/// `sender` affecting the membership state of the `state_key` user.
39///
40/// The membership for a given user can change over time. Previous membership can be retrieved
41/// from the `prev_content` object on an event. If not present, the user's previous membership
42/// must be assumed as leave.
43#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
44#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
45#[ruma_event(
46    type = "m.room.member",
47    kind = State,
48    state_key_type = OwnedUserId,
49    unsigned_type = RoomMemberUnsigned,
50    custom_redacted,
51    custom_possibly_redacted,
52)]
53pub struct RoomMemberEventContent {
54    /// The avatar URL for this user, if any.
55    ///
56    /// This is added by the homeserver. If you activate the `compat-empty-string-null` feature,
57    /// this field being an empty string in JSON will result in `None` here during deserialization.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    #[cfg_attr(
60        feature = "compat-empty-string-null",
61        serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
62    )]
63    pub avatar_url: Option<OwnedMxcUri>,
64
65    /// The display name for this user, if any.
66    ///
67    /// This is added by the homeserver.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub displayname: Option<String>,
70
71    /// Flag indicating whether the room containing this event was created with the intention of
72    /// being a direct chat.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub is_direct: Option<bool>,
75
76    /// The membership state of this user.
77    pub membership: MembershipState,
78
79    /// If this member event is the successor to a third party invitation, this field will
80    /// contain information about that invitation.
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub third_party_invite: Option<ThirdPartyInvite>,
83
84    /// The [BlurHash](https://blurha.sh) for the avatar pointed to by `avatar_url`.
85    ///
86    /// This uses the unstable prefix in
87    /// [MSC2448](https://github.com/matrix-org/matrix-spec-proposals/pull/2448).
88    #[cfg(feature = "unstable-msc2448")]
89    #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
90    pub blurhash: Option<String>,
91
92    /// User-supplied text for why their membership has changed.
93    ///
94    /// For kicks and bans, this is typically the reason for the kick or ban. For other membership
95    /// changes, this is a way for the user to communicate their intent without having to send a
96    /// message to the room, such as in a case where Bob rejects an invite from Alice about an
97    /// upcoming concert, but can't make it that day.
98    ///
99    /// Clients are not recommended to show this reason to users when receiving an invite due to
100    /// the potential for spam and abuse. Hiding the reason behind a button or other component
101    /// is recommended.
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub reason: Option<String>,
104
105    /// Arbitrarily chosen `UserId` (MxID) of a local user who can send an invite.
106    #[serde(rename = "join_authorised_via_users_server")]
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub join_authorized_via_users_server: Option<OwnedUserId>,
109}
110
111impl RoomMemberEventContent {
112    /// Creates a new `RoomMemberEventContent` with the given membership state.
113    pub fn new(membership: MembershipState) -> Self {
114        Self {
115            membership,
116            avatar_url: None,
117            displayname: None,
118            is_direct: None,
119            third_party_invite: None,
120            #[cfg(feature = "unstable-msc2448")]
121            blurhash: None,
122            reason: None,
123            join_authorized_via_users_server: None,
124        }
125    }
126
127    /// Obtain the details about this event that are required to calculate a membership change.
128    ///
129    /// This is required when you want to calculate the change a redacted `m.room.member` event
130    /// made.
131    pub fn details(&self) -> MembershipDetails<'_> {
132        MembershipDetails {
133            avatar_url: self.avatar_url.as_deref(),
134            displayname: self.displayname.as_deref(),
135            membership: &self.membership,
136        }
137    }
138
139    /// Helper function for membership change.
140    ///
141    /// This requires data from the full event:
142    ///
143    /// * The previous details computed from `event.unsigned.prev_content`,
144    /// * The sender of the event,
145    /// * The state key of the event.
146    ///
147    /// Check [the specification][spec] for details.
148    ///
149    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
150    pub fn membership_change<'a>(
151        &'a self,
152        prev_details: Option<MembershipDetails<'a>>,
153        sender: &UserId,
154        state_key: &UserId,
155    ) -> MembershipChange<'a> {
156        membership_change(self.details(), prev_details, sender, state_key)
157    }
158}
159
160impl RedactContent for RoomMemberEventContent {
161    type Redacted = RedactedRoomMemberEventContent;
162
163    fn redact(self, version: &RoomVersionId) -> RedactedRoomMemberEventContent {
164        RedactedRoomMemberEventContent {
165            membership: self.membership,
166            third_party_invite: self.third_party_invite.and_then(|i| i.redact(version)),
167            join_authorized_via_users_server: match version {
168                RoomVersionId::V1
169                | RoomVersionId::V2
170                | RoomVersionId::V3
171                | RoomVersionId::V4
172                | RoomVersionId::V5
173                | RoomVersionId::V6
174                | RoomVersionId::V7
175                | RoomVersionId::V8 => None,
176                _ => self.join_authorized_via_users_server,
177            },
178        }
179    }
180}
181
182/// The possibly redacted form of [`RoomMemberEventContent`].
183///
184/// This type is used when it's not obvious whether the content is redacted or not.
185pub type PossiblyRedactedRoomMemberEventContent = RoomMemberEventContent;
186
187impl PossiblyRedactedStateEventContent for RoomMemberEventContent {
188    type StateKey = OwnedUserId;
189}
190
191/// A member event that has been redacted.
192#[derive(Clone, Debug, Deserialize, Serialize)]
193#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
194pub struct RedactedRoomMemberEventContent {
195    /// The membership state of this user.
196    pub membership: MembershipState,
197
198    /// If this member event is the successor to a third party invitation, this field will
199    /// contain information about that invitation.
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub third_party_invite: Option<RedactedThirdPartyInvite>,
202
203    /// An arbitrary user who has the power to issue invites.
204    ///
205    /// This is redacted in room versions 8 and below. It is used for validating
206    /// joins when the join rule is restricted.
207    #[serde(rename = "join_authorised_via_users_server", skip_serializing_if = "Option::is_none")]
208    pub join_authorized_via_users_server: Option<OwnedUserId>,
209}
210
211impl RedactedRoomMemberEventContent {
212    /// Create a `RedactedRoomMemberEventContent` with the given membership.
213    pub fn new(membership: MembershipState) -> Self {
214        Self { membership, third_party_invite: None, join_authorized_via_users_server: None }
215    }
216
217    /// Obtain the details about this event that are required to calculate a membership change.
218    ///
219    /// This is required when you want to calculate the change a redacted `m.room.member` event
220    /// made.
221    pub fn details(&self) -> MembershipDetails<'_> {
222        MembershipDetails { avatar_url: None, displayname: None, membership: &self.membership }
223    }
224
225    /// Helper function for membership change.
226    ///
227    /// Since redacted events don't have `unsigned.prev_content`, you have to pass the `.details()`
228    /// of the previous `m.room.member` event manually (if there is a previous `m.room.member`
229    /// event).
230    ///
231    /// This also requires data from the full event:
232    ///
233    /// * The sender of the event,
234    /// * The state key of the event.
235    ///
236    /// Check [the specification][spec] for details.
237    ///
238    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
239    pub fn membership_change<'a>(
240        &'a self,
241        prev_details: Option<MembershipDetails<'a>>,
242        sender: &UserId,
243        state_key: &UserId,
244    ) -> MembershipChange<'a> {
245        membership_change(self.details(), prev_details, sender, state_key)
246    }
247}
248
249impl EventContent for RedactedRoomMemberEventContent {
250    type EventType = StateEventType;
251
252    fn event_type(&self) -> StateEventType {
253        StateEventType::RoomMember
254    }
255}
256
257impl RedactedStateEventContent for RedactedRoomMemberEventContent {
258    type StateKey = OwnedUserId;
259}
260
261impl RoomMemberEvent {
262    /// Obtain the membership state, regardless of whether this event is redacted.
263    pub fn membership(&self) -> &MembershipState {
264        match self {
265            Self::Original(ev) => &ev.content.membership,
266            Self::Redacted(ev) => &ev.content.membership,
267        }
268    }
269}
270
271impl SyncRoomMemberEvent {
272    /// Obtain the membership state, regardless of whether this event is redacted.
273    pub fn membership(&self) -> &MembershipState {
274        match self {
275            Self::Original(ev) => &ev.content.membership,
276            Self::Redacted(ev) => &ev.content.membership,
277        }
278    }
279}
280
281/// The membership state of a user.
282#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
283#[derive(Clone, PartialEq, Eq, StringEnum)]
284#[ruma_enum(rename_all = "lowercase")]
285#[non_exhaustive]
286pub enum MembershipState {
287    /// The user is banned.
288    Ban,
289
290    /// The user has been invited.
291    Invite,
292
293    /// The user has joined.
294    Join,
295
296    /// The user has requested to join.
297    Knock,
298
299    /// The user has left.
300    Leave,
301
302    #[doc(hidden)]
303    _Custom(PrivOwnedStr),
304}
305
306/// Information about a third party invitation.
307#[derive(Clone, Debug, Deserialize, Serialize)]
308#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
309pub struct ThirdPartyInvite {
310    /// A name which can be displayed to represent the user instead of their third party
311    /// identifier.
312    pub display_name: String,
313
314    /// A block of content which has been signed, which servers can use to verify the event.
315    ///
316    /// Clients should ignore this.
317    pub signed: SignedContent,
318}
319
320impl ThirdPartyInvite {
321    /// Creates a new `ThirdPartyInvite` with the given display name and signed content.
322    pub fn new(display_name: String, signed: SignedContent) -> Self {
323        Self { display_name, signed }
324    }
325
326    /// Transform `self` into a redacted form (removing most or all fields) according to the spec.
327    ///
328    /// Returns `None` if the field for this object was redacted in the given room version,
329    /// otherwise returns the redacted form.
330    fn redact(self, version: &RoomVersionId) -> Option<RedactedThirdPartyInvite> {
331        match version {
332            RoomVersionId::V1
333            | RoomVersionId::V2
334            | RoomVersionId::V3
335            | RoomVersionId::V4
336            | RoomVersionId::V5
337            | RoomVersionId::V6
338            | RoomVersionId::V7
339            | RoomVersionId::V8
340            | RoomVersionId::V9
341            | RoomVersionId::V10 => None,
342            _ => Some(RedactedThirdPartyInvite { signed: self.signed }),
343        }
344    }
345}
346
347/// Redacted information about a third party invitation.
348#[derive(Clone, Debug, Deserialize, Serialize)]
349#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
350pub struct RedactedThirdPartyInvite {
351    /// A block of content which has been signed, which servers can use to verify the event.
352    ///
353    /// Clients should ignore this.
354    pub signed: SignedContent,
355}
356
357/// A block of content which has been signed, which servers can use to verify a third party
358/// invitation.
359#[derive(Clone, Debug, Deserialize, Serialize)]
360#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
361pub struct SignedContent {
362    /// The invited Matrix user ID.
363    ///
364    /// Must be equal to the user_id property of the event.
365    pub mxid: OwnedUserId,
366
367    /// A single signature from the verifying server, in the format specified by the Signing Events
368    /// section of the server-server API.
369    pub signatures: ServerSignatures,
370
371    /// The token property of the containing `third_party_invite` object.
372    pub token: String,
373}
374
375impl SignedContent {
376    /// Creates a new `SignedContent` with the given mxid, signature and token.
377    pub fn new(signatures: ServerSignatures, mxid: OwnedUserId, token: String) -> Self {
378        Self { mxid, signatures, token }
379    }
380}
381
382impl OriginalRoomMemberEvent {
383    /// Obtain the details about this event that are required to calculate a membership change.
384    ///
385    /// This is required when you want to calculate the change a redacted `m.room.member` event
386    /// made.
387    pub fn details(&self) -> MembershipDetails<'_> {
388        self.content.details()
389    }
390
391    /// Get a reference to the `prev_content` in unsigned, if it exists.
392    ///
393    /// Shorthand for `event.unsigned.prev_content.as_ref()`
394    pub fn prev_content(&self) -> Option<&RoomMemberEventContent> {
395        self.unsigned.prev_content.as_ref()
396    }
397
398    fn prev_details(&self) -> Option<MembershipDetails<'_>> {
399        self.prev_content().map(|c| c.details())
400    }
401
402    /// Helper function for membership change.
403    ///
404    /// Check [the specification][spec] for details.
405    ///
406    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
407    pub fn membership_change(&self) -> MembershipChange<'_> {
408        membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
409    }
410}
411
412impl RedactedRoomMemberEvent {
413    /// Obtain the details about this event that are required to calculate a membership change.
414    ///
415    /// This is required when you want to calculate the change a redacted `m.room.member` event
416    /// made.
417    pub fn details(&self) -> MembershipDetails<'_> {
418        self.content.details()
419    }
420
421    /// Helper function for membership change.
422    ///
423    /// Since redacted events don't have `unsigned.prev_content`, you have to pass the `.details()`
424    /// of the previous `m.room.member` event manually (if there is a previous `m.room.member`
425    /// event).
426    ///
427    /// Check [the specification][spec] for details.
428    ///
429    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
430    pub fn membership_change<'a>(
431        &'a self,
432        prev_details: Option<MembershipDetails<'a>>,
433    ) -> MembershipChange<'a> {
434        membership_change(self.details(), prev_details, &self.sender, &self.state_key)
435    }
436}
437
438impl OriginalSyncRoomMemberEvent {
439    /// Obtain the details about this event that are required to calculate a membership change.
440    ///
441    /// This is required when you want to calculate the change a redacted `m.room.member` event
442    /// made.
443    pub fn details(&self) -> MembershipDetails<'_> {
444        self.content.details()
445    }
446
447    /// Get a reference to the `prev_content` in unsigned, if it exists.
448    ///
449    /// Shorthand for `event.unsigned.prev_content.as_ref()`
450    pub fn prev_content(&self) -> Option<&RoomMemberEventContent> {
451        self.unsigned.prev_content.as_ref()
452    }
453
454    fn prev_details(&self) -> Option<MembershipDetails<'_>> {
455        self.prev_content().map(|c| c.details())
456    }
457
458    /// Helper function for membership change.
459    ///
460    /// Check [the specification][spec] for details.
461    ///
462    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
463    pub fn membership_change(&self) -> MembershipChange<'_> {
464        membership_change(self.details(), self.prev_details(), &self.sender, &self.state_key)
465    }
466}
467
468impl RedactedSyncRoomMemberEvent {
469    /// Obtain the details about this event that are required to calculate a membership change.
470    ///
471    /// This is required when you want to calculate the change a redacted `m.room.member` event
472    /// made.
473    pub fn details(&self) -> MembershipDetails<'_> {
474        self.content.details()
475    }
476
477    /// Helper function for membership change.
478    ///
479    /// Since redacted events don't have `unsigned.prev_content`, you have to pass the `.details()`
480    /// of the previous `m.room.member` event manually (if there is a previous `m.room.member`
481    /// event).
482    ///
483    /// Check [the specification][spec] for details.
484    ///
485    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
486    pub fn membership_change<'a>(
487        &'a self,
488        prev_details: Option<MembershipDetails<'a>>,
489    ) -> MembershipChange<'a> {
490        membership_change(self.details(), prev_details, &self.sender, &self.state_key)
491    }
492}
493
494impl StrippedRoomMemberEvent {
495    /// Obtain the details about this event that are required to calculate a membership change.
496    ///
497    /// This is required when you want to calculate the change a redacted `m.room.member` event
498    /// made.
499    pub fn details(&self) -> MembershipDetails<'_> {
500        self.content.details()
501    }
502
503    /// Helper function for membership change.
504    ///
505    /// Since stripped events don't have `unsigned.prev_content`, you have to pass the `.details()`
506    /// of the previous `m.room.member` event manually (if there is a previous `m.room.member`
507    /// event).
508    ///
509    /// Check [the specification][spec] for details.
510    ///
511    /// [spec]: https://spec.matrix.org/latest/client-server-api/#mroommember
512    pub fn membership_change<'a>(
513        &'a self,
514        prev_details: Option<MembershipDetails<'a>>,
515    ) -> MembershipChange<'a> {
516        membership_change(self.details(), prev_details, &self.sender, &self.state_key)
517    }
518}
519
520/// Extra information about a message event that is not incorporated into the event's hash.
521#[derive(Clone, Debug, Default, Deserialize)]
522#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
523pub struct RoomMemberUnsigned {
524    /// The time in milliseconds that has elapsed since the event was sent.
525    ///
526    /// This field is generated by the local homeserver, and may be incorrect if the local time on
527    /// at least one of the two servers is out of sync, which can cause the age to either be
528    /// negative or greater than it actually is.
529    pub age: Option<Int>,
530
531    /// The client-supplied transaction ID, if the client being given the event is the same one
532    /// which sent it.
533    pub transaction_id: Option<OwnedTransactionId>,
534
535    /// Optional previous content of the event.
536    pub prev_content: Option<PossiblyRedactedRoomMemberEventContent>,
537
538    /// State events to assist the receiver in identifying the room.
539    #[serde(default)]
540    pub invite_room_state: Vec<Raw<AnyStrippedStateEvent>>,
541
542    /// [Bundled aggregations] of related child events.
543    ///
544    /// [Bundled aggregations]: https://spec.matrix.org/latest/client-server-api/#aggregations-of-child-events
545    #[serde(rename = "m.relations", default)]
546    pub relations: BundledStateRelations,
547}
548
549impl RoomMemberUnsigned {
550    /// Create a new `Unsigned` with fields set to `None`.
551    pub fn new() -> Self {
552        Self::default()
553    }
554}
555
556impl CanBeEmpty for RoomMemberUnsigned {
557    /// Whether this unsigned data is empty (all fields are `None`).
558    ///
559    /// This method is used to determine whether to skip serializing the `unsigned` field in room
560    /// events. Do not use it to determine whether an incoming `unsigned` field was present - it
561    /// could still have been present but contained none of the known fields.
562    fn is_empty(&self) -> bool {
563        self.age.is_none()
564            && self.transaction_id.is_none()
565            && self.prev_content.is_none()
566            && self.invite_room_state.is_empty()
567            && self.relations.is_empty()
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use assert_matches2::assert_matches;
574    use js_int::uint;
575    use maplit::btreemap;
576    use ruma_common::{
577        mxc_uri, serde::CanBeEmpty, server_name, server_signing_key_version, user_id,
578        MilliSecondsSinceUnixEpoch, ServerSigningKeyId, SigningKeyAlgorithm,
579    };
580    use serde_json::{from_value as from_json_value, json};
581
582    use super::{MembershipState, RoomMemberEventContent};
583    use crate::OriginalStateEvent;
584
585    #[test]
586    fn serde_with_no_prev_content() {
587        let json = json!({
588            "type": "m.room.member",
589            "content": {
590                "membership": "join"
591            },
592            "event_id": "$h29iv0s8:example.com",
593            "origin_server_ts": 1,
594            "room_id": "!n8f893n9:example.com",
595            "sender": "@carl:example.com",
596            "state_key": "@carl:example.com"
597        });
598
599        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
600        assert_eq!(ev.event_id, "$h29iv0s8:example.com");
601        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
602        assert_eq!(ev.room_id, "!n8f893n9:example.com");
603        assert_eq!(ev.sender, "@carl:example.com");
604        assert_eq!(ev.state_key, "@carl:example.com");
605        assert!(ev.unsigned.is_empty());
606
607        assert_eq!(ev.content.avatar_url, None);
608        assert_eq!(ev.content.displayname, None);
609        assert_eq!(ev.content.is_direct, None);
610        assert_eq!(ev.content.membership, MembershipState::Join);
611        assert_matches!(ev.content.third_party_invite, None);
612    }
613
614    #[test]
615    fn serde_with_prev_content() {
616        let json = json!({
617            "type": "m.room.member",
618            "content": {
619                "membership": "join"
620            },
621            "event_id": "$h29iv0s8:example.com",
622            "origin_server_ts": 1,
623            "room_id": "!n8f893n9:example.com",
624            "sender": "@carl:example.com",
625            "state_key": "@carl:example.com",
626            "unsigned": {
627                "prev_content": {
628                    "membership": "join"
629                },
630            },
631        });
632
633        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
634        assert_eq!(ev.event_id, "$h29iv0s8:example.com");
635        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
636        assert_eq!(ev.room_id, "!n8f893n9:example.com");
637        assert_eq!(ev.sender, "@carl:example.com");
638        assert_eq!(ev.state_key, "@carl:example.com");
639
640        assert_eq!(ev.content.avatar_url, None);
641        assert_eq!(ev.content.displayname, None);
642        assert_eq!(ev.content.is_direct, None);
643        assert_eq!(ev.content.membership, MembershipState::Join);
644        assert_matches!(ev.content.third_party_invite, None);
645
646        let prev_content = ev.unsigned.prev_content.unwrap();
647        assert_eq!(prev_content.avatar_url, None);
648        assert_eq!(prev_content.displayname, None);
649        assert_eq!(prev_content.is_direct, None);
650        assert_eq!(prev_content.membership, MembershipState::Join);
651        assert_matches!(prev_content.third_party_invite, None);
652    }
653
654    #[test]
655    fn serde_with_content_full() {
656        let json = json!({
657            "type": "m.room.member",
658            "content": {
659                "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
660                "displayname": "Alice Margatroid",
661                "is_direct": true,
662                "membership": "invite",
663                "third_party_invite": {
664                    "display_name": "alice",
665                    "signed": {
666                        "mxid": "@alice:example.org",
667                        "signatures": {
668                            "magic.forest": {
669                                "ed25519:3": "foobar"
670                            }
671                        },
672                        "token": "abc123"
673                    }
674                }
675            },
676            "event_id": "$143273582443PhrSn:example.org",
677            "origin_server_ts": 233,
678            "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
679            "sender": "@alice:example.org",
680            "state_key": "@alice:example.org"
681        });
682
683        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
684        assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
685        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
686        assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
687        assert_eq!(ev.sender, "@alice:example.org");
688        assert_eq!(ev.state_key, "@alice:example.org");
689        assert!(ev.unsigned.is_empty());
690
691        assert_eq!(
692            ev.content.avatar_url.as_deref(),
693            Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
694        );
695        assert_eq!(ev.content.displayname.as_deref(), Some("Alice Margatroid"));
696        assert_eq!(ev.content.is_direct, Some(true));
697        assert_eq!(ev.content.membership, MembershipState::Invite);
698
699        let third_party_invite = ev.content.third_party_invite.unwrap();
700        assert_eq!(third_party_invite.display_name, "alice");
701        assert_eq!(third_party_invite.signed.mxid, "@alice:example.org");
702        assert_eq!(third_party_invite.signed.signatures.len(), 1);
703        let server_signatures =
704            third_party_invite.signed.signatures.get(server_name!("magic.forest")).unwrap();
705        assert_eq!(
706            *server_signatures,
707            btreemap! {
708                ServerSigningKeyId::from_parts(
709                    SigningKeyAlgorithm::Ed25519,
710                    server_signing_key_version!("3")
711                ) => "foobar".to_owned()
712            }
713        );
714        assert_eq!(third_party_invite.signed.token, "abc123");
715    }
716
717    #[test]
718    fn serde_with_prev_content_full() {
719        let json = json!({
720            "type": "m.room.member",
721            "content": {
722                "membership": "join",
723            },
724            "event_id": "$143273582443PhrSn:example.org",
725            "origin_server_ts": 233,
726            "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
727            "sender": "@alice:example.org",
728            "state_key": "@alice:example.org",
729            "unsigned": {
730                "prev_content": {
731                    "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
732                    "displayname": "Alice Margatroid",
733                    "is_direct": true,
734                    "membership": "invite",
735                    "third_party_invite": {
736                        "display_name": "alice",
737                        "signed": {
738                            "mxid": "@alice:example.org",
739                            "signatures": {
740                                "magic.forest": {
741                                    "ed25519:3": "foobar",
742                                },
743                            },
744                            "token": "abc123"
745                        },
746                    },
747                },
748            },
749        });
750
751        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
752        assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
753        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
754        assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
755        assert_eq!(ev.sender, "@alice:example.org");
756        assert_eq!(ev.state_key, "@alice:example.org");
757
758        assert_eq!(ev.content.avatar_url, None);
759        assert_eq!(ev.content.displayname, None);
760        assert_eq!(ev.content.is_direct, None);
761        assert_eq!(ev.content.membership, MembershipState::Join);
762        assert_matches!(ev.content.third_party_invite, None);
763
764        let prev_content = ev.unsigned.prev_content.unwrap();
765        assert_eq!(
766            prev_content.avatar_url.as_deref(),
767            Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
768        );
769        assert_eq!(prev_content.displayname.as_deref(), Some("Alice Margatroid"));
770        assert_eq!(prev_content.is_direct, Some(true));
771        assert_eq!(prev_content.membership, MembershipState::Invite);
772
773        let third_party_invite = prev_content.third_party_invite.unwrap();
774        assert_eq!(third_party_invite.display_name, "alice");
775        assert_eq!(third_party_invite.signed.mxid, "@alice:example.org");
776        assert_eq!(third_party_invite.signed.signatures.len(), 1);
777        let server_signatures =
778            third_party_invite.signed.signatures.get(server_name!("magic.forest")).unwrap();
779        assert_eq!(
780            *server_signatures,
781            btreemap! {
782                ServerSigningKeyId::from_parts(
783                    SigningKeyAlgorithm::Ed25519,
784                    server_signing_key_version!("3")
785                ) => "foobar".to_owned()
786            }
787        );
788        assert_eq!(third_party_invite.signed.token, "abc123");
789    }
790
791    #[test]
792    fn serde_with_join_authorized() {
793        let json = json!({
794            "type": "m.room.member",
795            "content": {
796                "membership": "join",
797                "join_authorised_via_users_server": "@notcarl:example.com"
798            },
799            "event_id": "$h29iv0s8:example.com",
800            "origin_server_ts": 1,
801            "room_id": "!n8f893n9:example.com",
802            "sender": "@carl:example.com",
803            "state_key": "@carl:example.com"
804        });
805
806        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
807        assert_eq!(ev.event_id, "$h29iv0s8:example.com");
808        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
809        assert_eq!(ev.room_id, "!n8f893n9:example.com");
810        assert_eq!(ev.sender, "@carl:example.com");
811        assert_eq!(ev.state_key, "@carl:example.com");
812        assert!(ev.unsigned.is_empty());
813
814        assert_eq!(ev.content.avatar_url, None);
815        assert_eq!(ev.content.displayname, None);
816        assert_eq!(ev.content.is_direct, None);
817        assert_eq!(ev.content.membership, MembershipState::Join);
818        assert_matches!(ev.content.third_party_invite, None);
819        assert_eq!(
820            ev.content.join_authorized_via_users_server.as_deref(),
821            Some(user_id!("@notcarl:example.com"))
822        );
823    }
824}