diff --git a/ruma-events-macros/src/event.rs b/ruma-events-macros/src/event.rs index 40bb3ae7..565ffb17 100644 --- a/ruma-events-macros/src/event.rs +++ b/ruma-events-macros/src/event.rs @@ -37,7 +37,13 @@ pub fn expand_event(input: DeriveInput) -> syn::Result { .iter() .map(|field| { let name = field.ident.as_ref().unwrap(); - if name == "prev_content" { + if name == "content" && ident.to_string().contains("Redacted") { + quote! { + if ::ruma_events::RedactedEventContent::has_serialize_fields(&self.content) { + state.serialize_field("content", &self.content)?; + } + } + } else if name == "prev_content" { quote! { if let Some(content) = self.prev_content.as_ref() { state.serialize_field("prev_content", content)?; @@ -143,7 +149,16 @@ fn expand_deserialize_event( .map(|field| { let name = field.ident.as_ref().unwrap(); if name == "content" { - if is_generic { + if is_generic && ident.to_string().contains("Redacted") { + quote! { + let content = if !C::has_deserialize_fields() { + C::empty(&event_type).map_err(A::Error::custom)? + } else { + let json = content.ok_or_else(|| ::serde::de::Error::missing_field("content"))?; + C::from_parts(&event_type, json).map_err(A::Error::custom)? + }; + } + } else if is_generic { quote! { let json = content.ok_or_else(|| ::serde::de::Error::missing_field("content"))?; let content = C::from_parts(&event_type, json).map_err(A::Error::custom)?; diff --git a/ruma-events-macros/src/event_content.rs b/ruma-events-macros/src/event_content.rs index 940c1364..ab01ddc3 100644 --- a/ruma-events-macros/src/event_content.rs +++ b/ruma-events-macros/src/event_content.rs @@ -4,45 +4,277 @@ use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::{ parse::{Parse, ParseStream}, - DeriveInput, LitStr, Token, + DeriveInput, Ident, LitStr, Token, }; +mod kw { + // This `content` field is kept when the event is redacted. + syn::custom_keyword!(skip_redaction); + // Do not emit any redacted event code. + syn::custom_keyword!(custom_redacted); +} + /// Parses attributes for `*EventContent` derives. /// /// `#[ruma_event(type = "m.room.alias")]` +#[derive(Eq, PartialEq)] enum EventMeta { /// Variant holds the "m.whatever" event type. Type(LitStr), + + /// Fields marked with `#[ruma_event(skip_redaction)]` are kept when the event is + /// redacted. + SkipRedacted, + + /// This attribute signals that the events redacted form is manually implemented and should + /// not be generated. + CustomRedacted, +} + +impl EventMeta { + fn get_event_type(&self) -> Option<&LitStr> { + if let Self::Type(lit) = self { + Some(lit) + } else { + None + } + } } impl Parse for EventMeta { fn parse(input: ParseStream) -> syn::Result { - input.parse::()?; - input.parse::()?; - Ok(EventMeta::Type(input.parse::()?)) + if input.parse::().is_ok() { + input.parse::()?; + Ok(EventMeta::Type(input.parse::()?)) + } else if input.parse::().is_ok() { + Ok(EventMeta::SkipRedacted) + } else if input.parse::().is_ok() { + Ok(EventMeta::CustomRedacted) + } else { + Err(syn::Error::new(input.span(), "not a recognized `ruma_event` attribute")) + } } } /// Create an `EventContent` implementation for a struct. -pub fn expand_event_content(input: DeriveInput) -> syn::Result { +pub fn expand_event_content(input: &DeriveInput, emit_redacted: bool) -> syn::Result { let ident = &input.ident; - let event_type_attr = - input.attrs.iter().find(|attr| attr.path.is_ident("ruma_event")).ok_or_else(|| { - let msg = "no event type attribute found, \ + let content_attr = input + .attrs + .iter() + .filter(|attr| attr.path.is_ident("ruma_event")) + .map(|attr| attr.parse_args::()) + .collect::>>()?; + + let event_type = content_attr.iter().find_map(|a| a.get_event_type()).ok_or_else(|| { + let msg = "no event type attribute found, \ add `#[ruma_event(type = \"any.room.event\")]` \ below the event content derive"; - syn::Error::new(Span::call_site(), msg) - })?; + syn::Error::new(Span::call_site(), msg) + })?; - let event_type = { - let event_meta = event_type_attr.parse_args::()?; - let EventMeta::Type(lit) = event_meta; - lit + let redacted = if emit_redacted && needs_redacted(input) { + let doc = format!("The payload for a redacted `{}`", ident); + let redacted_ident = quote::format_ident!("Redacted{}", ident); + let kept_redacted_fields = if let syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Named(syn::FieldsNamed { named, .. }), + .. + }) = &input.data + { + // this is to validate the `#[ruma_event(skip_redaction)]` attribute + named + .iter() + .flat_map(|f| &f.attrs) + .filter(|a| a.path.is_ident("ruma_event")) + .find_map(|a| { + if let Err(e) = a.parse_args::() { + Some(Err(e)) + } else { + None + } + }) + .unwrap_or(Ok(()))?; + + let mut fields = named + .iter() + .filter(|f| { + f.attrs.iter().find_map(|a| a.parse_args::().ok()) + == Some(EventMeta::SkipRedacted) + }) + .cloned() + .collect::>(); + + // don't re-emit our `ruma_event` attributes + for f in &mut fields { + f.attrs.retain(|a| !a.path.is_ident("ruma_event")); + } + fields + } else { + vec![] + }; + let redaction_struct_fields = kept_redacted_fields.iter().flat_map(|f| &f.ident); + + // redacted_fields allows one to declare an empty redacted event without braces, + // otherwise `RedactedWhateverEventContent {}` is needed. + // The redacted_return is used in `EventContent::redacted` which only returns + // zero sized types (unit structs). + let (redacted_fields, redacted_return) = if kept_redacted_fields.is_empty() { + (quote! { ; }, quote! { Ok(#redacted_ident {}) }) + } else { + ( + quote! { + { #( #kept_redacted_fields, )* } + }, + quote! { + Err(::serde::de::Error::custom( + format!("this redacted event has fields that cannot be constructed") + )) + }, + ) + }; + + let has_fields = if kept_redacted_fields.is_empty() { + quote! { false } + } else { + quote! { true } + }; + + let redacted_event_content = generate_event_content_impl(&redacted_ident, event_type); + + quote! { + // this is the non redacted event content's impl + impl #ident { + /// Transforms the full event content into a redacted content according to spec. + pub fn redact(self) -> #redacted_ident { + #redacted_ident { #( #redaction_struct_fields: self.#redaction_struct_fields, )* } + } + } + + #[doc = #doc] + #[derive(Clone, Debug, ::serde::Deserialize, ::serde::Serialize)] + pub struct #redacted_ident #redacted_fields + + #redacted_event_content + + impl ::ruma_events::RedactedEventContent for #redacted_ident { + fn empty(ev_type: &str) -> Result { + if ev_type != #event_type { + return Err(::serde::de::Error::custom( + format!("expected event type `{}`, found `{}`", #event_type, ev_type) + )); + } + + #redacted_return + } + + fn has_serialize_fields(&self) -> bool { + #has_fields + } + + fn has_deserialize_fields() -> bool { + #has_fields + } + } + } + } else { + TokenStream::new() + }; + + let event_content = generate_event_content_impl(ident, event_type); + + Ok(quote! { + #event_content + + #redacted + }) +} + +/// Create a `BasicEventContent` implementation for a struct +pub fn expand_basic_event_content(input: &DeriveInput) -> syn::Result { + let ident = input.ident.clone(); + let event_content_impl = expand_event_content(input, false)?; + + Ok(quote! { + #event_content_impl + + impl ::ruma_events::BasicEventContent for #ident { } + }) +} + +/// Create a `EphemeralRoomEventContent` implementation for a struct +pub fn expand_ephemeral_room_event_content(input: &DeriveInput) -> syn::Result { + let ident = input.ident.clone(); + let event_content_impl = expand_event_content(input, false)?; + + Ok(quote! { + #event_content_impl + + impl ::ruma_events::EphemeralRoomEventContent for #ident { } + }) +} + +/// Create a `RoomEventContent` implementation for a struct. +pub fn expand_room_event_content(input: &DeriveInput) -> syn::Result { + let ident = input.ident.clone(); + let event_content_impl = expand_event_content(input, true)?; + + Ok(quote! { + #event_content_impl + + impl ::ruma_events::RoomEventContent for #ident { } + }) +} + +/// Create a `MessageEventContent` implementation for a struct +pub fn expand_message_event_content(input: &DeriveInput) -> syn::Result { + let ident = input.ident.clone(); + let room_ev_content = expand_room_event_content(input)?; + + let redacted_marker_trait = if needs_redacted(input) { + let ident = quote::format_ident!("Redacted{}", &ident); + quote! { + impl ::ruma_events::RedactedMessageEventContent for #ident { } + } + } else { + TokenStream::new() }; Ok(quote! { + #room_ev_content + + impl ::ruma_events::MessageEventContent for #ident { } + + #redacted_marker_trait + }) +} + +/// Create a `StateEventContent` implementation for a struct +pub fn expand_state_event_content(input: &DeriveInput) -> syn::Result { + let ident = input.ident.clone(); + let room_ev_content = expand_room_event_content(input)?; + + let redacted_marker_trait = if needs_redacted(input) { + let ident = quote::format_ident!("Redacted{}", input.ident); + quote! { + impl ::ruma_events::RedactedStateEventContent for #ident { } + } + } else { + TokenStream::new() + }; + + Ok(quote! { + #room_ev_content + + impl ::ruma_events::StateEventContent for #ident { } + + #redacted_marker_trait + }) +} + +fn generate_event_content_impl(ident: &Ident, event_type: &LitStr) -> TokenStream { + quote! { impl ::ruma_events::EventContent for #ident { fn event_type(&self) -> &str { #event_type @@ -61,65 +293,14 @@ pub fn expand_event_content(input: DeriveInput) -> syn::Result { ::serde_json::from_str(content.get()) } } - }) + } } -/// Create a `BasicEventContent` implementation for a struct -pub fn expand_basic_event_content(input: DeriveInput) -> syn::Result { - let ident = input.ident.clone(); - let event_content_impl = expand_event_content(input)?; - - Ok(quote! { - #event_content_impl - - impl ::ruma_events::BasicEventContent for #ident { } - }) -} - -/// Create a `EphemeralRoomEventContent` implementation for a struct -pub fn expand_ephemeral_room_event_content(input: DeriveInput) -> syn::Result { - let ident = input.ident.clone(); - let event_content_impl = expand_event_content(input)?; - - Ok(quote! { - #event_content_impl - - impl ::ruma_events::EphemeralRoomEventContent for #ident { } - }) -} - -/// Create a `RoomEventContent` implementation for a struct. -pub fn expand_room_event_content(input: DeriveInput) -> syn::Result { - let ident = input.ident.clone(); - let event_content_impl = expand_event_content(input)?; - - Ok(quote! { - #event_content_impl - - impl ::ruma_events::RoomEventContent for #ident { } - }) -} - -/// Create a `MessageEventContent` implementation for a struct -pub fn expand_message_event_content(input: DeriveInput) -> syn::Result { - let ident = input.ident.clone(); - let room_ev_content = expand_room_event_content(input)?; - - Ok(quote! { - #room_ev_content - - impl ::ruma_events::MessageEventContent for #ident { } - }) -} - -/// Create a `StateEventContent` implementation for a struct -pub fn expand_state_event_content(input: DeriveInput) -> syn::Result { - let ident = input.ident.clone(); - let room_ev_content = expand_room_event_content(input)?; - - Ok(quote! { - #room_ev_content - - impl ::ruma_events::StateEventContent for #ident { } - }) +fn needs_redacted(input: &DeriveInput) -> bool { + input + .attrs + .iter() + .flat_map(|a| a.parse_args::().ok()) + .find(|a| a == &EventMeta::CustomRedacted) + .is_none() } diff --git a/ruma-events-macros/src/event_enum.rs b/ruma-events-macros/src/event_enum.rs index 953c7014..19b1dba1 100644 --- a/ruma-events-macros/src/event_enum.rs +++ b/ruma-events-macros/src/event_enum.rs @@ -10,16 +10,40 @@ use syn::{ use crate::event_names::{ ANY_BASIC_EVENT, ANY_EPHEMERAL_EVENT, ANY_MESSAGE_EVENT, ANY_STATE_EVENT, ANY_STRIPPED_STATE_EVENT, ANY_SYNC_MESSAGE_EVENT, ANY_SYNC_STATE_EVENT, ANY_TO_DEVICE_EVENT, + REDACTED_MESSAGE_EVENT, REDACTED_STATE_EVENT, REDACTED_STRIPPED_STATE_EVENT, + REDACTED_SYNC_MESSAGE_EVENT, REDACTED_SYNC_STATE_EVENT, }; // Arrays of event enum names grouped by a field they share in common. -const ROOM_EVENT_KIND: &[&str] = - &[ANY_MESSAGE_EVENT, ANY_SYNC_MESSAGE_EVENT, ANY_STATE_EVENT, ANY_SYNC_STATE_EVENT]; +const ROOM_EVENT_KIND: &[&str] = &[ + ANY_MESSAGE_EVENT, + ANY_SYNC_MESSAGE_EVENT, + ANY_STATE_EVENT, + ANY_SYNC_STATE_EVENT, + REDACTED_MESSAGE_EVENT, + REDACTED_STATE_EVENT, + REDACTED_SYNC_MESSAGE_EVENT, + REDACTED_SYNC_STATE_EVENT, +]; -const ROOM_ID_KIND: &[&str] = &[ANY_MESSAGE_EVENT, ANY_STATE_EVENT, ANY_EPHEMERAL_EVENT]; +const ROOM_ID_KIND: &[&str] = &[ + ANY_MESSAGE_EVENT, + ANY_STATE_EVENT, + ANY_EPHEMERAL_EVENT, + REDACTED_STATE_EVENT, + REDACTED_MESSAGE_EVENT, +]; -const EVENT_ID_KIND: &[&str] = - &[ANY_MESSAGE_EVENT, ANY_SYNC_MESSAGE_EVENT, ANY_STATE_EVENT, ANY_SYNC_STATE_EVENT]; +const EVENT_ID_KIND: &[&str] = &[ + ANY_MESSAGE_EVENT, + ANY_SYNC_MESSAGE_EVENT, + ANY_STATE_EVENT, + ANY_SYNC_STATE_EVENT, + REDACTED_SYNC_STATE_EVENT, + REDACTED_SYNC_MESSAGE_EVENT, + REDACTED_STATE_EVENT, + REDACTED_MESSAGE_EVENT, +]; const SENDER_KIND: &[&str] = &[ ANY_MESSAGE_EVENT, @@ -28,11 +52,31 @@ const SENDER_KIND: &[&str] = &[ ANY_TO_DEVICE_EVENT, ANY_SYNC_MESSAGE_EVENT, ANY_STRIPPED_STATE_EVENT, + REDACTED_MESSAGE_EVENT, + REDACTED_STATE_EVENT, + REDACTED_STRIPPED_STATE_EVENT, + REDACTED_SYNC_MESSAGE_EVENT, + REDACTED_SYNC_STATE_EVENT, ]; const PREV_CONTENT_KIND: &[&str] = &[ANY_STATE_EVENT, ANY_SYNC_STATE_EVENT]; -const STATE_KEY_KIND: &[&str] = &[ANY_STATE_EVENT, ANY_SYNC_STATE_EVENT, ANY_STRIPPED_STATE_EVENT]; +const STATE_KEY_KIND: &[&str] = &[ + ANY_STATE_EVENT, + ANY_SYNC_STATE_EVENT, + ANY_STRIPPED_STATE_EVENT, + REDACTED_SYNC_STATE_EVENT, + REDACTED_STRIPPED_STATE_EVENT, + REDACTED_STATE_EVENT, +]; + +const REDACTED_EVENT_KIND: &[&str] = &[ + ANY_STATE_EVENT, + ANY_SYNC_STATE_EVENT, + ANY_STRIPPED_STATE_EVENT, + ANY_MESSAGE_EVENT, + ANY_SYNC_MESSAGE_EVENT, +]; /// This const is used to generate the accessor methods for the `Any*Event` enums. /// @@ -69,8 +113,14 @@ pub fn expand_event_enum(input: EventEnumInput) -> syn::Result { let event_stripped_enum = if needs_stripped_event { expand_stripped_enum(&input)? } else { TokenStream::new() }; + let redacted_event_enums = if needs_redacted(ident) { + expand_any_redacted_enum_with_deserialize(&input, ident)? + } else { + TokenStream::new() + }; + let event_content_enum = - if needs_event_content { expand_content_enum(&input)? } else { TokenStream::new() }; + if needs_event_content { expand_content_enum(&input, ident)? } else { TokenStream::new() }; Ok(quote! { #event_enum @@ -79,6 +129,8 @@ pub fn expand_event_enum(input: EventEnumInput) -> syn::Result { #event_stripped_enum + #redacted_event_enums + #event_content_enum }) } @@ -97,10 +149,59 @@ pub fn expand_stripped_enum(input: &EventEnumInput) -> syn::Result expand_any_enum_with_deserialize(input, &ident) } +/// Generates the 3 redacted state enums, 2 redacted message enums, +/// and `Deserialize` implementations. +/// +/// No content enums are generated since no part of the API deals with +/// redacted event's content. There are only five state variants that contain content. +fn expand_any_redacted_enum_with_deserialize( + input: &EventEnumInput, + ident: &Ident, +) -> syn::Result { + let name = ident.to_string().trim_start_matches("Any").to_string(); + + let redacted_enums_deserialize = if ident.to_string().contains("State") { + let ident = Ident::new(&format!("AnyRedacted{}", name), ident.span()); + + let full = expand_any_enum_with_deserialize(input, &ident)?; + + let ident = Ident::new(&format!("AnyRedacted{}Stub", name), ident.span()); + let stub = expand_any_enum_with_deserialize(input, &ident)?; + + let ident = Ident::new(&format!("AnyRedactedStripped{}Stub", name), ident.span()); + let stripped = expand_any_enum_with_deserialize(input, &ident)?; + + quote! { + #full + + #stub + + #stripped + } + } else { + let ident = Ident::new(&format!("AnyRedacted{}", name), ident.span()); + + let full = expand_any_enum_with_deserialize(input, &ident)?; + + let ident = Ident::new(&format!("AnyRedacted{}Stub", name), ident.span()); + let stub = expand_any_enum_with_deserialize(input, &ident)?; + + quote! { + #full + + #stub + } + }; + + Ok(quote! { + #redacted_enums_deserialize + }) +} + /// Create a content enum from `EventEnumInput`. -pub fn expand_content_enum(input: &EventEnumInput) -> syn::Result { +pub fn expand_content_enum(input: &EventEnumInput, ident: &Ident) -> syn::Result { let attrs = &input.attrs; - let ident = Ident::new(&format!("{}Content", input.name.to_string()), input.name.span()); + let ident = Ident::new(&format!("{}Content", ident), ident.span()); let event_type_str = &input.events; let variants = input.events.iter().map(to_camel_case).collect::>>()?; @@ -135,12 +236,12 @@ pub fn expand_content_enum(input: &EventEnumInput) -> syn::Result { #( #event_type_str => { let content = #content::from_parts(event_type, input)?; - Ok(#ident::#variants(content)) + Ok(Self::#variants(content)) }, )* ev_type => { let content = ::ruma_events::custom::CustomEventContent::from_parts(ev_type, input)?; - Ok(#ident::Custom(content)) + Ok(Self::Custom(content)) }, } } @@ -164,12 +265,14 @@ fn expand_any_enum_with_deserialize( ) -> syn::Result { let attrs = &input.attrs; let event_type_str = &input.events; - let event_struct = Ident::new(&ident.to_string().trim_start_matches("Any"), ident.span()); + let event_struct = Ident::new(&ident.to_string().replace("Any", ""), ident.span()); let variants = input.events.iter().map(to_camel_case).collect::>>()?; let content = input.events.iter().map(|event| to_event_path(event, &event_struct)).collect::>(); + let (custom_variant, custom_deserialize) = expand_custom_variant(ident, &event_struct); + let any_enum = quote! { #( #attrs )* #[derive(Clone, Debug, ::serde::Serialize)] @@ -180,8 +283,7 @@ fn expand_any_enum_with_deserialize( #[doc = #event_type_str] #variants(#content), )* - /// An event not defined by the Matrix specification - Custom(::ruma_events::#event_struct<::ruma_events::custom::CustomEventContent>), + #custom_variant } }; @@ -199,16 +301,10 @@ fn expand_any_enum_with_deserialize( #( #event_type_str => { let event = ::serde_json::from_str::<#content>(json.get()).map_err(D::Error::custom)?; - Ok(#ident::#variants(event)) + Ok(Self::#variants(event)) }, )* - event => { - let event = - ::serde_json::from_str::<::ruma_events::#event_struct<::ruma_events::custom::CustomEventContent>>(json.get()) - .map_err(D::Error::custom)?; - - Ok(Self::Custom(event)) - }, + #custom_deserialize } } } @@ -225,6 +321,43 @@ fn expand_any_enum_with_deserialize( }) } +fn expand_custom_variant(ident: &Ident, event_struct: &Ident) -> (TokenStream, TokenStream) { + if ident.to_string().contains("Redacted") { + ( + quote! { + /// A redacted event not defined by the Matrix specification + Custom(::ruma_events::#event_struct<::ruma_events::custom::RedactedCustomEventContent>), + }, + quote! { + event => { + let event = ::serde_json::from_str::< + ::ruma_events::#event_struct<::ruma_events::custom::RedactedCustomEventContent>, + >(json.get()) + .map_err(D::Error::custom)?; + + Ok(Self::Custom(event)) + }, + }, + ) + } else { + ( + quote! { + /// An event not defined by the Matrix specification + Custom(::ruma_events::#event_struct<::ruma_events::custom::CustomEventContent>), + }, + quote! { + event => { + let event = + ::serde_json::from_str::<::ruma_events::#event_struct<::ruma_events::custom::CustomEventContent>>(json.get()) + .map_err(D::Error::custom)?; + + Ok(Self::Custom(event)) + }, + }, + ) + } +} + fn marker_traits(ident: &Ident) -> TokenStream { match ident.to_string().as_str() { "AnyStateEventContent" => quote! { @@ -246,6 +379,10 @@ fn marker_traits(ident: &Ident) -> TokenStream { } fn accessor_methods(ident: &Ident, variants: &[Ident]) -> TokenStream { + if ident.to_string().contains("Redacted") { + return redacted_accessor_methods(ident, variants); + } + let fields = EVENT_FIELDS .iter() .map(|(name, has_field)| generate_accessor(name, ident, *has_field, variants)); @@ -296,6 +433,20 @@ fn accessor_methods(ident: &Ident, variants: &[Ident]) -> TokenStream { } } +/// Redacted events do NOT generate `content` or `prev_content` methods like +/// un-redacted events; otherwise, they are the same. +fn redacted_accessor_methods(ident: &Ident, variants: &[Ident]) -> TokenStream { + let fields = EVENT_FIELDS + .iter() + .map(|(name, has_field)| generate_accessor(name, ident, *has_field, variants)); + + quote! { + impl #ident { + #( #fields )* + } + } +} + fn to_event_path(name: &LitStr, struct_name: &Ident) -> TokenStream { let span = name.span(); let name = name.value(); @@ -325,6 +476,10 @@ fn to_event_path(name: &LitStr, struct_name: &Ident) -> TokenStream { let content = Ident::new(&format!("{}EventContent", event), span); quote! { ::ruma_events::#struct_name<::ruma_events::#( #path )::*::#content> } } + struct_str if struct_str.contains("Redacted") => { + let content = Ident::new(&format!("Redacted{}EventContent", event), span); + quote! { ::ruma_events::#struct_name<::ruma_events::#( #path )::*::#content> } + } _ => { let event_name = Ident::new(&format!("{}Event", event), span); quote! { ::ruma_events::#( #path )::*::#event_name } @@ -401,6 +556,11 @@ fn generate_accessor( } } +/// Returns true if the `ident` is a state or message event. +fn needs_redacted(ident: &Ident) -> bool { + REDACTED_EVENT_KIND.contains(&ident.to_string().as_str()) +} + fn field_return_type(name: &str) -> TokenStream { match name { "origin_server_ts" => quote! { ::std::time::SystemTime }, diff --git a/ruma-events-macros/src/event_names.rs b/ruma-events-macros/src/event_names.rs index 9148b5c6..c8f1341a 100644 --- a/ruma-events-macros/src/event_names.rs +++ b/ruma-events-macros/src/event_names.rs @@ -2,6 +2,11 @@ //! certain code for certain enums. If the names change this is the one source of truth, //! most comparisons and branching uses these constants. +#![allow(dead_code)] + +// Those marked with (UNUSED) are not used but, left for completeness sake. +// If you change this please remove the (UNUSED) comment. + // State events pub const ANY_STATE_EVENT: &str = "AnyStateEvent"; @@ -9,17 +14,27 @@ pub const ANY_SYNC_STATE_EVENT: &str = "AnyStateEventStub"; pub const ANY_STRIPPED_STATE_EVENT: &str = "AnyStrippedStateEventStub"; +// Redacted state events +pub const REDACTED_STATE_EVENT: &str = "AnyRedactedStateEvent"; // (UNUSED) + +pub const REDACTED_SYNC_STATE_EVENT: &str = "AnyRedactedStateEventStub"; // (UNUSED) + +pub const REDACTED_STRIPPED_STATE_EVENT: &str = "AnyRedactedStrippedStateEventStub"; // (UNUSED) + // Message events pub const ANY_MESSAGE_EVENT: &str = "AnyMessageEvent"; pub const ANY_SYNC_MESSAGE_EVENT: &str = "AnyMessageEventStub"; +// Redacted message events +pub const REDACTED_MESSAGE_EVENT: &str = "AnyRedactedMessageEvent"; // (UNUSED) + +pub const REDACTED_SYNC_MESSAGE_EVENT: &str = "AnyRedactedMessageEventStub"; // (UNUSED) + // Ephemeral events pub const ANY_EPHEMERAL_EVENT: &str = "AnyEphemeralRoomEvent"; -#[allow(dead_code)] -// This is currently not used but, left for completeness sake. -pub const ANY_SYNC_EPHEMERAL_EVENT: &str = "AnyEphemeralRoomEventStub"; +pub const ANY_SYNC_EPHEMERAL_EVENT: &str = "AnyEphemeralRoomEventStub"; // (UNUSED) // Basic event pub const ANY_BASIC_EVENT: &str = "AnyBasicEvent"; diff --git a/ruma-events-macros/src/lib.rs b/ruma-events-macros/src/lib.rs index 657897ab..7893903b 100644 --- a/ruma-events-macros/src/lib.rs +++ b/ruma-events-macros/src/lib.rs @@ -52,42 +52,42 @@ pub fn event_enum(input: TokenStream) -> TokenStream { #[proc_macro_derive(EventContent, attributes(ruma_event))] pub fn derive_event_content(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - expand_event_content(input).unwrap_or_else(|err| err.to_compile_error()).into() + expand_event_content(&input, true).unwrap_or_else(|err| err.to_compile_error()).into() } /// Generates an implementation of `ruma_events::BasicEventContent` and it's super traits. #[proc_macro_derive(BasicEventContent, attributes(ruma_event))] pub fn derive_basic_event_content(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - expand_basic_event_content(input).unwrap_or_else(|err| err.to_compile_error()).into() + expand_basic_event_content(&input).unwrap_or_else(|err| err.to_compile_error()).into() } /// Generates an implementation of `ruma_events::RoomEventContent` and it's super traits. #[proc_macro_derive(RoomEventContent, attributes(ruma_event))] pub fn derive_room_event_content(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - expand_room_event_content(input).unwrap_or_else(|err| err.to_compile_error()).into() + expand_room_event_content(&input).unwrap_or_else(|err| err.to_compile_error()).into() } /// Generates an implementation of `ruma_events::MessageEventContent` and it's super traits. #[proc_macro_derive(MessageEventContent, attributes(ruma_event))] pub fn derive_message_event_content(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - expand_message_event_content(input).unwrap_or_else(|err| err.to_compile_error()).into() + expand_message_event_content(&input).unwrap_or_else(|err| err.to_compile_error()).into() } /// Generates an implementation of `ruma_events::StateEventContent` and it's super traits. #[proc_macro_derive(StateEventContent, attributes(ruma_event))] pub fn derive_state_event_content(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - expand_state_event_content(input).unwrap_or_else(|err| err.to_compile_error()).into() + expand_state_event_content(&input).unwrap_or_else(|err| err.to_compile_error()).into() } /// Generates an implementation of `ruma_events::EphemeralRoomEventContent` and it's super traits. #[proc_macro_derive(EphemeralRoomEventContent, attributes(ruma_event))] pub fn derive_ephemeral_room_event_content(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - expand_ephemeral_room_event_content(input).unwrap_or_else(|err| err.to_compile_error()).into() + expand_ephemeral_room_event_content(&input).unwrap_or_else(|err| err.to_compile_error()).into() } /// Generates implementations needed to serialize and deserialize Matrix events. diff --git a/ruma-events/src/custom.rs b/ruma-events/src/custom.rs index 1cc717fd..ac7d7e7e 100644 --- a/ruma-events/src/custom.rs +++ b/ruma-events/src/custom.rs @@ -5,7 +5,8 @@ use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue}; use crate::{ BasicEventContent, EphemeralRoomEventContent, EventContent, MessageEventContent, - RoomEventContent, StateEventContent, + RedactedEventContent, RedactedMessageEventContent, RedactedStateEventContent, RoomEventContent, + StateEventContent, }; /// A custom event's type and `content` JSON object. @@ -20,6 +21,13 @@ pub struct CustomEventContent { pub json: JsonValue, } +impl CustomEventContent { + /// Transforms the full event content into a redacted content according to spec. + pub fn redact(self) -> RedactedCustomEventContent { + RedactedCustomEventContent { event_type: self.event_type } + } +} + impl EventContent for CustomEventContent { fn event_type(&self) -> &str { &self.event_type @@ -42,3 +50,44 @@ impl EphemeralRoomEventContent for CustomEventContent {} impl MessageEventContent for CustomEventContent {} impl StateEventContent for CustomEventContent {} + +/// A custom event that has been redacted. +#[derive(Clone, Debug, Serialize)] +pub struct RedactedCustomEventContent { + // This field is marked skipped but will be present because deserialization + // passes the `type` field of the JSON event to the events `EventContent::from_parts` method. + /// The event type string for this custom event "m.whatever". + #[serde(skip)] + pub event_type: String, +} + +impl EventContent for RedactedCustomEventContent { + fn event_type(&self) -> &str { + &self.event_type + } + + fn from_parts( + event_type: &str, + _content: Box, + ) -> Result { + Ok(Self { event_type: event_type.to_string() }) + } +} + +impl RedactedEventContent for RedactedCustomEventContent { + fn empty(event_type: &str) -> Result { + Ok(Self { event_type: event_type.to_string() }) + } + + fn has_serialize_fields(&self) -> bool { + false + } + + fn has_deserialize_fields() -> bool { + false + } +} + +impl RedactedMessageEventContent for RedactedCustomEventContent {} + +impl RedactedStateEventContent for RedactedCustomEventContent {} diff --git a/ruma-events/src/dummy.rs b/ruma-events/src/dummy.rs index 7fd2e5d2..b7862db8 100644 --- a/ruma-events/src/dummy.rs +++ b/ruma-events/src/dummy.rs @@ -19,9 +19,9 @@ use crate::BasicEvent; /// sending client receiving keys over the newly established session. pub type DummyEvent = BasicEvent; +/// The payload for `DummyEvent`. #[derive(Clone, Debug, Deserialize, Serialize, BasicEventContent)] #[ruma_event(type = "m.dummy")] -/// The payload for `DummyEvent`. pub struct DummyEventContent(pub Empty); impl Deref for DummyEventContent { diff --git a/ruma-events/src/enums.rs b/ruma-events/src/enums.rs index 25c52b01..36daad56 100644 --- a/ruma-events/src/enums.rs +++ b/ruma-events/src/enums.rs @@ -97,6 +97,10 @@ pub enum AnyEvent { Message(AnyMessageEvent), /// Any state event. State(AnyStateEvent), + /// Any message event that has been redacted. + RedactedMessage(AnyRedactedMessageEvent), + /// Any state event that has been redacted. + RedactedState(AnyRedactedStateEvent), } /// Any room event. @@ -107,6 +111,10 @@ pub enum AnyRoomEvent { Message(AnyMessageEvent), /// Any state event. State(AnyStateEvent), + /// Any message event that has been redacted. + RedactedMessage(AnyRedactedMessageEvent), + /// Any state event that has been redacted. + RedactedState(AnyRedactedStateEvent), } /// Any room event stub (room event without a `room_id`, as returned in `/sync` responses) @@ -117,6 +125,10 @@ pub enum AnyRoomEventStub { Message(AnyMessageEventStub), /// Any state event stub State(AnyStateEventStub), + /// Any message event stub that has been redacted. + RedactedMessage(AnyRedactedMessageEventStub), + /// Any state event stub that has been redacted. + RedactedState(AnyRedactedStateEventStub), } // FIXME `#[serde(untagged)]` deserialization fails for these enums which @@ -127,14 +139,25 @@ impl<'de> de::Deserialize<'de> for AnyEvent { D: de::Deserializer<'de>, { let json = Box::::deserialize(deserializer)?; - let EventDeHelper { state_key, event_id, room_id, .. } = from_raw_json_value(&json)?; + let EventDeHelper { state_key, event_id, room_id, unsigned, .. } = + from_raw_json_value(&json)?; // Determine whether the event is a state, message, ephemeral, or basic event // based on the fields present. if state_key.is_some() { - Ok(AnyEvent::State(from_raw_json_value(&json)?)) + Ok(match unsigned { + Some(unsigned) if unsigned.redacted_because.is_some() => { + AnyEvent::RedactedState(from_raw_json_value(&json)?) + } + _ => AnyEvent::State(from_raw_json_value(&json)?), + }) } else if event_id.is_some() { - Ok(AnyEvent::Message(from_raw_json_value(&json)?)) + Ok(match unsigned { + Some(unsigned) if unsigned.redacted_because.is_some() => { + AnyEvent::RedactedMessage(from_raw_json_value(&json)?) + } + _ => AnyEvent::Message(from_raw_json_value(&json)?), + }) } else if room_id.is_some() { Ok(AnyEvent::Ephemeral(from_raw_json_value(&json)?)) } else { @@ -149,12 +172,22 @@ impl<'de> de::Deserialize<'de> for AnyRoomEvent { D: de::Deserializer<'de>, { let json = Box::::deserialize(deserializer)?; - let EventDeHelper { state_key, .. } = from_raw_json_value(&json)?; + let EventDeHelper { state_key, unsigned, .. } = from_raw_json_value(&json)?; if state_key.is_some() { - Ok(AnyRoomEvent::State(from_raw_json_value(&json)?)) + Ok(match unsigned { + Some(unsigned) if unsigned.redacted_because.is_some() => { + AnyRoomEvent::RedactedState(from_raw_json_value(&json)?) + } + _ => AnyRoomEvent::State(from_raw_json_value(&json)?), + }) } else { - Ok(AnyRoomEvent::Message(from_raw_json_value(&json)?)) + Ok(match unsigned { + Some(unsigned) if unsigned.redacted_because.is_some() => { + AnyRoomEvent::RedactedMessage(from_raw_json_value(&json)?) + } + _ => AnyRoomEvent::Message(from_raw_json_value(&json)?), + }) } } } @@ -165,12 +198,22 @@ impl<'de> de::Deserialize<'de> for AnyRoomEventStub { D: de::Deserializer<'de>, { let json = Box::::deserialize(deserializer)?; - let EventDeHelper { state_key, .. } = from_raw_json_value(&json)?; + let EventDeHelper { state_key, unsigned, .. } = from_raw_json_value(&json)?; if state_key.is_some() { - Ok(AnyRoomEventStub::State(from_raw_json_value(&json)?)) + Ok(match unsigned { + Some(unsigned) if unsigned.redacted_because.is_some() => { + AnyRoomEventStub::RedactedState(from_raw_json_value(&json)?) + } + _ => AnyRoomEventStub::State(from_raw_json_value(&json)?), + }) } else { - Ok(AnyRoomEventStub::Message(from_raw_json_value(&json)?)) + Ok(match unsigned { + Some(unsigned) if unsigned.redacted_because.is_some() => { + AnyRoomEventStub::RedactedMessage(from_raw_json_value(&json)?) + } + _ => AnyRoomEventStub::Message(from_raw_json_value(&json)?), + }) } } } diff --git a/ruma-events/src/event_kinds.rs b/ruma-events/src/event_kinds.rs index 347f6ff8..13c6ae80 100644 --- a/ruma-events/src/event_kinds.rs +++ b/ruma-events/src/event_kinds.rs @@ -5,7 +5,7 @@ use ruma_identifiers::{EventId, RoomId, UserId}; use crate::{ BasicEventContent, EphemeralRoomEventContent, EventContent, MessageEventContent, - StateEventContent, UnsignedData, + RedactedMessageEventContent, RedactedStateEventContent, StateEventContent, UnsignedData, }; /// A basic event – one that consists only of it's type and the `content` object. @@ -73,6 +73,48 @@ pub struct MessageEventStub { pub unsigned: UnsignedData, } +/// A redacted message event. +#[derive(Clone, Debug, Event)] +pub struct RedactedMessageEvent { + /// Data specific to the event type. + pub content: C, + + /// The globally unique event identifier for the user who sent the event. + pub event_id: EventId, + + /// The fully-qualified ID of the user who sent this event. + pub sender: UserId, + + /// Timestamp in milliseconds on originating homeserver when this event was sent. + pub origin_server_ts: SystemTime, + + /// The ID of the room associated with this event. + pub room_id: RoomId, + + /// Additional key-value pairs not signed by the homeserver. + pub unsigned: UnsignedData, +} + +/// A redacted message event without a `room_id`. +#[derive(Clone, Debug, Event)] +pub struct RedactedMessageEventStub { + /// Data specific to the event type. + // #[serde(default, skip_serializing_if = "is_zst")] + pub content: C, + + /// The globally unique event identifier for the user who sent the event. + pub event_id: EventId, + + /// The fully-qualified ID of the user who sent this event. + pub sender: UserId, + + /// Timestamp in milliseconds on originating homeserver when this event was sent. + pub origin_server_ts: SystemTime, + + /// Additional key-value pairs not signed by the homeserver. + pub unsigned: UnsignedData, +} + /// State event. #[derive(Clone, Debug, Event)] pub struct StateEvent { @@ -149,6 +191,76 @@ pub struct StrippedStateEventStub { pub state_key: String, } +/// A redacted state event. +#[derive(Clone, Debug, Event)] +pub struct RedactedStateEvent { + /// Data specific to the event type. + pub content: C, + + /// The globally unique event identifier for the user who sent the event. + pub event_id: EventId, + + /// The fully-qualified ID of the user who sent this event. + pub sender: UserId, + + /// Timestamp in milliseconds on originating homeserver when this event was sent. + pub origin_server_ts: SystemTime, + + /// The ID of the room associated with this event. + pub room_id: RoomId, + + /// A unique key which defines the overwriting semantics for this piece of room state. + /// + /// This is often an empty string, but some events send a `UserId` to show + /// which user the event affects. + pub state_key: String, + + /// Additional key-value pairs not signed by the homeserver. + pub unsigned: UnsignedData, +} + +/// A redacted state event without a `room_id`. +#[derive(Clone, Debug, Event)] +pub struct RedactedStateEventStub { + /// Data specific to the event type. + // #[serde(default, skip_serializing_if = "is_zst")] + pub content: C, + + /// The globally unique event identifier for the user who sent the event. + pub event_id: EventId, + + /// The fully-qualified ID of the user who sent this event. + pub sender: UserId, + + /// Timestamp in milliseconds on originating homeserver when this event was sent. + pub origin_server_ts: SystemTime, + + /// A unique key which defines the overwriting semantics for this piece of room state. + /// + /// This is often an empty string, but some events send a `UserId` to show + /// which user the event affects. + pub state_key: String, + + /// Additional key-value pairs not signed by the homeserver. + pub unsigned: UnsignedData, +} + +/// A stripped-down redacted state event. +#[derive(Clone, Debug, Event)] +pub struct RedactedStrippedStateEventStub { + /// Data specific to the event type. + pub content: C, + + /// The fully-qualified ID of the user who sent this event. + pub sender: UserId, + + /// A unique key which defines the overwriting semantics for this piece of room state. + /// + /// This is often an empty string, but some events send a `UserId` to show + /// which user the event affects. + pub state_key: String, +} + /// An event sent using send-to-device messaging. #[derive(Clone, Debug, Event)] pub struct ToDeviceEvent { diff --git a/ruma-events/src/lib.rs b/ruma-events/src/lib.rs index 25cebfc1..b38c122e 100644 --- a/ruma-events/src/lib.rs +++ b/ruma-events/src/lib.rs @@ -166,13 +166,17 @@ pub use self::{ enums::{ AnyBasicEvent, AnyBasicEventContent, AnyEphemeralRoomEvent, AnyEphemeralRoomEventContent, AnyEphemeralRoomEventStub, AnyEvent, AnyMessageEvent, AnyMessageEventContent, - AnyMessageEventStub, AnyRoomEvent, AnyRoomEventStub, AnyStateEvent, AnyStateEventContent, - AnyStateEventStub, AnyStrippedStateEventStub, AnyToDeviceEvent, AnyToDeviceEventContent, + AnyMessageEventStub, AnyRedactedMessageEvent, AnyRedactedMessageEventStub, + AnyRedactedStateEvent, AnyRedactedStateEventStub, AnyRedactedStrippedStateEventStub, + AnyRoomEvent, AnyRoomEventStub, AnyStateEvent, AnyStateEventContent, AnyStateEventStub, + AnyStrippedStateEventStub, AnyToDeviceEvent, AnyToDeviceEventContent, }, error::{FromStrError, InvalidInput}, event_kinds::{ BasicEvent, EphemeralRoomEvent, EphemeralRoomEventStub, MessageEvent, MessageEventStub, - StateEvent, StateEventStub, StrippedStateEventStub, ToDeviceEvent, + RedactedMessageEvent, RedactedMessageEventStub, RedactedStateEvent, RedactedStateEventStub, + RedactedStrippedStateEventStub, StateEvent, StateEventStub, StrippedStateEventStub, + ToDeviceEvent, }, event_type::EventType, json::EventJson, @@ -237,6 +241,38 @@ pub trait MessageEventContent: RoomEventContent {} /// Marker trait for the content of a state event. pub trait StateEventContent: RoomEventContent {} +/// The base trait that all redacted event content types implement. +/// +/// Implementing this trait allows content types to be serialized as well as deserialized. +pub trait RedactedEventContent: EventContent { + /// Constructs the redacted event content. + /// + /// If called for anything but "empty" redacted content this will error. + fn empty(_event_type: &str) -> Result { + Err(serde::de::Error::custom("this event is not redacted")) + } + + /// Determines if the redacted event content needs to serialize fields. + fn has_serialize_fields(&self) -> bool; + + /// Determines if the redacted event content needs to deserialize fields. + fn has_deserialize_fields() -> bool; +} + +/// Marker trait for the content of a redacted message event. +pub trait RedactedMessageEventContent: RedactedEventContent {} + +/// Marker trait for the content of a redacted state event. +pub trait RedactedStateEventContent: RedactedEventContent {} + +/// Helper struct to determine if the event has been redacted. +#[doc(hidden)] +#[derive(Debug, Deserialize)] +pub struct UnsignedDeHelper { + /// This is the field that signals an event has been redacted. + pub redacted_because: Option, +} + /// Helper struct to determine the event kind from a serde_json::value::RawValue. #[doc(hidden)] #[derive(Debug, Deserialize)] @@ -255,6 +291,10 @@ pub struct EventDeHelper { /// If no `event_id` or `state_key` are found but a `room_id` is present /// the event will be deserialized as a ephemeral event. pub room_id: Option, + + /// If this `UnsignedData` contains a redacted_because key the event is + /// immediately deserialized as a redacted event. + pub unsigned: Option, } /// Helper function for serde_json::value::RawValue deserialization. diff --git a/ruma-events/src/room/aliases.rs b/ruma-events/src/room/aliases.rs index 5dbe5673..cd1508aa 100644 --- a/ruma-events/src/room/aliases.rs +++ b/ruma-events/src/room/aliases.rs @@ -3,8 +3,9 @@ use ruma_events_macros::StateEventContent; use ruma_identifiers::RoomAliasId; use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue as RawJsonValue; -use crate::StateEvent; +use crate::{EventContent, RedactedEventContent, RedactedStateEventContent, StateEvent}; /// Informs the room about what room aliases it has been given. pub type AliasesEvent = StateEvent; @@ -12,7 +13,49 @@ pub type AliasesEvent = StateEvent; /// The payload for `AliasesEvent`. #[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)] #[ruma_event(type = "m.room.aliases")] +#[ruma_event(custom_redacted)] pub struct AliasesEventContent { /// A list of room aliases. pub aliases: Vec, } + +/// An aliases event that has been redacted. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RedactedAliasesEventContent { + /// A list of room aliases. + /// + /// According to the Matrix spec version 1 redaction rules allowed this field to be + /// kept after redaction, this was changed in version 6. + pub aliases: Option>, +} + +impl EventContent for RedactedAliasesEventContent { + fn event_type(&self) -> &str { + "m.room.aliases" + } + + fn from_parts(event_type: &str, content: Box) -> Result { + if event_type != "m.room.aliases" { + return Err(::serde::de::Error::custom(format!( + "expected event type `m.room.aliases`, 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 RedactedAliasesEventContent { + fn has_serialize_fields(&self) -> bool { + self.aliases.is_some() + } + + fn has_deserialize_fields() -> bool { + true + } +} + +impl RedactedStateEventContent for RedactedAliasesEventContent {} diff --git a/ruma-events/src/room/create.rs b/ruma-events/src/room/create.rs index 7895e9cd..198f3fca 100644 --- a/ruma-events/src/room/create.rs +++ b/ruma-events/src/room/create.rs @@ -17,6 +17,7 @@ pub type CreateEvent = StateEvent; #[ruma_event(type = "m.room.create")] pub struct CreateEventContent { /// The `user_id` of the room creator. This is set by the homeserver. + #[ruma_event(skip_redaction)] pub creator: UserId, /// Whether or not this room's data should be transferred to other homeservers. diff --git a/ruma-events/src/room/history_visibility.rs b/ruma-events/src/room/history_visibility.rs index 8142266b..4062bbfa 100644 --- a/ruma-events/src/room/history_visibility.rs +++ b/ruma-events/src/room/history_visibility.rs @@ -15,6 +15,7 @@ pub type HistoryVisibilityEvent = StateEvent; #[ruma_event(type = "m.room.history_visibility")] pub struct HistoryVisibilityEventContent { /// Who can see the room history. + #[ruma_event(skip_redaction)] pub history_visibility: HistoryVisibility, } diff --git a/ruma-events/src/room/join_rules.rs b/ruma-events/src/room/join_rules.rs index 2a212f20..d48d7cc0 100644 --- a/ruma-events/src/room/join_rules.rs +++ b/ruma-events/src/room/join_rules.rs @@ -14,6 +14,7 @@ pub type JoinRulesEvent = StateEvent; #[ruma_event(type = "m.room.join_rules")] pub struct JoinRulesEventContent { /// The type of rules used for users wishing to join this room. + #[ruma_event(skip_redaction)] pub join_rule: JoinRule, } diff --git a/ruma-events/src/room/member.rs b/ruma-events/src/room/member.rs index 63ea364d..96f85b41 100644 --- a/ruma-events/src/room/member.rs +++ b/ruma-events/src/room/member.rs @@ -54,6 +54,7 @@ pub struct MemberEventContent { 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 diff --git a/ruma-events/src/room/power_levels.rs b/ruma-events/src/room/power_levels.rs index 8ca5ebc0..e2ec01dc 100644 --- a/ruma-events/src/room/power_levels.rs +++ b/ruma-events/src/room/power_levels.rs @@ -18,16 +18,19 @@ pub type PowerLevelsEvent = StateEvent; pub struct PowerLevelsEventContent { /// The level required to ban a user. #[serde(default = "default_power_level", skip_serializing_if = "is_default_power_level")] + #[ruma_event(skip_redaction)] pub ban: Int, /// The level required to send specific event types. /// /// This is a mapping from event type to power level required. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + #[ruma_event(skip_redaction)] pub events: BTreeMap, /// The default level required to send message events. #[serde(default, skip_serializing_if = "ruma_serde::is_default")] + #[ruma_event(skip_redaction)] pub events_default: Int, /// The level required to invite a user. @@ -36,24 +39,29 @@ pub struct PowerLevelsEventContent { /// The level required to kick a user. #[serde(default = "default_power_level", skip_serializing_if = "is_default_power_level")] + #[ruma_event(skip_redaction)] pub kick: Int, /// The level required to redact an event. #[serde(default = "default_power_level", skip_serializing_if = "is_default_power_level")] + #[ruma_event(skip_redaction)] pub redact: Int, /// The default level required to send state events. #[serde(default = "default_power_level", skip_serializing_if = "is_default_power_level")] + #[ruma_event(skip_redaction)] pub state_default: Int, /// The power levels for specific users. /// /// This is a mapping from `user_id` to power level for that user. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + #[ruma_event(skip_redaction)] pub users: BTreeMap, /// The default power level for every user in the room. #[serde(default, skip_serializing_if = "ruma_serde::is_default")] + #[ruma_event(skip_redaction)] pub users_default: Int, /// The power level requirements for specific notification types. diff --git a/ruma-events/src/room/redaction.rs b/ruma-events/src/room/redaction.rs index eaa7e581..d43d3a12 100644 --- a/ruma-events/src/room/redaction.rs +++ b/ruma-events/src/room/redaction.rs @@ -6,7 +6,10 @@ use ruma_events_macros::{Event, EventContent}; use ruma_identifiers::{EventId, RoomId, UserId}; use serde::{Deserialize, Serialize}; -use crate::UnsignedData; +use crate::{ + MessageEventContent, RedactedMessageEventContent, RedactedStateEventContent, RoomEventContent, + UnsignedData, +}; /// Redaction event. #[derive(Clone, Debug, Event)] @@ -64,84 +67,10 @@ pub struct RedactionEventContent { pub reason: Option, } -impl ruma_events::RoomEventContent for RedactionEventContent {} +impl RoomEventContent for RedactionEventContent {} -impl ruma_events::MessageEventContent for RedactionEventContent {} +impl MessageEventContent for RedactionEventContent {} -#[cfg(test)] -mod tests { - use std::{ - convert::TryFrom, - time::{Duration, UNIX_EPOCH}, - }; +impl RedactedMessageEventContent for RedactedRedactionEventContent {} - use matches::assert_matches; - use ruma_identifiers::{EventId, RoomId, UserId}; - use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; - - use super::{RedactionEvent, RedactionEventContent}; - use crate::{EventJson, UnsignedData}; - - #[test] - fn serialization() { - let event = RedactionEvent { - content: RedactionEventContent { reason: Some("redacted because".into()) }, - redacts: EventId::try_from("$h29iv0s8:example.com").unwrap(), - event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), - origin_server_ts: UNIX_EPOCH + Duration::from_millis(1), - room_id: RoomId::try_from("!roomid:room.com").unwrap(), - sender: UserId::try_from("@carl:example.com").unwrap(), - unsigned: UnsignedData::default(), - }; - - let json = json!({ - "content": { - "reason": "redacted because" - }, - "event_id": "$h29iv0s8:example.com", - "origin_server_ts": 1, - "redacts": "$h29iv0s8:example.com", - "room_id": "!roomid:room.com", - "sender": "@carl:example.com", - "type": "m.room.redaction", - }); - - assert_eq!(to_json_value(&event).unwrap(), json); - } - - #[test] - fn deserialization() { - let e_id = EventId::try_from("$h29iv0s8:example.com").unwrap(); - - let json = json!({ - "content": { - "reason": "redacted because" - }, - "event_id": "$h29iv0s8:example.com", - "origin_server_ts": 1, - "redacts": "$h29iv0s8:example.com", - "room_id": "!roomid:room.com", - "sender": "@carl:example.com", - "type": "m.room.redaction", - }); - - assert_matches!( - from_json_value::>(json) - .unwrap() - .deserialize() - .unwrap(), - RedactionEvent { - content: RedactionEventContent { - reason: Some(reason), - }, - sender, - event_id, origin_server_ts, redacts, room_id, unsigned, - } if reason == "redacted because" && redacts == e_id - && event_id == e_id - && sender == "@carl:example.com" - && origin_server_ts == UNIX_EPOCH + Duration::from_millis(1) - && room_id == RoomId::try_from("!roomid:room.com").unwrap() - && unsigned.is_empty() - ); - } -} +impl RedactedStateEventContent for RedactedRedactionEventContent {} diff --git a/ruma-events/src/tag.rs b/ruma-events/src/tag.rs index 84191b57..2f828947 100644 --- a/ruma-events/src/tag.rs +++ b/ruma-events/src/tag.rs @@ -9,6 +9,7 @@ use crate::BasicEvent; /// Informs the client of tags on a room. pub type TagEvent = BasicEvent; + /// Map of tag names to tag info. pub type Tags = BTreeMap; diff --git a/ruma-events/tests/redacted.rs b/ruma-events/tests/redacted.rs new file mode 100644 index 00000000..6e043fa0 --- /dev/null +++ b/ruma-events/tests/redacted.rs @@ -0,0 +1,268 @@ +use std::{ + convert::TryFrom, + time::{Duration, UNIX_EPOCH}, +}; + +use matches::assert_matches; +use ruma_events::{ + custom::RedactedCustomEventContent, + room::{ + aliases::RedactedAliasesEventContent, + create::RedactedCreateEventContent, + message::RedactedMessageEventContent, + redaction::{RedactionEvent, RedactionEventContent}, + }, + AnyRedactedMessageEvent, AnyRedactedMessageEventStub, AnyRedactedStateEventStub, AnyRoomEvent, + AnyRoomEventStub, EventJson, RedactedMessageEvent, RedactedMessageEventStub, + RedactedStateEventStub, UnsignedData, +}; +use ruma_identifiers::{EventId, RoomId, UserId}; +use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; + +fn is_zst(_: &T) -> bool { + std::mem::size_of::() == 0 +} + +#[test] +fn redacted_message_event_serialize() { + let redacted = RedactedMessageEventStub { + content: RedactedMessageEventContent, + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + origin_server_ts: UNIX_EPOCH + Duration::from_millis(1), + sender: UserId::try_from("@carl:example.com").unwrap(), + unsigned: UnsignedData::default(), + }; + + let expected = json!({ + "event_id": "$h29iv0s8:example.com", + "origin_server_ts": 1, + "sender": "@carl:example.com", + "type": "m.room.message" + }); + + let actual = to_json_value(&redacted).unwrap(); + assert_eq!(actual, expected); +} + +#[test] +fn redacted_aliases_event_serialize() { + let redacted = RedactedStateEventStub { + content: RedactedAliasesEventContent { aliases: None }, + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + state_key: "".to_string(), + origin_server_ts: UNIX_EPOCH + Duration::from_millis(1), + sender: UserId::try_from("@carl:example.com").unwrap(), + unsigned: UnsignedData::default(), + }; + + let expected = json!({ + "event_id": "$h29iv0s8:example.com", + "state_key": "", + "origin_server_ts": 1, + "sender": "@carl:example.com", + "type": "m.room.aliases" + }); + + let actual = to_json_value(&redacted).unwrap(); + assert_eq!(actual, expected); +} + +#[test] +fn redacted_deserialize_any_room() { + let mut unsigned = UnsignedData::default(); + // The presence of `redacted_because` triggers the event enum (AnyRoomEvent in this case) + // to return early with `RedactedContent` instead of failing to deserialize according + // to the event type string. + unsigned.redacted_because = Some(EventJson::from(RedactionEvent { + content: RedactionEventContent { reason: Some("redacted because".into()) }, + redacts: EventId::try_from("$h29iv0s8:example.com").unwrap(), + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + origin_server_ts: UNIX_EPOCH + Duration::from_millis(1), + room_id: RoomId::try_from("!roomid:room.com").unwrap(), + sender: UserId::try_from("@carl:example.com").unwrap(), + unsigned: UnsignedData::default(), + })); + + let redacted = json!({ + "event_id": "$h29iv0s8:example.com", + "room_id": "!roomid:room.com", + "origin_server_ts": 1, + "sender": "@carl:example.com", + "unsigned": unsigned, + "type": "m.room.message" + }); + + let actual = to_json_value(&redacted).unwrap(); + + assert_matches!( + from_json_value::>(actual) + .unwrap() + .deserialize() + .unwrap(), + AnyRoomEvent::RedactedMessage(AnyRedactedMessageEvent::RoomMessage(RedactedMessageEvent { + event_id, room_id, content, .. + })) if event_id == EventId::try_from("$h29iv0s8:example.com").unwrap() + && room_id == RoomId::try_from("!roomid:room.com").unwrap() + && is_zst(&content) + ) +} + +#[test] +fn redacted_deserialize_any_room_stub() { + let mut unsigned = UnsignedData::default(); + // The presence of `redacted_because` triggers the event enum (AnyRoomEventStub in this case) + // to return early with `RedactedContent` instead of failing to deserialize according + // to the event type string. + unsigned.redacted_because = Some(EventJson::from(RedactionEvent { + content: RedactionEventContent { reason: Some("redacted because".into()) }, + redacts: EventId::try_from("$h29iv0s8:example.com").unwrap(), + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + origin_server_ts: UNIX_EPOCH + Duration::from_millis(1), + room_id: RoomId::try_from("!roomid:room.com").unwrap(), + sender: UserId::try_from("@carl:example.com").unwrap(), + unsigned: UnsignedData::default(), + })); + + let redacted = json!({ + "event_id": "$h29iv0s8:example.com", + "origin_server_ts": 1, + "sender": "@carl:example.com", + "unsigned": unsigned, + "type": "m.room.message" + }); + + let actual = to_json_value(&redacted).unwrap(); + + assert_matches!( + from_json_value::>(actual) + .unwrap() + .deserialize() + .unwrap(), + AnyRoomEventStub::RedactedMessage(AnyRedactedMessageEventStub::RoomMessage(RedactedMessageEventStub { + event_id, content, .. + })) if event_id == EventId::try_from("$h29iv0s8:example.com").unwrap() + && is_zst(&content) + ) +} + +#[test] +fn redacted_state_event_deserialize() { + let mut unsigned = UnsignedData::default(); + unsigned.redacted_because = Some(EventJson::from(RedactionEvent { + content: RedactionEventContent { reason: Some("redacted because".into()) }, + redacts: EventId::try_from("$h29iv0s8:example.com").unwrap(), + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + origin_server_ts: UNIX_EPOCH + Duration::from_millis(1), + room_id: RoomId::try_from("!roomid:room.com").unwrap(), + sender: UserId::try_from("@carl:example.com").unwrap(), + unsigned: UnsignedData::default(), + })); + + let redacted = json!({ + "content": { + "creator": "@carl:example.com", + }, + "event_id": "$h29iv0s8:example.com", + "origin_server_ts": 1, + "sender": "@carl:example.com", + "state_key": "hello there", + "unsigned": unsigned, + "type": "m.room.create" + }); + + assert_matches!( + from_json_value::>(redacted) + .unwrap() + .deserialize() + .unwrap(), + AnyRoomEventStub::RedactedState(AnyRedactedStateEventStub::RoomCreate(RedactedStateEventStub { + content: RedactedCreateEventContent { + creator, + }, + event_id, state_key, unsigned, .. + })) if event_id == EventId::try_from("$h29iv0s8:example.com").unwrap() + && unsigned.redacted_because.is_some() + && state_key == "hello there" + && creator == UserId::try_from("@carl:example.com").unwrap() + ) +} + +#[test] +fn redacted_custom_event_serialize() { + let mut unsigned = UnsignedData::default(); + unsigned.redacted_because = Some(EventJson::from(RedactionEvent { + content: RedactionEventContent { reason: Some("redacted because".into()) }, + redacts: EventId::try_from("$h29iv0s8:example.com").unwrap(), + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + origin_server_ts: UNIX_EPOCH + Duration::from_millis(1), + room_id: RoomId::try_from("!roomid:room.com").unwrap(), + sender: UserId::try_from("@carl:example.com").unwrap(), + unsigned: UnsignedData::default(), + })); + + let redacted = json!({ + "event_id": "$h29iv0s8:example.com", + "origin_server_ts": 1, + "sender": "@carl:example.com", + "state_key": "hello there", + "unsigned": unsigned, + "type": "m.made.up" + }); + + assert_matches!( + from_json_value::>(redacted.clone()) + .unwrap() + .deserialize() + .unwrap(), + AnyRoomEventStub::RedactedState(AnyRedactedStateEventStub::Custom(RedactedStateEventStub { + content: RedactedCustomEventContent { + event_type, + }, + event_id, state_key, unsigned, .. + })) if event_id == EventId::try_from("$h29iv0s8:example.com").unwrap() + && unsigned.redacted_because.is_some() + && state_key == "hello there" + && event_type == "m.made.up" + ); + + let x = from_json_value::>(redacted) + .unwrap() + .deserialize() + .unwrap(); + assert_eq!(x.event_id(), &EventId::try_from("$h29iv0s8:example.com").unwrap()) +} + +#[test] +fn redacted_custom_event_deserialize() { + let mut unsigned = UnsignedData::default(); + unsigned.redacted_because = Some(EventJson::from(RedactionEvent { + content: RedactionEventContent { reason: Some("redacted because".into()) }, + redacts: EventId::try_from("$h29iv0s8:example.com").unwrap(), + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + origin_server_ts: UNIX_EPOCH + Duration::from_millis(1), + room_id: RoomId::try_from("!roomid:room.com").unwrap(), + sender: UserId::try_from("@carl:example.com").unwrap(), + unsigned: UnsignedData::default(), + })); + + let redacted = RedactedStateEventStub { + content: RedactedCustomEventContent { event_type: "m.made.up".to_string() }, + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + sender: UserId::try_from("@carl:example.com").unwrap(), + state_key: "hello there".to_string(), + origin_server_ts: UNIX_EPOCH + Duration::from_millis(1), + unsigned: unsigned.clone(), + }; + + let expected = json!({ + "event_id": "$h29iv0s8:example.com", + "origin_server_ts": 1, + "sender": "@carl:example.com", + "state_key": "hello there", + "unsigned": unsigned, + "type": "m.made.up" + }); + + let actual = to_json_value(&redacted).unwrap(); + assert_eq!(actual, expected); +} diff --git a/ruma-events/tests/ui/03-invalid-event-type.stderr b/ruma-events/tests/ui/03-invalid-event-type.stderr index d4fc4a3b..0569550d 100644 --- a/ruma-events/tests/ui/03-invalid-event-type.stderr +++ b/ruma-events/tests/ui/03-invalid-event-type.stderr @@ -1,4 +1,4 @@ -error: expected `type` +error: not a recognized `ruma_event` attribute --> $DIR/03-invalid-event-type.rs:11:14 | 11 | #[ruma_event(event = "m.macro.test")]