diff --git a/CHANGELOG.md b/CHANGELOG.md index ed18ef6c..149471d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # [unreleased] +Breaking changes: + +* Add `struct UnsignedData` and update all `unsigned` fields types from + `BTreeMap` to this new type. + * To access any additional fields of the `unsigned` property of an event, + deserialize the `EventJson` to another type that captures the field(s) you + are interested in. + Improvements: * Add a encrypted variant to the room `MessageEventContent` enum. diff --git a/ruma-events-macros/src/gen.rs b/ruma-events-macros/src/gen.rs index d6a67c32..fdd72263 100644 --- a/ruma-events-macros/src/gen.rs +++ b/ruma-events-macros/src/gen.rs @@ -153,7 +153,7 @@ impl ToTokens for RumaEvent { } /// Additional key-value pairs not signed by the homeserver. - fn unsigned(&self) -> &std::collections::BTreeMap { + fn unsigned(&self) -> &ruma_events::UnsignedData { &self.unsigned } } @@ -295,8 +295,8 @@ fn populate_room_event_fields(content_name: Ident, fields: Vec) -> Vec, + #[serde(skip_serializing_if = "ruma_events::UnsignedData::is_empty")] + pub unsigned: ruma_events::UnsignedData, }; fields.extend(punctuated_fields.into_iter().map(|p| p.field)); diff --git a/src/custom.rs b/src/custom.rs index cc911c94..0e95c171 100644 --- a/src/custom.rs +++ b/src/custom.rs @@ -1,8 +1,8 @@ //! Types for custom events outside of the Matrix specification. -use std::{collections::BTreeMap, time::SystemTime}; +use std::time::SystemTime; -use crate::{Event, EventType, RoomEvent, StateEvent}; +use crate::{Event, EventType, RoomEvent, StateEvent, UnsignedData}; use ruma_events_macros::FromRaw; use ruma_identifiers::{EventId, RoomId, UserId}; @@ -55,8 +55,8 @@ pub struct CustomRoomEvent { /// The unique identifier for the user who sent this event. pub sender: UserId, /// Additional key-value pairs not signed by the homeserver. - #[serde(skip_serializing_if = "std::collections::BTreeMap::is_empty")] - pub unsigned: BTreeMap, + #[serde(skip_serializing_if = "UnsignedData::is_empty")] + pub unsigned: UnsignedData, } /// The payload for `CustomRoomEvent`. @@ -102,7 +102,7 @@ impl RoomEvent for CustomRoomEvent { } /// Additional key-value pairs not signed by the homeserver. - fn unsigned(&self) -> &BTreeMap { + fn unsigned(&self) -> &UnsignedData { &self.unsigned } } @@ -129,8 +129,8 @@ pub struct CustomStateEvent { /// A key that determines which piece of room state the event represents. pub state_key: String, /// Additional key-value pairs not signed by the homeserver. - #[serde(skip_serializing_if = "std::collections::BTreeMap::is_empty")] - pub unsigned: BTreeMap, + #[serde(skip_serializing_if = "UnsignedData::is_empty")] + pub unsigned: UnsignedData, } /// The payload for `CustomStateEvent`. @@ -176,7 +176,7 @@ impl RoomEvent for CustomStateEvent { } /// Additional key-value pairs not signed by the homeserver. - fn unsigned(&self) -> &BTreeMap { + fn unsigned(&self) -> &UnsignedData { &self.unsigned } } @@ -194,13 +194,14 @@ impl StateEvent for CustomStateEvent { } pub(crate) mod raw { - use std::{collections::BTreeMap, time::SystemTime}; + use std::time::SystemTime; use ruma_identifiers::{EventId, RoomId, UserId}; use serde::Deserialize; - use serde_json::Value as JsonValue; - use super::{CustomEventContent, CustomRoomEventContent, CustomStateEventContent}; + use super::{ + CustomEventContent, CustomRoomEventContent, CustomStateEventContent, UnsignedData, + }; /// A custom event not covered by the Matrix specification. #[derive(Clone, Debug, PartialEq, Deserialize)] @@ -231,7 +232,7 @@ pub(crate) mod raw { pub sender: UserId, /// Additional key-value pairs not signed by the homeserver. #[serde(default)] - pub unsigned: BTreeMap, + pub unsigned: UnsignedData, } /// A custom state event not covered by the Matrix specification. @@ -257,6 +258,6 @@ pub(crate) mod raw { pub state_key: String, /// Additional key-value pairs not signed by the homeserver. #[serde(default)] - pub unsigned: BTreeMap, + pub unsigned: UnsignedData, } } diff --git a/src/json.rs b/src/json.rs index e097199e..61e2765f 100644 --- a/src/json.rs +++ b/src/json.rs @@ -96,6 +96,12 @@ impl Debug for EventJson { } } +impl PartialEq for EventJson { + fn eq(&self, other: &Self) -> bool { + self.json.get() == other.json.get() + } +} + impl<'de, T> Deserialize<'de> for EventJson { fn deserialize(deserializer: D) -> Result where diff --git a/src/lib.rs b/src/lib.rs index 42a0241b..b5bed9eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -114,11 +114,13 @@ // Since we support Rust 1.36.0, we can't apply this suggestion yet #![allow(clippy::use_self)] -use std::{collections::BTreeMap, fmt::Debug, time::SystemTime}; +use std::{fmt::Debug, time::SystemTime}; +use js_int::Int; use ruma_identifiers::{EventId, RoomId, UserId}; -use serde::Serialize; -use serde_json::Value; +use serde::{Deserialize, Serialize}; + +use self::room::redaction::RedactionEvent; pub use self::{ custom::{CustomEvent, CustomRoomEvent, CustomStateEvent}, @@ -209,7 +211,7 @@ pub trait RoomEvent: Event { fn sender(&self) -> &UserId; /// Additional key-value pairs not signed by the homeserver. - fn unsigned(&self) -> &BTreeMap; + fn unsigned(&self) -> &UnsignedData; } /// An event that describes persistent state about a room. @@ -220,3 +222,31 @@ pub trait StateEvent: RoomEvent { /// A key that determines which piece of room state the event represents. fn state_key(&self) -> &str; } + +/// Extra information about an event that is not incorporated into the event's +/// hash. +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +pub struct UnsignedData { + /// 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 + /// cause the age to either be negative or greater than it actually is. + age: Option, + /// The event that redacted this event, if any. + redacted_because: Option>, + /// The client-supplied transaction ID, if the client being given the event + /// is the same one which sent it. + transaction_id: Option, +} + +impl UnsignedData { + /// 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 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.age.is_none() && self.redacted_because.is_none() && self.transaction_id.is_none() + } +} diff --git a/src/macros.rs b/src/macros.rs index 2590f7a8..3d339b4e 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -72,7 +72,7 @@ macro_rules! impl_room_event { } /// Additional key-value pairs not signed by the homeserver. - fn unsigned(&self) -> &::std::collections::BTreeMap { + fn unsigned(&self) -> &::ruma_events::UnsignedData { &self.unsigned } } diff --git a/src/room/canonical_alias.rs b/src/room/canonical_alias.rs index 4eda21ee..26cbc4fa 100644 --- a/src/room/canonical_alias.rs +++ b/src/room/canonical_alias.rs @@ -1,7 +1,6 @@ //! Types for the *m.room.canonical_alias* event. use std::{ - collections::BTreeMap, convert::TryFrom, time::{SystemTime, UNIX_EPOCH}, }; @@ -11,9 +10,8 @@ use serde::{ ser::{Error, SerializeStruct}, Deserialize, Serialize, Serializer, }; -use serde_json::Value; -use crate::{util::empty_string_as_none, Event, EventType, FromRaw}; +use crate::{util::empty_string_as_none, Event, EventType, FromRaw, UnsignedData}; /// Informs the room as to which alias is the canonical one. #[derive(Clone, Debug, PartialEq)] @@ -40,7 +38,7 @@ pub struct CanonicalAliasEvent { pub state_key: String, /// Additional key-value pairs not signed by the homeserver. - pub unsigned: BTreeMap, + pub unsigned: UnsignedData, } /// The payload for `CanonicalAliasEvent`. @@ -122,7 +120,7 @@ impl Serialize for CanonicalAliasEvent { state.serialize_field("state_key", &self.state_key)?; state.serialize_field("type", &self.event_type())?; - if !self.unsigned.is_empty() { + if self.unsigned != UnsignedData::default() { state.serialize_field("unsigned", &self.unsigned)?; } @@ -166,7 +164,7 @@ pub(crate) mod raw { /// Additional key-value pairs not signed by the homeserver. #[serde(default)] - pub unsigned: BTreeMap, + pub unsigned: UnsignedData, } /// The payload of a `CanonicalAliasEvent`. @@ -186,7 +184,6 @@ pub(crate) mod raw { #[cfg(test)] mod tests { use std::{ - collections::BTreeMap, convert::TryFrom, time::{Duration, UNIX_EPOCH}, }; @@ -195,7 +192,7 @@ mod tests { use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::{CanonicalAliasEvent, CanonicalAliasEventContent}; - use crate::EventJson; + use crate::{EventJson, UnsignedData}; #[test] fn serialization_with_optional_fields_as_none() { @@ -209,7 +206,7 @@ mod tests { room_id: None, sender: UserId::try_from("@carl:example.com").unwrap(), state_key: "".to_string(), - unsigned: BTreeMap::new(), + unsigned: UnsignedData::default(), }; let actual = to_json_value(&canonical_alias_event).unwrap(); diff --git a/src/room/encrypted.rs b/src/room/encrypted.rs index 8da70ee4..d47b3bf0 100644 --- a/src/room/encrypted.rs +++ b/src/room/encrypted.rs @@ -7,7 +7,7 @@ use ruma_identifiers::{DeviceId, EventId, RoomId, UserId}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::{from_value, Value}; -use crate::{Algorithm, EventType, FromRaw}; +use crate::{Algorithm, EventType, FromRaw, UnsignedData}; /// This event type is used when sending encrypted events. /// @@ -34,8 +34,8 @@ pub struct EncryptedEvent { pub sender: UserId, /// Additional key-value pairs not signed by the homeserver. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] - pub unsigned: BTreeMap, + #[serde(skip_serializing_if = "ruma_serde::is_default")] + pub unsigned: UnsignedData, } /// The payload for `EncryptedEvent`. @@ -134,7 +134,7 @@ pub(crate) mod raw { /// Additional key-value pairs not signed by the homeserver. #[serde(default)] - pub unsigned: BTreeMap, + pub unsigned: UnsignedData, } /// The payload for `EncryptedEvent`. diff --git a/src/room/member.rs b/src/room/member.rs index 53dc1cde..b8448331 100644 --- a/src/room/member.rs +++ b/src/room/member.rs @@ -230,7 +230,7 @@ mod tests { use serde_json::json; use super::*; - use crate::util::serde_json_eq_try_from_raw; + use crate::{util::serde_json_eq_try_from_raw, UnsignedData}; #[test] fn serde_with_no_prev_content() { @@ -247,7 +247,7 @@ mod tests { room_id: Some(RoomId::try_from("!n8f893n9:example.com").unwrap()), sender: UserId::try_from("@carl:example.com").unwrap(), state_key: "example.com".to_string(), - unsigned: BTreeMap::new(), + unsigned: UnsignedData::default(), prev_content: None, }; let json = json!({ @@ -279,7 +279,7 @@ mod tests { room_id: Some(RoomId::try_from("!n8f893n9:example.com").unwrap()), sender: UserId::try_from("@carl:example.com").unwrap(), state_key: "example.com".to_string(), - unsigned: BTreeMap::new(), + unsigned: UnsignedData::default(), prev_content: Some(MemberEventContent { avatar_url: None, displayname: None, @@ -335,7 +335,7 @@ mod tests { room_id: Some(RoomId::try_from("!jEsUZKDJdhlrceRyVU:example.org").unwrap()), sender: UserId::try_from("@alice:example.org").unwrap(), state_key: "@alice:example.org".to_string(), - unsigned: BTreeMap::new(), + unsigned: UnsignedData::default(), prev_content: None, }; let json = json!({ @@ -390,7 +390,7 @@ mod tests { room_id: Some(RoomId::try_from("!jEsUZKDJdhlrceRyVU:example.org").unwrap()), sender: UserId::try_from("@alice:example.org").unwrap(), state_key: "@alice:example.org".to_string(), - unsigned: BTreeMap::new(), + unsigned: UnsignedData::default(), prev_content: Some(MemberEventContent { avatar_url: Some("mxc://example.org/SEsfnsuifSDFSSEF".to_owned()), displayname: Some("Alice Margatroid".to_owned()), diff --git a/src/room/message.rs b/src/room/message.rs index 77c05d68..d543ab9f 100644 --- a/src/room/message.rs +++ b/src/room/message.rs @@ -1,6 +1,6 @@ //! Types for the *m.room.message* event. -use std::{collections::BTreeMap, time::SystemTime}; +use std::time::SystemTime; use js_int::UInt; use ruma_identifiers::{EventId, RoomId, UserId}; @@ -8,7 +8,7 @@ use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializ use serde_json::{from_value, Value}; use super::{encrypted::EncryptedEventContent, EncryptedFile, ImageInfo, ThumbnailInfo}; -use crate::{EventType, FromRaw}; +use crate::{EventType, FromRaw, UnsignedData}; pub mod feedback; @@ -34,8 +34,8 @@ pub struct MessageEvent { pub sender: UserId, /// Additional key-value pairs not signed by the homeserver. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] - pub unsigned: BTreeMap, + #[serde(skip_serializing_if = "UnsignedData::is_empty")] + pub unsigned: UnsignedData, } /// The payload for `MessageEvent`. @@ -168,7 +168,7 @@ pub(crate) mod raw { /// Additional key-value pairs not signed by the homeserver. #[serde(default)] - pub unsigned: BTreeMap, + pub unsigned: UnsignedData, } /// The payload for `MessageEvent`. diff --git a/src/room/name.rs b/src/room/name.rs index d708cfdc..f65cefc6 100644 --- a/src/room/name.rs +++ b/src/room/name.rs @@ -1,12 +1,11 @@ //! Types for the *m.room.name* event. -use std::{collections::BTreeMap, time::SystemTime}; +use std::time::SystemTime; use ruma_identifiers::{EventId, RoomId, UserId}; use serde::{Deserialize, Serialize}; -use serde_json::Value; -use crate::{util::empty_string_as_none, EventType, InvalidInput, TryFromRaw}; +use crate::{util::empty_string_as_none, EventType, InvalidInput, TryFromRaw, UnsignedData}; /// A human-friendly room name designed to be displayed to the end-user. #[derive(Clone, Debug, PartialEq, Serialize)] @@ -37,8 +36,8 @@ pub struct NameEvent { pub state_key: String, /// Additional key-value pairs not signed by the homeserver. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] - pub unsigned: BTreeMap, + #[serde(skip_serializing_if = "ruma_serde::is_default")] + pub unsigned: UnsignedData, } /// The payload for `NameEvent`. @@ -136,7 +135,7 @@ pub(crate) mod raw { /// Additional key-value pairs not signed by the homeserver. #[serde(default)] - pub unsigned: BTreeMap, + pub unsigned: UnsignedData, } /// The payload of a `NameEvent`. @@ -154,7 +153,6 @@ pub(crate) mod raw { #[cfg(test)] mod tests { use std::{ - collections::BTreeMap, convert::TryFrom, iter::FromIterator, time::{Duration, UNIX_EPOCH}, @@ -163,7 +161,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; + use crate::{EventJson, UnsignedData}; use super::{NameEvent, NameEventContent}; @@ -179,7 +177,7 @@ mod tests { room_id: None, sender: UserId::try_from("@carl:example.com").unwrap(), state_key: "".to_string(), - unsigned: BTreeMap::new(), + unsigned: UnsignedData::default(), }; let actual = to_json_value(&name_event).unwrap(); diff --git a/src/room/pinned_events.rs b/src/room/pinned_events.rs index 409cc823..fe6752ed 100644 --- a/src/room/pinned_events.rs +++ b/src/room/pinned_events.rs @@ -17,17 +17,14 @@ ruma_event! { #[cfg(test)] mod tests { - use std::{ - collections::BTreeMap, - time::{Duration, UNIX_EPOCH}, - }; + use std::time::{Duration, UNIX_EPOCH}; use ruma_identifiers::{EventId, RoomId, UserId}; use serde_json::to_string; use crate::{ room::pinned_events::{PinnedEventsEvent, PinnedEventsEventContent}, - Event, EventJson, RoomEvent, StateEvent, + Event, EventJson, RoomEvent, StateEvent, UnsignedData, }; #[test] @@ -45,7 +42,7 @@ mod tests { room_id: Some(RoomId::new("example.com").unwrap()), sender: UserId::new("example.com").unwrap(), state_key: "".to_string(), - unsigned: BTreeMap::new(), + unsigned: UnsignedData::default(), }; let serialized_event = to_string(&event).unwrap(); diff --git a/src/room/power_levels.rs b/src/room/power_levels.rs index e7ae8e1e..2fa31321 100644 --- a/src/room/power_levels.rs +++ b/src/room/power_levels.rs @@ -5,9 +5,8 @@ use std::{collections::BTreeMap, time::SystemTime}; use js_int::Int; use ruma_identifiers::{EventId, RoomId, UserId}; use serde::{Deserialize, Serialize}; -use serde_json::Value; -use crate::{EventType, FromRaw}; +use crate::{EventType, FromRaw, UnsignedData}; /// Defines the power levels (privileges) of users in the room. #[derive(Clone, Debug, PartialEq, Serialize)] @@ -32,8 +31,8 @@ pub struct PowerLevelsEvent { pub room_id: Option, /// Additional key-value pairs not signed by the homeserver. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] - pub unsigned: BTreeMap, + #[serde(skip_serializing_if = "ruma_serde::is_default")] + pub unsigned: UnsignedData, /// The unique identifier for the user who sent this event. pub sender: UserId, @@ -158,7 +157,7 @@ pub(crate) mod raw { /// Additional key-value pairs not signed by the homeserver. #[serde(default)] - pub unsigned: BTreeMap, + pub unsigned: UnsignedData, /// The unique identifier for the user who sent this event. pub sender: UserId, @@ -276,7 +275,7 @@ mod tests { use super::{ default_power_level, NotificationPowerLevels, PowerLevelsEvent, PowerLevelsEventContent, }; - use crate::EventType; + use crate::{EventType,UnsignedData}; #[test] fn serialization_with_optional_fields_as_none() { @@ -299,7 +298,7 @@ mod tests { origin_server_ts: UNIX_EPOCH + Duration::from_millis(1), prev_content: None, room_id: None, - unsigned: BTreeMap::new(), + unsigned: UnsignedData::default(), sender: UserId::try_from("@carl:example.com").unwrap(), state_key: "".to_string(), }; diff --git a/src/room/server_acl.rs b/src/room/server_acl.rs index 121dcf61..53c0750d 100644 --- a/src/room/server_acl.rs +++ b/src/room/server_acl.rs @@ -1,12 +1,11 @@ //! Types for the *m.room.server_acl* event. -use std::{collections::BTreeMap, time::SystemTime}; +use std::time::SystemTime; use ruma_identifiers::{EventId, RoomId, UserId}; use serde::{Deserialize, Serialize}; -use serde_json::Value; -use crate::{util::default_true, EventType, FromRaw}; +use crate::{util::default_true, EventType, FromRaw, UnsignedData}; /// An event to indicate which servers are permitted to participate in the room. #[derive(Clone, Debug, PartialEq, Serialize)] @@ -37,8 +36,8 @@ pub struct ServerAclEvent { pub state_key: String, /// Additional key-value pairs not signed by the homeserver. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] - pub unsigned: BTreeMap, + #[serde(skip_serializing_if = "UnsignedData::is_empty")] + pub unsigned: UnsignedData, } /// The payload for `ServerAclEvent`. @@ -129,7 +128,7 @@ pub(crate) mod raw { /// Additional key-value pairs not signed by the homeserver. #[serde(default)] - pub unsigned: BTreeMap, + pub unsigned: UnsignedData, /// The unique identifier for the user who sent this event. pub sender: UserId, diff --git a/tests/ruma_events_macros.rs b/tests/ruma_events_macros.rs index a7742229..ca0e7f9d 100644 --- a/tests/ruma_events_macros.rs +++ b/tests/ruma_events_macros.rs @@ -5,6 +5,7 @@ use std::{ }; use ruma_events::util::serde_json_eq_try_from_raw; +use ruma_events::UnsignedData; use ruma_events_macros::ruma_event; use ruma_identifiers::{EventId, RoomAliasId, RoomId, UserId}; use serde_json::json; @@ -37,7 +38,7 @@ mod common_case { room_id: None, sender: UserId::try_from("@carl:example.com").unwrap(), state_key: "example.com".to_string(), - unsigned: std::collections::BTreeMap::new(), + unsigned: UnsignedData::default(), }; let json = json!({ "content": { @@ -66,7 +67,7 @@ mod common_case { room_id: Some(RoomId::try_from("!n8f893n9:example.com").unwrap()), sender: UserId::try_from("@carl:example.com").unwrap(), state_key: "example.com".to_string(), - unsigned: std::collections::BTreeMap::new(), + unsigned: UnsignedData::default(), }; let json = json!({ "content": {