diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index 6904fe811f..1b643a5c47 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -562,9 +562,12 @@ mod tests { json!({ "latest_event": { "event": { - "encryption_info": null, - "event": { - "event_id": "$1" + "inner_event": { + "PlainText": { + "event": { + "event_id": "$1" + } + } } }, } @@ -578,6 +581,23 @@ mod tests { assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none()); // The previous format can also be deserialized. + let serialized = json!({ + "latest_event": { + "event": { + "encryption_info": null, + "event": { + "event_id": "$1" + } + }, + } + }); + + let deserialized: TestStruct = serde_json::from_value(serialized).unwrap(); + assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1"); + assert!(deserialized.latest_event.sender_profile.is_none()); + assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none()); + + // The even older format can also be deserialized. let serialized = json!({ "latest_event": event }); diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index d62d2be666..0669acd502 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -1805,10 +1805,7 @@ mod tests { "encryption_state_synced": true, "latest_event": { "event": { - "encryption_info": null, - "event": { - "sender": "@u:i.uk", - }, + "inner_event": {"PlainText": {"event": {"sender": "@u:i.uk"}}}, }, }, "base_info": { diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 7d9c19799f..e071c69ace 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -300,107 +300,80 @@ pub struct EncryptionInfo { /// Represents a matrix room event that has been returned from `/sync`, /// after initial processing. /// -/// This is almost identical to [`TimelineEvent`], but wraps an -/// [`AnySyncTimelineEvent`] instead of [`AnyTimelineEvent`]. -#[derive(Clone, Deserialize, Serialize)] +/// Previously, this differed from [`TimelineEvent`] by wrapping an +/// [`AnySyncTimelineEvent`] instead of an [`AnyTimelineEvent`], but nowadays +/// they are essentially identical, and one of them should probably be removed. +#[derive(Clone, Serialize)] pub struct SyncTimelineEvent { - /// The actual event. - #[serde(rename = "event")] - inner_event: Raw, - - /// The encryption info about the event. Will be `None` if the event was not - /// encrypted. - #[serde(rename = "encryption_info")] - inner_encryption_info: Option, + /// The event itself, together with any information on decryption. + pub inner_event: TimelineEventInner, /// The push actions associated with this event. - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde(skip_serializing_if = "Vec::is_empty")] pub push_actions: Vec, - /// The encryption info about the events bundled in the `unsigned` object. - /// - /// Will be `None` if no bundled event was encrypted. - #[serde(skip_serializing_if = "Option::is_none")] - pub unsigned_encryption_info: Option>, } impl SyncTimelineEvent { /// Create a new `SyncTimelineEvent` from the given raw event. /// /// This is a convenience constructor for when you don't need to set - /// `encryption_info` or `push_action`, for example inside a test. + /// encryption info or `push_action`, for example inside a test. pub fn new(event: Raw) -> Self { - Self { - inner_event: event, - inner_encryption_info: None, - push_actions: vec![], - unsigned_encryption_info: None, - } + Self { inner_event: TimelineEventInner::PlainText { event }, push_actions: vec![] } } /// Create a new `SyncTimelineEvent` from the given raw event and push /// actions. /// /// This is a convenience constructor for when you don't need to set - /// `encryption_info`, for example inside a test. + /// encryption info, for example inside a test. pub fn new_with_push_actions( event: Raw, push_actions: Vec, ) -> Self { - Self { - inner_event: event, - inner_encryption_info: None, - push_actions, - unsigned_encryption_info: None, - } + Self { inner_event: TimelineEventInner::PlainText { event }, push_actions } } /// Get the event id of this `SyncTimelineEvent` if the event has any valid /// id. pub fn event_id(&self) -> Option { - self.inner_event.get_field::("event_id").ok().flatten() + self.inner_event.event().get_field::("event_id").ok().flatten() } /// Returns a reference to the (potentially decrypted) Matrix event inside /// this `TimelineEvent`. pub fn event(&self) -> &Raw { - &self.inner_event + &self.inner_event.event() } /// If the event was a decrypted event that was successfully decrypted, get /// its encryption info. Otherwise, `None`. pub fn encryption_info(&self) -> Option<&EncryptionInfo> { - self.inner_encryption_info.as_ref() + self.inner_event.encryption_info() } /// Takes ownership of this `TimelineEvent`, returning the (potentially /// decrypted) Matrix event within. pub fn into_event(self) -> Raw { - self.inner_event + self.inner_event.into_event() } /// Replace the Matrix event within this event. Used in response to /// redaction. pub fn replace_inner(&mut self, event: Raw) { - self.inner_event = event; + self.inner_event = TimelineEventInner::PlainText { event }; } } #[cfg(not(tarpaulin_include))] impl fmt::Debug for SyncTimelineEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let SyncTimelineEvent { - inner_event, - inner_encryption_info, - push_actions, - unsigned_encryption_info, - } = self; + let SyncTimelineEvent { inner_event, push_actions } = self; let mut s = f.debug_struct("SyncTimelineEvent"); - s.field("event", &DebugRawEvent(inner_event)); - s.maybe_field("encryption_info", inner_encryption_info); + s.field("event", &inner_event); if !push_actions.is_empty() { s.field("push_actions", push_actions); } - s.maybe_field("unsigned_encryption_info", unsigned_encryption_info); s.finish() } } @@ -413,12 +386,7 @@ impl From> for SyncTimelineEvent { impl From for SyncTimelineEvent { fn from(o: TimelineEvent) -> Self { - Self { - inner_event: o.inner_event, - inner_encryption_info: o.inner_encryption_info, - push_actions: o.push_actions.unwrap_or_default(), - unsigned_encryption_info: o.unsigned_encryption_info, - } + Self { inner_event: o.inner_event, push_actions: o.push_actions.unwrap_or_default() } } } @@ -429,6 +397,48 @@ impl From for SyncTimelineEvent { } } +impl<'de> Deserialize<'de> for SyncTimelineEvent { + /// Custom deserializer for [`SyncTimelineEvent`], to support older formats. + /// + /// Ideally we might use an untagged enum and then convert from that; + /// however, that doesn't work due to a [serde bug](https://github.com/serde-rs/json/issues/497). + /// + /// Instead, we first deserialize into an unstructured JSON map, and then + /// inspect the json to figure out which format we have. + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde_json::{Map, Value}; + + // First, deserialize to an unstructured JSON map + let value = Map::::deserialize(deserializer)?; + + // If we have a top-level `event`, it's V0 + if value.contains_key("event") { + let v0: SyncTimelineEventDeserializationHelperV0 = + serde_json::from_value(Value::Object(value)).map_err(|e| { + serde::de::Error::custom(format!( + "Unable to deserialize V0-format SyncTimelineEvent: {}", + e + )) + })?; + Ok(v0.into()) + } + // Otherwise, it's V1 + else { + let v1: SyncTimelineEventDeserializationHelperV1 = + serde_json::from_value(Value::Object(value)).map_err(|e| { + serde::de::Error::custom(format!( + "Unable to deserialize V1-format SyncTimelineEvent: {}", + e + )) + })?; + Ok(v1.into()) + } + } +} + /// Represents a matrix room event that has been returned from a Matrix /// client-server API endpoint such as `/messages`, after initial processing. /// @@ -436,102 +446,146 @@ impl From for SyncTimelineEvent { /// the main thing this adds over [`AnyTimelineEvent`] is information on /// encryption. /// -/// See also [`SyncTimelineEvent`] which is almost identical, but is used for -/// results from the `/sync` endpoint (which lack a `room_id` property) and -/// hence wraps an [`AnySyncTimelineEvent`] instead of [`AnyTimelineEvent`]. +/// Previously, this differed from [`SyncTimelineEvent`] by wrapping an +/// [`AnyTimelineEvent`] instead of an [`AnySyncTimelineEvent`], but nowadays +/// they are essentially identical, and one of them should probably be removed. #[derive(Clone)] pub struct TimelineEvent { - /// The actual event. - /// - /// Depending on the source, this may actually be a `Raw`. - /// The difference is only whether the json contains a `room_id` field. - inner_event: Raw, - /// The encryption info about the event. Will be `None` if the event was not - /// encrypted. - inner_encryption_info: Option, + /// The event itself, together with any information on decryption. + pub inner_event: TimelineEventInner, + /// The push actions associated with this event, if we had sufficient /// context to compute them. pub push_actions: Option>, - /// The encryption info about the events bundled in the `unsigned` object. - /// - /// Will be `None` if no bundled event was encrypted. - pub unsigned_encryption_info: Option>, } impl TimelineEvent { /// Create a new `TimelineEvent` from the given raw event. /// /// This is a convenience constructor for when you don't need to set - /// `encryption_info` or `push_action`, for example inside a test. + /// encryption info or `push_action`, for example inside a test. pub fn new(event: Raw) -> Self { Self { // This conversion is unproblematic since a `SyncTimelineEvent` is just a // `TimelineEvent` without the `room_id`. By converting the raw value in // this way, we simply cause the `room_id` field in the json to be // ignored by a subsequent deserialization. - inner_event: event.cast(), - inner_encryption_info: None, + inner_event: TimelineEventInner::PlainText { event: event.cast() }, push_actions: None, - unsigned_encryption_info: None, } } /// Returns a reference to the (potentially decrypted) Matrix event inside /// this `TimelineEvent`. pub fn event(&self) -> &Raw { - &self.inner_event + &self.inner_event.event() } /// If the event was a decrypted event that was successfully decrypted, get /// its encryption info. Otherwise, `None`. pub fn encryption_info(&self) -> Option<&EncryptionInfo> { - self.inner_encryption_info.as_ref() + self.inner_event.encryption_info() } /// Takes ownership of this `TimelineEvent`, returning the (potentially /// decrypted) Matrix event within. pub fn into_event(self) -> Raw { - self.inner_event + self.inner_event.into_event() } } impl From for TimelineEvent { fn from(decrypted: DecryptedRoomEvent) -> Self { - Self { - // Casting from the more specific `AnyMessageLikeEvent` (i.e. an event that contains - // a `room_id`, but not a `state_key`) to a more generic `AnySyncTimelineEvent` - // (i.e. one that does *not* contain a `room_id`, and *may* contain a `state_key`) - // is safe. It means that the `room_id` is ignored. - inner_event: decrypted.event.cast(), - inner_encryption_info: Some(decrypted.encryption_info), - push_actions: None, - unsigned_encryption_info: decrypted.unsigned_encryption_info, - } + Self { inner_event: TimelineEventInner::Decrypted(decrypted), push_actions: None } } } #[cfg(not(tarpaulin_include))] impl fmt::Debug for TimelineEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let TimelineEvent { - inner_event, - inner_encryption_info, - push_actions, - unsigned_encryption_info, - } = self; + let TimelineEvent { inner_event, push_actions } = self; let mut s = f.debug_struct("TimelineEvent"); - s.field("event", &DebugRawEvent(inner_event)); - s.maybe_field("encryption_info", inner_encryption_info); + s.field("event", &inner_event); if let Some(push_actions) = &push_actions { if !push_actions.is_empty() { s.field("push_actions", push_actions); } } - s.maybe_field("unsigned_encryption_info", unsigned_encryption_info); s.finish() } } +/// The event within a [`TimelineEvent`] or [`SyncTimelineEvent`], together with +/// encryption data. +#[derive(Clone, Serialize, Deserialize)] +pub enum TimelineEventInner { + /// A successfully-decrypted encrypted event. + Decrypted(DecryptedRoomEvent), + + /// An unencrypted event. + PlainText { + /// The actual event. Depending on the source of the event, it could + /// actually be a [`AnyTimelineEvent`] (which differs from + /// [`AnySyncTimelineEvent`] by the addition of a `room_id` property). + event: Raw, + }, +} + +impl TimelineEventInner { + /// Returns a reference to the (potentially decrypted) Matrix event inside + /// this `TimelineEvent`. + pub fn event(&self) -> &Raw { + match self { + // It is safe to cast from an `AnyMessageLikeEvent` (i.e. JSON which does + // *not* contain a `state_key` and *does* contain a `room_id`) into an + // `AnySyncTimelineEvent` (i.e. JSON which *may* contain a `state_key` and is *not* + // expected to contain a `room_id`). It just means that the `room_id` will be ignored + // in a future deserialization. + TimelineEventInner::Decrypted(d) => d.event.cast_ref(), + TimelineEventInner::PlainText { event } => event, + } + } + + /// If the event was a decrypted event that was successfully decrypted, get + /// its encryption info. Otherwise, `None`. + pub fn encryption_info(&self) -> Option<&EncryptionInfo> { + match self { + TimelineEventInner::Decrypted(d) => Some(&d.encryption_info), + TimelineEventInner::PlainText { .. } => None, + } + } + + /// Takes ownership of this `TimelineEvent`, returning the (potentially + /// decrypted) Matrix event within. + pub fn into_event(self) -> Raw { + match self { + // It is safe to cast from an `AnyMessageLikeEvent` (i.e. JSON which does + // *not* contain a `state_key` and *does* contain a `room_id`) into an + // `AnySyncTimelineEvent` (i.e. JSON which *may* contain a `state_key` and is *not* + // expected to contain a `room_id`). It just means that the `room_id` will be ignored + // in a future deserialization. + TimelineEventInner::Decrypted(d) => d.event.cast(), + TimelineEventInner::PlainText { event } => event, + } + } +} + +#[cfg(not(tarpaulin_include))] +impl fmt::Debug for TimelineEventInner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self { + Self::PlainText { event } => f + .debug_struct("TimelineEventDecryptionResult::PlainText") + .field("event", &DebugRawEvent(event)) + .finish(), + + Self::Decrypted(decrypted) => { + f.debug_tuple("TimelineEventDecryptionResult::Decrypted").field(decrypted).finish() + } + } + } +} + #[derive(Clone, Serialize, Deserialize)] /// A successfully-decrypted encrypted event. pub struct DecryptedRoomEvent { @@ -545,6 +599,7 @@ pub struct DecryptedRoomEvent { /// object. /// /// Will be `None` if no bundled event was encrypted. + #[serde(skip_serializing_if = "Option::is_none")] pub unsigned_encryption_info: Option>, } @@ -609,6 +664,79 @@ pub struct UnableToDecryptInfo { pub session_id: Option, } +/// Deserialization helper for [`SyncTimelineEvent`], for the modern format. +/// +/// This has the exact same fields as [`SyncTimelineEvent`] itself, but has a +/// regular `Deserialize` implementation. +#[derive(Debug, Deserialize)] +struct SyncTimelineEventDeserializationHelperV1 { + /// The event itself, together with any information on decryption. + inner_event: TimelineEventInner, + + /// The push actions associated with this event. + #[serde(default)] + push_actions: Vec, +} + +impl From for SyncTimelineEvent { + fn from(value: SyncTimelineEventDeserializationHelperV1) -> Self { + let SyncTimelineEventDeserializationHelperV1 { inner_event, push_actions } = value; + SyncTimelineEvent { inner_event, push_actions } + } +} + +/// Deserialization helper for [`SyncTimelineEvent`], for an older format. +#[derive(Deserialize)] +struct SyncTimelineEventDeserializationHelperV0 { + /// The actual event. + event: Raw, + + /// The encryption info about the event. Will be `None` if the event + /// was not encrypted. + encryption_info: Option, + + /// The push actions associated with this event. + #[serde(default)] + push_actions: Vec, + + /// The encryption info about the events bundled in the `unsigned` + /// object. + /// + /// Will be `None` if no bundled event was encrypted. + unsigned_encryption_info: Option>, +} + +impl From for SyncTimelineEvent { + fn from(value: SyncTimelineEventDeserializationHelperV0) -> Self { + let SyncTimelineEventDeserializationHelperV0 { + event, + encryption_info, + push_actions, + unsigned_encryption_info, + } = value; + + let inner_event = match encryption_info { + Some(encryption_info) => { + TimelineEventInner::Decrypted(DecryptedRoomEvent { + // We cast from `Raw` to + // `Raw`, which means + // we are asserting that it contains a room_id. + // That *should* be ok, because if this is genuinely a decrypted + // room event (as the encryption_info indicates), then it will have + // a room_id. + event: event.cast(), + encryption_info, + unsigned_encryption_info, + }) + } + + None => TimelineEventInner::PlainText { event }, + }; + + SyncTimelineEvent { inner_event, push_actions } + } +} + #[cfg(test)] mod tests { use assert_matches::assert_matches; @@ -622,7 +750,8 @@ mod tests { use serde_json::json; use super::{ - AlgorithmInfo, EncryptionInfo, SyncTimelineEvent, TimelineEvent, VerificationState, + AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEvent, + TimelineEventInner, VerificationState, }; use crate::deserialized_responses::{DeviceLinkProblem, VerificationLevel}; @@ -702,18 +831,20 @@ mod tests { #[test] fn sync_timeline_event_serialisation() { let room_event = SyncTimelineEvent { - inner_event: Raw::new(&example_event()).unwrap().cast(), - inner_encryption_info: Some(EncryptionInfo { - sender: user_id!("@sender:example.com").to_owned(), - sender_device: None, - algorithm_info: AlgorithmInfo::MegolmV1AesSha2 { - curve25519_key: "xxx".to_owned(), - sender_claimed_keys: Default::default(), + inner_event: TimelineEventInner::Decrypted(DecryptedRoomEvent { + event: Raw::new(&example_event()).unwrap().cast(), + encryption_info: EncryptionInfo { + sender: user_id!("@sender:example.com").to_owned(), + sender_device: None, + algorithm_info: AlgorithmInfo::MegolmV1AesSha2 { + curve25519_key: "xxx".to_owned(), + sender_claimed_keys: Default::default(), + }, + verification_state: VerificationState::Verified, }, - verification_state: VerificationState::Verified, + unsigned_encryption_info: None, }), push_actions: Default::default(), - unsigned_encryption_info: None, }; let serialized = serde_json::to_value(&room_event).unwrap(); @@ -722,25 +853,29 @@ mod tests { assert_eq!( serialized, json!({ - "event": { - "content": {"body": "secret", "msgtype": "m.text"}, - "event_id": "$xxxxx:example.org", - "origin_server_ts": 2189, - "room_id": "!someroom:example.com", - "sender": "@carl:example.com", - "type": "m.room.message", - }, - "encryption_info": { - "sender": "@sender:example.com", - "sender_device": null, - "algorithm_info": { - "MegolmV1AesSha2": { - "curve25519_key": "xxx", - "sender_claimed_keys": {} - } - }, - "verification_state": "Verified", - }, + "inner_event": { + "Decrypted": { + "event": { + "content": {"body": "secret", "msgtype": "m.text"}, + "event_id": "$xxxxx:example.org", + "origin_server_ts": 2189, + "room_id": "!someroom:example.com", + "sender": "@carl:example.com", + "type": "m.room.message", + }, + "encryption_info": { + "sender": "@sender:example.com", + "sender_device": null, + "algorithm_info": { + "MegolmV1AesSha2": { + "curve25519_key": "xxx", + "sender_claimed_keys": {} + } + }, + "verification_state": "Verified", + }, + } + } }) ); @@ -750,6 +885,35 @@ mod tests { assert_matches!( event.encryption_info().unwrap().algorithm_info, AlgorithmInfo::MegolmV1AesSha2 { .. } - ) + ); + + // Test that the previous format can also be deserialized. + let serialized = json!({ + "event": { + "content": {"body": "secret", "msgtype": "m.text"}, + "event_id": "$xxxxx:example.org", + "origin_server_ts": 2189, + "room_id": "!someroom:example.com", + "sender": "@carl:example.com", + "type": "m.room.message", + }, + "encryption_info": { + "sender": "@sender:example.com", + "sender_device": null, + "algorithm_info": { + "MegolmV1AesSha2": { + "curve25519_key": "xxx", + "sender_claimed_keys": {} + } + }, + "verification_state": "Verified", + }, + }); + let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap(); + assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned())); + assert_matches!( + event.encryption_info().unwrap().algorithm_info, + AlgorithmInfo::MegolmV1AesSha2 { .. } + ); } } diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index 33401a36f7..cfa44c55e0 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -657,7 +657,7 @@ mod tests { "prev_batch": "foo", "timeline": [ { - "event": { + "inner_event": { "PlainText": { "event": { "content": { "body": "let it gooo!", "msgtype": "m.text" @@ -667,8 +667,7 @@ mod tests { "room_id": "!someroom:example.com", "sender": "@bob:example.com", "type": "m.room.message" - }, - "encryption_info": null + }}} } ] })