Repository: ruma/ruma
Pull request #89 (2020-06-22 21:26:34)

Generate stub/stripped Any*Event enums in event_enum! macro
I didn't want to push this to the other PR incase it wasn't the direction you were thinking, if it is I can either merge it into the first PR or close that one. I also took a shot at handling the State, Message, and Basic event deserialization when a custom event is encountered.

jimmycuadra: Thanks for the PR! The spec doesn't say anything about what deactivating means other than "removing all ability for the user to login again." If the user record were to be deleted from the database, that might be a problem since there will still be events in the graph for any rooms they joined that reference them. Given the limited scope of being able to log in that the spec describes, I think you're right that we should just have an "active" field for users that is true initially and stays true until the deactivate. Then we'd need to check the value of this field before issuing any new access tokens for the user.
erikjohnston: (The spec may also want to mention that the API should probably also delete
any personal information associated with the account, e.g. email addresses
etc, fwiw)

On Sat, 17 Sep 2016 19:43 Jimmy Cuadra, <notifications@github.com> wrote:

> Thanks for the PR! The spec doesn't say anything about what deactivating
> means other than "removing all ability for the user to login again." If the
> user record were to be deleted from the database, that might be a problem
> since there will still be events in the graph for any rooms they joined
> that reference them. Given the limited scope of being able to log in that
> the spec describes, I think you're right that we should just have an
> "active" field for users that is true initially and stays true until the
> deactivate. Then we'd need to check the value of this field before issuing
> any new access tokens for the user.
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/ruma/ruma/pull/89#issuecomment-247796759>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/AICaWMbBznQFL8quqffDjOWV1EbCg_RKks5qrDTJgaJpZM4J_eL0>
> .
>

jplatte: Shouldn't the endpoint be called `AccountDeactivate` though, to stay consistent with the existing naming scheme?
mujx: I updated the PR using the `active` flag. What would be the process of reactivating the account?
I couldn't find anything relevant in the spec. 

Maybe the `POST /_matrix/client/r0/account/password/email/requestToken` endpoint?

@jplatte I think it would be a little awkward to read. Also the other endpoints use the verb first,
which is more natural. The `AccountPassword` could be renamed to `ChangeAccountPassword`.
jplatte: The other endpoints don't have a descriptive verb first, they have the HTTP method first (in case the URL supports multiple HTTP methods). I agree that if the endpoints should be reflective of what they do instead of their http method / url, changing `AccountPassword` to `ChangeAccountPassword` or similar would make sense. But I guess the naming scheme is up to @jimmycuadra to decide.
jimmycuadra: My guess is that once the account is deactivated it is gone for good. This seems supported by Erik's comment that personal information should be removed from the user when deactivated.

Everyone is right that the struct names for the API endpoints are a bit awkward right now. You can leave them as is here and I'll change them all at once afterwards.
mujx: - The `user/{userId}/account_data/{type}` endpoint was added. #63 
- The `deactivate` endpoint was updated to delete the account data.

I think that was the info @erikjohnston was referring to.
The account data per room has similar implementation, and it will be added soon. #62 

Let me know if you have any improvements/changes in mind.
jimmycuadra: Looks great! Thanks so much for your work on this.
mujx: Thanks for merging!

Patch:
diff --git a/ruma-events-macros/src/event_enum.rs b/ruma-events-macros/src/event_enum.rs
index 689c3338..d3806ed0 100644
--- a/ruma-events-macros/src/event_enum.rs
+++ b/ruma-events-macros/src/event_enum.rs
@@ -9,71 +9,56 @@ use syn::{
 
 /// Create a content enum from `EventEnumInput`.
 pub fn expand_event_enum(input: EventEnumInput) -> syn::Result<TokenStream> {
-    let attrs = &input.attrs;
     let ident = &input.name;
-    let event_type_str = &input.events;
-    let event_struct = Ident::new(&ident.to_string().trim_start_matches("Any"), ident.span());
 
-    let variants = input.events.iter().map(to_camel_case).collect::<syn::Result<Vec<_>>>()?;
-    let content = input.events.iter().map(to_event_path).collect::<Vec<_>>();
+    let event_enum = expand_any_enum_with_deserialize(&input, ident)?;
 
-    let event_enum = quote! {
-        #( #attrs )*
-        #[derive(Clone, Debug, ::serde::Serialize)]
-        #[serde(untagged)]
-        #[allow(clippy::large_enum_variant)]
-        pub enum #ident {
-            #(
-                #[doc = #event_type_str]
-                #variants(#content),
-            )*
-            /// An event not defined by the Matrix specification
-            Custom(::ruma_events::#event_struct<::ruma_events::custom::CustomEventContent>),
-        }
-    };
+    let needs_event_content = ident == "AnyStateEvent"
+        || ident == "AnyMessageEvent"
+        || ident == "AnyToDeviceEvent"
+        || ident == "AnyEphemeralRoomEvent"
+        || ident == "AnyBasicEvent";
 
-    let event_deserialize_impl = quote! {
-        impl<'de> ::serde::de::Deserialize<'de> for #ident {
-            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-            where
-                D: ::serde::de::Deserializer<'de>,
-            {
-                use ::serde::de::Error as _;
+    let needs_event_stub = ident == "AnyStateEvent" || ident == "AnyMessageEvent";
 
-                let json = Box::<::serde_json::value::RawValue>::deserialize(deserializer)?;
-                let ::ruma_events::EventDeHelper { ev_type } = ::ruma_events::from_raw_json_value(&json)?;
-                match ev_type.as_str() {
-                    #(
-                        #event_type_str => {
-                            let event = ::serde_json::from_str::<#content>(json.get()).map_err(D::Error::custom)?;
-                            Ok(#ident::#variants(event))
-                        },
-                    )*
-                    event => {
-                        let event =
-                            ::serde_json::from_str::<::ruma_events::#event_struct<::ruma_events::custom::CustomEventContent>>(json.get())
-                                .map_err(D::Error::custom)?;
+    let needs_stripped_event = ident == "AnyStateEvent";
 
-                        Ok(Self::Custom(event))
-                    },
-                }
-            }
-        }
-    };
+    let event_stub_enum =
+        if needs_event_stub { expand_stub_enum(&input)? } else { TokenStream::new() };
 
-    let event_content_enum = expand_content_enum(input)?;
+    let event_stripped_enum =
+        if needs_stripped_event { expand_stripped_enum(&input)? } else { TokenStream::new() };
+
+    let event_content_enum =
+        if needs_event_content { expand_content_enum(&input)? } else { TokenStream::new() };
 
     Ok(quote! {
         #event_enum
 
-        #event_deserialize_impl
+        #event_stub_enum
+
+        #event_stripped_enum
 
         #event_content_enum
     })
 }
 
+/// Create a "stub" enum from `EventEnumInput`.
+pub fn expand_stub_enum(input: &EventEnumInput) -> syn::Result<TokenStream> {
+    let ident = Ident::new(&format!("{}Stub", input.name.to_string()), input.name.span());
+
+    expand_any_enum_with_deserialize(input, &ident)
+}
+
+/// Create a "stripped" enum from `EventEnumInput`.
+pub fn expand_stripped_enum(input: &EventEnumInput) -> syn::Result<TokenStream> {
+    let ident = Ident::new("AnyStrippedStateEventStub", input.name.span());
+
+    expand_any_enum_with_deserialize(input, &ident)
+}
+
 /// Create a content enum from `EventEnumInput`.
-pub fn expand_content_enum(input: EventEnumInput) -> syn::Result<TokenStream> {
+pub fn expand_content_enum(input: &EventEnumInput) -> syn::Result<TokenStream> {
     let attrs = &input.attrs;
     let ident = Ident::new(&format!("{}Content", input.name.to_string()), input.name.span());
     let event_type_str = &input.events;
@@ -122,30 +107,80 @@ pub fn expand_content_enum(input: EventEnumInput) -> syn::Result<TokenStream> {
         }
     };
 
-    let any_event_variant_impl = quote! {
-        impl #ident {
-            fn is_compatible(event_type: &str) -> bool {
-                match event_type {
-                    #( #event_type_str => true, )*
-                    _ => false,
-                }
-            }
-        }
-    };
-
     let marker_trait_impls = marker_traits(&ident);
 
     Ok(quote! {
         #content_enum
 
-        #any_event_variant_impl
-
         #event_content_impl
 
         #marker_trait_impls
     })
 }
 
+fn expand_any_enum_with_deserialize(
+    input: &EventEnumInput,
+    ident: &Ident,
+) -> syn::Result<TokenStream> {
+    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 variants = input.events.iter().map(to_camel_case).collect::<syn::Result<Vec<_>>>()?;
+    let content =
+        input.events.iter().map(|event| to_event_path(event, &event_struct)).collect::<Vec<_>>();
+
+    let any_enum = quote! {
+        #( #attrs )*
+        #[derive(Clone, Debug, ::serde::Serialize)]
+        #[serde(untagged)]
+        #[allow(clippy::large_enum_variant)]
+        pub enum #ident {
+            #(
+                #[doc = #event_type_str]
+                #variants(#content),
+            )*
+            /// An event not defined by the Matrix specification
+            Custom(::ruma_events::#event_struct<::ruma_events::custom::CustomEventContent>),
+        }
+    };
+
+    let event_deserialize_impl = quote! {
+        impl<'de> ::serde::de::Deserialize<'de> for #ident {
+            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+            where
+                D: ::serde::de::Deserializer<'de>,
+            {
+                use ::serde::de::Error as _;
+
+                let json = Box::<::serde_json::value::RawValue>::deserialize(deserializer)?;
+                let ::ruma_events::EventDeHelper { ev_type, .. } = ::ruma_events::from_raw_json_value(&json)?;
+                match ev_type.as_str() {
+                    #(
+                        #event_type_str => {
+                            let event = ::serde_json::from_str::<#content>(json.get()).map_err(D::Error::custom)?;
+                            Ok(#ident::#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))
+                    },
+                }
+            }
+        }
+    };
+
+    Ok(quote! {
+        #any_enum
+
+        #event_deserialize_impl
+    })
+}
+
 fn marker_traits(ident: &Ident) -> TokenStream {
     match ident.to_string().as_str() {
         "AnyStateEventContent" => quote! {
@@ -166,7 +201,7 @@ fn marker_traits(ident: &Ident) -> TokenStream {
     }
 }
 
-fn to_event_path(name: &LitStr) -> TokenStream {
+fn to_event_path(name: &LitStr, struct_name: &Ident) -> TokenStream {
     let span = name.span();
     let name = name.value();
 
@@ -180,10 +215,25 @@ fn to_event_path(name: &LitStr) -> TokenStream {
         .map(|s| s.chars().next().unwrap().to_uppercase().to_string() + &s[1..])
         .collect::<String>();
 
-    let content_str = Ident::new(&format!("{}Event", event), span);
     let path = path.iter().map(|s| Ident::new(s, span));
-    quote! {
-        ::ruma_events::#( #path )::*::#content_str
+
+    match struct_name.to_string().as_str() {
+        "MessageEvent" | "MessageEventStub" if *event_str == "m.room.redaction" => {
+            let redaction = if struct_name == "MessageEvent" {
+                quote! { RedactionEvent }
+            } else {
+                quote! { RedactionEventStub }
+            };
+            quote! { ::ruma_events::room::redaction::#redaction }
+        }
+        "ToDeviceEvent" | "StateEventStub" | "StrippedStateEventStub" | "MessageEventStub" => {
+            let content = Ident::new(&format!("{}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 }
+        }
     }
 }
 
@@ -253,18 +303,19 @@ pub struct EventEnumInput {
 impl Parse for EventEnumInput {
     fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
         let attrs = input.call(Attribute::parse_outer)?;
-        // name field
+        // "name" field
         input.parse::<kw::name>()?;
         input.parse::<Token![:]>()?;
+
         // the name of our event enum
         let name: Ident = input.parse()?;
         input.parse::<Token![,]>()?;
 
-        // events field
+        // "events" field
         input.parse::<kw::events>()?;
         input.parse::<Token![:]>()?;
 
-        // an array of event names `["m.room.whatever"]`
+        // an array of event names `["m.room.whatever", ...]`
         let ev_array = input.parse::<syn::ExprArray>()?;
         let events = ev_array
             .elems
diff --git a/ruma-events/src/enums.rs b/ruma-events/src/enums.rs
index 2476cbe2..1269f0f4 100644
--- a/ruma-events/src/enums.rs
+++ b/ruma-events/src/enums.rs
@@ -1,14 +1,8 @@
 use ruma_events_macros::event_enum;
-use serde::{
-    de::{self, Error},
-    Serialize,
-};
+use serde::{de, Serialize};
 use serde_json::value::RawValue as RawJsonValue;
 
-use crate::{
-    event_kinds::{MessageEventStub, StateEventStub, StrippedStateEventStub},
-    from_raw_json_value, EventDeHelper,
-};
+use crate::{from_raw_json_value, EventDeHelper};
 
 event_enum! {
     /// Any basic event.
@@ -46,7 +40,6 @@ event_enum! {
         "m.room.message.feedback",
         "m.room.redaction",
         "m.sticker",
-
     ]
 }
 
@@ -66,7 +59,6 @@ event_enum! {
         "m.room.name",
         "m.room.pinned_events",
         "m.room.power_levels",
-        "m.room.redaction",
         "m.room.server_acl",
         "m.room.third_party_invite",
         "m.room.tombstone",
@@ -92,16 +84,6 @@ event_enum! {
     ]
 }
 
-/// Any message event stub (message event without a `room_id`, as returned in `/sync` responses)
-pub type AnyMessageEventStub = MessageEventStub<AnyMessageEventContent>;
-
-/// Any state event stub (state event without a `room_id`, as returned in `/sync` responses)
-pub type AnyStateEventStub = StateEventStub<AnyStateEventContent>;
-
-/// Any stripped state event stub (stripped-down state event, as returned for rooms the user has
-/// been invited to in `/sync` responses)
-pub type AnyStrippedStateEventStub = StrippedStateEventStub<AnyStateEventContent>;
-
 /// Any event.
 #[derive(Clone, Debug, Serialize)]
 #[serde(untagged)]
@@ -144,22 +126,18 @@ impl<'de> de::Deserialize<'de> for AnyEvent {
         D: de::Deserializer<'de>,
     {
         let json = Box::<RawJsonValue>::deserialize(deserializer)?;
-        let EventDeHelper { ev_type } = from_raw_json_value(&json)?;
-
-        match ev_type.as_str() {
-            ev_type if AnyBasicEventContent::is_compatible(ev_type) => {
-                Ok(AnyEvent::Basic(from_raw_json_value(&json)?))
-            }
-            ev_type if AnyEphemeralRoomEventContent::is_compatible(ev_type) => {
-                Ok(AnyEvent::Ephemeral(from_raw_json_value(&json)?))
-            }
-            ev_type if AnyMessageEventContent::is_compatible(ev_type) => {
-                Ok(AnyEvent::Message(from_raw_json_value(&json)?))
-            }
-            ev_type if AnyStateEventContent::is_compatible(ev_type) => {
-                Ok(AnyEvent::State(from_raw_json_value(&json)?))
-            }
-            _ => Err(D::Error::custom(format!("event type `{}` is not a valid event", ev_type))),
+        let EventDeHelper { state_key, event_id, room_id, .. } = 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)?))
+        } else if event_id.is_some() {
+            Ok(AnyEvent::Message(from_raw_json_value(&json)?))
+        } else if room_id.is_some() {
+            Ok(AnyEvent::Ephemeral(from_raw_json_value(&json)?))
+        } else {
+            Ok(AnyEvent::Basic(from_raw_json_value(&json)?))
         }
     }
 }
@@ -170,16 +148,12 @@ impl<'de> de::Deserialize<'de> for AnyRoomEvent {
         D: de::Deserializer<'de>,
     {
         let json = Box::<RawJsonValue>::deserialize(deserializer)?;
-        let EventDeHelper { ev_type } = from_raw_json_value(&json)?;
+        let EventDeHelper { state_key, .. } = from_raw_json_value(&json)?;
 
-        match ev_type.as_str() {
-            ev_type if AnyMessageEventContent::is_compatible(ev_type) => {
-                Ok(AnyRoomEvent::Message(from_raw_json_value(&json)?))
-            }
-            ev_type if AnyStateEventContent::is_compatible(ev_type) => {
-                Ok(AnyRoomEvent::State(from_raw_json_value(&json)?))
-            }
-            _ => Err(D::Error::custom(format!("event type `{}` is not a valid event", ev_type))),
+        if state_key.is_some() {
+            Ok(AnyRoomEvent::State(from_raw_json_value(&json)?))
+        } else {
+            Ok(AnyRoomEvent::Message(from_raw_json_value(&json)?))
         }
     }
 }
@@ -190,16 +164,12 @@ impl<'de> de::Deserialize<'de> for AnyRoomEventStub {
         D: de::Deserializer<'de>,
     {
         let json = Box::<RawJsonValue>::deserialize(deserializer)?;
-        let EventDeHelper { ev_type } = from_raw_json_value(&json)?;
+        let EventDeHelper { state_key, .. } = from_raw_json_value(&json)?;
 
-        match ev_type.as_str() {
-            ev_type if AnyMessageEventContent::is_compatible(ev_type) => {
-                Ok(AnyRoomEventStub::Message(from_raw_json_value(&json)?))
-            }
-            ev_type if AnyStateEventContent::is_compatible(ev_type) => {
-                Ok(AnyRoomEventStub::State(from_raw_json_value(&json)?))
-            }
-            _ => Err(D::Error::custom(format!("event type `{}` is not a valid event", ev_type))),
+        if state_key.is_some() {
+            Ok(AnyRoomEventStub::State(from_raw_json_value(&json)?))
+        } else {
+            Ok(AnyRoomEventStub::Message(from_raw_json_value(&json)?))
         }
     }
 }
diff --git a/ruma-events/src/lib.rs b/ruma-events/src/lib.rs
index 6444b610..e0620757 100644
--- a/ruma-events/src/lib.rs
+++ b/ruma-events/src/lib.rs
@@ -120,7 +120,10 @@
 use std::fmt::Debug;
 
 use js_int::Int;
-use serde::{de, Deserialize, Serialize};
+use serde::{
+    de::{self, IgnoredAny},
+    Deserialize, Serialize,
+};
 use serde_json::value::RawValue as RawJsonValue;
 
 use self::room::redaction::RedactionEvent;
@@ -234,12 +237,24 @@ pub trait MessageEventContent: RoomEventContent {}
 /// Marker trait for the content of a state event.
 pub trait StateEventContent: RoomEventContent {}
 
-/// Helper struct to obtain the event type from a serde_json::value::RawValue.
+/// Helper struct to determine the event kind from a serde_json::value::RawValue.
 #[doc(hidden)]
 #[derive(Debug, Deserialize)]
 pub struct EventDeHelper {
+    /// the Matrix event type string "m.room.whatever".
     #[serde(rename = "type")]
     pub ev_type: String,
+
+    /// If `state_key` is present the event will be deserialized as a state event.
+    pub state_key: Option<IgnoredAny>,
+
+    /// If no `state_key` is found but an `event_id` is present the event
+    /// will be deserialized as a message event.
+    pub event_id: Option<IgnoredAny>,
+
+    /// 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<IgnoredAny>,
 }
 
 /// Helper function for serde_json::value::RawValue deserialization.
diff --git a/ruma-events/src/room/redaction.rs b/ruma-events/src/room/redaction.rs
index 7a4acd83..eaa7e581 100644
--- a/ruma-events/src/room/redaction.rs
+++ b/ruma-events/src/room/redaction.rs
@@ -64,6 +64,10 @@ pub struct RedactionEventContent {
     pub reason: Option<String>,
 }
 
+impl ruma_events::RoomEventContent for RedactionEventContent {}
+
+impl ruma_events::MessageEventContent for RedactionEventContent {}
+
 #[cfg(test)]
 mod tests {
     use std::{
diff --git a/ruma-identifiers/CHANGELOG.md b/ruma-identifiers/CHANGELOG.md
index 790468ca..85890ffb 100644
--- a/ruma-identifiers/CHANGELOG.md
+++ b/ruma-identifiers/CHANGELOG.md
@@ -2,7 +2,10 @@
 
 Breaking changes:
 
-* Removed diesel integration. If you were using it, please comment on the corresponding issue:
+* Remove `RoomVersionId::custom`. It could be used to create invalid room versions (empty or
+  exceeding 32 code points in length). Use the `TryFrom<&str>` or `TryFrom<String>` implementation
+  instead.
+* Remove diesel integration. If you were using it, please comment on the corresponding issue:
   https://github.com/ruma/ruma-identifiers/issues/22
 * Remove `TryFrom<Cow<'_, str>>` implementations for identifier types
 * Update `parse_with_server_name`s signature (instead of `Into<String>` it now requires
diff --git a/ruma-identifiers/src/key_algorithms.rs b/ruma-identifiers/src/key_algorithms.rs
index 3b489f2a..a96fe6e2 100644
--- a/ruma-identifiers/src/key_algorithms.rs
+++ b/ruma-identifiers/src/key_algorithms.rs
@@ -9,17 +9,15 @@ use strum::{AsRefStr, Display, EnumString};
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, AsRefStr, Display, EnumString)]
 #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
 #[non_exhaustive]
+#[strum(serialize_all = "snake_case")]
 pub enum DeviceKeyAlgorithm {
     /// The Ed25519 signature algorithm.
-    #[strum(to_string = "ed25519")]
     Ed25519,
 
     /// The Curve25519 ECDH algorithm.
-    #[strum(to_string = "curve25519")]
     Curve25519,
 
     /// The Curve25519 ECDH algorithm, but the key also contains signatures
-    #[strum(to_string = "signed_curve25519")]
     SignedCurve25519,
 }
 
@@ -27,8 +25,8 @@ pub enum DeviceKeyAlgorithm {
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, AsRefStr, Display, EnumString)]
 #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
 #[non_exhaustive]
+#[strum(serialize_all = "snake_case")]
 pub enum ServerKeyAlgorithm {
     /// The Ed25519 signature algorithm.
-    #[strum(to_string = "ed25519")]
     Ed25519,
 }
diff --git a/ruma-identifiers/src/room_version_id.rs b/ruma-identifiers/src/room_version_id.rs
index 776a5028..cdd2ef69 100644
--- a/ruma-identifiers/src/room_version_id.rs
+++ b/ruma-identifiers/src/room_version_id.rs
@@ -87,14 +87,6 @@ impl<T> RoomVersionId<T> {
         Self(InnerRoomVersionId::Version6)
     }
 
-    /// Creates a custom room version ID from the given string slice.
-    pub fn custom(id: String) -> Self
-    where
-        String: Into<T>,
-    {
-        Self(InnerRoomVersionId::Custom(id.into()))
-    }
-
     /// Whether or not this room version is an official one specified by the Matrix protocol.
     pub fn is_official(&self) -> bool {
         !self.is_custom()
@@ -408,7 +400,6 @@ mod tests {
         assert!(RoomVersionId::version_3().is_version_3());
         assert!(RoomVersionId::version_4().is_version_4());
         assert!(RoomVersionId::version_5().is_version_5());
-        assert!(RoomVersionId::custom("foo".into()).is_custom());
     }
 
     #[test]


Test patch:
diff --git a/ruma-events/tests/custom.rs b/ruma-events/tests/custom.rs
index ee24044e..e5d525ec 100644
--- a/ruma-events/tests/custom.rs
+++ b/ruma-events/tests/custom.rs
@@ -5,8 +5,9 @@ use std::{
 
 use matches::assert_matches;
 use ruma_events::{
-    custom::CustomEventContent, AnyMessageEvent, AnyStateEvent, AnyStateEventContent, EventJson,
-    MessageEvent, StateEvent, StateEventStub, UnsignedData,
+    custom::CustomEventContent, AnyMessageEvent, AnyMessageEventStub, AnyRoomEventStub,
+    AnyStateEvent, AnyStateEventContent, EventJson, MessageEvent, MessageEventStub, StateEvent,
+    StateEventStub, UnsignedData,
 };
 use ruma_identifiers::{EventId, RoomId, UserId};
 use serde_json::{
@@ -188,3 +189,50 @@ fn deserialize_custom_state_stub_event() {
             && !unsigned.is_empty()
     );
 }
+
+#[test]
+fn deserialize_custom_message_stub_event() {
+    let json_data = json!({
+        "content": {
+            "m.relates_to": {
+                "event_id": "$MDitXXXXXX",
+                "key": "👍",
+                "rel_type": "m.annotation"
+            }
+        },
+        "event_id": "$h29iv0s8:example.com",
+        "origin_server_ts": 10,
+        "room_id": "!room:room.com",
+        "sender": "@carl:example.com",
+        "type": "m.reaction",
+        "unsigned": {
+            "age": 85
+        }
+    });
+
+    let expected_content = json!({
+        "m.relates_to": {
+            "event_id": "$MDitXXXXXX",
+            "key": "👍",
+            "rel_type": "m.annotation"
+        }
+    });
+
+    assert_matches!(
+        from_json_value::<AnyRoomEventStub>(json_data)
+            .unwrap(),
+        AnyRoomEventStub::Message(AnyMessageEventStub::Custom(MessageEventStub {
+            content: CustomEventContent {
+                json, event_type,
+            },
+            event_id,
+            origin_server_ts,
+            sender,
+            unsigned,
+        })) if json == expected_content && event_type == "m.reaction"
+            && event_id == EventId::try_from("$h29iv0s8:example.com").unwrap()
+            && origin_server_ts == UNIX_EPOCH + Duration::from_millis(10)
+            && sender == UserId::try_from("@carl:example.com").unwrap()
+            && !unsigned.is_empty()
+    );
+}
diff --git a/ruma-events/tests/enums.rs b/ruma-events/tests/enums.rs
index 50ce88c3..58b3f124 100644
--- a/ruma-events/tests/enums.rs
+++ b/ruma-events/tests/enums.rs
@@ -10,9 +10,8 @@ use ruma_events::{
         message::{MessageEventContent, TextMessageEventContent},
         power_levels::PowerLevelsEventContent,
     },
-    AnyEvent, AnyMessageEvent, AnyMessageEventContent, AnyRoomEvent, AnyRoomEventStub,
-    AnyStateEvent, AnyStateEventContent, MessageEvent, MessageEventStub, StateEvent,
-    StateEventStub,
+    AnyEvent, AnyMessageEvent, AnyMessageEventStub, AnyRoomEvent, AnyRoomEventStub, AnyStateEvent,
+    AnyStateEventStub, MessageEvent, MessageEventStub, StateEvent, StateEventStub,
 };
 
 fn message_event() -> JsonValue {
@@ -120,12 +119,12 @@ fn power_event_stub_deserialization() {
     assert_matches!(
         from_json_value::<AnyRoomEventStub>(json_data),
         Ok(AnyRoomEventStub::State(
-            StateEventStub {
-                content: AnyStateEventContent::RoomPowerLevels(PowerLevelsEventContent {
+            AnyStateEventStub::RoomPowerLevels(StateEventStub {
+                content: PowerLevelsEventContent {
                     ban, ..
-                }),
+                },
                 ..
-            }
+            })
         ))
         if ban == js_int::Int::new(50).unwrap()
     );
@@ -138,14 +137,14 @@ fn message_event_stub_deserialization() {
     assert_matches!(
         from_json_value::<AnyRoomEventStub>(json_data),
         Ok(AnyRoomEventStub::Message(
-            MessageEventStub {
-                content: AnyMessageEventContent::RoomMessage(MessageEventContent::Text(TextMessageEventContent {
+            AnyMessageEventStub::RoomMessage(MessageEventStub {
+                content: MessageEventContent::Text(TextMessageEventContent {
                     body,
                     formatted: Some(formatted),
                     relates_to: None,
-                })),
+                }),
                 ..
-            }
+            })
         ))
         if body == "baba" && formatted.body == "<strong>baba</strong>"
     );
@@ -158,12 +157,12 @@ fn aliases_event_stub_deserialization() {
     assert_matches!(
         from_json_value::<AnyRoomEventStub>(json_data),
         Ok(AnyRoomEventStub::State(
-            StateEventStub {
-                content: AnyStateEventContent::RoomAliases(AliasesEventContent {
+            AnyStateEventStub::RoomAliases(StateEventStub {
+                content: AliasesEventContent {
                     aliases,
-                }),
+                },
                 ..
-            }
+            })
         ))
         if aliases == vec![ RoomAliasId::try_from("#somewhere:localhost").unwrap() ]
     );
diff --git a/ruma-events/tests/redaction.rs b/ruma-events/tests/redaction.rs
new file mode 100644
index 00000000..f6cd9cd7
--- /dev/null
+++ b/ruma-events/tests/redaction.rs
@@ -0,0 +1,73 @@
+use std::{
+    convert::TryFrom,
+    time::{Duration, UNIX_EPOCH},
+};
+
+use matches::assert_matches;
+use ruma_events::{
+    room::redaction::{RedactionEvent, RedactionEventContent},
+    AnyMessageEvent, EventJson, UnsignedData,
+};
+use ruma_identifiers::{EventId, RoomId, UserId};
+use serde_json::{
+    from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
+};
+
+fn redaction() -> JsonValue {
+    json!({
+        "content": {
+            "reason": "being a turd"
+        },
+        "redacts": "$nomore:example.com",
+        "event_id": "$h29iv0s8:example.com",
+        "sender": "@carl:example.com",
+        "origin_server_ts": 1,
+        "room_id": "!roomid:room.com",
+        "type": "m.room.redaction"
+    })
+}
+
+#[test]
+fn serialize_redaction() {
+    let aliases_event = RedactionEvent {
+        content: RedactionEventContent { reason: Some("being a turd".to_string()) },
+        redacts: EventId::try_from("$nomore: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 actual = to_json_value(&aliases_event).unwrap();
+    let expected = redaction();
+
+    assert_eq!(actual, expected);
+}
+
+#[test]
+fn deserialize_redaction() {
+    let json_data = redaction();
+
+    assert_matches!(
+        from_json_value::<EventJson<AnyMessageEvent>>(json_data)
+            .unwrap()
+            .deserialize()
+            .unwrap(),
+        AnyMessageEvent::RoomRedaction(RedactionEvent {
+            content: RedactionEventContent { reason: Some(reas) },
+            redacts,
+            event_id,
+            origin_server_ts,
+            room_id,
+            sender,
+            unsigned,
+        }) if reas == "being a turd"
+            && event_id == EventId::try_from("$h29iv0s8:example.com").unwrap()
+            && redacts == EventId::try_from("$nomore: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.is_empty()
+    );
+}
diff --git a/ruma-events/tests/stripped.rs b/ruma-events/tests/stripped.rs
index e98eb71d..5342159c 100644
--- a/ruma-events/tests/stripped.rs
+++ b/ruma-events/tests/stripped.rs
@@ -3,14 +3,14 @@ use std::convert::TryFrom;
 use js_int::uint;
 use ruma_events::{
     room::{join_rules::JoinRule, topic::TopicEventContent},
-    AnyStateEventContent, AnyStrippedStateEventStub,
+    AnyStateEventContent, AnyStrippedStateEventStub, StrippedStateEventStub,
 };
 use ruma_identifiers::UserId;
 use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
 
 #[test]
-fn serialize_stripped_state_event() {
-    let event = AnyStrippedStateEventStub {
+fn serialize_stripped_state_event_any_content() {
+    let event = StrippedStateEventStub {
         content: AnyStateEventContent::RoomTopic(TopicEventContent {
             topic: "Testing room".to_string(),
         }),
@@ -30,6 +30,26 @@ fn serialize_stripped_state_event() {
     assert_eq!(to_json_value(&event).unwrap(), json_data);
 }
 
+#[test]
+fn serialize_stripped_state_event_any_event() {
+    let event = AnyStrippedStateEventStub::RoomTopic(StrippedStateEventStub {
+        content: TopicEventContent { topic: "Testing room".to_string() },
+        state_key: "".to_string(),
+        sender: UserId::try_from("@example:localhost").unwrap(),
+    });
+
+    let json_data = json!({
+        "content": {
+            "topic": "Testing room"
+        },
+        "type": "m.room.topic",
+        "state_key": "",
+        "sender": "@example:localhost"
+    });
+
+    assert_eq!(to_json_value(&event).unwrap(), json_data);
+}
+
 #[test]
 fn deserialize_stripped_state_events() {
     let name_event = json!({
@@ -76,9 +96,9 @@ fn deserialize_stripped_state_events() {
     });
 
     let event = from_json_value::<AnyStrippedStateEventStub>(name_event).unwrap();
-    match event.content {
-        AnyStateEventContent::RoomName(content) => {
-            assert_eq!(content.name(), Some("Ruma"));
+    match event {
+        AnyStrippedStateEventStub::RoomName(event) => {
+            assert_eq!(event.content.name(), Some("Ruma"));
             assert_eq!(event.state_key, "");
             assert_eq!(event.sender.to_string(), "@example:localhost");
         }
@@ -86,9 +106,9 @@ fn deserialize_stripped_state_events() {
     }
 
     let event = from_json_value::<AnyStrippedStateEventStub>(join_rules_event).unwrap();
-    match event.content {
-        AnyStateEventContent::RoomJoinRules(content) => {
-            assert_eq!(content.join_rule, JoinRule::Public);
+    match event {
+        AnyStrippedStateEventStub::RoomJoinRules(event) => {
+            assert_eq!(event.content.join_rule, JoinRule::Public);
             assert_eq!(event.state_key, "");
             assert_eq!(event.sender.to_string(), "@example:localhost");
         }
@@ -96,16 +116,16 @@ fn deserialize_stripped_state_events() {
     }
 
     let event = from_json_value::<AnyStrippedStateEventStub>(avatar_event).unwrap();
-    match event.content {
-        AnyStateEventContent::RoomAvatar(content) => {
-            let image_info = content.info.unwrap();
+    match event {
+        AnyStrippedStateEventStub::RoomAvatar(event) => {
+            let image_info = event.content.info.unwrap();
 
             assert_eq!(image_info.height.unwrap(), uint!(128));
             assert_eq!(image_info.width.unwrap(), uint!(128));
             assert_eq!(image_info.mimetype.unwrap(), "image/jpeg");
             assert_eq!(image_info.size.unwrap(), uint!(1024));
             assert_eq!(image_info.thumbnail_info.unwrap().size.unwrap(), uint!(32));
-            assert_eq!(content.url, "https://example.com/image.jpg");
+            assert_eq!(event.content.url, "https://example.com/image.jpg");
             assert_eq!(event.state_key, "");
             assert_eq!(event.sender.to_string(), "@example:localhost");
         }