diff --git a/ruma-events-macros/src/gen.rs b/ruma-events-macros/src/gen.rs index 18f52318..4c788908 100644 --- a/ruma-events-macros/src/gen.rs +++ b/ruma-events-macros/src/gen.rs @@ -87,9 +87,10 @@ impl ToTokens for RumaEvent { let content = match &self.content { Content::Struct(fields) => { + // TODO remove serde::Deserialize when this macro actually generates generic events quote! { #[doc = #content_docstring] - #[derive(Clone, Debug, serde::Serialize)] + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct #content_name { #(#fields),* } diff --git a/src/lib.rs b/src/lib.rs index 6f1106dd..8f82b46d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -119,7 +119,6 @@ use std::fmt::Debug; use js_int::Int; -use ruma_identifiers::{EventId, RoomId, UserId}; use serde::{Deserialize, Serialize}; // use self::room::redaction::RedactionEvent; @@ -165,6 +164,7 @@ pub mod receipt; pub mod room; // pub mod room_key; pub mod room_key_request; +pub mod state; pub mod sticker; // pub mod stripped; pub mod tag; @@ -211,3 +211,23 @@ impl UnsignedData { // && self.redacted_because.is_none() } } + +/// The base trait that all event content types implement. +/// +/// Implementing this trait allows content types to be serialized as well as deserialized. +pub trait EventContent: Sized + Serialize { + /// Constructs the given event content. + fn from_parts( + event_type: &str, + content: &serde_json::value::RawValue, + ) -> Result; + + /// A matrix event identifier, like `m.room.message`. + fn event_type(&self) -> &str; +} + +/// Marker trait for room events. +pub trait RoomEventContent: EventContent {} + +/// Marker trait for state events. +pub trait StateEventContent: RoomEventContent {} diff --git a/src/room/aliases.rs b/src/room/aliases.rs index 2c1cab8b..72677307 100644 --- a/src/room/aliases.rs +++ b/src/room/aliases.rs @@ -2,6 +2,12 @@ use ruma_events_macros::ruma_event; use ruma_identifiers::RoomAliasId; +use serde_json::value::RawValue as RawJsonValue; + +use crate::{ + error::{InvalidEvent, InvalidEventKind}, + EventContent, RoomEventContent, StateEventContent, +}; ruma_event! { /// Informs the room about what room aliases it has been given. @@ -14,3 +20,26 @@ ruma_event! { }, } } + +impl EventContent for AliasesEventContent { + fn event_type(&self) -> &str { + "m.room.aliases" + } + + fn from_parts(event_type: &str, content: &RawJsonValue) -> Result { + if event_type != "m.room.aliases" { + return Err(InvalidEvent { + kind: InvalidEventKind::Deserialization, + message: format!("expected `m.room.aliases` found {}", event_type), + }); + } + serde_json::from_str::(content.get()).map_err(|e| InvalidEvent { + kind: InvalidEventKind::Deserialization, + message: e.to_string(), + }) + } +} + +impl RoomEventContent for AliasesEventContent {} + +impl StateEventContent for AliasesEventContent {} diff --git a/src/room/avatar.rs b/src/room/avatar.rs index 2ec7dbb1..54304a80 100644 --- a/src/room/avatar.rs +++ b/src/room/avatar.rs @@ -1,8 +1,13 @@ //! Types for the *m.room.avatar* event. use ruma_events_macros::ruma_event; +use serde_json::value::RawValue as RawJsonValue; use super::ImageInfo; +use crate::{ + error::{InvalidEvent, InvalidEventKind}, + EventContent, RoomEventContent, StateEventContent, +}; ruma_event! { /// A picture that is associated with the room. @@ -22,3 +27,26 @@ ruma_event! { }, } } + +impl EventContent for AvatarEventContent { + fn event_type(&self) -> &str { + "m.room.avatar" + } + + fn from_parts(event_type: &str, content: &RawJsonValue) -> Result { + if event_type != "m.room.avatar" { + return Err(InvalidEvent { + kind: InvalidEventKind::Deserialization, + message: format!("expected `m.room.avatar` found {}", event_type), + }); + } + serde_json::from_str::(content.get()).map_err(|e| InvalidEvent { + kind: InvalidEventKind::Deserialization, + message: e.to_string(), + }) + } +} + +impl RoomEventContent for AvatarEventContent {} + +impl StateEventContent for AvatarEventContent {} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 00000000..cb44c19d --- /dev/null +++ b/src/state.rs @@ -0,0 +1,498 @@ +//! An enum that represents any state event. A state event is represented by +//! a parameterized struct allowing more flexibility in whats being sent. + +use std::{ + convert::TryFrom, + fmt, + marker::PhantomData, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use js_int::UInt; +use ruma_identifiers::{EventId, RoomId, UserId}; +use serde::{ + de::{self, Deserialize, Deserializer, Error as _, MapAccess, Visitor}, + ser::{Error, SerializeStruct}, + Serialize, Serializer, +}; +use serde_json::value::RawValue as RawJsonValue; + +use crate::{ + error::{InvalidEvent, InvalidEventKind}, + room::{aliases::AliasesEventContent, avatar::AvatarEventContent}, + EventContent, RoomEventContent, StateEventContent, +}; + +/// A state event. +#[derive(Clone, Debug, Serialize)] +#[serde(untagged)] +#[allow(clippy::large_enum_variant)] +pub enum AnyStateEventContent { + /// m.room.aliases + RoomAliases(AliasesEventContent), + + /// m.room.avatar + RoomAvatar(AvatarEventContent), + // /// m.room.canonical_alias + // RoomCanonicalAlias(StateEvent), + + // /// m.room.create + // RoomCreate(StateEvent), + + // /// m.room.encryption + // RoomEncryption(StateEvent), + + // /// m.room.guest_access + // RoomGuestAccess(StateEvent), + + // /// m.room.history_visibility + // RoomHistoryVisibility(StateEvent), + + // /// m.room.join_rules + // RoomJoinRules(StateEvent), + + // /// m.room.member + // RoomMember(StateEvent), + + // /// m.room.name + // RoomName(StateEvent), + + // /// m.room.pinned_events + // RoomPinnedEvents(StateEvent), + + // /// m.room.power_levels + // RoomPowerLevels(StateEvent), + + // /// m.room.server_acl + // RoomServerAcl(StateEvent), + + // /// m.room.third_party_invite + // RoomThirdPartyInvite(StateEvent), + + // /// m.room.tombstone + // RoomTombstone(StateEvent), + + // /// m.room.topic + // RoomTopic(StateEvent), + + // /// Any state event that is not part of the specification. + // CustomState(StateEvent), +} + +/// To-device event. +#[derive(Clone, Debug)] +pub struct StateEvent { + /// Data specific to the event type. + pub content: C, + + /// The globally unique event identifier for the user who sent the event. + pub event_id: EventId, + + /// Contains the fully-qualified ID of the user who sent this event. + pub sender: UserId, + + /// Timestamp in milliseconds on originating homeserver when this event was sent. + pub origin_server_ts: SystemTime, + + /// The ID of the room associated with this event. + pub room_id: RoomId, + + /// A unique key which defines the overwriting semantics for this piece of room state. + /// + /// This is often an empty string, but some events send a `UserId` to show + /// which user the event affects. + pub state_key: String, + + /// Optional previous content for this event. + pub prev_content: Option, +} + +impl EventContent for AnyStateEventContent { + fn event_type(&self) -> &str { + match self { + AnyStateEventContent::RoomAliases(content) => content.event_type(), + AnyStateEventContent::RoomAvatar(content) => content.event_type(), + } + } + + fn from_parts(event_type: &str, content: &RawJsonValue) -> Result { + fn deserialize_variant( + ev_type: &str, + input: &RawJsonValue, + variant: fn(T) -> AnyStateEventContent, + ) -> Result { + let content = T::from_parts(ev_type, input)?; + Ok(variant(content)) + } + + match event_type { + "m.room.avatar" => deserialize_variant::( + event_type, + content, + AnyStateEventContent::RoomAvatar, + ), + "m.room.aliases" => deserialize_variant::( + event_type, + content, + AnyStateEventContent::RoomAliases, + ), + ev => Err(InvalidEvent { + kind: InvalidEventKind::Deserialization, + message: format!("event not supported {}", ev), + }), + } + } +} + +impl RoomEventContent for AnyStateEventContent {} + +impl StateEventContent for AnyStateEventContent {} + +impl Serialize for StateEvent { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let event_type = self.content.event_type(); + + let time_since_epoch = self.origin_server_ts.duration_since(UNIX_EPOCH).unwrap(); + let timestamp = match UInt::try_from(time_since_epoch.as_millis()) { + Ok(uint) => uint, + Err(err) => return Err(S::Error::custom(err)), + }; + + let mut state = serializer.serialize_struct("StateEvent", 7)?; + state.serialize_field("content", &self.content)?; + state.serialize_field("event_id", &self.event_id)?; + state.serialize_field("sender", &self.sender)?; + state.serialize_field("origin_server_ts", ×tamp)?; + state.serialize_field("room_id", &self.room_id)?; + state.serialize_field("state_key", &self.state_key)?; + if let Some(content) = self.prev_content.as_ref() { + state.serialize_field("prev_content", content)?; + } + state.serialize_field("type", event_type)?; + state.end() + } +} + +impl<'de, C: StateEventContent> Deserialize<'de> for StateEvent { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_map(StateEventVisitor(std::marker::PhantomData)) + } +} + +#[derive(serde::Deserialize)] +#[serde(field_identifier, rename_all = "snake_case")] +enum Field { + Type, + Content, + EventId, + Sender, + OriginServerTs, + RoomId, + StateKey, + PrevContent, +} + +/// Visits the fields of a StateEvent to handle deserialization of +/// the `content` and `prev_content` fields. +struct StateEventVisitor(PhantomData); + +impl<'de, C: StateEventContent> Visitor<'de> for StateEventVisitor { + type Value = StateEvent; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "struct implementing StateEventContent") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut content: Option> = None; + let mut event_type: Option = None; + let mut event_id: Option = None; + let mut sender: Option = None; + let mut origin_server_ts: Option = None; + let mut room_id: Option = None; + let mut state_key: Option = None; + let mut prev_content: Option> = None; + + while let Some(key) = map.next_key()? { + match key { + Field::Content => { + if content.is_some() { + return Err(de::Error::duplicate_field("content")); + } + content = Some(map.next_value()?); + } + Field::EventId => { + if event_id.is_some() { + return Err(de::Error::duplicate_field("event_id")); + } + event_id = Some(map.next_value()?); + } + Field::Sender => { + if sender.is_some() { + return Err(de::Error::duplicate_field("sender")); + } + sender = Some(map.next_value()?); + } + Field::OriginServerTs => { + if origin_server_ts.is_some() { + return Err(de::Error::duplicate_field("origin_server_ts")); + } + origin_server_ts = Some(map.next_value()?); + } + Field::RoomId => { + if room_id.is_some() { + return Err(de::Error::duplicate_field("room_id")); + } + room_id = Some(map.next_value()?); + } + Field::StateKey => { + if state_key.is_some() { + return Err(de::Error::duplicate_field("state_key")); + } + state_key = Some(map.next_value()?); + } + Field::PrevContent => { + if prev_content.is_some() { + return Err(de::Error::duplicate_field("prev_content")); + } + prev_content = Some(map.next_value()?); + } + Field::Type => { + if event_type.is_some() { + return Err(de::Error::duplicate_field("type")); + } + event_type = Some(map.next_value()?); + } + } + } + + let event_type = event_type.ok_or_else(|| de::Error::missing_field("type"))?; + + let raw = content.ok_or_else(|| de::Error::missing_field("content"))?; + let content = C::from_parts(&event_type, &raw).map_err(A::Error::custom)?; + + let event_id = event_id.ok_or_else(|| de::Error::missing_field("event_id"))?; + let sender = sender.ok_or_else(|| de::Error::missing_field("sender"))?; + + let origin_server_ts = origin_server_ts + .map(|time| UNIX_EPOCH + Duration::from_millis(time.into())) + .ok_or_else(|| de::Error::missing_field("origin_server_ts"))?; + + let room_id = room_id.ok_or_else(|| de::Error::missing_field("room_id"))?; + let state_key = state_key.ok_or_else(|| de::Error::missing_field("state_key"))?; + + let prev_content = if let Some(raw) = prev_content { + Some(C::from_parts(&event_type, &raw).map_err(A::Error::custom)?) + } else { + None + }; + + Ok(StateEvent { + content, + event_id, + sender, + origin_server_ts, + room_id, + state_key, + prev_content, + }) + } +} + +#[cfg(test)] +mod tests { + use std::{ + convert::TryFrom, + time::{Duration, UNIX_EPOCH}, + }; + + use js_int::UInt; + use matches::assert_matches; + use ruma_identifiers::{EventId, RoomAliasId, RoomId, UserId}; + use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; + + use super::{AliasesEventContent, AnyStateEventContent, AvatarEventContent, StateEvent}; + use crate::room::{ImageInfo, ThumbnailInfo}; + + #[test] + fn serialize_aliases_with_prev_content() { + let aliases_event = StateEvent { + content: AnyStateEventContent::RoomAliases(AliasesEventContent { + aliases: vec![RoomAliasId::try_from("#somewhere:localhost").unwrap()], + }), + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + origin_server_ts: UNIX_EPOCH + Duration::from_millis(1), + prev_content: Some(AnyStateEventContent::RoomAliases(AliasesEventContent { + aliases: vec![RoomAliasId::try_from("#somewhere:localhost").unwrap()], + })), + room_id: RoomId::try_from("!roomid:room.com").unwrap(), + sender: UserId::try_from("@carl:example.com").unwrap(), + state_key: "".to_string(), + }; + + let actual = to_json_value(&aliases_event).unwrap(); + let expected = json!({ + "content": { + "aliases": [ "#somewhere:localhost" ] + }, + "event_id": "$h29iv0s8:example.com", + "origin_server_ts": 1, + "prev_content": { + "aliases": [ "#somewhere:localhost" ] + }, + "room_id": "!roomid:room.com", + "sender": "@carl:example.com", + "state_key": "", + "type": "m.room.aliases", + }); + + assert_eq!(actual, expected); + } + + #[test] + fn serialize_aliases_without_prev_content() { + let aliases_event = StateEvent { + content: AnyStateEventContent::RoomAliases(AliasesEventContent { + aliases: vec![RoomAliasId::try_from("#somewhere:localhost").unwrap()], + }), + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + origin_server_ts: UNIX_EPOCH + Duration::from_millis(1), + prev_content: None, + room_id: RoomId::try_from("!roomid:room.com").unwrap(), + sender: UserId::try_from("@carl:example.com").unwrap(), + state_key: "".to_string(), + }; + + let actual = to_json_value(&aliases_event).unwrap(); + let expected = json!({ + "content": { + "aliases": [ "#somewhere:localhost" ] + }, + "event_id": "$h29iv0s8:example.com", + "origin_server_ts": 1, + "room_id": "!roomid:room.com", + "sender": "@carl:example.com", + "state_key": "", + "type": "m.room.aliases", + }); + + assert_eq!(actual, expected); + } + + #[test] + fn deserialize_aliases_with_prev_content() { + let json_data = json!({ + "content": { + "aliases": [ "#somewhere:localhost" ] + }, + "event_id": "$h29iv0s8:example.com", + "origin_server_ts": 1, + "prev_content": { + "aliases": [ "#inner:localhost" ] + }, + "room_id": "!roomid:room.com", + "sender": "@carl:example.com", + "state_key": "", + "type": "m.room.aliases" + }); + + assert_matches!( + from_json_value::>(json_data).unwrap(), + StateEvent { + content: AnyStateEventContent::RoomAliases(content), + event_id, + origin_server_ts, + prev_content: Some(AnyStateEventContent::RoomAliases(prev_content)), + room_id, + sender, + state_key, + } if content.aliases == vec![RoomAliasId::try_from("#somewhere:localhost").unwrap()] + && event_id == EventId::try_from("$h29iv0s8:example.com").unwrap() + && origin_server_ts == UNIX_EPOCH + Duration::from_millis(1) + && prev_content.aliases == vec![RoomAliasId::try_from("#inner:localhost").unwrap()] + && room_id == RoomId::try_from("!roomid:room.com").unwrap() + && sender == UserId::try_from("@carl:example.com").unwrap() + && state_key == "" + ); + } + + #[test] + fn deserialize_avatar_without_prev_content() { + let json_data = json!({ + "content": { + "info": { + "h": 423, + "mimetype": "image/png", + "size": 84242, + "thumbnail_info": { + "h": 334, + "mimetype": "image/png", + "size": 82595, + "w": 800 + }, + "thumbnail_url": "mxc://matrix.org", + "w": 1011 + }, + "url": "http://www.matrix.org" + }, + "event_id": "$h29iv0s8:example.com", + "origin_server_ts": 1, + "room_id": "!roomid:room.com", + "sender": "@carl:example.com", + "state_key": "", + "type": "m.room.avatar" + }); + + assert_matches!( + from_json_value::>(json_data).unwrap(), + StateEvent { + content: AnyStateEventContent::RoomAvatar(AvatarEventContent { + info: Some(ImageInfo { + height, + width, + mimetype: Some(mimetype), + size, + thumbnail_info: Some(ThumbnailInfo { + width: thumb_width, + height: thumb_height, + mimetype: thumb_mimetype, + size: thumb_size, + }), + thumbnail_url: Some(thumbnail_url), + thumbnail_file: None, + }), + url, + }), + event_id, + origin_server_ts, + prev_content: None, + room_id, + sender, + state_key, + } if 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() + && state_key == "" + && height == UInt::new(423) + && width == UInt::new(1011) + && mimetype == "image/png" + && size == UInt::new(84242) + && thumb_width == UInt::new(800) + && thumb_height == UInt::new(334) + && thumb_mimetype == Some("image/png".to_string()) + && thumb_size == UInt::new(82595) + && thumbnail_url == "mxc://matrix.org" + && url == "http://www.matrix.org" + ); + } +}