From cd74cdcc0ea25b167f9d940865b6bf16386d77bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= <76261501+zecakeh@users.noreply.github.com> Date: Tue, 3 Jan 2023 10:28:40 +0100 Subject: [PATCH] events: Generate PossiblyRedacted type for original state events Fix deserialization of redacted prev_content Can be overriden with the `custom_possibly_redacted` attribute Co-authored-by: Jonas Platte --- crates/ruma-common/CHANGELOG.md | 1 + crates/ruma-common/src/events/_custom.rs | 1 + crates/ruma-common/src/events/content.rs | 3 + crates/ruma-common/src/events/kinds.rs | 6 +- crates/ruma-common/src/events/policy/rule.rs | 22 ++ .../src/events/policy/rule/room.rs | 35 ++- .../src/events/policy/rule/server.rs | 35 ++- .../src/events/policy/rule/user.rs | 35 ++- crates/ruma-common/src/events/room/member.rs | 8 +- .../ruma-common/tests/events/state_event.rs | 6 +- .../events/ui/03-invalid-event-type.stderr | 2 +- .../ruma-macros/src/events/event_content.rs | 222 +++++++++++++++++- 12 files changed, 352 insertions(+), 24 deletions(-) diff --git a/crates/ruma-common/CHANGELOG.md b/crates/ruma-common/CHANGELOG.md index 857b3eee..6f63dfe8 100644 --- a/crates/ruma-common/CHANGELOG.md +++ b/crates/ruma-common/CHANGELOG.md @@ -8,6 +8,7 @@ Bug fixes: and `events::secret` modules * Fix deserialization of `RoomMessageEventContent` and `RoomEncryptedEventContent` when there is no relation +* Fix deserialization of `StateUnsigned` when the `prev_content` is redacted Breaking changes: diff --git a/crates/ruma-common/src/events/_custom.rs b/crates/ruma-common/src/events/_custom.rs index b26acbfc..24697a92 100644 --- a/crates/ruma-common/src/events/_custom.rs +++ b/crates/ruma-common/src/events/_custom.rs @@ -72,6 +72,7 @@ impl StateEventContent for CustomStateEventContent { } impl OriginalStateEventContent for CustomStateEventContent { type Unsigned = StateUnsigned; + type PossiblyRedacted = Self; } impl RedactedStateEventContent for CustomStateEventContent {} diff --git a/crates/ruma-common/src/events/content.rs b/crates/ruma-common/src/events/content.rs index 82ba9bbb..3a0a5797 100644 --- a/crates/ruma-common/src/events/content.rs +++ b/crates/ruma-common/src/events/content.rs @@ -130,6 +130,9 @@ pub trait StateEventContent: EventContent { pub trait OriginalStateEventContent: StateEventContent + RedactContent { /// The type of the event's `unsigned` field. type Unsigned: Clone + fmt::Debug + Default + CanBeEmpty + StateUnsignedFromParts; + + /// The possibly redacted form of the event's content. + type PossiblyRedacted: StateEventContent; } /// Content of a redacted state event. diff --git a/crates/ruma-common/src/events/kinds.rs b/crates/ruma-common/src/events/kinds.rs index eedb9ab3..a71be062 100644 --- a/crates/ruma-common/src/events/kinds.rs +++ b/crates/ruma-common/src/events/kinds.rs @@ -471,7 +471,7 @@ pub struct DecryptedMegolmV1Event { /// A non-redacted content also contains the `prev_content` from the unsigned event data. #[allow(clippy::exhaustive_enums)] #[derive(Clone, Debug)] -pub enum FullStateEventContent +pub enum FullStateEventContent where C::Redacted: RedactedStateEventContent, { @@ -481,14 +481,14 @@ where content: C, /// Previous content of the room state. - prev_content: Option, + prev_content: Option, }, /// Redacted content of the event. Redacted(C::Redacted), } -impl FullStateEventContent +impl FullStateEventContent where C::Redacted: RedactedStateEventContent, { diff --git a/crates/ruma-common/src/events/policy/rule.rs b/crates/ruma-common/src/events/policy/rule.rs index e0634f31..c5e39b24 100644 --- a/crates/ruma-common/src/events/policy/rule.rs +++ b/crates/ruma-common/src/events/policy/rule.rs @@ -32,6 +32,28 @@ impl PolicyRuleEventContent { } } +/// The possibly redacted form of [`PolicyRuleEventContent`]. +/// +/// This type is used when it's not obvious whether the content is redacted or not. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct PossiblyRedactedPolicyRuleEventContent { + /// The entity affected by this rule. + /// + /// Glob characters `*` and `?` can be used to match zero or more characters or exactly one + /// character respectively. + #[serde(skip_serializing_if = "Option::is_none")] + pub entity: Option, + + /// The suggested action to take. + #[serde(skip_serializing_if = "Option::is_none")] + pub recommendation: Option, + + /// The human-readable description for the recommendation. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + /// The possible actions that can be taken. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] diff --git a/crates/ruma-common/src/events/policy/rule/room.rs b/crates/ruma-common/src/events/policy/rule/room.rs index 0fcc9bc5..6e017025 100644 --- a/crates/ruma-common/src/events/policy/rule/room.rs +++ b/crates/ruma-common/src/events/policy/rule/room.rs @@ -4,17 +4,48 @@ use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue as RawJsonValue; -use super::PolicyRuleEventContent; +use super::{PolicyRuleEventContent, PossiblyRedactedPolicyRuleEventContent}; +use crate::events::{EventContent, StateEventContent, StateEventType}; /// The content of an `m.policy.rule.room` event. /// /// This event type is used to apply rules to room entities. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[allow(clippy::exhaustive_structs)] -#[ruma_event(type = "m.policy.rule.room", kind = State, state_key_type = String)] +#[ruma_event(type = "m.policy.rule.room", kind = State, state_key_type = String, custom_possibly_redacted)] pub struct PolicyRuleRoomEventContent(pub PolicyRuleEventContent); +/// The possibly redacted form of [`PolicyRuleRoomEventContent`]. +/// +/// This type is used when it's not obvious whether the content is redacted or not. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[allow(clippy::exhaustive_structs)] +pub struct PossiblyRedactedPolicyRuleRoomEventContent(pub PossiblyRedactedPolicyRuleEventContent); + +impl EventContent for PossiblyRedactedPolicyRuleRoomEventContent { + type EventType = StateEventType; + + fn event_type(&self) -> Self::EventType { + StateEventType::PolicyRuleRoom + } + + fn from_parts(event_type: &str, content: &RawJsonValue) -> serde_json::Result { + if event_type != "m.policy.rule.room" { + return Err(::serde::de::Error::custom(format!( + "expected event type `m.policy.rule.room`, found `{event_type}`", + ))); + } + + serde_json::from_str(content.get()) + } +} + +impl StateEventContent for PossiblyRedactedPolicyRuleRoomEventContent { + type StateKey = String; +} + #[cfg(test)] mod tests { use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; diff --git a/crates/ruma-common/src/events/policy/rule/server.rs b/crates/ruma-common/src/events/policy/rule/server.rs index d4fe159a..ce6084e9 100644 --- a/crates/ruma-common/src/events/policy/rule/server.rs +++ b/crates/ruma-common/src/events/policy/rule/server.rs @@ -4,13 +4,44 @@ use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue as RawJsonValue; -use super::PolicyRuleEventContent; +use super::{PolicyRuleEventContent, PossiblyRedactedPolicyRuleEventContent}; +use crate::events::{EventContent, StateEventContent, StateEventType}; /// The content of an `m.policy.rule.server` event. /// /// This event type is used to apply rules to server entities. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[allow(clippy::exhaustive_structs)] -#[ruma_event(type = "m.policy.rule.server", kind = State, state_key_type = String)] +#[ruma_event(type = "m.policy.rule.server", kind = State, state_key_type = String, custom_possibly_redacted)] pub struct PolicyRuleServerEventContent(pub PolicyRuleEventContent); + +/// The possibly redacted form of [`PolicyRuleServerEventContent`]. +/// +/// This type is used when it's not obvious whether the content is redacted or not. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[allow(clippy::exhaustive_structs)] +pub struct PossiblyRedactedPolicyRuleServerEventContent(pub PossiblyRedactedPolicyRuleEventContent); + +impl EventContent for PossiblyRedactedPolicyRuleServerEventContent { + type EventType = StateEventType; + + fn event_type(&self) -> Self::EventType { + StateEventType::PolicyRuleServer + } + + fn from_parts(event_type: &str, content: &RawJsonValue) -> serde_json::Result { + if event_type != "m.policy.rule.server" { + return Err(::serde::de::Error::custom(format!( + "expected event type `m.policy.rule.server`, found `{event_type}`", + ))); + } + + serde_json::from_str(content.get()) + } +} + +impl StateEventContent for PossiblyRedactedPolicyRuleServerEventContent { + type StateKey = String; +} diff --git a/crates/ruma-common/src/events/policy/rule/user.rs b/crates/ruma-common/src/events/policy/rule/user.rs index 6277a5bf..dfcd267c 100644 --- a/crates/ruma-common/src/events/policy/rule/user.rs +++ b/crates/ruma-common/src/events/policy/rule/user.rs @@ -4,13 +4,44 @@ use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue as RawJsonValue; -use super::PolicyRuleEventContent; +use super::{PolicyRuleEventContent, PossiblyRedactedPolicyRuleEventContent}; +use crate::events::{EventContent, StateEventContent, StateEventType}; /// The content of an `m.policy.rule.user` event. /// /// This event type is used to apply rules to user entities. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[allow(clippy::exhaustive_structs)] -#[ruma_event(type = "m.policy.rule.user", kind = State, state_key_type = String)] +#[ruma_event(type = "m.policy.rule.user", kind = State, state_key_type = String, custom_possibly_redacted)] pub struct PolicyRuleUserEventContent(pub PolicyRuleEventContent); + +/// The possibly redacted form of [`PolicyRuleUserEventContent`]. +/// +/// This type is used when it's not obvious whether the content is redacted or not. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[allow(clippy::exhaustive_structs)] +pub struct PossiblyRedactedPolicyRuleUserEventContent(pub PossiblyRedactedPolicyRuleEventContent); + +impl EventContent for PossiblyRedactedPolicyRuleUserEventContent { + type EventType = StateEventType; + + fn event_type(&self) -> Self::EventType { + StateEventType::PolicyRuleUser + } + + fn from_parts(event_type: &str, content: &RawJsonValue) -> serde_json::Result { + if event_type != "m.policy.rule.user" { + return Err(::serde::de::Error::custom(format!( + "expected event type `m.policy.rule.user`, found `{event_type}`", + ))); + } + + serde_json::from_str(content.get()) + } +} + +impl StateEventContent for PossiblyRedactedPolicyRuleUserEventContent { + type StateKey = String; +} diff --git a/crates/ruma-common/src/events/room/member.rs b/crates/ruma-common/src/events/room/member.rs index 9f85cc6f..70237fee 100644 --- a/crates/ruma-common/src/events/room/member.rs +++ b/crates/ruma-common/src/events/room/member.rs @@ -53,6 +53,7 @@ pub use self::change::{Change, MembershipChange, MembershipDetails}; state_key_type = OwnedUserId, unsigned_type = RoomMemberUnsigned, custom_redacted, + custom_possibly_redacted, )] pub struct RoomMemberEventContent { /// The avatar URL for this user, if any. @@ -179,6 +180,11 @@ impl RedactContent for RoomMemberEventContent { } } +/// The possibly redacted form of [`RoomMemberEventContent`]. +/// +/// This type is used when it's not obvious whether the content is redacted or not. +pub type PossiblyRedactedRoomMemberEventContent = RoomMemberEventContent; + /// A member event that has been redacted. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] @@ -509,7 +515,7 @@ pub struct RoomMemberUnsigned { pub transaction_id: Option, /// Optional previous content of the event. - pub prev_content: Option, + pub prev_content: Option, /// State events to assist the receiver in identifying the room. #[serde(default)] diff --git a/crates/ruma-common/tests/events/state_event.rs b/crates/ruma-common/tests/events/state_event.rs index 8418ca3b..6b58a398 100644 --- a/crates/ruma-common/tests/events/state_event.rs +++ b/crates/ruma-common/tests/events/state_event.rs @@ -60,7 +60,7 @@ fn deserialize_aliases_with_prev_content() { assert_eq!(ev.sender, "@carl:example.com"); let prev_content = ev.unsigned.prev_content.unwrap(); - assert_eq!(prev_content.aliases, vec![room_alias_id!("#inner:localhost")]); + assert_eq!(prev_content.aliases.unwrap(), vec![room_alias_id!("#inner:localhost")]); } #[test] @@ -78,7 +78,7 @@ fn deserialize_aliases_sync_with_room_id() { assert_eq!(ev.sender, "@carl:example.com"); let prev_content = ev.unsigned.prev_content.unwrap(); - assert_eq!(prev_content.aliases, vec![room_alias_id!("#inner:localhost")]); + assert_eq!(prev_content.aliases.unwrap(), vec![room_alias_id!("#inner:localhost")]); } #[test] @@ -177,7 +177,7 @@ fn deserialize_full_event_convert_to_sync() { assert_eq!(sync_ev.event_id, "$h29iv0s8:example.com"); assert_eq!(sync_ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1))); assert_eq!( - sync_ev.unsigned.prev_content.unwrap().aliases, + sync_ev.unsigned.prev_content.unwrap().aliases.unwrap(), vec![room_alias_id!("#inner:localhost")] ); assert_eq!(sync_ev.sender, "@carl:example.com"); diff --git a/crates/ruma-common/tests/events/ui/03-invalid-event-type.stderr b/crates/ruma-common/tests/events/ui/03-invalid-event-type.stderr index d15dc467..18fac49b 100644 --- a/crates/ruma-common/tests/events/ui/03-invalid-event-type.stderr +++ b/crates/ruma-common/tests/events/ui/03-invalid-event-type.stderr @@ -6,7 +6,7 @@ error: no event type attribute found, add `#[ruma_event(type = "any.room.event", | = note: this error originates in the derive macro `EventContent` (in Nightly builds, run with -Z macro-backtrace for more info) -error: expected one of: `type`, `kind`, `custom_redacted`, `state_key_type`, `unsigned_type`, `alias`, `without_relation` +error: expected one of: `type`, `kind`, `custom_redacted`, `custom_possibly_redacted`, `state_key_type`, `unsigned_type`, `alias`, `without_relation` --> tests/events/ui/03-invalid-event-type.rs:11:14 | 11 | #[ruma_event(event = "m.macro.test", kind = State)] diff --git a/crates/ruma-macros/src/events/event_content.rs b/crates/ruma-macros/src/events/event_content.rs index d780ceaf..c8302c17 100644 --- a/crates/ruma-macros/src/events/event_content.rs +++ b/crates/ruma-macros/src/events/event_content.rs @@ -7,8 +7,9 @@ use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, ToTokens}; use syn::{ parse::{Parse, ParseStream}, + parse_quote, punctuated::Punctuated, - DeriveInput, Field, Ident, LitStr, Token, Type, + DeriveInput, Field, Ident, LitStr, Meta, NestedMeta, Token, Type, }; use crate::util::m_prefix_name_to_type_name; @@ -20,6 +21,8 @@ mod kw { syn::custom_keyword!(skip_redaction); // Do not emit any redacted event code. syn::custom_keyword!(custom_redacted); + // Do not emit any possibly redacted event code. + syn::custom_keyword!(custom_possibly_redacted); // The kind of event content this is. syn::custom_keyword!(kind); syn::custom_keyword!(type_fragment); @@ -66,6 +69,7 @@ struct ContentMeta { event_type: Option, event_kind: Option, custom_redacted: Option, + custom_possibly_redacted: Option, state_key_type: Option>, unsigned_type: Option>, aliases: Vec, @@ -101,6 +105,10 @@ impl ContentMeta { event_type: either_spanned(self.event_type, other.event_type)?, event_kind: either_named("event_kind", self.event_kind, other.event_kind)?, custom_redacted: either_spanned(self.custom_redacted, other.custom_redacted)?, + custom_possibly_redacted: either_spanned( + self.custom_possibly_redacted, + other.custom_possibly_redacted, + )?, state_key_type: either_spanned(self.state_key_type, other.state_key_type)?, unsigned_type: either_spanned(self.unsigned_type, other.unsigned_type)?, aliases: [self.aliases, other.aliases].concat(), @@ -128,6 +136,13 @@ impl Parse for ContentMeta { let custom_redacted: kw::custom_redacted = input.parse()?; Ok(Self { custom_redacted: Some(custom_redacted), ..Default::default() }) + } else if lookahead.peek(kw::custom_possibly_redacted) { + let custom_possibly_redacted: kw::custom_possibly_redacted = input.parse()?; + + Ok(Self { + custom_possibly_redacted: Some(custom_possibly_redacted), + ..Default::default() + }) } else if lookahead.peek(kw::state_key_type) { let _: kw::state_key_type = input.parse()?; let _: Token![=] = input.parse()?; @@ -162,7 +177,8 @@ struct ContentAttrs { state_key_type: Option, unsigned_type: Option, aliases: Vec, - is_custom: bool, + is_custom_redacted: bool, + is_custom_possibly_redacted: bool, has_without_relation: bool, } @@ -174,6 +190,7 @@ impl TryFrom for ContentAttrs { event_type, event_kind, custom_redacted, + custom_possibly_redacted, state_key_type, unsigned_type, aliases, @@ -206,7 +223,8 @@ impl TryFrom for ContentAttrs { } }; - let is_custom = custom_redacted.is_some(); + let is_custom_redacted = custom_redacted.is_some(); + let is_custom_possibly_redacted = custom_possibly_redacted.is_some(); let unsigned_type = unsigned_type.map(|ty| quote! { #ty }); @@ -244,7 +262,8 @@ impl TryFrom for ContentAttrs { state_key_type, unsigned_type, aliases, - is_custom, + is_custom_redacted, + is_custom_possibly_redacted, has_without_relation, }) } @@ -272,7 +291,8 @@ pub fn expand_event_content( state_key_type, unsigned_type, aliases, - is_custom, + is_custom_redacted, + is_custom_possibly_redacted, has_without_relation, } = content_meta.try_into()?; @@ -288,7 +308,7 @@ pub fn expand_event_content( }; // We only generate redacted content structs for state and message-like events - let redacted_event_content = needs_redacted(is_custom, event_kind).then(|| { + let redacted_event_content = needs_redacted(is_custom_redacted, event_kind).then(|| { generate_redacted_event_content( ident, fields.clone(), @@ -302,6 +322,22 @@ pub fn expand_event_content( .unwrap_or_else(syn::Error::into_compile_error) }); + // We only generate possibly redacted content structs for state events. + let possibly_redacted_event_content = + needs_possibly_redacted(is_custom_possibly_redacted, event_kind).then(|| { + generate_possibly_redacted_event_content( + ident, + fields.clone(), + &event_type, + event_kind, + state_key_type.as_ref(), + unsigned_type.clone(), + &aliases, + ruma_common, + ) + .unwrap_or_else(syn::Error::into_compile_error) + }); + let event_content_without_relation = has_without_relation.then(|| { generate_event_content_without_relation(ident, fields.clone(), ruma_common) .unwrap_or_else(syn::Error::into_compile_error) @@ -328,6 +364,7 @@ pub fn expand_event_content( Ok(quote! { #redacted_event_content + #possibly_redacted_event_content #event_content_without_relation #event_content_impl #static_event_content_impl @@ -452,6 +489,157 @@ fn generate_redacted_event_content<'a>( }) } +fn generate_possibly_redacted_event_content<'a>( + ident: &Ident, + fields: impl Iterator, + event_type: &LitStr, + event_kind: Option, + state_key_type: Option<&TokenStream>, + unsigned_type: Option, + aliases: &[LitStr], + ruma_common: &TokenStream, +) -> syn::Result { + assert!( + !event_type.value().contains('*'), + "Event type shouldn't contain a `*`, this should have been checked previously" + ); + + let serde = quote! { #ruma_common::exports::serde }; + + let doc = format!( + "The possibly redacted form of [`{ident}`].\n\n\ + This type is used when it's not obvious whether the content is redacted or not." + ); + let possibly_redacted_ident = format_ident!("PossiblyRedacted{ident}"); + + let mut field_changed = false; + let possibly_redacted_fields: Vec<_> = fields + .map(|f| { + let mut keep_field = false; + let mut unsupported_serde_attribute = None; + + if let Type::Path(type_path) = &f.ty { + if type_path.path.segments.first().filter(|s| s.ident == "Option").is_some() { + // Keep the field if it's an `Option`. + keep_field = true; + } + } + + let mut attrs = f + .attrs + .iter() + .map(|a| -> syn::Result<_> { + if a.path.is_ident("ruma_event") { + // Keep the field if it is not redacted. + if let EventFieldMeta::SkipRedaction = a.parse_args()? { + keep_field = true; + } + + // Don't re-emit our `ruma_event` attributes. + Ok(None) + } else { + if a.path.is_ident("serde") { + let serde_meta = a.parse_meta()?; + + if let Meta::List(list) = serde_meta { + for meta in list.nested.iter().filter_map(|nested_meta| match nested_meta { + NestedMeta::Meta(meta) => Some(meta), + NestedMeta::Lit(_) => None, + }) { + if meta.path().is_ident("default") { + // Keep the field if it deserializes to its default value. + keep_field = true; + } else if !meta.path().is_ident("rename") && !meta.path().is_ident("alias") && unsupported_serde_attribute.is_none() { + // Error if the field is not kept and uses an unsupported serde attribute. + unsupported_serde_attribute = Some( + syn::Error::new_spanned( + meta, + "Can't generate PossiblyRedacted struct with unsupported serde attribute\n\ + Expected one of `default`, `rename` or `alias`\n\ + Use the `custom_possibly_redacted` attribute and create the struct manually" + ) + ); + } + } + } + } + + Ok(Some(a.clone())) + } + }) + .filter_map(Result::transpose) + .collect::>()?; + + if keep_field { + Ok(Field { attrs, ..f.clone() }) + } else if let Some(err) = unsupported_serde_attribute { + Err(err) + } else if f.ident.is_none() { + // If the field has no `ident`, it's a tuple struct. Since `content` is an object, + // it will need a custom struct to deserialize from an empty object. + Err(syn::Error::new( + Span::call_site(), + "Can't generate PossiblyRedacted struct for tuple structs\n\ + Use the `custom_possibly_redacted` attribute and create the struct manually", + )) + } else { + // Change the field to an `Option`. + field_changed = true; + + let old_type = &f.ty; + let ty = parse_quote!{ Option<#old_type> }; + attrs.push(parse_quote! { #[serde(skip_serializing_if = "Option::is_none")] }); + + Ok(Field { attrs, ty, ..f.clone() }) + } + }) + .collect::>()?; + + // If at least one field needs to change, generate a new struct, else use a type alias. + if field_changed { + let possibly_redacted_event_content = generate_event_content_impl( + &possibly_redacted_ident, + possibly_redacted_fields.iter(), + event_type, + event_kind, + state_key_type, + unsigned_type, + aliases, + ruma_common, + false, + ) + .unwrap_or_else(syn::Error::into_compile_error); + + let static_event_content_impl = event_kind.map(|kind| { + generate_static_event_content_impl( + &possibly_redacted_ident, + kind, + true, + event_type, + ruma_common, + ) + }); + + Ok(quote! { + #[doc = #doc] + #[derive(Clone, Debug, #serde::Deserialize, #serde::Serialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct #possibly_redacted_ident { + #( #possibly_redacted_fields, )* + } + + #possibly_redacted_event_content + + #static_event_content_impl + }) + } else { + Ok(quote! { + #[doc = #doc] + pub type #possibly_redacted_ident = #ident; + }) + } +} + fn generate_event_content_without_relation<'a>( ident: &Ident, fields: impl Iterator, @@ -679,14 +867,17 @@ fn generate_event_content_impl<'a>( let original_state_event_content_impl = (event_kind == Some(EventKind::State) && is_original).then(|| { let trait_name = format_ident!("Original{kind}Content"); + let possibly_redacted_ident = format_ident!("PossiblyRedacted{ident}"); - let unsigned_type = unsigned_type - .unwrap_or_else(|| quote! { #ruma_common::events::StateUnsigned }); + let unsigned_type = unsigned_type.unwrap_or_else( + || quote! { #ruma_common::events::StateUnsigned }, + ); quote! { #[automatically_derived] impl #ruma_common::events::#trait_name for #ident { type Unsigned = #unsigned_type; + type PossiblyRedacted = #possibly_redacted_ident; } } }); @@ -797,9 +988,20 @@ fn generate_static_event_content_impl( } } -fn needs_redacted(is_custom: bool, event_kind: Option) -> bool { +fn needs_redacted(is_custom_redacted: bool, event_kind: Option) -> bool { // `is_custom` means that the content struct does not need a generated // redacted struct also. If no `custom_redacted` attrs are found the content // needs a redacted struct generated. - !is_custom && matches!(event_kind, Some(EventKind::MessageLike) | Some(EventKind::State)) + !is_custom_redacted + && matches!(event_kind, Some(EventKind::MessageLike) | Some(EventKind::State)) +} + +fn needs_possibly_redacted( + is_custom_possibly_redacted: bool, + event_kind: Option, +) -> bool { + // `is_custom_possibly_redacted` means that the content struct does not need + // a generated possibly redacted struct also. If no `custom_possibly_redacted` + // attrs are found the content needs a possibly redacted struct generated. + !is_custom_possibly_redacted && event_kind == Some(EventKind::State) }