diff --git a/crates/ruma-events/Cargo.toml b/crates/ruma-events/Cargo.toml index 346bd24a..3c5a1006 100644 --- a/crates/ruma-events/Cargo.toml +++ b/crates/ruma-events/Cargo.toml @@ -21,7 +21,7 @@ markdown = ["pulldown-cmark"] unstable-exhaustive-types = [] unstable-pdu = [] -unstable-pre-spec = [] +unstable-pre-spec = ["ruma-identifiers/unstable-pre-spec"] [dependencies] criterion = { version = "0.3.3", optional = true } diff --git a/crates/ruma-events/src/room/member.rs b/crates/ruma-events/src/room/member.rs index 707becc6..cca9017d 100644 --- a/crates/ruma-events/src/room/member.rs +++ b/crates/ruma-events/src/room/member.rs @@ -3,11 +3,15 @@ use std::collections::BTreeMap; use ruma_events_macros::EventContent; -use ruma_identifiers::{MxcUri, ServerName, ServerSigningKeyId, UserId}; +use ruma_identifiers::{MxcUri, RoomVersionId, ServerName, ServerSigningKeyId, UserId}; use ruma_serde::StringEnum; use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue as RawJsonValue; -use crate::{StrippedStateEvent, SyncStateEvent}; +use crate::{ + EventContent, HasDeserializeFields, RedactContent, RedactedEventContent, + RedactedStateEventContent, StrippedStateEvent, SyncStateEvent, +}; /// The content of an `m.room.member` event. /// @@ -38,7 +42,7 @@ use crate::{StrippedStateEvent, SyncStateEvent}; /// must be assumed as leave. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[ruma_event(type = "m.room.member", kind = State)] +#[ruma_event(type = "m.room.member", kind = State, custom_redacted)] pub struct RoomMemberEventContent { /// The avatar URL for this user, if any. /// @@ -63,7 +67,6 @@ pub struct RoomMemberEventContent { pub is_direct: Option, /// The membership state of this user. - #[ruma_event(skip_redaction)] pub membership: MembershipState, /// If this member event is the successor to a third party invitation, this field will @@ -92,6 +95,12 @@ pub struct RoomMemberEventContent { #[cfg(feature = "unstable-pre-spec")] #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, + + /// Arbitrarily chosen `UserId` (MxID) of a local user who can send an invite. + #[cfg(feature = "unstable-pre-spec")] + #[serde(rename = "join_authorised_via_users_server")] + #[serde(skip_serializing_if = "Option::is_none")] + pub join_authorized_via_users_server: Option>, } impl RoomMemberEventContent { @@ -107,10 +116,86 @@ impl RoomMemberEventContent { blurhash: None, #[cfg(feature = "unstable-pre-spec")] reason: None, + #[cfg(feature = "unstable-pre-spec")] + join_authorized_via_users_server: None, } } } +impl RedactContent for RoomMemberEventContent { + type Redacted = RedactedRoomMemberEventContent; + + fn redact(self, _version: &RoomVersionId) -> RedactedRoomMemberEventContent { + RedactedRoomMemberEventContent { + membership: self.membership, + #[cfg(feature = "unstable-pre-spec")] + join_authorized_via_users_server: match _version { + #[cfg(feature = "unstable-pre-spec")] + RoomVersionId::V9 => self.join_authorized_via_users_server, + _ => None, + }, + } + } +} + +/// A member event that has been redacted. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct RedactedRoomMemberEventContent { + /// The membership state of this user. + pub membership: MembershipState, + + /// An arbitrary user who has the power to issue invites. + /// + /// This is redacted in room versions 8 and below. It is used for validating + /// joins when the join rule is restricted. + #[cfg(feature = "unstable-pre-spec")] + #[serde(rename = "join_authorised_via_users_server")] + pub join_authorized_via_users_server: Option>, +} + +impl RedactedRoomMemberEventContent { + /// Create a `RedactedRoomMemberEventContent` with the given membership. + pub fn new(membership: MembershipState) -> Self { + Self { + membership, + #[cfg(feature = "unstable-pre-spec")] + join_authorized_via_users_server: None, + } + } +} + +impl EventContent for RedactedRoomMemberEventContent { + fn event_type(&self) -> &str { + "m.room.member" + } + + fn from_parts(event_type: &str, content: &RawJsonValue) -> serde_json::Result { + if event_type != "m.room.member" { + return Err(::serde::de::Error::custom(format!( + "expected event type `m.room.member`, found `{}`", + event_type + ))); + } + + serde_json::from_str(content.get()) + } +} + +// Since this redacted event has fields we leave the default `empty` method +// that will error if called. +impl RedactedEventContent for RedactedRoomMemberEventContent { + fn has_serialize_fields(&self) -> bool { + true + } + + fn has_deserialize_fields() -> HasDeserializeFields { + HasDeserializeFields::Optional + } +} + +impl RedactedStateEventContent for RedactedRoomMemberEventContent {} + /// The membership state of a user. /// /// This type can hold an arbitrary string. To check for formats that are not available as a @@ -269,6 +354,8 @@ fn membership_change( blurhash: None, #[cfg(feature = "unstable-pre-spec")] reason: None, + #[cfg(feature = "unstable-pre-spec")] + join_authorized_via_users_server: None, } }; @@ -660,4 +747,49 @@ mod tests { && token == "abc123" ); } + + #[cfg(feature = "unstable-pre-spec")] + #[test] + fn serde_with_join_authorized() { + let json = json!({ + "type": "m.room.member", + "content": { + "membership": "join", + "join_authorised_via_users_server": "@notcarl:example.com" + }, + "event_id": "$h29iv0s8:example.com", + "origin_server_ts": 1, + "room_id": "!n8f893n9:example.com", + "sender": "@carl:example.com", + "state_key": "example.com" + }); + + assert_matches!( + from_json_value::>(json).unwrap(), + StateEvent { + content: RoomMemberEventContent { + avatar_url: None, + displayname: None, + is_direct: None, + membership: MembershipState::Join, + third_party_invite: None, + join_authorized_via_users_server: Some(authed), + .. + }, + event_id, + origin_server_ts, + room_id, + sender, + state_key, + unsigned, + prev_content: None, + } if event_id == "$h29iv0s8:example.com" + && origin_server_ts == MilliSecondsSinceUnixEpoch(uint!(1)) + && room_id == "!n8f893n9:example.com" + && sender == "@carl:example.com" + && authed == "@notcarl:example.com" + && state_key == "example.com" + && unsigned.is_empty() + ); + } }