diff --git a/crates/ruma-common/src/events/_custom.rs b/crates/ruma-common/src/events/_custom.rs index b5c2e0a6..c4d34671 100644 --- a/crates/ruma-common/src/events/_custom.rs +++ b/crates/ruma-common/src/events/_custom.rs @@ -74,7 +74,7 @@ impl StaticStateEventContent for CustomStateEventContent { // Like `StateUnsigned`, but without `prev_content`. // We don't care about `prev_content` since we'd only store the event type that is the same // as in the content. - type Unsigned = MessageLikeUnsigned; + type Unsigned = MessageLikeUnsigned; type PossiblyRedacted = Self; } impl PossiblyRedactedStateEventContent for CustomStateEventContent { diff --git a/crates/ruma-common/src/events/kinds.rs b/crates/ruma-common/src/events/kinds.rs index bb99b001..83698d90 100644 --- a/crates/ruma-common/src/events/kinds.rs +++ b/crates/ruma-common/src/events/kinds.rs @@ -119,7 +119,7 @@ pub struct OriginalMessageLikeEvent { pub room_id: OwnedRoomId, /// Additional key-value pairs not signed by the homeserver. - pub unsigned: MessageLikeUnsigned, + pub unsigned: MessageLikeUnsigned, } /// An unredacted message-like event without a `room_id`. @@ -141,7 +141,7 @@ pub struct OriginalSyncMessageLikeEvent { pub origin_server_ts: MilliSecondsSinceUnixEpoch, /// Additional key-value pairs not signed by the homeserver. - pub unsigned: MessageLikeUnsigned, + pub unsigned: MessageLikeUnsigned, } /// A redacted message-like event. diff --git a/crates/ruma-common/src/events/relation.rs b/crates/ruma-common/src/events/relation.rs index 7692009b..c5e2b46c 100644 --- a/crates/ruma-common/src/events/relation.rs +++ b/crates/ruma-common/src/events/relation.rs @@ -10,9 +10,11 @@ use serde::{Deserialize, Serialize}; use super::AnyMessageLikeEvent; use crate::{ serde::{Raw, StringEnum}, - MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, PrivOwnedStr, + OwnedEventId, PrivOwnedStr, }; +mod rel_serde; + /// Information about the event a [rich reply] is replying to. /// /// [rich reply]: https://spec.matrix.org/latest/client-server-api/#rich-replies @@ -58,31 +60,6 @@ impl Annotation { } } -/// A bundled replacement. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct BundledReplacement { - /// The ID of the replacing event. - pub event_id: OwnedEventId, - - /// The user ID of the sender of the latest replacement. - pub sender: OwnedUserId, - - /// Timestamp in milliseconds on originating homeserver when the latest replacement was sent. - pub origin_server_ts: MilliSecondsSinceUnixEpoch, -} - -impl BundledReplacement { - /// Creates a new `BundledReplacement` with the given event ID, sender and timestamp. - pub fn new( - event_id: OwnedEventId, - sender: OwnedUserId, - origin_server_ts: MilliSecondsSinceUnixEpoch, - ) -> Self { - Self { event_id, sender, origin_server_ts } - } -} - /// The content of a [replacement] relation. /// /// [replacement]: https://spec.matrix.org/latest/client-server-api/#event-replacements @@ -221,12 +198,18 @@ impl ReferenceChunk { /// [Bundled aggregations] of related child events of a message-like event. /// /// [Bundled aggregations]: https://spec.matrix.org/latest/client-server-api/#aggregations -#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Debug, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct BundledMessageLikeRelations { +pub struct BundledMessageLikeRelations { /// Replacement relation. #[serde(rename = "m.replace", skip_serializing_if = "Option::is_none")] - pub replace: Option>, + pub replace: Option>, + + /// Set when the above fails to deserialize. + /// + /// Intentionally *not* public. + #[serde(skip_serializing)] + has_invalid_replacement: bool, /// Thread relation. #[serde(rename = "m.thread", skip_serializing_if = "Option::is_none")] @@ -237,10 +220,20 @@ pub struct BundledMessageLikeRelations { pub reference: Option>, } -impl BundledMessageLikeRelations { +impl BundledMessageLikeRelations { /// Creates a new empty `BundledMessageLikeRelations`. pub const fn new() -> Self { - Self { replace: None, thread: None, reference: None } + Self { replace: None, has_invalid_replacement: false, thread: None, reference: None } + } + + /// Whether this bundle contains a replacement relation. + /// + /// This may be `true` even if the `replace` field is `None`, because Matrix versions prior to + /// 1.7 had a different incompatible format for bundled replacements. Use this method to check + /// whether an event was replaced. If this returns `true` but `replace` is `None`, use one of + /// the endpoints from `ruma::api::client::relations` to fetch the relation details. + pub fn has_replacement(&self) -> bool { + self.replace.is_some() || self.has_invalid_replacement } /// Returns `true` if all fields are empty. @@ -249,6 +242,12 @@ impl BundledMessageLikeRelations { } } +impl Default for BundledMessageLikeRelations { + fn default() -> Self { + Self::new() + } +} + /// [Bundled aggregations] of related child events of a state event. /// /// [Bundled aggregations]: https://spec.matrix.org/latest/client-server-api/#aggregations diff --git a/crates/ruma-common/src/events/relation/rel_serde.rs b/crates/ruma-common/src/events/relation/rel_serde.rs new file mode 100644 index 00000000..34460c70 --- /dev/null +++ b/crates/ruma-common/src/events/relation/rel_serde.rs @@ -0,0 +1,35 @@ +use serde::{de::DeserializeOwned, Deserialize, Deserializer}; + +use super::{BundledMessageLikeRelations, BundledThread, ReferenceChunk}; +use crate::serde::Raw; + +#[derive(Deserialize)] +struct BundledMessageLikeRelationsJsonRepr { + #[serde(rename = "m.replace")] + replace: Option>>, + #[serde(rename = "m.thread")] + thread: Option>, + #[serde(rename = "m.reference")] + reference: Option>, +} + +impl<'de, E> Deserialize<'de> for BundledMessageLikeRelations +where + E: DeserializeOwned, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let BundledMessageLikeRelationsJsonRepr { replace, thread, reference } = + BundledMessageLikeRelationsJsonRepr::deserialize(deserializer)?; + + let (replace, has_invalid_replacement) = + match replace.as_ref().map(Raw::deserialize).transpose() { + Ok(replace) => (replace, false), + Err(_) => (None, true), + }; + + Ok(BundledMessageLikeRelations { replace, has_invalid_replacement, thread, reference }) + } +} diff --git a/crates/ruma-common/src/events/room/redaction.rs b/crates/ruma-common/src/events/room/redaction.rs index 4d754765..7e49d833 100644 --- a/crates/ruma-common/src/events/room/redaction.rs +++ b/crates/ruma-common/src/events/room/redaction.rs @@ -60,7 +60,7 @@ pub struct OriginalRoomRedactionEvent { pub room_id: OwnedRoomId, /// Additional key-value pairs not signed by the homeserver. - pub unsigned: MessageLikeUnsigned, + pub unsigned: MessageLikeUnsigned, } /// Redacted redaction event. @@ -106,7 +106,7 @@ pub struct OriginalSyncRoomRedactionEvent { pub origin_server_ts: MilliSecondsSinceUnixEpoch, /// Additional key-value pairs not signed by the homeserver. - pub unsigned: MessageLikeUnsigned, + pub unsigned: MessageLikeUnsigned, } /// Redacted redaction event without a `room_id`. diff --git a/crates/ruma-common/src/events/unsigned.rs b/crates/ruma-common/src/events/unsigned.rs index 242842f6..4a02b910 100644 --- a/crates/ruma-common/src/events/unsigned.rs +++ b/crates/ruma-common/src/events/unsigned.rs @@ -1,19 +1,20 @@ use js_int::Int; -use serde::Deserialize; +use serde::{de::DeserializeOwned, Deserialize}; use super::{ relation::{BundledMessageLikeRelations, BundledStateRelations}, room::redaction::RoomRedactionEventContent, - PossiblyRedactedStateEventContent, + MessageLikeEventContent, OriginalSyncMessageLikeEvent, PossiblyRedactedStateEventContent, }; use crate::{ serde::CanBeEmpty, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, }; /// Extra information about a message event that is not incorporated into the event's hash. -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Deserialize)] +#[serde(bound = "OriginalSyncMessageLikeEvent: DeserializeOwned")] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct MessageLikeUnsigned { +pub struct MessageLikeUnsigned { /// 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 @@ -29,17 +30,23 @@ pub struct MessageLikeUnsigned { /// /// [Bundled aggregations]: https://spec.matrix.org/latest/client-server-api/#aggregations #[serde(rename = "m.relations", default)] - pub relations: BundledMessageLikeRelations, + pub relations: BundledMessageLikeRelations>, } -impl MessageLikeUnsigned { +impl MessageLikeUnsigned { /// Create a new `Unsigned` with fields set to `None`. pub fn new() -> Self { - Self::default() + Self { age: None, transaction_id: None, relations: BundledMessageLikeRelations::default() } } } -impl CanBeEmpty for MessageLikeUnsigned { +impl Default for MessageLikeUnsigned { + fn default() -> Self { + Self::new() + } +} + +impl CanBeEmpty for MessageLikeUnsigned { /// Whether this unsigned data is empty (all fields are `None`). /// /// This method is used to determine whether to skip serializing the `unsigned` field in room @@ -142,5 +149,5 @@ pub struct UnsignedRoomRedactionEvent { /// Additional key-value pairs not signed by the homeserver. #[serde(default)] - pub unsigned: MessageLikeUnsigned, + pub unsigned: MessageLikeUnsigned, }