From ba2538dda995aabbd3962b774985b2efef3c8f19 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 12 Jun 2019 16:20:10 -0700 Subject: [PATCH] Update existing events for spec r0.5.0 and add m.fully_read and m.room.message.feedback. --- src/call/hangup.rs | 25 ++++++++ src/collections/all.rs | 61 ++++++++++++++++++-- src/collections/only.rs | 32 +++++++++- src/direct.rs | 4 +- src/fully_read.rs | 22 +++++++ src/lib.rs | 9 +++ src/presence.rs | 2 +- src/receipt.rs | 10 +--- src/room/avatar.rs | 5 -- src/room/create.rs | 17 +++++- src/room/member.rs | 28 ++++----- src/room/message.rs | 109 ++++++++++++++++++++++++++++++----- src/room/message/feedback.rs | 41 +++++++++++++ src/room/mod.rs | 39 ++++++++++++- src/room/name.rs | 1 - src/room/pinned_events.rs | 2 +- src/room/power_levels.rs | 13 +++++ src/stripped.rs | 8 +-- src/tag.rs | 2 +- src/typing.rs | 6 +- 20 files changed, 370 insertions(+), 66 deletions(-) create mode 100644 src/fully_read.rs create mode 100644 src/room/message/feedback.rs diff --git a/src/call/hangup.rs b/src/call/hangup.rs index 6a87b875..180c1016 100644 --- a/src/call/hangup.rs +++ b/src/call/hangup.rs @@ -15,4 +15,29 @@ pub struct HangupEventContent { pub call_id: String, /// The version of the VoIP specification this messages adheres to. pub version: u64, + /// Optional error reason for the hangup. + pub reason: Option, +} + +/// A reason for a hangup. +/// +/// This should not be provided when the user naturally ends or rejects the call. When there was an +/// error in the call negotiation, this should be `ice_failed` for when ICE negotiation fails or +/// `invite_timeout` for when the other party did not answer in time. +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] +pub enum Reason { + /// ICE negotiation failure. + #[serde(rename = "ice_failed")] + IceFailed, + + /// Party did not answer in time. + #[serde(rename = "invite_timeout")] + InviteTimeout, +} + +impl_enum! { + Reason { + IceFailed => "ice_failed", + InviteTimeout => "invite_timeout", + } } diff --git a/src/collections/all.rs b/src/collections/all.rs index dc4a46a5..49e5640d 100644 --- a/src/collections/all.rs +++ b/src/collections/all.rs @@ -6,15 +6,25 @@ use crate::{ answer::AnswerEvent, candidates::CandidatesEvent, hangup::HangupEvent, invite::InviteEvent, }, direct::DirectEvent, + fully_read::FullyReadEvent, presence::PresenceEvent, receipt::ReceiptEvent, room::{ - aliases::AliasesEvent, avatar::AvatarEvent, canonical_alias::CanonicalAliasEvent, - create::CreateEvent, guest_access::GuestAccessEvent, - history_visibility::HistoryVisibilityEvent, join_rules::JoinRulesEvent, - member::MemberEvent, message::MessageEvent, name::NameEvent, - pinned_events::PinnedEventsEvent, power_levels::PowerLevelsEvent, - redaction::RedactionEvent, third_party_invite::ThirdPartyInviteEvent, topic::TopicEvent, + aliases::AliasesEvent, + avatar::AvatarEvent, + canonical_alias::CanonicalAliasEvent, + create::CreateEvent, + guest_access::GuestAccessEvent, + history_visibility::HistoryVisibilityEvent, + join_rules::JoinRulesEvent, + member::MemberEvent, + message::{feedback::FeedbackEvent, MessageEvent}, + name::NameEvent, + pinned_events::PinnedEventsEvent, + power_levels::PowerLevelsEvent, + redaction::RedactionEvent, + third_party_invite::ThirdPartyInviteEvent, + topic::TopicEvent, }, tag::TagEvent, typing::TypingEvent, @@ -38,6 +48,8 @@ pub enum Event { CallInvite(InviteEvent), /// m.direct Direct(DirectEvent), + /// m.fully_read + FullyRead(FullyReadEvent), /// m.presence Presence(PresenceEvent), /// m.receipt @@ -60,6 +72,8 @@ pub enum Event { RoomMember(MemberEvent), /// m.room.message RoomMessage(MessageEvent), + /// m.room.message.feedback + RoomMessageFeedback(FeedbackEvent), /// m.room.name RoomName(NameEvent), /// m.room.pinned_events @@ -114,6 +128,8 @@ pub enum RoomEvent { RoomMember(MemberEvent), /// m.room.message RoomMessage(MessageEvent), + /// m.room.message.feedback + RoomMessageFeedback(FeedbackEvent), /// m.room.name RoomName(NameEvent), /// m.room.pinned_events @@ -177,6 +193,7 @@ impl Serialize for Event { Event::CallHangup(ref event) => event.serialize(serializer), Event::CallInvite(ref event) => event.serialize(serializer), Event::Direct(ref event) => event.serialize(serializer), + Event::FullyRead(ref event) => event.serialize(serializer), Event::Presence(ref event) => event.serialize(serializer), Event::Receipt(ref event) => event.serialize(serializer), Event::RoomAliases(ref event) => event.serialize(serializer), @@ -188,6 +205,7 @@ impl Serialize for Event { Event::RoomJoinRules(ref event) => event.serialize(serializer), Event::RoomMember(ref event) => event.serialize(serializer), Event::RoomMessage(ref event) => event.serialize(serializer), + Event::RoomMessageFeedback(ref event) => event.serialize(serializer), Event::RoomName(ref event) => event.serialize(serializer), Event::RoomPinnedEvents(ref event) => event.serialize(serializer), Event::RoomPowerLevels(ref event) => event.serialize(serializer), @@ -261,6 +279,14 @@ impl<'de> Deserialize<'de> for Event { Ok(Event::Direct(event)) } + EventType::FullyRead => { + let event = match from_value::(value) { + Ok(event) => event, + Err(error) => return Err(D::Error::custom(error.to_string())), + }; + + Ok(Event::FullyRead(event)) + } EventType::Presence => { let event = match from_value::(value) { Ok(event) => event, @@ -349,6 +375,14 @@ impl<'de> Deserialize<'de> for Event { Ok(Event::RoomMessage(event)) } + EventType::RoomMessageFeedback => { + let event = match from_value::(value) { + Ok(event) => event, + Err(error) => return Err(D::Error::custom(error.to_string())), + }; + + Ok(Event::RoomMessageFeedback(event)) + } EventType::RoomName => { let event = match from_value::(value) { Ok(event) => event, @@ -463,6 +497,7 @@ impl Serialize for RoomEvent { RoomEvent::RoomJoinRules(ref event) => event.serialize(serializer), RoomEvent::RoomMember(ref event) => event.serialize(serializer), RoomEvent::RoomMessage(ref event) => event.serialize(serializer), + RoomEvent::RoomMessageFeedback(ref event) => event.serialize(serializer), RoomEvent::RoomName(ref event) => event.serialize(serializer), RoomEvent::RoomPinnedEvents(ref event) => event.serialize(serializer), RoomEvent::RoomPowerLevels(ref event) => event.serialize(serializer), @@ -597,6 +632,14 @@ impl<'de> Deserialize<'de> for RoomEvent { Ok(RoomEvent::RoomMessage(event)) } + EventType::RoomMessageFeedback => { + let event = match from_value::(value) { + Ok(event) => event, + Err(error) => return Err(D::Error::custom(error.to_string())), + }; + + Ok(RoomEvent::RoomMessageFeedback(event)) + } EventType::RoomName => { let event = match from_value::(value) { Ok(event) => event, @@ -663,6 +706,7 @@ impl<'de> Deserialize<'de> for RoomEvent { } } EventType::Direct + | EventType::FullyRead | EventType::Presence | EventType::Receipt | EventType::Tag @@ -830,9 +874,11 @@ impl<'de> Deserialize<'de> for StateEvent { | EventType::CallHangup | EventType::CallInvite | EventType::Direct + | EventType::FullyRead | EventType::Presence | EventType::Receipt | EventType::RoomMessage + | EventType::RoomMessageFeedback | EventType::RoomRedaction | EventType::Tag | EventType::Typing => Err(D::Error::custom("not a state event".to_string())), @@ -855,6 +901,7 @@ impl_from_t_for_event!(CandidatesEvent, CallCandidates); impl_from_t_for_event!(HangupEvent, CallHangup); impl_from_t_for_event!(InviteEvent, CallInvite); impl_from_t_for_event!(DirectEvent, Direct); +impl_from_t_for_event!(FullyReadEvent, FullyRead); impl_from_t_for_event!(PresenceEvent, Presence); impl_from_t_for_event!(ReceiptEvent, Receipt); impl_from_t_for_event!(AliasesEvent, RoomAliases); @@ -866,6 +913,7 @@ impl_from_t_for_event!(HistoryVisibilityEvent, RoomHistoryVisibility); impl_from_t_for_event!(JoinRulesEvent, RoomJoinRules); impl_from_t_for_event!(MemberEvent, RoomMember); impl_from_t_for_event!(MessageEvent, RoomMessage); +impl_from_t_for_event!(FeedbackEvent, RoomMessageFeedback); impl_from_t_for_event!(NameEvent, RoomName); impl_from_t_for_event!(PinnedEventsEvent, RoomPinnedEvents); impl_from_t_for_event!(PowerLevelsEvent, RoomPowerLevels); @@ -901,6 +949,7 @@ impl_from_t_for_room_event!(HistoryVisibilityEvent, RoomHistoryVisibility); impl_from_t_for_room_event!(JoinRulesEvent, RoomJoinRules); impl_from_t_for_room_event!(MemberEvent, RoomMember); impl_from_t_for_room_event!(MessageEvent, RoomMessage); +impl_from_t_for_room_event!(FeedbackEvent, RoomMessageFeedback); impl_from_t_for_room_event!(NameEvent, RoomName); impl_from_t_for_room_event!(PinnedEventsEvent, RoomPinnedEvents); impl_from_t_for_room_event!(PowerLevelsEvent, RoomPowerLevels); diff --git a/src/collections/only.rs b/src/collections/only.rs index c1b48e78..f90d84ef 100644 --- a/src/collections/only.rs +++ b/src/collections/only.rs @@ -10,9 +10,13 @@ use crate::{ answer::AnswerEvent, candidates::CandidatesEvent, hangup::HangupEvent, invite::InviteEvent, }, direct::DirectEvent, + fully_read::FullyReadEvent, presence::PresenceEvent, receipt::ReceiptEvent, - room::{message::MessageEvent, redaction::RedactionEvent}, + room::{ + message::{feedback::FeedbackEvent, MessageEvent}, + redaction::RedactionEvent, + }, tag::TagEvent, typing::TypingEvent, CustomEvent, CustomRoomEvent, EventType, @@ -23,6 +27,8 @@ use crate::{ pub enum Event { /// m.direct Direct(DirectEvent), + /// m.fully_read + FullyRead(FullyReadEvent), /// m.presence Presence(PresenceEvent), /// m.receipt @@ -49,6 +55,8 @@ pub enum RoomEvent { CallInvite(InviteEvent), /// m.room.message RoomMessage(MessageEvent), + /// m.room.message.feedback + RoomMessageFeedback(FeedbackEvent), /// m.room.redaction RoomRedaction(RedactionEvent), /// Any room event that is not part of the specification. @@ -62,6 +70,7 @@ impl Serialize for Event { { match *self { Event::Direct(ref event) => event.serialize(serializer), + Event::FullyRead(ref event) => event.serialize(serializer), Event::Presence(ref event) => event.serialize(serializer), Event::Receipt(ref event) => event.serialize(serializer), Event::Tag(ref event) => event.serialize(serializer), @@ -97,6 +106,14 @@ impl<'de> Deserialize<'de> for Event { Ok(Event::Direct(event)) } + EventType::FullyRead => { + let event = match from_value::(value) { + Ok(event) => event, + Err(error) => return Err(D::Error::custom(error.to_string())), + }; + + Ok(Event::FullyRead(event)) + } EventType::Presence => { let event = match from_value::(value) { Ok(event) => event, @@ -150,6 +167,7 @@ impl<'de> Deserialize<'de> for Event { | EventType::RoomJoinRules | EventType::RoomMember | EventType::RoomMessage + | EventType::RoomMessageFeedback | EventType::RoomName | EventType::RoomPinnedEvents | EventType::RoomPowerLevels @@ -173,6 +191,7 @@ impl Serialize for RoomEvent { RoomEvent::CallHangup(ref event) => event.serialize(serializer), RoomEvent::CallInvite(ref event) => event.serialize(serializer), RoomEvent::RoomMessage(ref event) => event.serialize(serializer), + RoomEvent::RoomMessageFeedback(ref event) => event.serialize(serializer), RoomEvent::RoomRedaction(ref event) => event.serialize(serializer), RoomEvent::CustomRoom(ref event) => event.serialize(serializer), } @@ -237,6 +256,14 @@ impl<'de> Deserialize<'de> for RoomEvent { Ok(RoomEvent::RoomMessage(event)) } + EventType::RoomMessageFeedback => { + let event = match from_value::(value) { + Ok(event) => event, + Err(error) => return Err(D::Error::custom(error.to_string())), + }; + + Ok(RoomEvent::RoomMessageFeedback(event)) + } EventType::RoomRedaction => { let event = match from_value::(value) { Ok(event) => event, @@ -254,6 +281,7 @@ impl<'de> Deserialize<'de> for RoomEvent { Ok(RoomEvent::CustomRoom(event)) } EventType::Direct + | EventType::FullyRead | EventType::Presence | EventType::Receipt | EventType::RoomAliases @@ -287,6 +315,7 @@ macro_rules! impl_from_t_for_event { } impl_from_t_for_event!(DirectEvent, Direct); +impl_from_t_for_event!(FullyReadEvent, FullyRead); impl_from_t_for_event!(PresenceEvent, Presence); impl_from_t_for_event!(ReceiptEvent, Receipt); impl_from_t_for_event!(TagEvent, Tag); @@ -308,5 +337,6 @@ impl_from_t_for_room_event!(CandidatesEvent, CallCandidates); impl_from_t_for_room_event!(HangupEvent, CallHangup); impl_from_t_for_room_event!(InviteEvent, CallInvite); impl_from_t_for_room_event!(MessageEvent, RoomMessage); +impl_from_t_for_room_event!(FeedbackEvent, RoomMessageFeedback); impl_from_t_for_room_event!(RedactionEvent, RoomRedaction); impl_from_t_for_room_event!(CustomRoomEvent, CustomRoom); diff --git a/src/direct.rs b/src/direct.rs index ffa786fe..a7d22ed8 100644 --- a/src/direct.rs +++ b/src/direct.rs @@ -85,7 +85,7 @@ mod tests { assert!(direct_rooms.contains(&rooms[0])); assert!(direct_rooms.contains(&rooms[1])); } - _ => assert!(false), + _ => unreachable!(), }; match from_str::(&json_data).unwrap() { @@ -96,7 +96,7 @@ mod tests { assert!(direct_rooms.contains(&rooms[0])); assert!(direct_rooms.contains(&rooms[1])); } - _ => assert!(false), + _ => unreachable!(), }; } } diff --git a/src/fully_read.rs b/src/fully_read.rs new file mode 100644 index 00000000..21233a76 --- /dev/null +++ b/src/fully_read.rs @@ -0,0 +1,22 @@ +//! Types for the *m.fully_read* event. + +use ruma_identifiers::{EventId, RoomId}; +use serde::{Deserialize, Serialize}; + +event! { + /// The current location of the user's read marker in a room. + /// + /// This event appears in the user's room account data for the room the marker is applicable + /// for. + pub struct FullyReadEvent(FullyReadEventContent) { + /// The unique identifier for the room associated with this event. + pub room_id: RoomId + } +} + +/// The payload of a `FullyReadEvent`. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct FullyReadEventContent { + /// The event the user's read marker is located at in the room. + pub event_id: EventId, +} diff --git a/src/lib.rs b/src/lib.rs index 8fe898d3..2e63dd8d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -118,6 +118,7 @@ pub mod collections { pub mod only; } pub mod direct; +pub mod fully_read; pub mod presence; pub mod receipt; pub mod room; @@ -142,6 +143,8 @@ pub enum EventType { CallInvite, /// m.direct Direct, + /// m.fully_read + FullyRead, /// m.presence Presence, /// m.receipt @@ -164,6 +167,8 @@ pub enum EventType { RoomMember, /// m.room.message RoomMessage, + /// m.room.message.feedback + RoomMessageFeedback, /// m.room.name RoomName, /// m.room.pinned_events @@ -253,6 +258,7 @@ impl Display for EventType { EventType::CallHangup => "m.call.hangup", EventType::CallInvite => "m.call.invite", EventType::Direct => "m.direct", + EventType::FullyRead => "m.fully_read", EventType::Presence => "m.presence", EventType::Receipt => "m.receipt", EventType::RoomAliases => "m.room.aliases", @@ -264,6 +270,7 @@ impl Display for EventType { EventType::RoomJoinRules => "m.room.join_rules", EventType::RoomMember => "m.room.member", EventType::RoomMessage => "m.room.message", + EventType::RoomMessageFeedback => "m.room.message.feedback", EventType::RoomName => "m.room.name", EventType::RoomPinnedEvents => "m.room.pinned_events", EventType::RoomPowerLevels => "m.room.power_levels", @@ -287,6 +294,7 @@ impl<'a> From<&'a str> for EventType { "m.call.hangup" => EventType::CallHangup, "m.call.invite" => EventType::CallInvite, "m.direct" => EventType::Direct, + "m.fully_read" => EventType::FullyRead, "m.presence" => EventType::Presence, "m.receipt" => EventType::Receipt, "m.room.aliases" => EventType::RoomAliases, @@ -298,6 +306,7 @@ impl<'a> From<&'a str> for EventType { "m.room.join_rules" => EventType::RoomJoinRules, "m.room.member" => EventType::RoomMember, "m.room.message" => EventType::RoomMessage, + "m.room.message.feedback" => EventType::RoomMessageFeedback, "m.room.name" => EventType::RoomName, "m.room.pinned_events" => EventType::RoomPinnedEvents, "m.room.power_levels" => EventType::RoomPowerLevels, diff --git a/src/presence.rs b/src/presence.rs index 0aeca202..ece7ccee 100644 --- a/src/presence.rs +++ b/src/presence.rs @@ -78,7 +78,7 @@ mod tests { avatar_url: Some("mxc://localhost:wefuiwegh8742w".to_string()), currently_active: Some(false), displayname: None, - last_active_ago: Some(2478593), + last_active_ago: Some(2_478_593), presence: PresenceState::Online, }, event_type: EventType::Presence, diff --git a/src/receipt.rs b/src/receipt.rs index 35a07234..c0ed27a9 100644 --- a/src/receipt.rs +++ b/src/receipt.rs @@ -9,11 +9,7 @@ event! { /// Informs the client of new receipts. pub struct ReceiptEvent(ReceiptEventContent) { /// The unique identifier for the room associated with this event. - /// - /// This can be `None` if the event came from a context where there is - /// no ambiguity which room it belongs to, like a `/sync` response for example. - #[serde(skip_serializing_if="Option::is_none")] - pub room_id: Option + pub room_id: RoomId } } @@ -29,7 +25,7 @@ pub struct Receipts { /// A collection of users who have sent *m.read* receipts for this event. #[serde(rename = "m.read")] #[serde(default)] - pub m_read: UserReceipts, + pub read: UserReceipts, } /// A mapping of user ID to receipt. @@ -40,6 +36,6 @@ pub type UserReceipts = HashMap; /// An acknowledgement of an event. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Receipt { - /// The timestamp the receipt was sent at. + /// The timestamp (milliseconds since the Unix epoch) when the receipt was sent. pub ts: u64, } diff --git a/src/room/avatar.rs b/src/room/avatar.rs index bd218775..ffeb6580 100644 --- a/src/room/avatar.rs +++ b/src/room/avatar.rs @@ -18,11 +18,6 @@ pub struct AvatarEventContent { #[serde(skip_serializing_if = "Option::is_none")] pub info: Option, /// Information about the avatar thumbnail image. - #[serde(skip_serializing_if = "Option::is_none")] - pub thumbnail_info: Option, - /// URL of the avatar thumbnail image. - #[serde(skip_serializing_if = "Option::is_none")] - pub thumbnail_url: Option, /// URL of the avatar image. pub url: String, } diff --git a/src/room/create.rs b/src/room/create.rs index 9deeb956..50053588 100644 --- a/src/room/create.rs +++ b/src/room/create.rs @@ -1,6 +1,6 @@ //! Types for the *m.room.create* event. -use ruma_identifiers::UserId; +use ruma_identifiers::{EventId, RoomId, RoomVersionId, UserId}; use serde::{Deserialize, Serialize}; state_event! { @@ -10,11 +10,24 @@ state_event! { } /// The payload of a `CreateEvent`. -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct CreateEventContent { /// The `user_id` of the room creator. This is set by the homeserver. pub creator: UserId, /// Whether or not this room's data should be transferred to other homeservers. #[serde(rename = "m.federate")] pub federate: Option, + /// The version of the room. Defaults to "1" if the key does not exist. + pub room_version: RoomVersionId, + /// A reference to the room this room replaces, if the previous room was upgraded. + pub predecessor: PreviousRoom, +} + +/// A reference to an old room replaced during a room version upgrade. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct PreviousRoom { + /// The ID of the old room. + pub room_id: RoomId, + /// The event ID of the last known event in the old room. + pub event_id: EventId, } diff --git a/src/room/member.rs b/src/room/member.rs index 75aed7c0..170631c7 100644 --- a/src/room/member.rs +++ b/src/room/member.rs @@ -4,38 +4,40 @@ use ruma_identifiers::UserId; use ruma_signatures::Signatures; use serde::{Deserialize, Serialize}; -use crate::stripped::StrippedState; - state_event! { /// The current membership state of a user in the room. /// /// Adjusts the membership state for a user in a room. It is preferable to use the membership - /// APIs (``/rooms//invite`` etc) when performing membership actions rather than + /// APIs (`/rooms//invite` etc) when performing membership actions rather than /// adjusting the state directly as there are a restricted set of valid transformations. For /// example, user A cannot force user B to join a room, and trying to force this state change /// directly will fail. /// - /// The *third_party_invite* property will be set if this invite is an *invite* event and is the + /// The `third_party_invite` property will be set if this invite is an *invite* event and is the /// successor of an *m.room.third_party_invite* event, and absent otherwise. /// - /// This event may also include an *invite_room_state* key outside the *content* key. If + /// This event may also include an `invite_room_state` key inside the event's unsigned data. If /// present, this contains an array of `StrippedState` events. These events provide information - /// on a few select state events such as the room name. - pub struct MemberEvent(MemberEventContent) { - /// A subset of the state of the room at the time of the invite. - #[serde(skip_serializing_if = "Option::is_none")] - pub invite_room_state: Option> - } + /// on a subset of state events such as the room name. + /// + /// The user for which a membership applies is represented by the `state_key`. Under some + /// conditions, the `sender` and `state_key` may not match - this may be interpreted as the + /// `sender` affecting the membership state of the `state_key` user. + /// + /// The membership for a given user can change over time. Previous membership can be retrieved + /// from the `prev_content` object on an event. If not present, the user's previous membership + /// must be assumed as leave. + pub struct MemberEvent(MemberEventContent) {} } /// The payload of a `MemberEvent`. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct MemberEventContent { - /// The avatar URL for this user. + /// The avatar URL for this user, if any. This is added by the homeserver. #[serde(skip_serializing_if = "Option::is_none")] pub avatar_url: Option, - /// The display name for this user. + /// The display name for this user, if any. This is added by the homeserver. #[serde(skip_serializing_if = "Option::is_none")] pub displayname: Option, diff --git a/src/room/message.rs b/src/room/message.rs index e4763d8a..0f7fdb9d 100644 --- a/src/room/message.rs +++ b/src/room/message.rs @@ -1,9 +1,12 @@ //! Types for the *m.room.message* event. +use ruma_identifiers::EventId; use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; use serde_json::{from_value, Value}; -use super::{ImageInfo, ThumbnailInfo}; +use super::{EncryptedFile, ImageInfo, ThumbnailInfo}; + +pub mod feedback; room_event! { /// A message sent to a room. @@ -47,6 +50,7 @@ pub enum MessageType { } /// The payload of a message event. +#[allow(clippy::large_enum_variant)] #[derive(Clone, Debug, PartialEq)] pub enum MessageEventContent { /// An audio message. @@ -84,8 +88,13 @@ pub struct AudioMessageEventContent { pub info: Option, /// The message type. Always *m.audio*. pub msgtype: MessageType, - /// The URL to the audio clip. - pub url: String, + /// The URL to the audio clip. Required if the file is unencrypted. The URL (typically + /// [MXC URI](https://matrix.org/docs/spec/client_server/r0.5.0#mxc-uri)) to the audio clip. + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + /// Required if the audio clip is encrypted. Information on the encrypted audio clip. + #[serde(skip_serializing_if = "Option::is_none")] + pub file: Option, } /// Metadata about an audio clip. @@ -109,6 +118,13 @@ pub struct EmoteMessageEventContent { pub body: String, /// The message type. Always *m.emote*. pub msgtype: MessageType, + /// The format used in the `formatted_body`. Currently only `org.matrix.custom.html` is + /// supported. + #[serde(skip_serializing_if = "Option::is_none")] + pub format: Option, + /// The formatted version of the `body`. This is required if `format` is specified. + #[serde(skip_serializing_if = "Option::is_none")] + pub formatted_body: Option, } /// The payload of a file message. @@ -118,14 +134,20 @@ pub struct FileMessageEventContent { /// original upload. pub body: String, /// The original filename of the uploaded file. - pub filename: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub filename: Option, /// Metadata about the file referred to in `url`. #[serde(skip_serializing_if = "Option::is_none")] pub info: Option, /// The message type. Always *m.file*. pub msgtype: MessageType, - /// The URL to the file. - pub url: String, + /// The URL to the file. Required if the file is unencrypted. The URL (typically + /// [MXC URI](https://matrix.org/docs/spec/client_server/r0.5.0#mxc-uri)) to the file. + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + /// Required if file is encrypted. Information on the encrypted file. + #[serde(skip_serializing_if = "Option::is_none")] + pub file: Option, } /// Metadata about a file. @@ -138,9 +160,12 @@ pub struct FileInfo { /// Metadata about the image referred to in `thumbnail_url`. #[serde(skip_serializing_if = "Option::is_none")] pub thumbnail_info: Option, - /// The URL to the thumbnail of the file. + /// The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted. #[serde(skip_serializing_if = "Option::is_none")] pub thumbnail_url: Option, + /// Information on the encrypted thumbnail file. Only present if the thumbnail is encrypted. + #[serde(skip_serializing_if = "Option::is_none")] + pub thumbnail_file: Option, } /// The payload of an image message. @@ -154,8 +179,12 @@ pub struct ImageMessageEventContent { pub info: Option, /// The message type. Always *m.image*. pub msgtype: MessageType, - /// The URL to the image. - pub url: String, + /// The URL to the image. Required if the file is unencrypted. The URL (typically + /// [MXC URI](https://matrix.org/docs/spec/client_server/r0.5.0#mxc-uri)) to the image. + pub url: Option, + /// Required if image is encrypted. Information on the encrypted image. + #[serde(skip_serializing_if = "Option::is_none")] + pub file: Option, } /// The payload of a location message. @@ -176,12 +205,17 @@ pub struct LocationMessageEventContent { /// Thumbnail info associated with a location. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct LocationInfo { - /// Metadata about the image referred to in `thumbnail_url`. + /// Metadata about the image referred to in `thumbnail_url` or `thumbnail_file`. #[serde(skip_serializing_if = "Option::is_none")] pub thumbnail_info: Option, - /// The URL to a thumbnail of the location being represented. + /// The URL to a thumbnail of the location being represented. Only present if the thumbnail is + /// unencrypted. #[serde(skip_serializing_if = "Option::is_none")] pub thumbnail_url: Option, + /// Information on an encrypted thumbnail of the location being represented. Only present if the + /// thumbnail is encrypted. + #[serde(skip_serializing_if = "Option::is_none")] + pub thumbnail_file: Option, } /// The payload of a notice message. @@ -191,6 +225,11 @@ pub struct NoticeMessageEventContent { pub body: String, /// The message type. Always *m.notice*. pub msgtype: MessageType, + /// Information about related messages for + /// [rich replies](https://matrix.org/docs/spec/client_server/r0.5.0#rich-replies). + #[serde(rename = "m.relates_to")] + #[serde(skip_serializing_if = "Option::is_none")] + pub relates_to: Option, } /// The payload of a text message. @@ -200,6 +239,18 @@ pub struct TextMessageEventContent { pub body: String, /// The message type. Always *m.text*. pub msgtype: MessageType, + /// The format used in the `formatted_body`. Currently only `org.matrix.custom.html` is + /// supported. + #[serde(skip_serializing_if = "Option::is_none")] + pub format: Option, + /// The formatted version of the `body`. This is required if `format` is specified. + #[serde(skip_serializing_if = "Option::is_none")] + pub formatted_body: Option, + /// Information about related messages for + /// [rich replies](https://matrix.org/docs/spec/client_server/r0.5.0#rich-replies). + #[serde(rename = "m.relates_to")] + #[serde(skip_serializing_if = "Option::is_none")] + pub relates_to: Option, } /// The payload of a video message. @@ -213,8 +264,12 @@ pub struct VideoMessageEventContent { pub info: Option, /// The message type. Always *m.video*. pub msgtype: MessageType, - /// The URL to the video clip. - pub url: String, + /// The URL to the video clip. Required if the file is unencrypted. The URL (typically + /// [MXC URI](https://matrix.org/docs/spec/client_server/r0.5.0#mxc-uri)) to the video clip. + pub url: Option, + /// Required if video clip is encrypted. Information on the encrypted video clip. + #[serde(skip_serializing_if = "Option::is_none")] + pub file: Option, } /// Metadata about a video. @@ -236,15 +291,35 @@ pub struct VideoInfo { /// Metadata about an image. #[serde(skip_serializing_if = "Option::is_none")] pub thumbnail_info: Option, - /// The URL to a thumbnail of the video clip. + /// The URL (typically [MXC URI](https://matrix.org/docs/spec/client_server/r0.5.0#mxc-uri)) to + /// an image thumbnail of the video clip. Only present if the thumbnail is unencrypted. #[serde(skip_serializing_if = "Option::is_none")] pub thumbnail_url: Option, + /// Information on the encrypted thumbnail file. Only present if the thumbnail is encrypted. + #[serde(skip_serializing_if = "Option::is_none")] + pub thumbnail_file: Option, /// The width of the video in pixels. #[serde(rename = "w")] #[serde(skip_serializing_if = "Option::is_none")] pub width: Option, } +/// Information about related messages for +/// [rich replies](https://matrix.org/docs/spec/client_server/r0.5.0#rich-replies). +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct RelatesTo { + /// Information about another message being replied to. + #[serde(rename = "m.in_reply_to")] + pub in_reply_to: InReplyTo, +} + +/// Information about the event a "rich reply" is replying to. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct InReplyTo { + /// The event being replied to. + pub event_id: EventId, +} + impl_enum! { MessageType { Audio => "m.audio", @@ -374,7 +449,8 @@ mod tests { body: "test".to_string(), info: None, msgtype: MessageType::Audio, - url: "http://example.com/audio.mp3".to_string(), + url: Some("http://example.com/audio.mp3".to_string()), + file: None, }); assert_eq!( @@ -389,7 +465,8 @@ mod tests { body: "test".to_string(), info: None, msgtype: MessageType::Audio, - url: "http://example.com/audio.mp3".to_string(), + url: Some("http://example.com/audio.mp3".to_string()), + file: None, }); assert_eq!( diff --git a/src/room/message/feedback.rs b/src/room/message/feedback.rs new file mode 100644 index 00000000..51ec0f80 --- /dev/null +++ b/src/room/message/feedback.rs @@ -0,0 +1,41 @@ +//! Types for the *m.room.message.feedback* event. + +use ruma_identifiers::EventId; +use serde::{Deserialize, Serialize}; + +room_event! { + /// An acknowledgement of a message. + /// + /// N.B.: Usage of this event is discouraged in favor of the receipts module. Most clients will + /// not recognise this event. + pub struct FeedbackEvent(FeedbackEventContent) {} +} + +/// The payload of an *m.room.message.feedback* event. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct FeedbackEventContent { + /// The event that this feedback is related to. + pub target_event_id: EventId, + /// The type of feedback. + #[serde(rename = "type")] + pub feedback_type: FeedbackType, +} + +/// A type of feedback. +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] +pub enum FeedbackType { + /// Sent when a message is received. + #[serde(rename = "delivered")] + Delivered, + + /// Sent when a message has been observed by the end user. + #[serde(rename = "read")] + Read, +} + +impl_enum! { + FeedbackType { + Delivered => "delivered", + Read => "read", + } +} diff --git a/src/room/mod.rs b/src/room/mod.rs index d05a03d6..b460c77e 100644 --- a/src/room/mod.rs +++ b/src/room/mod.rs @@ -2,6 +2,8 @@ //! //! This module also contains types shared by events in its child namespaces. +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; pub mod aliases; @@ -33,9 +35,12 @@ pub struct ImageInfo { /// Metadata about the image referred to in `thumbnail_url`. #[serde(skip_serializing_if = "Option::is_none")] pub thumbnail_info: Option, - /// The URL to the thumbnail of the image. + /// The URL to the thumbnail of the image. Only present if the thumbnail is unencrypted. #[serde(skip_serializing_if = "Option::is_none")] pub thumbnail_url: Option, + /// Information on the encrypted thumbnail image. Only present if the thumbnail is encrypted. + #[serde(skip_serializing_if = "Option::is_none")] + pub thumbnail_file: Option, /// The width of the image in pixels. #[serde(rename = "w")] pub width: u64, @@ -55,3 +60,35 @@ pub struct ThumbnailInfo { #[serde(rename = "w")] pub width: u64, } + +/// A file sent to a room with end-to-end encryption enabled. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct EncryptedFile { + /// The URL to the file. + pub url: String, + /// A [JSON Web Key](https://tools.ietf.org/html/rfc7517#appendix-A.3) object. + pub key: JsonWebKey, + /// The initialization vector used by AES-CTR, encoded as unpadded base64. + pub iv: String, + /// A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64. + /// Clients should support the SHA-256 hash, which uses the key sha256. + pub hashes: HashMap, + /// Version of the encrypted attachments protocol. Must be `v2`. + pub v: String, +} + +/// A [JSON Web Key](https://tools.ietf.org/html/rfc7517#appendix-A.3) object. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct JsonWebKey { + /// Key type. Must be `oct`. + pub kty: String, + /// Key operations. Must at least contain `encrypt` and `decrypt`. + pub key_ops: Vec, + /// Required. Algorithm. Must be `A256CTR`. + pub alg: String, + /// The key, encoded as urlsafe unpadded base64. + pub k: String, + /// Extractable. Must be `true`. This is a + /// [W3C extension](https://w3c.github.io/webcrypto/#iana-section-jwk). + pub ext: bool, +} diff --git a/src/room/name.rs b/src/room/name.rs index ce34b734..894bfee4 100644 --- a/src/room/name.rs +++ b/src/room/name.rs @@ -12,7 +12,6 @@ state_event! { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct NameEventContent { /// The name of the room. This MUST NOT exceed 255 bytes. - /// Rooms with `name: None` should be treated the same as a room with no name. // The spec says “A room with an m.room.name event with an absent, null, or empty name field // should be treated the same as a room with no m.room.name event.”. // Serde maps null fields to None by default, serde(default) maps an absent field to None, diff --git a/src/room/pinned_events.rs b/src/room/pinned_events.rs index de07120d..52438658 100644 --- a/src/room/pinned_events.rs +++ b/src/room/pinned_events.rs @@ -36,7 +36,7 @@ mod tests { content: content.clone(), event_id: EventId::new("example.com").unwrap(), event_type: EventType::RoomPinnedEvents, - origin_server_ts: 1432804485886, + origin_server_ts: 1_432_804_485_886, prev_content: None, room_id: Some(RoomId::new("example.com").unwrap()), sender: UserId::new("example.com").unwrap(), diff --git a/src/room/power_levels.rs b/src/room/power_levels.rs index 6d787278..5209b74c 100644 --- a/src/room/power_levels.rs +++ b/src/room/power_levels.rs @@ -52,6 +52,19 @@ pub struct PowerLevelsEventContent { /// The default power level for every user in the room. #[serde(default)] pub users_default: u64, + + /// The power level requirements for specific notification types. + /// + /// This is a mapping from `key` to power level for that notifications key. + pub notifications: NotificationPowerLevels, +} + +/// The power level requirements for specific notification types. +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub struct NotificationPowerLevels { + /// The level required to trigger an `@room` notification. + #[serde(default = "default_power_level")] + pub room: u64, } fn default_power_level() -> u64 { diff --git a/src/stripped.rs b/src/stripped.rs index 650b928c..2f08d835 100644 --- a/src/stripped.rs +++ b/src/stripped.rs @@ -338,7 +338,7 @@ mod tests { assert_eq!(event.sender.to_string(), "@example:localhost"); } _ => { - assert!(false); + unreachable!(); } }; @@ -350,7 +350,7 @@ mod tests { assert_eq!(event.sender.to_string(), "@example:localhost"); } _ => { - assert!(false); + unreachable!(); } }; @@ -362,14 +362,14 @@ mod tests { assert_eq!(image_info.width, 128); assert_eq!(image_info.mimetype, "image/jpeg"); assert_eq!(image_info.size, 1024); - assert_eq!(event.content.thumbnail_info.unwrap().size, 32); + assert_eq!(image_info.thumbnail_info.unwrap().size, 32); assert_eq!(event.content.url, "https://domain.com/image.jpg"); assert_eq!(event.event_type, EventType::RoomAvatar); assert_eq!(event.state_key, ""); assert_eq!(event.sender.to_string(), "@example:localhost"); } _ => { - assert!(false); + unreachable!(); } }; } diff --git a/src/tag.rs b/src/tag.rs index 7fee06e9..404cfbf0 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -21,5 +21,5 @@ pub struct TagEventContent { pub struct TagInfo { /// Value to use for lexicographically ordering rooms with this tag. #[serde(skip_serializing_if = "Option::is_none")] - pub order: Option, + pub order: Option, } diff --git a/src/typing.rs b/src/typing.rs index d0542be4..dc907395 100644 --- a/src/typing.rs +++ b/src/typing.rs @@ -7,11 +7,7 @@ event! { /// Informs the client of the list of users currently typing. pub struct TypingEvent(TypingEventContent) { /// The unique identifier for the room associated with this event. - /// - /// This can be `None` if the event came from a context where there is - /// no ambiguity which room it belongs to, like a `/sync` response for example. - #[serde(skip_serializing_if="Option::is_none")] - pub room_id: Option + pub room_id: RoomId } }