diff --git a/ruma-events-macros/src/collection.rs b/ruma-events-macros/src/collection.rs index fd608b4a..c4c69edd 100644 --- a/ruma-events-macros/src/collection.rs +++ b/ruma-events-macros/src/collection.rs @@ -6,6 +6,20 @@ use syn::{Ident, LitStr}; use parse::RumaCollectionInput; +fn marker_traits(ident: &Ident) -> TokenStream { + match ident.to_string().as_str() { + "AnyStateEventContent" => quote! { + impl ::ruma_events::RoomEventContent for #ident {} + impl ::ruma_events::StateEventContent for #ident {} + }, + "AnyMessageEventContent" => quote! { + impl ::ruma_events::RoomEventContent for #ident {} + impl ::ruma_events::MessageEventContent for #ident {} + }, + _ => TokenStream::new(), + } +} + /// Create a collection from `RumaCollectionInput. pub fn expand_collection(input: RumaCollectionInput) -> syn::Result { let attrs = &input.attrs; @@ -36,7 +50,7 @@ pub fn expand_collection(input: RumaCollectionInput) -> syn::Result impl ::ruma_events::EventContent for #ident { fn event_type(&self) -> &str { match self { - #( Self::#variants(content) => content.event_type()),* + #( Self::#variants(content) => content.event_type() ),* } } } @@ -63,6 +77,8 @@ pub fn expand_collection(input: RumaCollectionInput) -> syn::Result } }; + let marker_trait_impls = marker_traits(ident); + let raw_mod = expand_raw_content_event(&input, &variants)?; Ok(quote! { @@ -72,9 +88,7 @@ pub fn expand_collection(input: RumaCollectionInput) -> syn::Result #event_content_impl - impl RoomEventContent for AnyStateEventContent {} - - impl StateEventContent for AnyStateEventContent {} + #marker_trait_impls #raw_mod }) @@ -139,17 +153,18 @@ fn to_event_content_path( assert_eq!(&name[..2], "m."); - let event_str = name[2..].split('.').last().unwrap(); + let path = name[2..].split('.').collect::>(); + let event_str = path.last().unwrap(); let event = event_str .split('_') .map(|s| s.chars().next().unwrap().to_uppercase().to_string() + &s[1..]) .collect::(); - let module = Ident::new(event_str, span); let content_str = Ident::new(&format!("{}EventContent", event), span); + let path = path.iter().map(|s| Ident::new(s, span)); syn::parse_quote! { - ::ruma_events::room::#module::#content_str + ::ruma_events::#( #path )::*::#content_str } } @@ -161,17 +176,18 @@ fn to_raw_event_content_path( assert_eq!(&name[..2], "m."); - let event_str = name[2..].split('.').last().unwrap(); + let path = name[2..].split('.').collect::>(); + let event_str = path.last().unwrap(); let event = event_str .split('_') .map(|s| s.chars().next().unwrap().to_uppercase().to_string() + &s[1..]) .collect::(); - let module = Ident::new(event_str, span); let content_str = Ident::new(&format!("{}EventContent", event), span); + let path = path.iter().map(|s| Ident::new(s, span)); syn::parse_quote! { - ::ruma_events::room::#module::raw::#content_str + ::ruma_events::#( #path )::*::raw::#content_str } } diff --git a/src/call/answer.rs b/src/call/answer.rs index ad30258c..9ff7d251 100644 --- a/src/call/answer.rs +++ b/src/call/answer.rs @@ -9,7 +9,7 @@ use super::SessionDescription; /// This event is sent by the callee when they wish to answer the call. #[derive(Clone, Debug, Serialize, FromRaw, MessageEventContent)] #[ruma_event(type = "m.call.answer")] -pub struct AnswerEventContenet { +pub struct AnswerEventContent { /// The VoIP session description object. The session description type must be *answer*. pub answer: SessionDescription, diff --git a/src/lib.rs b/src/lib.rs index 4e8e5704..ebf3c34f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -134,6 +134,7 @@ mod error; mod event_type; mod from_raw; mod json; +mod message; mod state; #[doc(hidden)] // only public for external tests pub mod util; diff --git a/src/message.rs b/src/message.rs new file mode 100644 index 00000000..da2d2340 --- /dev/null +++ b/src/message.rs @@ -0,0 +1,491 @@ +//! An enum that represents any message event. A message event is represented by +//! a parameterized struct allowing more flexibility in whats being sent. + +use std::{ + convert::TryFrom, + time::{SystemTime, UNIX_EPOCH}, +}; + +use js_int::UInt; +use ruma_identifiers::{EventId, RoomId, UserId}; +use serde::{ + ser::{Error, SerializeStruct}, + Serialize, Serializer, +}; + +use crate::{MessageEventContent, RawEventContent, RoomEventContent, TryFromRaw, UnsignedData}; +use ruma_events_macros::event_content_collection; + +event_content_collection! { + /// A message event. + name: AnyMessageEventContent, + events: [ + "m.call.answer", + "m.call.invite", + "m.call.hangup", + "m.call.candidates", + "m.sticker", + ] +} + +/// Message event. +#[derive(Clone, Debug)] +pub struct MessageEvent +where + C::Raw: RawEventContent, +{ + /// 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, + + /// Additional key-value pairs not signed by the homeserver. + pub unsigned: UnsignedData, +} + +impl TryFromRaw for MessageEvent +where + C: MessageEventContent + TryFromRaw, + C::Raw: RawEventContent, +{ + type Raw = raw_message_event::MessageEvent; + type Err = C::Err; + + fn try_from_raw(raw: Self::Raw) -> Result { + Ok(Self { + content: C::try_from_raw(raw.content)?, + event_id: raw.event_id, + sender: raw.sender, + origin_server_ts: raw.origin_server_ts, + room_id: raw.room_id, + unsigned: raw.unsigned, + }) + } +} + +impl Serialize for MessageEvent +where + C::Raw: RawEventContent, +{ + 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 message = serializer.serialize_struct("MessageEvent", 7)?; + message.serialize_field("content", &self.content)?; + message.serialize_field("event_id", &self.event_id)?; + message.serialize_field("sender", &self.sender)?; + message.serialize_field("origin_server_ts", ×tamp)?; + message.serialize_field("room_id", &self.room_id)?; + message.serialize_field("type", event_type)?; + message.end() + } +} + +mod raw_message_event { + use std::{ + 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}; + use serde_json::value::RawValue as RawJsonValue; + + use crate::{RawEventContent, UnsignedData}; + + /// The raw half of a message event. + #[derive(Clone, Debug)] + pub struct MessageEvent { + /// 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, + + /// Additional key-value pairs not signed by the homeserver. + pub unsigned: UnsignedData, + } + + impl<'de, C> Deserialize<'de> for MessageEvent + where + C: RawEventContent, + { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_map(MessageEventVisitor(std::marker::PhantomData)) + } + } + + #[derive(serde::Deserialize)] + #[serde(field_identifier, rename_all = "snake_case")] + enum Field { + Type, + Content, + EventId, + Sender, + OriginServerTs, + RoomId, + Unsigned, + } + + /// Visits the fields of a MessageEvent to handle deserialization of + /// the `content` and `prev_content` fields. + struct MessageEventVisitor(PhantomData); + + impl<'de, C> Visitor<'de> for MessageEventVisitor + where + C: RawEventContent, + { + type Value = MessageEvent; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "struct implementing MessageEventContent") + } + + 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 unsigned: 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::Type => { + if event_type.is_some() { + return Err(de::Error::duplicate_field("type")); + } + event_type = Some(map.next_value()?); + } + Field::Unsigned => { + if unsigned.is_some() { + return Err(de::Error::duplicate_field("unsigned")); + } + unsigned = 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 unsigned = unsigned.unwrap_or_default(); + + Ok(MessageEvent { + content, + event_id, + sender, + origin_server_ts, + room_id, + unsigned, + }) + } + } +} + +#[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::{AnyMessageEventContent, MessageEvent}; + use crate::{ + call::{answer::AnswerEventContent, SessionDescription, SessionDescriptionType}, + room::{ImageInfo, ThumbnailInfo}, + sticker::StickerEventContent, + EventJson, UnsignedData, + }; + + #[test] + fn message_serialize_aliases() { + let aliases_event = MessageEvent { + content: AnyMessageEventContent::Sticker(StickerEventContent { + body: "Hello".into(), + info: ImageInfo { + height: UInt::new(423), + width: UInt::new(1011), + mimetype: Some("image/png".into()), + size: UInt::new(84242), + thumbnail_info: Some(Box::new(ThumbnailInfo { + width: UInt::new(800), + height: UInt::new(334), + mimetype: Some("image/png".into()), + size: UInt::new(82595), + })), + thumbnail_url: Some("mxc://matrix.org".into()), + thumbnail_file: None, + }, + url: "http://www.matrix.org".into(), + }), + 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(), + }; + + let actual = to_json_value(&aliases_event).unwrap(); + let expected = json!({ + "content": { + "body": "Hello", + "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", + "type": "m.sticker", + }); + + assert_eq!(actual, expected); + } + + #[test] + fn deserialize_message_aliases_content() { + let json_data = json!({ + "answer": { + "type": "answer", + "sdp": "Hello" + }, + "call_id": "foofoo", + "version": 1 + }); + + assert_matches!( + from_json_value::>(json_data) + .unwrap() + .deserialize_content("m.call.answer") + .unwrap(), + AnyMessageEventContent::CallAnswer(AnswerEventContent { + answer: SessionDescription { + session_type: SessionDescriptionType::Answer, + sdp, + }, + call_id, + version, + }) if sdp == "Hello" && call_id == "foofoo" && version == UInt::new(1).unwrap() + ); + } + + #[test] + fn deserialize_message_aliases() { + let json_data = json!({ + "content": { + "answer": { + "type": "answer", + "sdp": "Hello" + }, + "call_id": "foofoo", + "version": 1 + }, + "event_id": "$h29iv0s8:example.com", + "origin_server_ts": 1, + "room_id": "!roomid:room.com", + "sender": "@carl:example.com", + "type": "m.call.answer" + }); + + assert_matches!( + from_json_value::>>(json_data) + .unwrap() + .deserialize() + .unwrap(), + MessageEvent { + content: AnyMessageEventContent::CallAnswer(AnswerEventContent { + answer: SessionDescription { + session_type: SessionDescriptionType::Answer, + sdp, + }, + call_id, + version, + }), + event_id, + origin_server_ts, + room_id, + sender, + unsigned, + } if sdp == "Hello" && call_id == "foofoo" && version == UInt::new(1).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.is_empty() + ); + } + + #[test] + fn deserialize_message_avatar() { + let json_data = json!({ + "content": { + "body": "Hello", + "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", + "type": "m.sticker" + }); + + assert_matches!( + from_json_value::>>(json_data) + .unwrap() + .deserialize() + .unwrap(), + MessageEvent { + content: AnyMessageEventContent::Sticker(StickerEventContent { + body, + info: ImageInfo { + height, + width, + mimetype: Some(mimetype), + size, + thumbnail_info: Some(thumbnail_info), + thumbnail_url: Some(thumbnail_url), + thumbnail_file: None, + }, + url, + }), + event_id, + origin_server_ts, + room_id, + sender, + unsigned + } if event_id == EventId::try_from("$h29iv0s8:example.com").unwrap() + && body == "Hello" + && 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() + && height == UInt::new(423) + && width == UInt::new(1011) + && mimetype == "image/png" + && size == UInt::new(84242) + && thumbnail_url == "mxc://matrix.org" + && matches!( + thumbnail_info.as_ref(), + ThumbnailInfo { + width: thumb_width, + height: thumb_height, + mimetype: thumb_mimetype, + size: thumb_size, + } if *thumb_width == UInt::new(800) + && *thumb_height == UInt::new(334) + && *thumb_mimetype == Some("image/png".to_string()) + && *thumb_size == UInt::new(82595) + ) + && url == "http://www.matrix.org" + && unsigned.is_empty() + ); + } +}