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, PartialEq, Eq, 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    /// State events to assist the receiver in identifying the room.
527    #[serde(default)]
528    pub invite_room_state: Vec<Raw<AnyStrippedStateEvent>>,
529
530    /// [Bundled aggregations] of related child events.
531    ///
532    /// [Bundled aggregations]: https://spec.matrix.org/latest/client-server-api/#aggregations-of-child-events
533    #[serde(rename = "m.relations", default)]
534    pub relations: BundledStateRelations,
535}
536
537impl RoomMemberUnsigned {
538    /// Create a new `Unsigned` with fields set to `None`.
539    pub fn new() -> Self {
540        Self::default()
541    }
542}
543
544impl CanBeEmpty for RoomMemberUnsigned {
545    /// Whether this unsigned data is empty (all fields are `None`).
546    ///
547    /// This method is used to determine whether to skip serializing the `unsigned` field in room
548    /// events. Do not use it to determine whether an incoming `unsigned` field was present - it
549    /// could still have been present but contained none of the known fields.
550    fn is_empty(&self) -> bool {
551        self.age.is_none()
552            && self.transaction_id.is_none()
553            && self.prev_content.is_none()
554            && self.invite_room_state.is_empty()
555            && self.relations.is_empty()
556    }
557}
558
559#[cfg(test)]
560mod tests {
561    use assert_matches2::assert_matches;
562    use js_int::uint;
563    use maplit::btreemap;
564    use ruma_common::{
565        mxc_uri, serde::CanBeEmpty, server_name, server_signing_key_version, user_id,
566        MilliSecondsSinceUnixEpoch, ServerSigningKeyId, SigningKeyAlgorithm,
567    };
568    use serde_json::{from_value as from_json_value, json};
569
570    use super::{MembershipState, RoomMemberEventContent};
571    use crate::OriginalStateEvent;
572
573    #[test]
574    fn serde_with_no_prev_content() {
575        let json = json!({
576            "type": "m.room.member",
577            "content": {
578                "membership": "join"
579            },
580            "event_id": "$h29iv0s8:example.com",
581            "origin_server_ts": 1,
582            "room_id": "!n8f893n9:example.com",
583            "sender": "@carl:example.com",
584            "state_key": "@carl:example.com"
585        });
586
587        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
588        assert_eq!(ev.event_id, "$h29iv0s8:example.com");
589        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
590        assert_eq!(ev.room_id, "!n8f893n9:example.com");
591        assert_eq!(ev.sender, "@carl:example.com");
592        assert_eq!(ev.state_key, "@carl:example.com");
593        assert!(ev.unsigned.is_empty());
594
595        assert_eq!(ev.content.avatar_url, None);
596        assert_eq!(ev.content.displayname, None);
597        assert_eq!(ev.content.is_direct, None);
598        assert_eq!(ev.content.membership, MembershipState::Join);
599        assert_matches!(ev.content.third_party_invite, None);
600    }
601
602    #[test]
603    fn serde_with_prev_content() {
604        let json = json!({
605            "type": "m.room.member",
606            "content": {
607                "membership": "join"
608            },
609            "event_id": "$h29iv0s8:example.com",
610            "origin_server_ts": 1,
611            "room_id": "!n8f893n9:example.com",
612            "sender": "@carl:example.com",
613            "state_key": "@carl:example.com",
614            "unsigned": {
615                "prev_content": {
616                    "membership": "join"
617                },
618            },
619        });
620
621        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
622        assert_eq!(ev.event_id, "$h29iv0s8:example.com");
623        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
624        assert_eq!(ev.room_id, "!n8f893n9:example.com");
625        assert_eq!(ev.sender, "@carl:example.com");
626        assert_eq!(ev.state_key, "@carl:example.com");
627
628        assert_eq!(ev.content.avatar_url, None);
629        assert_eq!(ev.content.displayname, None);
630        assert_eq!(ev.content.is_direct, None);
631        assert_eq!(ev.content.membership, MembershipState::Join);
632        assert_matches!(ev.content.third_party_invite, None);
633
634        let prev_content = ev.unsigned.prev_content.unwrap();
635        assert_eq!(prev_content.avatar_url, None);
636        assert_eq!(prev_content.displayname, None);
637        assert_eq!(prev_content.is_direct, None);
638        assert_eq!(prev_content.membership, MembershipState::Join);
639        assert_matches!(prev_content.third_party_invite, None);
640    }
641
642    #[test]
643    fn serde_with_content_full() {
644        let json = json!({
645            "type": "m.room.member",
646            "content": {
647                "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
648                "displayname": "Alice Margatroid",
649                "is_direct": true,
650                "membership": "invite",
651                "third_party_invite": {
652                    "display_name": "alice",
653                    "signed": {
654                        "mxid": "@alice:example.org",
655                        "signatures": {
656                            "magic.forest": {
657                                "ed25519:3": "foobar"
658                            }
659                        },
660                        "token": "abc123"
661                    }
662                }
663            },
664            "event_id": "$143273582443PhrSn:example.org",
665            "origin_server_ts": 233,
666            "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
667            "sender": "@alice:example.org",
668            "state_key": "@alice:example.org"
669        });
670
671        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
672        assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
673        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
674        assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
675        assert_eq!(ev.sender, "@alice:example.org");
676        assert_eq!(ev.state_key, "@alice:example.org");
677        assert!(ev.unsigned.is_empty());
678
679        assert_eq!(
680            ev.content.avatar_url.as_deref(),
681            Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
682        );
683        assert_eq!(ev.content.displayname.as_deref(), Some("Alice Margatroid"));
684        assert_eq!(ev.content.is_direct, Some(true));
685        assert_eq!(ev.content.membership, MembershipState::Invite);
686
687        let third_party_invite = ev.content.third_party_invite.unwrap();
688        assert_eq!(third_party_invite.display_name, "alice");
689        let signed = third_party_invite.signed.deserialize().unwrap();
690        assert_eq!(signed.mxid, "@alice:example.org");
691        assert_eq!(signed.signatures.len(), 1);
692        let server_signatures = signed.signatures.get(server_name!("magic.forest")).unwrap();
693        assert_eq!(
694            *server_signatures,
695            btreemap! {
696                ServerSigningKeyId::from_parts(
697                    SigningKeyAlgorithm::Ed25519,
698                    server_signing_key_version!("3")
699                ) => "foobar".to_owned()
700            }
701        );
702        assert_eq!(signed.token, "abc123");
703    }
704
705    #[test]
706    fn serde_with_prev_content_full() {
707        let json = json!({
708            "type": "m.room.member",
709            "content": {
710                "membership": "join",
711            },
712            "event_id": "$143273582443PhrSn:example.org",
713            "origin_server_ts": 233,
714            "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
715            "sender": "@alice:example.org",
716            "state_key": "@alice:example.org",
717            "unsigned": {
718                "prev_content": {
719                    "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
720                    "displayname": "Alice Margatroid",
721                    "is_direct": true,
722                    "membership": "invite",
723                    "third_party_invite": {
724                        "display_name": "alice",
725                        "signed": {
726                            "mxid": "@alice:example.org",
727                            "signatures": {
728                                "magic.forest": {
729                                    "ed25519:3": "foobar",
730                                },
731                            },
732                            "token": "abc123"
733                        },
734                    },
735                },
736            },
737        });
738
739        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
740        assert_eq!(ev.event_id, "$143273582443PhrSn:example.org");
741        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(233)));
742        assert_eq!(ev.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
743        assert_eq!(ev.sender, "@alice:example.org");
744        assert_eq!(ev.state_key, "@alice:example.org");
745
746        assert_eq!(ev.content.avatar_url, None);
747        assert_eq!(ev.content.displayname, None);
748        assert_eq!(ev.content.is_direct, None);
749        assert_eq!(ev.content.membership, MembershipState::Join);
750        assert_matches!(ev.content.third_party_invite, None);
751
752        let prev_content = ev.unsigned.prev_content.unwrap();
753        assert_eq!(
754            prev_content.avatar_url.as_deref(),
755            Some(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
756        );
757        assert_eq!(prev_content.displayname.as_deref(), Some("Alice Margatroid"));
758        assert_eq!(prev_content.is_direct, Some(true));
759        assert_eq!(prev_content.membership, MembershipState::Invite);
760
761        let third_party_invite = prev_content.third_party_invite.unwrap();
762        assert_eq!(third_party_invite.display_name, "alice");
763        let signed = third_party_invite.signed.deserialize().unwrap();
764        assert_eq!(signed.mxid, "@alice:example.org");
765        assert_eq!(signed.signatures.len(), 1);
766        let server_signatures = signed.signatures.get(server_name!("magic.forest")).unwrap();
767        assert_eq!(
768            *server_signatures,
769            btreemap! {
770                ServerSigningKeyId::from_parts(
771                    SigningKeyAlgorithm::Ed25519,
772                    server_signing_key_version!("3")
773                ) => "foobar".to_owned()
774            }
775        );
776        assert_eq!(signed.token, "abc123");
777    }
778
779    #[test]
780    fn serde_with_join_authorized() {
781        let json = json!({
782            "type": "m.room.member",
783            "content": {
784                "membership": "join",
785                "join_authorised_via_users_server": "@notcarl:example.com"
786            },
787            "event_id": "$h29iv0s8:example.com",
788            "origin_server_ts": 1,
789            "room_id": "!n8f893n9:example.com",
790            "sender": "@carl:example.com",
791            "state_key": "@carl:example.com"
792        });
793
794        let ev = from_json_value::<OriginalStateEvent<RoomMemberEventContent>>(json).unwrap();
795        assert_eq!(ev.event_id, "$h29iv0s8:example.com");
796        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
797        assert_eq!(ev.room_id, "!n8f893n9:example.com");
798        assert_eq!(ev.sender, "@carl:example.com");
799        assert_eq!(ev.state_key, "@carl:example.com");
800        assert!(ev.unsigned.is_empty());
801
802        assert_eq!(ev.content.avatar_url, None);
803        assert_eq!(ev.content.displayname, None);
804        assert_eq!(ev.content.is_direct, None);
805        assert_eq!(ev.content.membership, MembershipState::Join);
806        assert_matches!(ev.content.third_party_invite, None);
807        assert_eq!(
808            ev.content.join_authorized_via_users_server.as_deref(),
809            Some(user_id!("@notcarl:example.com"))
810        );
811    }
812}