Repository: ruma/ruma (rust)

Issue:
Create separate unsigned types for regular & redacted events
`redacted_because` should be the only field in redacted event's `unsigned` struct. Regular events shouldn't have `redacted_because`.

Pull request description:
Add UnsignedSync type to accomadate for sync events
fixes #118

Patch:
diff --git a/.builds/beta.yml b/.builds/beta.yml
index 0b5c350d..ea256dfa 100644
--- a/.builds/beta.yml
+++ b/.builds/beta.yml
@@ -34,4 +34,14 @@ tasks:
       cargo test --all-features --verbose
       id_test_2_exit=$?
 
+      # ruma-client_api also has a few features. Make sure it works both with
+      # all of them and none of them being enabled.
+      cd ruma-client-api
+
+      cargo test --no-default-features --verbose
+      id_test_1_exit=$?
+
+      cargo test --all-features --verbose
+      id_test_2_exit=$?
+
       exit $(( $fmt_exit || $clippy_exit || $test_exit || $id_test_1_exit || $id_test_2_exit ))
diff --git a/.builds/stable.yml b/.builds/stable.yml
index ecb28ee4..a362e266 100644
--- a/.builds/stable.yml
+++ b/.builds/stable.yml
@@ -34,6 +34,16 @@ tasks:
       cargo test --all-features --verbose
       id_test_2_exit=$?
 
+      # ruma-client_api also has a few features. Make sure it works both with
+      # all of them and none of them being enabled.
+      cd ruma-client-api
+
+      cargo test --no-default-features --verbose
+      id_test_1_exit=$?
+
+      cargo test --all-features --verbose
+      id_test_2_exit=$?
+
       exit $(( $fmt_exit || $clippy_exit || $test_exit || $id_test_1_exit || $id_test_2_exit ))
   # TODO: Add audit task once cargo-audit binary releases are available.
   #       See https://github.com/RustSec/cargo-audit/issues/66
diff --git a/ruma-api-macros/src/api/request.rs b/ruma-api-macros/src/api/request.rs
index 3e6395b6..95e3ef0c 100644
--- a/ruma-api-macros/src/api/request.rs
+++ b/ruma-api-macros/src/api/request.rs
@@ -168,8 +168,11 @@ impl Request {
                 let field_name =
                     field.ident.as_ref().expect("expected field to have an identifier");
                 let span = field.span();
+                let cfg_attrs =
+                    field.attrs.iter().filter(|a| a.path.is_ident("cfg")).collect::<Vec<_>>();
 
                 quote_spanned! {span=>
+                    #( #cfg_attrs )*
                     #field_name: #src.#field_name
                 }
             })
diff --git a/ruma-api-macros/src/api/response.rs b/ruma-api-macros/src/api/response.rs
index 78330717..7c9f64b7 100644
--- a/ruma-api-macros/src/api/response.rs
+++ b/ruma-api-macros/src/api/response.rs
@@ -43,10 +43,13 @@ impl Response {
             let field = response_field.field();
             let field_name = field.ident.as_ref().expect("expected field to have an identifier");
             let span = field.span();
+            let cfg_attrs =
+                field.attrs.iter().filter(|a| a.path.is_ident("cfg")).collect::<Vec<_>>();
 
             match response_field {
                 ResponseField::Body(_) => {
                     quote_spanned! {span=>
+                        #( #cfg_attrs )*
                         #field_name: response_body.#field_name
                     }
                 }
@@ -114,8 +117,11 @@ impl Response {
                     let field_name =
                         field.ident.as_ref().expect("expected field to have an identifier");
                     let span = field.span();
+                    let cfg_attrs =
+                        field.attrs.iter().filter(|a| a.path.is_ident("cfg")).collect::<Vec<_>>();
 
                     Some(quote_spanned! {span=>
+                        #( #cfg_attrs )*
                         #field_name: response.#field_name
                     })
                 } else {
diff --git a/ruma-client-api/Cargo.toml b/ruma-client-api/Cargo.toml
index 3fb6a753..4fa18304 100644
--- a/ruma-client-api/Cargo.toml
+++ b/ruma-client-api/Cargo.toml
@@ -34,3 +34,4 @@ matches = "0.1.8"
 
 [features]
 unstable-pre-spec = []
+unstable-synapse-quirks = []
diff --git a/ruma-client-api/src/r0.rs b/ruma-client-api/src/r0.rs
index d1577682..f33715e0 100644
--- a/ruma-client-api/src/r0.rs
+++ b/ruma-client-api/src/r0.rs
@@ -3,7 +3,6 @@
 pub mod account;
 pub mod alias;
 pub mod appservice;
-#[cfg(feature = "unstable-pre-spec")]
 pub mod backup;
 pub mod capabilities;
 pub mod config;
diff --git a/ruma-client-api/src/r0/backup.rs b/ruma-client-api/src/r0/backup.rs
index f25b41ff..600f8660 100644
--- a/ruma-client-api/src/r0/backup.rs
+++ b/ruma-client-api/src/r0/backup.rs
@@ -14,6 +14,15 @@ use std::collections::BTreeMap;
 
 use crate::r0::keys::AlgorithmAndDeviceId;
 
+// TODO: remove
+/// A wrapper around a mapping of session IDs to key data.
+#[cfg(feature = "unstable-synapse-quirks")]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct Sessions {
+    ///  A map of session IDs to key data.
+    pub sessions: BTreeMap<String, KeyData>,
+}
+
 /// The algorithm used for storing backups.
 #[derive(Clone, Debug, Serialize, Deserialize)]
 #[serde(tag = "algorithm", content = "auth_data")]
diff --git a/ruma-client-api/src/r0/backup/add_backup_keys.rs b/ruma-client-api/src/r0/backup/add_backup_keys.rs
index 22b025f5..8ab6537c 100644
--- a/ruma-client-api/src/r0/backup/add_backup_keys.rs
+++ b/ruma-client-api/src/r0/backup/add_backup_keys.rs
@@ -5,9 +5,6 @@ use std::collections::BTreeMap;
 use js_int::UInt;
 use ruma_api::ruma_api;
 use ruma_identifiers::RoomId;
-use serde::{Deserialize, Serialize};
-
-use super::KeyData;
 
 ruma_api! {
     metadata: {
@@ -27,7 +24,12 @@ ruma_api! {
         /// A map from room IDs to session IDs to key data.
         ///
         /// Note: synapse has the `sessions: {}` wrapper, the Matrix spec does not.
-        pub rooms: BTreeMap<RoomId, Sessions>,
+        #[cfg(feature = "unstable-synapse-quirks")]
+        pub rooms: BTreeMap<RoomId, super::Sessions>,
+
+        /// A map from room IDs to session IDs to key data.
+        #[cfg(not(feature = "unstable-synapse-quirks"))]
+        pub rooms: BTreeMap<RoomId, BTreeMap<String, super::KeyData>>,
     }
 
     response: {
@@ -41,12 +43,3 @@ ruma_api! {
 
     error: crate::Error
 }
-
-// TODO: remove
-/// A wrapper around a mapping of session IDs to key data.
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct Sessions {
-    // TODO: remove
-    ///  A map of session IDs to key data.
-    pub sessions: BTreeMap<String, KeyData>,
-}
diff --git a/ruma-client-api/src/r0/backup/get_backup_keys.rs b/ruma-client-api/src/r0/backup/get_backup_keys.rs
index ffc11a3e..0fd70dd3 100644
--- a/ruma-client-api/src/r0/backup/get_backup_keys.rs
+++ b/ruma-client-api/src/r0/backup/get_backup_keys.rs
@@ -4,9 +4,6 @@ use std::collections::BTreeMap;
 
 use ruma_api::ruma_api;
 use ruma_identifiers::RoomId;
-use serde::{Deserialize, Serialize};
-
-use super::KeyData;
 
 ruma_api! {
     metadata: {
@@ -28,17 +25,13 @@ ruma_api! {
         /// A map from room IDs to session IDs to key data.
         ///
         /// Note: synapse has the `sessions: {}` wrapper, the Matrix spec does not.
-        pub rooms: BTreeMap<RoomId, Sessions>,
+        #[cfg(feature = "unstable-synapse-quirks")]
+        pub rooms: BTreeMap<RoomId, super::Sessions>,
+
+        /// A map from room IDs to session IDs to key data.
+        #[cfg(not(feature = "unstable-synapse-quirks"))]
+        pub rooms: BTreeMap<RoomId, BTreeMap<String, super::KeyData>>,
     }
 
     error: crate::Error
 }
-
-// TODO: remove
-/// A wrapper around a mapping of session IDs to key data.
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct Sessions {
-    // TODO: remove
-    ///  A map of session IDs to key data.
-    pub sessions: BTreeMap<String, KeyData>,
-}
diff --git a/ruma-client-api/src/r0/keys/get_keys.rs b/ruma-client-api/src/r0/keys/get_keys.rs
index 8f98c17b..60629ad7 100644
--- a/ruma-client-api/src/r0/keys/get_keys.rs
+++ b/ruma-client-api/src/r0/keys/get_keys.rs
@@ -45,10 +45,27 @@ ruma_api! {
         /// If any remote homeservers could not be reached, they are recorded
         /// here. The names of the properties are the names of the unreachable
         /// servers.
+        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
         pub failures: BTreeMap<String, JsonValue>,
 
         /// Information on the queried devices.
+        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
         pub device_keys: BTreeMap<UserId, BTreeMap<Box<DeviceId>, DeviceKeys>>,
+
+        /// Information on the master cross-signing keys of the queried users.
+        #[cfg(feature = "unstable-pre-spec")]
+        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
+        pub master_keys: BTreeMap<UserId, super::CrossSigningKey>,
+
+        /// Information on the self-signing keys of the queried users.
+        #[cfg(feature = "unstable-pre-spec")]
+        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
+        pub self_signing_keys: BTreeMap<UserId, super::CrossSigningKey>,
+
+        /// Information on the user-signing keys of the queried users.
+        #[cfg(feature = "unstable-pre-spec")]
+        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
+        pub user_signing_keys: BTreeMap<UserId, super::CrossSigningKey>,
     }
 
     error: crate::Error
diff --git a/ruma-events-macros/src/event_enum.rs b/ruma-events-macros/src/event_enum.rs
index 67f6ec1e..4487139a 100644
--- a/ruma-events-macros/src/event_enum.rs
+++ b/ruma-events-macros/src/event_enum.rs
@@ -482,7 +482,7 @@ fn generate_accessor(
     variants: &[Ident],
 ) -> TokenStream {
     if is_event_kind(kind, var) {
-        let field_type = field_return_type(name);
+        let field_type = field_return_type(name, var);
 
         let name = Ident::new(name, Span::call_site());
         let docs = format!("Returns this events {} field.", name);
@@ -502,14 +502,20 @@ fn generate_accessor(
     }
 }
 
-fn field_return_type(name: &str) -> TokenStream {
+fn field_return_type(name: &str, var: &EventKindVariation) -> TokenStream {
     match name {
         "origin_server_ts" => quote! { ::std::time::SystemTime },
         "room_id" => quote! { ::ruma_identifiers::RoomId },
         "event_id" => quote! { ::ruma_identifiers::EventId },
         "sender" => quote! { ::ruma_identifiers::UserId },
         "state_key" => quote! { str },
-        "unsigned" => quote! { ::ruma_events::UnsignedData },
+        "unsigned" if &EventKindVariation::RedactedSync == var => {
+            quote! { ::ruma_events::RedactedSyncUnsigned }
+        }
+        "unsigned" if &EventKindVariation::Redacted == var => {
+            quote! { ::ruma_events::RedactedUnsigned }
+        }
+        "unsigned" => quote! { ::ruma_events::Unsigned },
         _ => panic!("the `ruma_events_macros::event_enum::EVENT_FIELD` const was changed"),
     }
 }
@@ -521,6 +527,7 @@ mod kw {
 }
 
 // If the variants of this enum change `to_event_path` needs to be updated as well.
+#[derive(Eq, PartialEq)]
 enum EventKindVariation {
     Full,
     Sync,
diff --git a/ruma-events/src/event_kinds.rs b/ruma-events/src/event_kinds.rs
index c5b90b7f..94949f85 100644
--- a/ruma-events/src/event_kinds.rs
+++ b/ruma-events/src/event_kinds.rs
@@ -5,7 +5,8 @@ use ruma_identifiers::{EventId, RoomId, UserId};
 
 use crate::{
     BasicEventContent, EphemeralRoomEventContent, EventContent, MessageEventContent,
-    RedactedMessageEventContent, RedactedStateEventContent, StateEventContent, UnsignedData,
+    RedactedMessageEventContent, RedactedStateEventContent, RedactedSyncUnsigned, RedactedUnsigned,
+    StateEventContent, Unsigned,
 };
 
 /// A basic event – one that consists only of it's type and the `content` object.
@@ -51,7 +52,7 @@ pub struct MessageEvent<C: MessageEventContent> {
     pub room_id: RoomId,
 
     /// Additional key-value pairs not signed by the homeserver.
-    pub unsigned: UnsignedData,
+    pub unsigned: Unsigned,
 }
 
 /// A message event without a `room_id`.
@@ -70,7 +71,7 @@ pub struct SyncMessageEvent<C: MessageEventContent> {
     pub origin_server_ts: SystemTime,
 
     /// Additional key-value pairs not signed by the homeserver.
-    pub unsigned: UnsignedData,
+    pub unsigned: Unsigned,
 }
 
 /// A redacted message event.
@@ -92,7 +93,7 @@ pub struct RedactedMessageEvent<C: RedactedMessageEventContent> {
     pub room_id: RoomId,
 
     /// Additional key-value pairs not signed by the homeserver.
-    pub unsigned: UnsignedData,
+    pub unsigned: RedactedUnsigned,
 }
 
 /// A redacted message event without a `room_id`.
@@ -112,7 +113,7 @@ pub struct RedactedSyncMessageEvent<C: RedactedMessageEventContent> {
     pub origin_server_ts: SystemTime,
 
     /// Additional key-value pairs not signed by the homeserver.
-    pub unsigned: UnsignedData,
+    pub unsigned: RedactedSyncUnsigned,
 }
 
 /// State event.
@@ -143,7 +144,7 @@ pub struct StateEvent<C: StateEventContent> {
     pub prev_content: Option<C>,
 
     /// Additional key-value pairs not signed by the homeserver.
-    pub unsigned: UnsignedData,
+    pub unsigned: Unsigned,
 }
 
 /// A state event without a `room_id`.
@@ -171,7 +172,7 @@ pub struct SyncStateEvent<C: StateEventContent> {
     pub prev_content: Option<C>,
 
     /// Additional key-value pairs not signed by the homeserver.
-    pub unsigned: UnsignedData,
+    pub unsigned: Unsigned,
 }
 
 /// A stripped-down state event, used for previews of rooms the user has been
@@ -216,7 +217,7 @@ pub struct RedactedStateEvent<C: RedactedStateEventContent> {
     pub state_key: String,
 
     /// Additional key-value pairs not signed by the homeserver.
-    pub unsigned: UnsignedData,
+    pub unsigned: RedactedUnsigned,
 }
 
 /// A redacted state event without a `room_id`.
@@ -242,7 +243,7 @@ pub struct RedactedSyncStateEvent<C: RedactedStateEventContent> {
     pub state_key: String,
 
     /// Additional key-value pairs not signed by the homeserver.
-    pub unsigned: UnsignedData,
+    pub unsigned: RedactedSyncUnsigned,
 }
 
 /// A stripped-down redacted state event.
diff --git a/ruma-events/src/lib.rs b/ruma-events/src/lib.rs
index 6357d390..c04af193 100644
--- a/ruma-events/src/lib.rs
+++ b/ruma-events/src/lib.rs
@@ -126,7 +126,7 @@ use serde::{
 };
 use serde_json::value::RawValue as RawJsonValue;
 
-use self::room::redaction::RedactionEvent;
+use self::room::redaction::{RedactionEvent, SyncRedactionEvent};
 
 #[deprecated = "Use ruma_serde::empty::Empty directly instead."]
 pub use ruma_serde::empty::Empty;
@@ -185,7 +185,7 @@ pub use self::{
 /// Extra information about an event that is not incorporated into the event's
 /// hash.
 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
-pub struct UnsignedData {
+pub struct Unsigned {
     /// The time in milliseconds that has elapsed since the event was sent. This
     /// field is generated by the local homeserver, and may be incorrect if the
     /// local time on at least one of the two servers is out of sync, which can
@@ -193,17 +193,13 @@ pub struct UnsignedData {
     #[serde(skip_serializing_if = "Option::is_none")]
     pub age: Option<Int>,
 
-    /// The event that redacted this event, if any.
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub redacted_because: Option<EventJson<RedactionEvent>>,
-
     /// The client-supplied transaction ID, if the client being given the event
     /// is the same one which sent it.
     #[serde(skip_serializing_if = "Option::is_none")]
     pub transaction_id: Option<String>,
 }
 
-impl UnsignedData {
+impl Unsigned {
     /// Whether this unsigned data is empty (all fields are `None`).
     ///
     /// This method is used to determine whether to skip serializing the
@@ -211,7 +207,49 @@ impl UnsignedData {
     /// an incoming `unsigned` field was present - it could still have been
     /// present but contained none of the known fields.
     pub fn is_empty(&self) -> bool {
-        self.age.is_none() && self.transaction_id.is_none() && self.redacted_because.is_none()
+        self.age.is_none() && self.transaction_id.is_none()
+    }
+}
+
+/// Extra information about a redacted event that is not incorporated into the event's
+/// hash.
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+pub struct RedactedUnsigned {
+    /// The event that redacted this event, if any.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub redacted_because: Option<EventJson<RedactionEvent>>,
+}
+
+impl RedactedUnsigned {
+    /// Whether this unsigned data is empty (`redacted_because` is `None`).
+    ///
+    /// This method is used to determine whether to skip serializing the
+    /// `unsigned` field in redacted room events. Do not use it to determine whether
+    /// an incoming `unsigned` field was present - it could still have been
+    /// present but contained none of the known fields.
+    pub fn is_empty(&self) -> bool {
+        self.redacted_because.is_none()
+    }
+}
+
+/// Extra information about a redacted sync event that is not incorporated into the sync event's
+/// hash.
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+pub struct RedactedSyncUnsigned {
+    /// The event that redacted this event, if any.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub redacted_because: Option<EventJson<SyncRedactionEvent>>,
+}
+
+impl RedactedSyncUnsigned {
+    /// Whether this unsigned data is empty (`redacted_because` is `None`).
+    ///
+    /// This method is used to determine whether to skip serializing the
+    /// `unsignedSync` field in redacted room events. Do not use it to determine whether
+    /// an incoming `unsignedSync` field was present - it could still have been
+    /// present but contained none of the known fields.
+    pub fn is_empty(&self) -> bool {
+        self.redacted_because.is_none()
     }
 }
 
diff --git a/ruma-events/src/room/canonical_alias.rs b/ruma-events/src/room/canonical_alias.rs
index 427d86d9..e4ebbc2a 100644
--- a/ruma-events/src/room/canonical_alias.rs
+++ b/ruma-events/src/room/canonical_alias.rs
@@ -40,7 +40,7 @@ mod tests {
     use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
 
     use super::CanonicalAliasEventContent;
-    use crate::{EventJson, StateEvent, UnsignedData};
+    use crate::{EventJson, StateEvent, Unsigned};
 
     #[test]
     fn serialization_with_optional_fields_as_none() {
@@ -55,7 +55,7 @@ mod tests {
             room_id: RoomId::try_from("!dummy:example.com").unwrap(),
             sender: UserId::try_from("@carl:example.com").unwrap(),
             state_key: "".into(),
-            unsigned: UnsignedData::default(),
+            unsigned: Unsigned::default(),
         };
 
         let actual = to_json_value(&canonical_alias_event).unwrap();
diff --git a/ruma-events/src/room/message.rs b/ruma-events/src/room/message.rs
index 362ebbba..3fb34f3c 100644
--- a/ruma-events/src/room/message.rs
+++ b/ruma-events/src/room/message.rs
@@ -405,7 +405,7 @@ mod tests {
     use super::{AudioMessageEventContent, FormattedBody, MessageEventContent, MessageFormat};
     use crate::{
         room::message::{InReplyTo, RelatesTo, TextMessageEventContent},
-        EventJson, MessageEvent, UnsignedData,
+        EventJson, MessageEvent, Unsigned,
     };
 
     #[test]
@@ -421,7 +421,7 @@ mod tests {
             origin_server_ts: UNIX_EPOCH + Duration::from_millis(10_000),
             room_id: RoomId::try_from("!testroomid:example.org").unwrap(),
             sender: UserId::try_from("@user:example.org").unwrap(),
-            unsigned: UnsignedData::default(),
+            unsigned: Unsigned::default(),
         };
 
         assert_eq!(
diff --git a/ruma-events/src/room/name.rs b/ruma-events/src/room/name.rs
index 434b3e77..7645feea 100644
--- a/ruma-events/src/room/name.rs
+++ b/ruma-events/src/room/name.rs
@@ -67,7 +67,7 @@ mod tests {
     use ruma_identifiers::{EventId, RoomId, UserId};
     use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
 
-    use crate::{EventJson, StateEvent, UnsignedData};
+    use crate::{EventJson, StateEvent, Unsigned};
 
     use super::NameEventContent;
 
@@ -81,7 +81,7 @@ mod tests {
             room_id: RoomId::try_from("!n8f893n9:example.com").unwrap(),
             sender: UserId::try_from("@carl:example.com").unwrap(),
             state_key: "".into(),
-            unsigned: UnsignedData::default(),
+            unsigned: Unsigned::default(),
         };
 
         let actual = to_json_value(&name_event).unwrap();
@@ -110,7 +110,7 @@ mod tests {
             room_id: RoomId::try_from("!n8f893n9:example.com").unwrap(),
             sender: UserId::try_from("@carl:example.com").unwrap(),
             state_key: "".into(),
-            unsigned: UnsignedData { age: Some(Int::from(100)), ..UnsignedData::default() },
+            unsigned: Unsigned { age: Some(Int::from(100)), ..Unsigned::default() },
         };
 
         let actual = to_json_value(&name_event).unwrap();
diff --git a/ruma-events/src/room/pinned_events.rs b/ruma-events/src/room/pinned_events.rs
index 66f29755..39dc25fb 100644
--- a/ruma-events/src/room/pinned_events.rs
+++ b/ruma-events/src/room/pinned_events.rs
@@ -28,7 +28,7 @@ mod tests {
     use serde_json::to_string;
 
     use super::PinnedEventsEventContent;
-    use crate::{EventJson, StateEvent, UnsignedData};
+    use crate::{EventJson, StateEvent, Unsigned};
 
     #[test]
     fn serialization_deserialization() {
@@ -46,7 +46,7 @@ mod tests {
             room_id: RoomId::new(server_name),
             sender: UserId::new(server_name),
             state_key: "".into(),
-            unsigned: UnsignedData::default(),
+            unsigned: Unsigned::default(),
         };
 
         let serialized_event = to_string(&event).unwrap();
diff --git a/ruma-events/src/room/power_levels.rs b/ruma-events/src/room/power_levels.rs
index a2c20d13..3446afcf 100644
--- a/ruma-events/src/room/power_levels.rs
+++ b/ruma-events/src/room/power_levels.rs
@@ -129,7 +129,7 @@ mod tests {
     use serde_json::{json, to_value as to_json_value};
 
     use super::{default_power_level, NotificationPowerLevels, PowerLevelsEventContent};
-    use crate::{EventType, StateEvent, UnsignedData};
+    use crate::{EventType, StateEvent, Unsigned};
 
     #[test]
     fn serialization_with_optional_fields_as_none() {
@@ -152,7 +152,7 @@ mod tests {
             origin_server_ts: UNIX_EPOCH + Duration::from_millis(1),
             prev_content: None,
             room_id: RoomId::try_from("!n8f893n9:example.com").unwrap(),
-            unsigned: UnsignedData::default(),
+            unsigned: Unsigned::default(),
             sender: UserId::try_from("@carl:example.com").unwrap(),
             state_key: "".into(),
         };
@@ -211,7 +211,7 @@ mod tests {
                 notifications: NotificationPowerLevels { room: Int::from(42) },
             }),
             room_id: RoomId::try_from("!n8f893n9:example.com").unwrap(),
-            unsigned: UnsignedData { age: Some(Int::from(100)), ..UnsignedData::default() },
+            unsigned: Unsigned { age: Some(Int::from(100)), ..Unsigned::default() },
             sender: user,
             state_key: "".into(),
         };
diff --git a/ruma-events/src/room/redaction.rs b/ruma-events/src/room/redaction.rs
index 71cb3ddb..92aad5cf 100644
--- a/ruma-events/src/room/redaction.rs
+++ b/ruma-events/src/room/redaction.rs
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
 
 use crate::{
     MessageEventContent, RedactedMessageEventContent, RedactedStateEventContent, RoomEventContent,
-    UnsignedData,
+    Unsigned,
 };
 
 /// Redaction event.
@@ -33,7 +33,7 @@ pub struct RedactionEvent {
     pub room_id: RoomId,
 
     /// Additional key-value pairs not signed by the homeserver.
-    pub unsigned: UnsignedData,
+    pub unsigned: Unsigned,
 }
 
 /// Redaction event without a `room_id`.
@@ -55,7 +55,7 @@ pub struct SyncRedactionEvent {
     pub origin_server_ts: SystemTime,
 
     /// Additional key-value pairs not signed by the homeserver.
-    pub unsigned: UnsignedData,
+    pub unsigned: Unsigned,
 }
 
 /// A redaction of an event.
diff --git a/ruma-federation-api/CHANGELOG.md b/ruma-federation-api/CHANGELOG.md
index 8ea78692..864476de 100644
--- a/ruma-federation-api/CHANGELOG.md
+++ b/ruma-federation-api/CHANGELOG.md
@@ -28,7 +28,7 @@ Improvements:
     },
     transactions::send_transaction_message::v1,
     version::get_server_version::v1
-  ```
+    ```
 
 # 0.0.1
 
diff --git a/ruma/Cargo.toml b/ruma/Cargo.toml
index 1126b45d..61bb3b84 100644
--- a/ruma/Cargo.toml
+++ b/ruma/Cargo.toml
@@ -16,6 +16,7 @@ edition = "2018"
 either = ["ruma-identifiers/either"]
 rand = ["ruma-identifiers/rand"]
 unstable-pre-spec = ["ruma-client-api/unstable-pre-spec"]
+unstable-synapse-quirks = ["ruma-client-api/unstable-synapse-quirks"]
 
 appservice-api = ["ruma-api", "ruma-appservice-api", "ruma-events"]
 client-api = ["ruma-api", "ruma-client-api", "ruma-events"]


Test patch:
diff --git a/ruma-events/tests/custom.rs b/ruma-events/tests/custom.rs
index c1329de2..a93874e1 100644
--- a/ruma-events/tests/custom.rs
+++ b/ruma-events/tests/custom.rs
@@ -7,7 +7,7 @@ use matches::assert_matches;
 use ruma_events::{
     custom::CustomEventContent, AnyMessageEvent, AnyStateEvent, AnyStateEventContent,
     AnySyncMessageEvent, AnySyncRoomEvent, EventJson, MessageEvent, StateEvent, SyncMessageEvent,
-    SyncStateEvent, UnsignedData,
+    SyncStateEvent, Unsigned,
 };
 use ruma_identifiers::{EventId, RoomId, UserId};
 use serde_json::{
@@ -57,7 +57,7 @@ fn serialize_custom_message_event() {
         origin_server_ts: UNIX_EPOCH + Duration::from_millis(10),
         room_id: RoomId::try_from("!room:room.com").unwrap(),
         sender: UserId::try_from("@carl:example.com").unwrap(),
-        unsigned: UnsignedData::default(),
+        unsigned: Unsigned::default(),
     });
 
     let actual = to_json_value(&aliases_event).unwrap();
@@ -99,7 +99,7 @@ fn serialize_custom_state_event() {
         room_id: RoomId::try_from("!roomid:room.com").unwrap(),
         sender: UserId::try_from("@carl:example.com").unwrap(),
         state_key: "".into(),
-        unsigned: UnsignedData::default(),
+        unsigned: Unsigned::default(),
     });
 
     let actual = to_json_value(&aliases_event).unwrap();
diff --git a/ruma-events/tests/event_enums.rs b/ruma-events/tests/event_enums.rs
index e92f9c24..a88e62f0 100644
--- a/ruma-events/tests/event_enums.rs
+++ b/ruma-events/tests/event_enums.rs
@@ -12,7 +12,7 @@ use ruma_events::{
     call::{answer::AnswerEventContent, SessionDescription, SessionDescriptionType},
     room::{ImageInfo, ThumbnailInfo},
     sticker::StickerEventContent,
-    AnyMessageEvent, MessageEvent, UnsignedData,
+    AnyMessageEvent, MessageEvent, Unsigned,
 };
 
 #[test]
@@ -92,7 +92,7 @@ fn serialize_message_event() {
         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(),
+        unsigned: Unsigned::default(),
     });
 
     let actual = to_json_value(&aliases_event).unwrap();
diff --git a/ruma-events/tests/message_event.rs b/ruma-events/tests/message_event.rs
index d253cc60..446af84e 100644
--- a/ruma-events/tests/message_event.rs
+++ b/ruma-events/tests/message_event.rs
@@ -9,7 +9,7 @@ use ruma_events::{
     call::{answer::AnswerEventContent, SessionDescription, SessionDescriptionType},
     room::{ImageInfo, ThumbnailInfo},
     sticker::StickerEventContent,
-    AnyMessageEventContent, EventJson, MessageEvent, UnsignedData,
+    AnyMessageEventContent, EventJson, MessageEvent, Unsigned,
 };
 use ruma_identifiers::{EventId, RoomId, UserId};
 use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
@@ -39,7 +39,7 @@ fn message_serialize_sticker() {
         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(),
+        unsigned: Unsigned::default(),
     };
 
     let actual = to_json_value(&aliases_event).unwrap();
diff --git a/ruma-events/tests/redacted.rs b/ruma-events/tests/redacted.rs
index 5cafc87b..972dea4a 100644
--- a/ruma-events/tests/redacted.rs
+++ b/ruma-events/tests/redacted.rs
@@ -10,28 +10,27 @@ use ruma_events::{
         aliases::RedactedAliasesEventContent,
         create::RedactedCreateEventContent,
         message::RedactedMessageEventContent,
-        redaction::{RedactionEvent, RedactionEventContent},
+        redaction::{RedactionEvent, RedactionEventContent, SyncRedactionEvent},
     },
     AnyRedactedMessageEvent, AnyRedactedSyncMessageEvent, AnyRedactedSyncStateEvent, AnyRoomEvent,
     AnySyncRoomEvent, EventJson, RedactedMessageEvent, RedactedSyncMessageEvent,
-    RedactedSyncStateEvent, UnsignedData,
+    RedactedSyncStateEvent, RedactedSyncUnsigned, RedactedUnsigned, Unsigned,
 };
 use ruma_identifiers::{EventId, RoomId, UserId};
 use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
 
-fn full_unsigned() -> UnsignedData {
-    let mut unsigned = UnsignedData::default();
+fn full_unsigned() -> RedactedSyncUnsigned {
+    let mut unsigned = RedactedSyncUnsigned::default();
     // The presence of `redacted_because` triggers the event enum to return early
     // with `RedactedContent` instead of failing to deserialize according
     // to the event type string.
-    unsigned.redacted_because = Some(EventJson::from(RedactionEvent {
+    unsigned.redacted_because = Some(EventJson::from(SyncRedactionEvent {
         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(),
+        unsigned: Unsigned::default(),
     }));
 
     unsigned
@@ -44,7 +43,7 @@ fn redacted_message_event_serialize() {
         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(),
+        unsigned: RedactedSyncUnsigned::default(),
     };
 
     let expected = json!({
@@ -66,7 +65,7 @@ fn redacted_aliases_event_serialize_no_content() {
         state_key: "".into(),
         origin_server_ts: UNIX_EPOCH + Duration::from_millis(1),
         sender: UserId::try_from("@carl:example.com").unwrap(),
-        unsigned: UnsignedData::default(),
+        unsigned: RedactedSyncUnsigned::default(),
     };
 
     let expected = json!({
@@ -89,7 +88,7 @@ fn redacted_aliases_event_serialize_with_content() {
         state_key: "".to_string(),
         origin_server_ts: UNIX_EPOCH + Duration::from_millis(1),
         sender: UserId::try_from("@carl:example.com").unwrap(),
-        unsigned: UnsignedData::default(),
+        unsigned: RedactedSyncUnsigned::default(),
     };
 
     let expected = json!({
@@ -165,7 +164,7 @@ fn redacted_deserialize_any_room() {
 
 #[test]
 fn redacted_deserialize_any_room_sync() {
-    let mut unsigned = UnsignedData::default();
+    let mut unsigned = RedactedUnsigned::default();
     // The presence of `redacted_because` triggers the event enum (AnySyncRoomEvent in this case)
     // to return early with `RedactedContent` instead of failing to deserialize according
     // to the event type string.
@@ -176,7 +175,7 @@ fn redacted_deserialize_any_room_sync() {
         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(),
+        unsigned: Unsigned::default(),
     }));
 
     let redacted = json!({
diff --git a/ruma-events/tests/redaction.rs b/ruma-events/tests/redaction.rs
index 6aa5ede8..e72b914f 100644
--- a/ruma-events/tests/redaction.rs
+++ b/ruma-events/tests/redaction.rs
@@ -6,7 +6,7 @@ use std::{
 use matches::assert_matches;
 use ruma_events::{
     room::redaction::{RedactionEvent, RedactionEventContent},
-    AnyMessageEvent, EventJson, UnsignedData,
+    AnyMessageEvent, EventJson, Unsigned,
 };
 use ruma_identifiers::{EventId, RoomId, UserId};
 use serde_json::{
@@ -36,7 +36,7 @@ fn serialize_redaction() {
         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(),
+        unsigned: Unsigned::default(),
     };
 
     let actual = to_json_value(&aliases_event).unwrap();
diff --git a/ruma-events/tests/state_event.rs b/ruma-events/tests/state_event.rs
index b8afd777..519f08f3 100644
--- a/ruma-events/tests/state_event.rs
+++ b/ruma-events/tests/state_event.rs
@@ -8,7 +8,7 @@ use matches::assert_matches;
 use ruma_events::{
     room::{aliases::AliasesEventContent, avatar::AvatarEventContent, ImageInfo, ThumbnailInfo},
     AnyRoomEvent, AnyStateEvent, AnyStateEventContent, EventJson, StateEvent, SyncStateEvent,
-    UnsignedData,
+    Unsigned,
 };
 use ruma_identifiers::{EventId, RoomAliasId, RoomId, UserId};
 use serde_json::{
@@ -46,7 +46,7 @@ fn serialize_aliases_with_prev_content() {
         room_id: RoomId::try_from("!roomid:room.com").unwrap(),
         sender: UserId::try_from("@carl:example.com").unwrap(),
         state_key: "".into(),
-        unsigned: UnsignedData::default(),
+        unsigned: Unsigned::default(),
     };
 
     let actual = to_json_value(&aliases_event).unwrap();
@@ -67,7 +67,7 @@ fn serialize_aliases_without_prev_content() {
         room_id: RoomId::try_from("!roomid:room.com").unwrap(),
         sender: UserId::try_from("@carl:example.com").unwrap(),
         state_key: "".into(),
-        unsigned: UnsignedData::default(),
+        unsigned: Unsigned::default(),
     };
 
     let actual = to_json_value(&aliases_event).unwrap();