From c377f8ec911dada1df41e78ef394996d51e8a7bf Mon Sep 17 00:00:00 2001 From: tezlm Date: Sun, 3 Dec 2023 17:46:16 -0800 Subject: [PATCH] armageddon --- Cargo.toml | 2 +- README.md | 5 + crates/ruma-client-api/src/push.rs | 2 + crates/ruma-client-api/src/push/get_inbox.rs | 118 ++++++ crates/ruma-client-api/src/threads.rs | 1 + .../src/threads/set_participation.rs | 15 +- crates/ruma-events/src/attachment.rs | 6 + crates/ruma-events/src/attachment/embed.rs | 59 +++ crates/ruma-events/src/attachment/file.rs | 89 +++++ crates/ruma-events/src/audio.rs | 158 -------- .../ruma-events/src/audio/amplitude_serde.rs | 16 - crates/ruma-events/src/emote.rs | 91 ----- crates/ruma-events/src/encrypted.rs | 35 +- .../{room/encrypted.rs => encrypted/temp.rs} | 136 +------ crates/ruma-events/src/enums.rs | 170 ++------ crates/ruma-events/src/file.rs | 275 ------------- crates/ruma-events/src/image.rs | 295 -------------- crates/ruma-events/src/lib.rs | 67 ++-- crates/ruma-events/src/location.rs | 196 --------- .../src/location/zoomlevel_serde.rs | 20 - crates/ruma-events/src/message.rs | 146 ++----- .../src/message/historical_serde.rs | 100 ----- crates/ruma-events/src/mixins.rs | 31 ++ crates/ruma-events/src/poll.rs | 203 ---------- crates/ruma-events/src/poll/end.rs | 125 ------ crates/ruma-events/src/poll/response.rs | 129 ------ crates/ruma-events/src/poll/start.rs | 285 ------------- .../src/poll/start/poll_answers_serde.rs | 18 - crates/ruma-events/src/poll/unstable_end.rs | 57 --- .../ruma-events/src/poll/unstable_response.rs | 98 ----- crates/ruma-events/src/poll/unstable_start.rs | 377 ------------------ .../src/poll/unstable_start/content_serde.rs | 82 ---- .../unstable_poll_answers_serde.rs | 19 - .../unstable_poll_kind_serde.rs | 37 -- crates/ruma-events/src/reaction.rs | 20 +- crates/ruma-events/src/relation.rs | 180 ++++++--- .../{rel_serde.rs => bundle_serde.rs} | 8 +- .../src/relation/relation_serde.rs | 170 ++++++++ crates/ruma-events/src/room.rs | 9 +- .../src/room/encrypted/relation_serde.rs | 100 ----- crates/ruma-events/src/room/message.rs | 21 +- crates/ruma-events/src/room/message/audio.rs | 196 --------- .../src/room/message/content_serde.rs | 147 ------- crates/ruma-events/src/room/message/emote.rs | 43 -- crates/ruma-events/src/room/message/file.rs | 96 ----- crates/ruma-events/src/room/message/image.rs | 51 --- .../room/message/key_verification_request.rs | 52 --- .../ruma-events/src/room/message/location.rs | 137 ------- crates/ruma-events/src/room/message/notice.rs | 43 -- .../ruma-events/src/room/message/relation.rs | 121 ------ .../src/room/message/relation_serde.rs | 253 ------------ crates/ruma-events/src/room/message/reply.rs | 179 --------- .../ruma-events/src/room/message/sanitize.rs | 56 --- .../src/room/message/server_notice.rs | 64 --- crates/ruma-events/src/room/message/text.rs | 43 -- crates/ruma-events/src/room/message/video.rs | 108 ----- .../src/room/message/without_relation.rs | 255 ------------ crates/ruma-events/src/threads.rs | 3 + crates/ruma-events/src/threads/call.rs | 1 + crates/ruma-events/src/threads/message.rs | 1 + crates/ruma-events/src/threads/poll.rs | 1 + crates/ruma-events/src/video.rs | 128 ------ crates/ruma-events/src/voice.rs | 111 ------ crates/ruma-events/tests/it/encrypted.rs | 6 +- crates/ruma-events/tests/it/message.rs | 40 -- crates/ruma-events/tests/it/relations.rs | 10 +- crates/ruma-events/tests/it/room_message.rs | 1 - .../ruma-events/tests/it/without_relation.rs | 6 +- .../ruma-macros/src/events/event_content.rs | 24 +- 69 files changed, 803 insertions(+), 5344 deletions(-) create mode 100644 crates/ruma-client-api/src/push/get_inbox.rs create mode 100644 crates/ruma-events/src/attachment.rs create mode 100644 crates/ruma-events/src/attachment/embed.rs create mode 100644 crates/ruma-events/src/attachment/file.rs delete mode 100644 crates/ruma-events/src/audio.rs delete mode 100644 crates/ruma-events/src/audio/amplitude_serde.rs delete mode 100644 crates/ruma-events/src/emote.rs rename crates/ruma-events/src/{room/encrypted.rs => encrypted/temp.rs} (64%) delete mode 100644 crates/ruma-events/src/file.rs delete mode 100644 crates/ruma-events/src/image.rs delete mode 100644 crates/ruma-events/src/location.rs delete mode 100644 crates/ruma-events/src/location/zoomlevel_serde.rs delete mode 100644 crates/ruma-events/src/message/historical_serde.rs create mode 100644 crates/ruma-events/src/mixins.rs delete mode 100644 crates/ruma-events/src/poll.rs delete mode 100644 crates/ruma-events/src/poll/end.rs delete mode 100644 crates/ruma-events/src/poll/response.rs delete mode 100644 crates/ruma-events/src/poll/start.rs delete mode 100644 crates/ruma-events/src/poll/start/poll_answers_serde.rs delete mode 100644 crates/ruma-events/src/poll/unstable_end.rs delete mode 100644 crates/ruma-events/src/poll/unstable_response.rs delete mode 100644 crates/ruma-events/src/poll/unstable_start.rs delete mode 100644 crates/ruma-events/src/poll/unstable_start/content_serde.rs delete mode 100644 crates/ruma-events/src/poll/unstable_start/unstable_poll_answers_serde.rs delete mode 100644 crates/ruma-events/src/poll/unstable_start/unstable_poll_kind_serde.rs rename crates/ruma-events/src/relation/{rel_serde.rs => bundle_serde.rs} (85%) create mode 100644 crates/ruma-events/src/relation/relation_serde.rs delete mode 100644 crates/ruma-events/src/room/encrypted/relation_serde.rs delete mode 100644 crates/ruma-events/src/room/message/audio.rs delete mode 100644 crates/ruma-events/src/room/message/content_serde.rs delete mode 100644 crates/ruma-events/src/room/message/emote.rs delete mode 100644 crates/ruma-events/src/room/message/file.rs delete mode 100644 crates/ruma-events/src/room/message/image.rs delete mode 100644 crates/ruma-events/src/room/message/key_verification_request.rs delete mode 100644 crates/ruma-events/src/room/message/location.rs delete mode 100644 crates/ruma-events/src/room/message/notice.rs delete mode 100644 crates/ruma-events/src/room/message/relation.rs delete mode 100644 crates/ruma-events/src/room/message/relation_serde.rs delete mode 100644 crates/ruma-events/src/room/message/reply.rs delete mode 100644 crates/ruma-events/src/room/message/sanitize.rs delete mode 100644 crates/ruma-events/src/room/message/server_notice.rs delete mode 100644 crates/ruma-events/src/room/message/text.rs delete mode 100644 crates/ruma-events/src/room/message/video.rs delete mode 100644 crates/ruma-events/src/room/message/without_relation.rs create mode 100644 crates/ruma-events/src/threads.rs create mode 100644 crates/ruma-events/src/threads/call.rs create mode 100644 crates/ruma-events/src/threads/message.rs create mode 100644 crates/ruma-events/src/threads/poll.rs delete mode 100644 crates/ruma-events/src/video.rs delete mode 100644 crates/ruma-events/src/voice.rs diff --git a/Cargo.toml b/Cargo.toml index d96f13e4..b62ab3e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/*", "examples/*", "xtask"] +members = ["crates/*", "xtask"] # Only compile / check / document the public crates by default default-members = ["crates/*"] resolver = "2" diff --git a/README.md b/README.md index 50542bf2..f465c884 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +**Under construction** - I'm making this code work on the server side +fisrt and will fix and improve it later on. + +Original readme below. + # Ruma – Your home in Matrix. A set of [Rust] crates (libraries) for interacting with the [Matrix] chat diff --git a/crates/ruma-client-api/src/push.rs b/crates/ruma-client-api/src/push.rs index 41996f5c..7c4045dd 100644 --- a/crates/ruma-client-api/src/push.rs +++ b/crates/ruma-client-api/src/push.rs @@ -14,6 +14,7 @@ use serde::{Deserialize, Serialize}; use crate::PrivOwnedStr; +// TODO: remove almost all of these pub mod delete_pushrule; pub mod get_notifications; pub mod get_pushers; @@ -27,6 +28,7 @@ pub mod set_pusher; pub mod set_pushrule; pub mod set_pushrule_actions; pub mod set_pushrule_enabled; +pub mod get_inbox; /// Like `SimplePushRule`, but may represent any kind of push rule thanks to `pattern` and /// `conditions` being optional. diff --git a/crates/ruma-client-api/src/push/get_inbox.rs b/crates/ruma-client-api/src/push/get_inbox.rs new file mode 100644 index 00000000..bf5fce17 --- /dev/null +++ b/crates/ruma-client-api/src/push/get_inbox.rs @@ -0,0 +1,118 @@ +//! `POST /_matrix/client/*/inbox/query` +//! +//! Paginate through the list of events that the user has been, or would have been notified about. + +pub mod v3 { + //! `/v3/` ([spec]) + //! + //! [spec]: TODO: write and link spec + + use js_int::UInt; + use ruma_common::{ + api::{request, response, Metadata}, + metadata, + serde::Raw, + OwnedRoomId, + }; + use ruma_events::AnyTimelineEvent; + use serde::{Deserialize, Serialize}; + + const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + 1.0 => "/_matrix/client/v1/inbox", + } + }; + + /// Request type for the `get_notifications` endpoint. + #[request(error = crate::Error)] + #[derive(Default)] + pub struct Request { + /// Pagination token given to retrieve the next set of events. + #[ruma_api(query)] + #[serde(skip_serializing_if = "Option::is_none")] + pub from: Option, + + /// Limit on the number of events to return in this request. + #[ruma_api(query)] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + + /// Which rooms to include. An empty vec searches all rooms. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub room_ids: Vec, + + /// Allows basic filtering of events returned. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub filter: Vec, + } + + /// Response type for the `get_inbox` endpoint. + #[response(error = crate::Error)] + pub struct Response { + /// The token to supply in the from param of the next /inbox request in order to + /// request more events. + /// + /// If this is absent, there are no more results. + #[serde(skip_serializing_if = "Option::is_none")] + pub next_batch: Option, + + /// The list of threads mention events are in + pub threads: Vec, + + /// The list of thread and notification events + pub chunk: Vec, + } + + impl Request { + /// Creates an empty `Request`. + pub fn new() -> Self { + Default::default() + } + } + + impl Response { + /// Creates a new `Response` with the given notifications. + pub fn new(chunk: Vec) -> Self { + Self { next_batch: None, chunk, threads: vec![] } + } + } + + /// An inbox filter. + #[derive(Clone, Debug, Default, Deserialize, Serialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub enum InboxFilter { + #[default] + /// The default filter: MentionsUser | MentionsBulk | ThreadsParticipating | ThreadsInteresting + Default, + + /// Get user mentions. + MentionsUser, + + /// Get "bulk" (@room, @thread) mentions. + MentionsBulk, + + /// Get threads that the user is participating in. + ThreadsParticipating, + + /// Get "interesting" threads. + ThreadsInteresting, + + /// Include read threads. + IncludeRead, + } + + /// Represents a notification. + #[derive(Clone, Debug, Deserialize, Serialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Notification { + /// The event that triggered the notification. + pub event: Raw, + + /// Indicates whether the user has sent a read receipt indicating that they have read this + /// message. + pub read: bool, + } +} diff --git a/crates/ruma-client-api/src/threads.rs b/crates/ruma-client-api/src/threads.rs index bcc5720f..cde47177 100644 --- a/crates/ruma-client-api/src/threads.rs +++ b/crates/ruma-client-api/src/threads.rs @@ -2,3 +2,4 @@ pub mod get_threads; pub mod bulk_threads; +pub mod set_participation; diff --git a/crates/ruma-client-api/src/threads/set_participation.rs b/crates/ruma-client-api/src/threads/set_participation.rs index e5d78594..8e2549d6 100644 --- a/crates/ruma-client-api/src/threads/set_participation.rs +++ b/crates/ruma-client-api/src/threads/set_participation.rs @@ -7,14 +7,13 @@ pub mod v1 { //! //! [spec]: TODO - use js_int::UInt; use ruma_common::{ api::{request, response, Metadata}, metadata, - serde::{Raw, StringEnum}, + serde::StringEnum, OwnedRoomId, OwnedEventId, }; - use ruma_events::AnyTimelineEvent; + use serde::{Serialize, Deserialize}; use crate::PrivOwnedStr; @@ -49,20 +48,22 @@ pub mod v1 { impl Response { /// Creates a new `Response` with the given chunk. - pub fn new(chunk: Vec>) -> Self { - Self { chunk, next_batch: None } + pub fn new() -> Self { + Self { } } } + /// Which thread to modify. + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ParticipationUpdate { room_id: OwnedRoomId, event_id: OwnedEventId, participation: Participation, } - /// Which threads to include in the response. + /// The current user's participation in a thread. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] - #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, StringEnum)] + #[derive(Clone, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "lowercase")] #[non_exhaustive] pub enum Participation { diff --git a/crates/ruma-events/src/attachment.rs b/crates/ruma-events/src/attachment.rs new file mode 100644 index 00000000..9f1ae979 --- /dev/null +++ b/crates/ruma-events/src/attachment.rs @@ -0,0 +1,6 @@ +//! Events that "attach" to other events. Somewhat similar to annotation, +//! but is more strongly correlated and should be considered part of the +//! original event's content. + +pub mod file; +pub mod embed; diff --git a/crates/ruma-events/src/attachment/embed.rs b/crates/ruma-events/src/attachment/embed.rs new file mode 100644 index 00000000..f72250ad --- /dev/null +++ b/crates/ruma-events/src/attachment/embed.rs @@ -0,0 +1,59 @@ +//! A url or bot embed, for rich content. + +use serde::{Deserialize, Serialize}; +use crate::attachment::file::File; +use crate::relation::Relation; +use ruma_macros::EventContent; + +/// The content of an embed attachment. These may be generated from urls, +/// or sent by bots. +/// +/// An url generated embed should be skipped if it doesn't have one of +/// `title`, `description`, or any file attachments. Only `media` without `titktle` or +/// `description` should be shown like a file. +#[derive(Clone, Debug, Deserialize, Serialize, EventContent)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[ruma_event(type = "m.attachment.embed", kind = MessageLike, without_relation)] +pub struct AttachmentEmbedEventContent { + /// The title of this embed. + #[serde(skip_serializing_if = "Option::is_none")] + title: Option, + + /// A helpful description or summary. + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + + /// Where this embed links to. + #[serde(skip_serializing_if = "Option::is_none")] + url: Option, + + /// An accent color associated with this embed, in the format `#rrggbb`. + #[serde(skip_serializing_if = "Option::is_none")] + color: Option, + + /// The source of this embed. + #[serde(skip_serializing_if = "Option::is_none")] + source: Option, + + /// Information about related events. + #[serde( + flatten, + skip_serializing_if = "Vec::is_empty", + deserialize_with = "crate::relation::deserialize_relation" + )] + pub relations: Vec>, +} + +/// The source of an embed. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct EmbedSource { + /// The human readable name of this source; the site_name for urls + pub name: Option, + + /// The canonical url representing this source; the base url for urls + pub url: Option, + + /// A small icon representing this source; the favicon for urls + pub icon: Option, +} diff --git a/crates/ruma-events/src/attachment/file.rs b/crates/ruma-events/src/attachment/file.rs new file mode 100644 index 00000000..6d131ae7 --- /dev/null +++ b/crates/ruma-events/src/attachment/file.rs @@ -0,0 +1,89 @@ +//! A file attachment for a message. + +use js_int::UInt; +use serde::{Deserialize, Serialize}; +use crate::room::{MediaSource, ThumbnailInfo}; +use crate::relation::Relation; +use ruma_macros::EventContent; + +/// The content of a file attachment. +#[derive(Clone, Debug, Deserialize, Serialize, EventContent)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[ruma_event(type = "m.attachment.file", kind = MessageLike, without_relation)] +pub struct AttachmentFileEventContent { + /// The file + pub file: File, + + /// Information about related events. + #[serde( + flatten, + skip_serializing_if = "Vec::is_empty", + deserialize_with = "crate::relation::deserialize_relation" + )] + pub relations: Vec>, +} + +/// A file. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct File { + /// The original filename of the uploaded file. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// A textual description of the file's contents. + #[serde(skip_serializing_if = "Option::is_none")] + pub alt: Option, + + /// The source of the file. + #[serde(flatten)] + pub source: MediaSource, + + /// Metadata about the file referred to in `source`. + #[serde(skip_serializing_if = "Option::is_none")] + pub info: Option>, +} + +/// Metadata about a file. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct FileInfo { + /// The mimetype of the file, e.g. "application/msword". + #[serde(skip_serializing_if = "Option::is_none")] + pub mimetype: Option, + + /// The size of the file in bytes. + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, + + /// The width of the {image, video} + #[serde(skip_serializing_if = "Option::is_none")] + pub width: Option, + + /// The height of the {image, video} + #[serde(skip_serializing_if = "Option::is_none")] + pub height: Option, + + /// The duration of the {audio, video} + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// Metadata about the image referred to in `thumbnail_source`. + #[serde(skip_serializing_if = "Option::is_none")] + pub thumbnail_info: Option>, + + /// The source of the thumbnail of the file. + #[serde( + flatten, + with = "crate::room::thumbnail_source_serde", + skip_serializing_if = "Option::is_none" + )] + pub thumbnail_source: Option, +} + +impl FileInfo { + /// Creates an empty `FileInfo`. + pub fn new() -> Self { + Self::default() + } +} diff --git a/crates/ruma-events/src/audio.rs b/crates/ruma-events/src/audio.rs deleted file mode 100644 index c4ca4aa0..00000000 --- a/crates/ruma-events/src/audio.rs +++ /dev/null @@ -1,158 +0,0 @@ -//! Types for extensible audio message events ([MSC3927]). -//! -//! [MSC3927]: https://github.com/matrix-org/matrix-spec-proposals/pull/3927 - -use std::time::Duration; - -use js_int::UInt; -use ruma_macros::EventContent; -use serde::{Deserialize, Serialize}; - -#[cfg(feature = "unstable-msc3246")] -mod amplitude_serde; - -use super::{ - file::{CaptionContentBlock, FileContentBlock}, - message::TextContentBlock, - room::message::Relation, -}; - -/// The payload for an extensible audio message. -/// -/// This is the new primary type introduced in [MSC3927] and should only be sent in rooms with a -/// version that supports it. See the documentation of the [`message`] module for more information. -/// -/// [MSC3927]: https://github.com/matrix-org/matrix-spec-proposals/pull/3927 -/// [`message`]: super::message -#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[ruma_event(type = "org.matrix.msc1767.audio", kind = MessageLike, without_relation)] -pub struct AudioEventContent { - /// The text representations of the message. - #[serde(rename = "org.matrix.msc1767.text")] - pub text: TextContentBlock, - - /// The file content of the message. - #[serde(rename = "org.matrix.msc1767.file")] - pub file: FileContentBlock, - - /// The audio details of the message, if any. - #[serde(rename = "org.matrix.msc1767.audio_details", skip_serializing_if = "Option::is_none")] - pub audio_details: Option, - - /// The caption of the message, if any. - #[serde(rename = "org.matrix.msc1767.caption", skip_serializing_if = "Option::is_none")] - pub caption: Option, - - /// Whether this message is automated. - #[cfg(feature = "unstable-msc3955")] - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - rename = "org.matrix.msc1767.automated" - )] - pub automated: bool, - - /// Information about related messages. - #[serde( - flatten, - skip_serializing_if = "Option::is_none", - deserialize_with = "crate::room::message::relation_serde::deserialize_relation" - )] - pub relates_to: Option>, -} - -impl AudioEventContent { - /// Creates a new `AudioEventContent` with the given text fallback and file. - pub fn new(text: TextContentBlock, file: FileContentBlock) -> Self { - Self { - text, - file, - audio_details: None, - caption: None, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } - - /// Creates a new `AudioEventContent` with the given plain text fallback representation and - /// file. - pub fn with_plain_text(plain_text: impl Into, file: FileContentBlock) -> Self { - Self { - text: TextContentBlock::plain(plain_text), - file, - audio_details: None, - caption: None, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } -} - -/// A block for details of audio content. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct AudioDetailsContentBlock { - /// The duration of the audio in seconds. - #[serde(with = "ruma_common::serde::duration::secs")] - pub duration: Duration, - - /// The waveform representation of the audio content, if any. - /// - /// This is optional and defaults to an empty array. - #[cfg(feature = "unstable-msc3246")] - #[serde( - rename = "org.matrix.msc3246.waveform", - default, - skip_serializing_if = "Vec::is_empty" - )] - pub waveform: Vec, -} - -impl AudioDetailsContentBlock { - /// Creates a new `AudioDetailsContentBlock` with the given duration. - pub fn new(duration: Duration) -> Self { - Self { - duration, - #[cfg(feature = "unstable-msc3246")] - waveform: Default::default(), - } - } -} - -/// The amplitude of a waveform sample. -/// -/// Must be an integer between 0 and 256. -#[cfg(feature = "unstable-msc3246")] -#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)] -pub struct Amplitude(UInt); - -#[cfg(feature = "unstable-msc3246")] -impl Amplitude { - /// The smallest value that can be represented by this type, 0. - pub const MIN: u16 = 0; - - /// The largest value that can be represented by this type, 256. - pub const MAX: u16 = 256; - - /// Creates a new `Amplitude` with the given value. - /// - /// It will saturate if it is bigger than [`Amplitude::MAX`]. - pub fn new(value: u16) -> Self { - Self(value.min(Self::MAX).into()) - } - - /// The value of this `Amplitude`. - pub fn get(&self) -> UInt { - self.0 - } -} - -#[cfg(feature = "unstable-msc3246")] -impl From for Amplitude { - fn from(value: u16) -> Self { - Self::new(value) - } -} diff --git a/crates/ruma-events/src/audio/amplitude_serde.rs b/crates/ruma-events/src/audio/amplitude_serde.rs deleted file mode 100644 index 5e51dbb3..00000000 --- a/crates/ruma-events/src/audio/amplitude_serde.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! `Serialize` and `Deserialize` implementations for extensible events (MSC1767). - -use js_int::UInt; -use serde::Deserialize; - -use super::Amplitude; - -impl<'de> Deserialize<'de> for Amplitude { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let uint = UInt::deserialize(deserializer)?; - Ok(Self(uint.min(Self::MAX.into()))) - } -} diff --git a/crates/ruma-events/src/emote.rs b/crates/ruma-events/src/emote.rs deleted file mode 100644 index 0355bb4b..00000000 --- a/crates/ruma-events/src/emote.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! Types for extensible emote message events ([MSC3954]). -//! -//! [MSC3954]: https://github.com/matrix-org/matrix-spec-proposals/pull/3954 - -use ruma_macros::EventContent; -use serde::{Deserialize, Serialize}; - -use super::{message::TextContentBlock, room::message::Relation}; - -/// The payload for an extensible emote message. -/// -/// This is the new primary type introduced in [MSC3954] and should only be sent in rooms with a -/// version that supports it. See the documentation of the [`message`] module for more information. -/// -/// To construct an `EmoteEventContent` with a custom [`TextContentBlock`], convert it with -/// `EmoteEventContent::from()` / `.into()`. -/// -/// [MSC3954]: https://github.com/matrix-org/matrix-spec-proposals/pull/3954 -/// [`message`]: super::message -#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[ruma_event(type = "org.matrix.msc1767.emote", kind = MessageLike, without_relation)] -pub struct EmoteEventContent { - /// The message's text content. - #[serde(rename = "org.matrix.msc1767.text")] - pub text: TextContentBlock, - - /// Whether this message is automated. - #[cfg(feature = "unstable-msc3955")] - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - rename = "org.matrix.msc1767.automated" - )] - pub automated: bool, - - /// Information about related messages. - #[serde( - flatten, - skip_serializing_if = "Option::is_none", - deserialize_with = "crate::room::message::relation_serde::deserialize_relation" - )] - pub relates_to: Option>, -} - -impl EmoteEventContent { - /// A convenience constructor to create a plain text emote. - pub fn plain(body: impl Into) -> Self { - Self { - text: TextContentBlock::plain(body), - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } - - /// A convenience constructor to create an HTML emote. - pub fn html(body: impl Into, html_body: impl Into) -> Self { - Self { - text: TextContentBlock::html(body, html_body), - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } - - /// A convenience constructor to create an emote from Markdown. - /// - /// The content includes an HTML message if some Markdown formatting was detected, otherwise - /// only a plain text message is included. - #[cfg(feature = "markdown")] - pub fn markdown(body: impl AsRef + Into) -> Self { - Self { - text: TextContentBlock::markdown(body), - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } -} - -impl From for EmoteEventContent { - fn from(text: TextContentBlock) -> Self { - Self { - text, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } -} diff --git a/crates/ruma-events/src/encrypted.rs b/crates/ruma-events/src/encrypted.rs index 6fe9086c..3d10c12d 100644 --- a/crates/ruma-events/src/encrypted.rs +++ b/crates/ruma-events/src/encrypted.rs @@ -2,10 +2,13 @@ //! //! [MSC3956]: https://github.com/matrix-org/matrix-spec-proposals/pull/3956 +mod temp; + +use crate::{relation::Relation, Mentions}; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; - -use super::room::encrypted::{EncryptedEventScheme, Relation}; +use temp::EncryptedEventScheme; +pub use temp::{ToDeviceRoomEncryptedEvent, ToDeviceRoomEncryptedEventContent}; /// The payload for an extensible encrypted message. /// @@ -16,27 +19,41 @@ use super::room::encrypted::{EncryptedEventScheme, Relation}; /// [`message`]: super::message #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[ruma_event(type = "org.matrix.msc1767.encrypted", kind = MessageLike)] +#[ruma_event(type = "m.encrypted", kind = MessageLike)] pub struct EncryptedEventContent { /// The encrypted content. - #[serde(rename = "org.matrix.msc1767.encrypted")] + #[serde(rename = "m.encrypted")] pub encrypted: EncryptedContentBlock, /// Information about related events. - #[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")] - pub relates_to: Option, + #[serde( + flatten, + skip_serializing_if = "Vec::is_empty", + deserialize_with = "crate::relation::deserialize_relation" + )] + pub relations: Vec>, + + /// The [mentions] of this event. + /// + /// This should always be set to avoid triggering the legacy mention push rules. It is + /// recommended to use [`Self::set_mentions()`] to set this field, that will take care of + /// populating the fields correctly if this is a replacement. + /// + /// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions + #[serde(rename = "m.mentions", skip_serializing_if = "Option::is_none")] + pub mentions: Option, } impl EncryptedEventContent { /// Creates a new `EncryptedEventContent` with the given scheme and relation. - pub fn new(scheme: EncryptedEventScheme, relates_to: Option) -> Self { - Self { encrypted: scheme.into(), relates_to } + pub fn new(scheme: EncryptedEventScheme) -> Self { + Self { encrypted: scheme.into(), relations: vec![], mentions: None } } } impl From for EncryptedEventContent { fn from(scheme: EncryptedEventScheme) -> Self { - Self { encrypted: scheme.into(), relates_to: None } + Self { encrypted: scheme.into(), relations: vec![], mentions: None } } } diff --git a/crates/ruma-events/src/room/encrypted.rs b/crates/ruma-events/src/encrypted/temp.rs similarity index 64% rename from crates/ruma-events/src/room/encrypted.rs rename to crates/ruma-events/src/encrypted/temp.rs index 2bef8535..d37fa137 100644 --- a/crates/ruma-events/src/room/encrypted.rs +++ b/crates/ruma-events/src/encrypted/temp.rs @@ -2,45 +2,12 @@ //! //! [`m.room.encrypted`]: https://spec.matrix.org/latest/client-server-api/#mroomencrypted -use std::{borrow::Cow, collections::BTreeMap}; - +use std::collections::BTreeMap; use js_int::UInt; -use ruma_common::{serde::JsonObject, OwnedDeviceId, OwnedEventId}; +use ruma_common::OwnedDeviceId; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; -use super::message; -use crate::relation::{Annotation, CustomRelation, InReplyTo, Reference, RelationType, Thread}; - -mod relation_serde; - -/// The content of an `m.room.encrypted` event. -#[derive(Clone, Debug, Deserialize, Serialize, EventContent)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[ruma_event(type = "m.room.encrypted", kind = MessageLike)] -pub struct RoomEncryptedEventContent { - /// Algorithm-specific fields. - #[serde(flatten)] - pub scheme: EncryptedEventScheme, - - /// Information about related events. - #[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")] - pub relates_to: Option, -} - -impl RoomEncryptedEventContent { - /// Creates a new `RoomEncryptedEventContent` with the given scheme and relation. - pub fn new(scheme: EncryptedEventScheme, relates_to: Option) -> Self { - Self { scheme, relates_to } - } -} - -impl From for RoomEncryptedEventContent { - fn from(scheme: EncryptedEventScheme) -> Self { - Self { scheme, relates_to: None } - } -} - /// The to-device content of an `m.room.encrypted` event. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] @@ -78,105 +45,6 @@ pub enum EncryptedEventScheme { MegolmV1AesSha2(MegolmV1AesSha2Content), } -/// Relationship information about an encrypted event. -/// -/// Outside of the encrypted payload to support server aggregation. -#[derive(Clone, Debug)] -#[allow(clippy::manual_non_exhaustive)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub enum Relation { - /// An `m.in_reply_to` relation indicating that the event is a reply to another event. - Reply { - /// Information about another message being replied to. - in_reply_to: InReplyTo, - }, - - /// An event that replaces another event. - Replacement(Replacement), - - /// A reference to another event. - Reference(Reference), - - /// An annotation to an event. - Annotation(Annotation), - - /// An event that belongs to a thread. - Thread(Thread), - - #[doc(hidden)] - _Custom(CustomRelation), -} - -impl Relation { - /// The type of this `Relation`. - /// - /// Returns an `Option` because the `Reply` relation does not have a`rel_type` field. - pub fn rel_type(&self) -> Option { - match self { - Relation::Reply { .. } => None, - Relation::Replacement(_) => Some(RelationType::Replacement), - Relation::Reference(_) => Some(RelationType::Reference), - Relation::Annotation(_) => Some(RelationType::Annotation), - Relation::Thread(_) => Some(RelationType::Thread), - Relation::_Custom(c) => c.rel_type(), - } - } - - /// The associated data. - /// - /// The returned JSON object holds the contents of `m.relates_to`, including `rel_type` and - /// `event_id` if present, but not things like `m.new_content` for `m.replace` relations that - /// live next to `m.relates_to`. - /// - /// Prefer to use the public variants of `Relation` where possible; this method is meant to - /// be used for custom relations only. - pub fn data(&self) -> Cow<'_, JsonObject> { - if let Relation::_Custom(CustomRelation(data)) = self { - Cow::Borrowed(data) - } else { - Cow::Owned(self.serialize_data()) - } - } -} - -impl From> for Relation { - fn from(rel: message::Relation) -> Self { - match rel { - message::Relation::Reply { in_reply_to } => Self::Reply { in_reply_to }, - message::Relation::Replacement(re) => { - Self::Replacement(Replacement { event_id: re.event_id }) - } - message::Relation::Thread(t) => Self::Thread(Thread { - event_id: t.event_id, - in_reply_to: t.in_reply_to, - is_falling_back: t.is_falling_back, - }), - message::Relation::_Custom(c) => Self::_Custom(c), - } - } -} - -/// The event this relation belongs to [replaces another event]. -/// -/// In contrast to [`relation::Replacement`](crate::relation::Replacement), this -/// struct doesn't store the new content, since that is part of the encrypted content of an -/// `m.room.encrypted` events. -/// -/// [replaces another event]: https://spec.matrix.org/latest/client-server-api/#event-replacements -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct Replacement { - /// The ID of the event being replaced. - pub event_id: OwnedEventId, -} - -impl Replacement { - /// Creates a new `Replacement` with the given event ID. - pub fn new(event_id: OwnedEventId) -> Self { - Self { event_id } - } -} - /// The content of an `m.room.encrypted` event using the `m.olm.v1.curve25519-aes-sha2` algorithm. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] diff --git a/crates/ruma-events/src/enums.rs b/crates/ruma-events/src/enums.rs index 79db4609..ff332854 100644 --- a/crates/ruma-events/src/enums.rs +++ b/crates/ruma-events/src/enums.rs @@ -6,13 +6,9 @@ use ruma_macros::{event_enum, EventEnumFromEvent}; use serde::{de, Deserialize}; use serde_json::value::RawValue as RawJsonValue; -use super::room::encrypted; - event_enum! { /// Any global account data event. enum GlobalAccountData { - "m.direct" => super::direct, - "m.identity_server" => super::identity_server, "m.ignored_user_list" => super::ignored_user_list, "m.push_rules" => super::push_rules, "m.secret_storage.default_key" => super::secret_storage::default_key, @@ -21,7 +17,6 @@ event_enum! { /// Any room account data event. enum RoomAccountData { - "m.fully_read" => super::fully_read, "m.tag" => super::tag, } @@ -33,29 +28,6 @@ event_enum! { /// Any message-like event. enum MessageLike { - #[cfg(feature = "unstable-msc3927")] - #[ruma_enum(alias = "m.audio")] - "org.matrix.msc1767.audio" => super::audio, - // TODO: make these ephemeral events if possible - "m.call.answer" => super::call::answer, - "m.call.invite" => super::call::invite, - "m.call.hangup" => super::call::hangup, - "m.call.candidates" => super::call::candidates, - "m.call.negotiate" => super::call::negotiate, - "m.call.reject" => super::call::reject, - "m.call.select_answer" => super::call::select_answer, - #[cfg(feature = "unstable-msc3954")] - #[ruma_enum(alias = "m.emote")] - "org.matrix.msc1767.emote" => super::emote, - #[cfg(feature = "unstable-msc3956")] - #[ruma_enum(alias = "m.encrypted")] - "org.matrix.msc1767.encrypted" => super::encrypted, - #[cfg(feature = "unstable-msc3551")] - #[ruma_enum(alias = "m.file")] - "org.matrix.msc1767.file" => super::file, - #[cfg(feature = "unstable-msc3552")] - #[ruma_enum(alias = "m.image")] - "org.matrix.msc1767.image" => super::image, "m.key.verification.ready" => super::key::verification::ready, "m.key.verification.start" => super::key::verification::start, "m.key.verification.cancel" => super::key::verification::cancel, @@ -63,40 +35,16 @@ event_enum! { "m.key.verification.key" => super::key::verification::key, "m.key.verification.mac" => super::key::verification::mac, "m.key.verification.done" => super::key::verification::done, - #[cfg(feature = "unstable-msc3488")] - "m.location" => super::location, - #[cfg(feature = "unstable-msc1767")] - #[ruma_enum(alias = "m.message")] - "org.matrix.msc1767.message" => super::message, - #[cfg(feature = "unstable-msc3381")] - "m.poll.start" => super::poll::start, - #[cfg(feature = "unstable-msc3381")] - #[ruma_enum(ident = UnstablePollStart)] - "org.matrix.msc3381.poll.start" => super::poll::unstable_start, - #[cfg(feature = "unstable-msc3381")] - "m.poll.response" => super::poll::response, - #[cfg(feature = "unstable-msc3381")] - #[ruma_enum(ident = UnstablePollResponse)] - "org.matrix.msc3381.poll.response" => super::poll::unstable_response, - #[cfg(feature = "unstable-msc3381")] - "m.poll.end" => super::poll::end, - #[cfg(feature = "unstable-msc3381")] - #[ruma_enum(ident = UnstablePollEnd)] - "org.matrix.msc3381.poll.end" => super::poll::unstable_end, "m.reaction" => super::reaction, - "m.room.encrypted" => super::room::encrypted, - "m.room.message" => super::room::message, + // "m.thread.message" => super::thread::message, + // "m.thread.poll" => super::thread::poll, + // "m.thread.call" => super::thread::call, + "m.attachment.file" => super::attachment::file, + "m.attachment.embed" => super::attachment::embed, + "m.encrypted" => super::encrypted, "m.room.redaction" => super::room::redaction, + "m.message" => super::message, "m.sticker" => super::sticker, - #[cfg(feature = "unstable-msc3553")] - #[ruma_enum(alias = "m.video")] - "org.matrix.msc1767.video" => super::video, - #[cfg(feature = "unstable-msc3245")] - #[ruma_enum(alias = "m.voice")] - "org.matrix.msc3245.voice.v2" => super::voice, - #[cfg(feature = "unstable-msc4075")] - #[ruma_enum(alias = "m.call.notify")] - "org.matrix.msc4075.call.notify" => super::call::notify, } /// Any state event. @@ -141,7 +89,7 @@ event_enum! { "m.key.verification.key" => super::key::verification::key, "m.key.verification.mac" => super::key::verification::mac, "m.key.verification.done" => super::key::verification::done, - "m.room.encrypted" => super::room::encrypted, + "m.room.encrypted" => super::encrypted, "m.secret.request"=> super::secret::request, "m.secret.send" => super::secret::send, } @@ -297,77 +245,35 @@ impl<'de> Deserialize<'de> for AnySyncTimelineEvent { } impl AnyMessageLikeEventContent { - /// Get a copy of the event's `m.relates_to` field, if any. - /// - /// This is a helper function intended for encryption. There should not be a reason to access - /// `m.relates_to` without first destructuring an `AnyMessageLikeEventContent` otherwise. - pub fn relation(&self) -> Option { - use super::key::verification::{ - accept::KeyVerificationAcceptEventContent, cancel::KeyVerificationCancelEventContent, - done::KeyVerificationDoneEventContent, key::KeyVerificationKeyEventContent, - mac::KeyVerificationMacEventContent, ready::KeyVerificationReadyEventContent, - start::KeyVerificationStartEventContent, - }; - #[cfg(feature = "unstable-msc3381")] - use super::poll::{ - end::PollEndEventContent, response::PollResponseEventContent, - unstable_end::UnstablePollEndEventContent, - unstable_response::UnstablePollResponseEventContent, - }; + // / Get a copy of the event's `m.relates_to` field, if any. + // / + // / This is a helper function intended for encryption. There should not be a reason to access + // / `m.relates_to` without first destructuring an `AnyMessageLikeEventContent` otherwise. + // pub fn relations(&self) -> Option> { + // use super::key::verification::{ + // accept::KeyVerificationAcceptEventContent, cancel::KeyVerificationCancelEventContent, + // done::KeyVerificationDoneEventContent, key::KeyVerificationKeyEventContent, + // mac::KeyVerificationMacEventContent, ready::KeyVerificationReadyEventContent, + // start::KeyVerificationStartEventContent, + // }; - match self { - #[rustfmt::skip] - Self::KeyVerificationReady(KeyVerificationReadyEventContent { relates_to, .. }) - | Self::KeyVerificationStart(KeyVerificationStartEventContent { relates_to, .. }) - | Self::KeyVerificationCancel(KeyVerificationCancelEventContent { relates_to, .. }) - | Self::KeyVerificationAccept(KeyVerificationAcceptEventContent { relates_to, .. }) - | Self::KeyVerificationKey(KeyVerificationKeyEventContent { relates_to, .. }) - | Self::KeyVerificationMac(KeyVerificationMacEventContent { relates_to, .. }) - | Self::KeyVerificationDone(KeyVerificationDoneEventContent { relates_to, .. }) => { - Some(encrypted::Relation::Reference(relates_to.clone())) - }, - Self::Reaction(ev) => Some(encrypted::Relation::Annotation(ev.relates_to.clone())), - Self::RoomEncrypted(ev) => ev.relates_to.clone(), - Self::RoomMessage(ev) => ev.relates_to.clone().map(Into::into), - #[cfg(feature = "unstable-msc1767")] - Self::Message(ev) => ev.relates_to.clone().map(Into::into), - #[cfg(feature = "unstable-msc3954")] - Self::Emote(ev) => ev.relates_to.clone().map(Into::into), - #[cfg(feature = "unstable-msc3956")] - Self::Encrypted(ev) => ev.relates_to.clone(), - #[cfg(feature = "unstable-msc3245")] - Self::Voice(ev) => ev.relates_to.clone().map(Into::into), - #[cfg(feature = "unstable-msc3927")] - Self::Audio(ev) => ev.relates_to.clone().map(Into::into), - #[cfg(feature = "unstable-msc3488")] - Self::Location(ev) => ev.relates_to.clone().map(Into::into), - #[cfg(feature = "unstable-msc3551")] - Self::File(ev) => ev.relates_to.clone().map(Into::into), - #[cfg(feature = "unstable-msc3552")] - Self::Image(ev) => ev.relates_to.clone().map(Into::into), - #[cfg(feature = "unstable-msc3553")] - Self::Video(ev) => ev.relates_to.clone().map(Into::into), - #[cfg(feature = "unstable-msc3381")] - Self::PollResponse(PollResponseEventContent { relates_to, .. }) - | Self::UnstablePollResponse(UnstablePollResponseEventContent { relates_to, .. }) - | Self::PollEnd(PollEndEventContent { relates_to, .. }) - | Self::UnstablePollEnd(UnstablePollEndEventContent { relates_to, .. }) => { - Some(encrypted::Relation::Reference(relates_to.clone())) - } - #[cfg(feature = "unstable-msc3381")] - Self::PollStart(_) | Self::UnstablePollStart(_) => None, - #[cfg(feature = "unstable-msc4075")] - Self::CallNotify(_) => None, - Self::CallNegotiate(_) - | Self::CallReject(_) - | Self::CallSelectAnswer(_) - | Self::CallAnswer(_) - | Self::CallInvite(_) - | Self::CallHangup(_) - | Self::CallCandidates(_) - | Self::RoomRedaction(_) - | Self::Sticker(_) - | Self::_Custom { .. } => None, - } - } + // match self { + // #[rustfmt::skip] + // Self::KeyVerificationReady(KeyVerificationReadyEventContent { relates_to, .. }) + // | Self::KeyVerificationStart(KeyVerificationStartEventContent { relates_to, .. }) + // | Self::KeyVerificationCancel(KeyVerificationCancelEventContent { relates_to, .. }) + // | Self::KeyVerificationAccept(KeyVerificationAcceptEventContent { relates_to, .. }) + // | Self::KeyVerificationKey(KeyVerificationKeyEventContent { relates_to, .. }) + // | Self::KeyVerificationMac(KeyVerificationMacEventContent { relates_to, .. }) + // | Self::KeyVerificationDone(KeyVerificationDoneEventContent { relates_to, .. }) => { + // Some(vec![encrypted::Relation::Reference(relates_to.clone())]) + // }, + // Self::Reaction(ev) => Some(encrypted::Relation::Annotation(ev.relates_to.clone())), + // Self::Encrypted(ev) => Some(ev.relations.clone()), + // Self::Message(ev) => Some(ev.relations.clone().map(Into::into)), + // Self::RoomRedaction(_) + // | Self::Sticker(_) + // | Self::_Custom { .. } => None, + // } + // } } diff --git a/crates/ruma-events/src/file.rs b/crates/ruma-events/src/file.rs deleted file mode 100644 index e97461f3..00000000 --- a/crates/ruma-events/src/file.rs +++ /dev/null @@ -1,275 +0,0 @@ -//! Types for extensible file message events ([MSC3551]). -//! -//! [MSC3551]: https://github.com/matrix-org/matrix-spec-proposals/pull/3551 - -use std::collections::BTreeMap; - -use js_int::UInt; -use ruma_common::{serde::Base64, OwnedMxcUri}; -use ruma_macros::EventContent; -use serde::{Deserialize, Serialize}; - -use super::{ - message::TextContentBlock, - room::{message::Relation, EncryptedFile, JsonWebKey}, -}; - -/// The payload for an extensible file message. -/// -/// This is the new primary type introduced in [MSC3551] and should only be sent in rooms with a -/// version that supports it. See the documentation of the [`message`] module for more information. -/// -/// [MSC3551]: https://github.com/matrix-org/matrix-spec-proposals/pull/3551 -/// [`message`]: super::message -#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[ruma_event(type = "org.matrix.msc1767.file", kind = MessageLike, without_relation)] -pub struct FileEventContent { - /// The text representation of the message. - #[serde(rename = "org.matrix.msc1767.text")] - pub text: TextContentBlock, - - /// The file content of the message. - #[serde(rename = "org.matrix.msc1767.file")] - pub file: FileContentBlock, - - /// The caption of the message, if any. - #[serde(rename = "org.matrix.msc1767.caption", skip_serializing_if = "Option::is_none")] - pub caption: Option, - - /// Whether this message is automated. - #[cfg(feature = "unstable-msc3955")] - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - rename = "org.matrix.msc1767.automated" - )] - pub automated: bool, - - /// Information about related messages. - #[serde( - flatten, - skip_serializing_if = "Option::is_none", - deserialize_with = "crate::room::message::relation_serde::deserialize_relation" - )] - pub relates_to: Option>, -} - -impl FileEventContent { - /// Creates a new non-encrypted `FileEventContent` with the given fallback representation, url - /// and file info. - pub fn plain(text: TextContentBlock, url: OwnedMxcUri, name: String) -> Self { - Self { - text, - file: FileContentBlock::plain(url, name), - caption: None, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } - - /// Creates a new non-encrypted `FileEventContent` with the given plain text fallback - /// representation, url and name. - pub fn plain_with_plain_text( - plain_text: impl Into, - url: OwnedMxcUri, - name: String, - ) -> Self { - Self { - text: TextContentBlock::plain(plain_text), - file: FileContentBlock::plain(url, name), - caption: None, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } - - /// Creates a new encrypted `FileEventContent` with the given fallback representation, url, - /// name and encryption info. - pub fn encrypted( - text: TextContentBlock, - url: OwnedMxcUri, - name: String, - encryption_info: EncryptedContent, - ) -> Self { - Self { - text, - file: FileContentBlock::encrypted(url, name, encryption_info), - caption: None, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } - - /// Creates a new encrypted `FileEventContent` with the given plain text fallback - /// representation, url, name and encryption info. - pub fn encrypted_with_plain_text( - plain_text: impl Into, - url: OwnedMxcUri, - name: String, - encryption_info: EncryptedContent, - ) -> Self { - Self { - text: TextContentBlock::plain(plain_text), - file: FileContentBlock::encrypted(url, name, encryption_info), - caption: None, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } -} - -/// A block for file content. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct FileContentBlock { - /// The URL to the file. - pub url: OwnedMxcUri, - - /// The original filename of the uploaded file. - pub name: String, - - /// The mimetype of the file, e.g. "application/msword". - #[serde(skip_serializing_if = "Option::is_none")] - pub mimetype: Option, - - /// The size of the file in bytes. - #[serde(skip_serializing_if = "Option::is_none")] - pub size: Option, - - /// Information on the encrypted file. - /// - /// Required if the file is encrypted. - #[serde(flatten, skip_serializing_if = "Option::is_none")] - pub encryption_info: Option>, -} - -impl FileContentBlock { - /// Creates a new non-encrypted `FileContentBlock` with the given url and name. - pub fn plain(url: OwnedMxcUri, name: String) -> Self { - Self { url, name, mimetype: None, size: None, encryption_info: None } - } - - /// Creates a new encrypted `FileContentBlock` with the given url, name and encryption info. - pub fn encrypted(url: OwnedMxcUri, name: String, encryption_info: EncryptedContent) -> Self { - Self { - url, - name, - mimetype: None, - size: None, - encryption_info: Some(Box::new(encryption_info)), - } - } - - /// Whether the file is encrypted. - pub fn is_encrypted(&self) -> bool { - self.encryption_info.is_some() - } -} - -/// The encryption info of a file sent to a room with end-to-end encryption enabled. -/// -/// To create an instance of this type, first create a `EncryptedContentInit` and convert it via -/// `EncryptedContent::from` / `.into()`. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct EncryptedContent { - /// A [JSON Web Key](https://tools.ietf.org/html/rfc7517#appendix-A.3) object. - pub key: JsonWebKey, - - /// The 128-bit unique counter block used by AES-CTR, encoded as unpadded base64. - pub iv: Base64, - - /// 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: BTreeMap, - - /// Version of the encrypted attachments protocol. - /// - /// Must be `v2`. - pub v: String, -} - -/// Initial set of fields of `EncryptedContent`. -/// -/// This struct will not be updated even if additional fields are added to `EncryptedContent` in a -/// new (non-breaking) release of the Matrix specification. -#[derive(Debug)] -#[allow(clippy::exhaustive_structs)] -pub struct EncryptedContentInit { - /// A [JSON Web Key](https://tools.ietf.org/html/rfc7517#appendix-A.3) object. - pub key: JsonWebKey, - - /// The 128-bit unique counter block used by AES-CTR, encoded as unpadded base64. - pub iv: Base64, - - /// 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: BTreeMap, - - /// Version of the encrypted attachments protocol. - /// - /// Must be `v2`. - pub v: String, -} - -impl From for EncryptedContent { - fn from(init: EncryptedContentInit) -> Self { - let EncryptedContentInit { key, iv, hashes, v } = init; - Self { key, iv, hashes, v } - } -} - -impl From<&EncryptedFile> for EncryptedContent { - fn from(encrypted: &EncryptedFile) -> Self { - let EncryptedFile { key, iv, hashes, v, .. } = encrypted; - Self { key: key.to_owned(), iv: iv.to_owned(), hashes: hashes.to_owned(), v: v.to_owned() } - } -} - -/// A block for caption content. -/// -/// A caption is usually a text message that should be displayed next to some media content. -/// -/// To construct a `CaptionContentBlock` with a custom [`TextContentBlock`], convert it with -/// `CaptionContentBlock::from()` / `.into()`. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct CaptionContentBlock { - /// The text message of the caption. - #[serde(rename = "org.matrix.msc1767.text")] - pub text: TextContentBlock, -} - -impl CaptionContentBlock { - /// A convenience constructor to create a plain text caption content block. - pub fn plain(body: impl Into) -> Self { - Self { text: TextContentBlock::plain(body) } - } - - /// A convenience constructor to create an HTML caption content block. - pub fn html(body: impl Into, html_body: impl Into) -> Self { - Self { text: TextContentBlock::html(body, html_body) } - } - - /// A convenience constructor to create a caption content block from Markdown. - /// - /// The content includes an HTML message if some Markdown formatting was detected, otherwise - /// only a plain text message is included. - #[cfg(feature = "markdown")] - pub fn markdown(body: impl AsRef + Into) -> Self { - Self { text: TextContentBlock::markdown(body) } - } -} - -impl From for CaptionContentBlock { - fn from(text: TextContentBlock) -> Self { - Self { text } - } -} diff --git a/crates/ruma-events/src/image.rs b/crates/ruma-events/src/image.rs deleted file mode 100644 index 97138ba2..00000000 --- a/crates/ruma-events/src/image.rs +++ /dev/null @@ -1,295 +0,0 @@ -//! Types for extensible image message events ([MSC3552]). -//! -//! [MSC3552]: https://github.com/matrix-org/matrix-spec-proposals/pull/3552 - -use std::ops::Deref; - -use js_int::UInt; -use ruma_common::OwnedMxcUri; -use ruma_macros::EventContent; -use serde::{Deserialize, Serialize}; - -use super::{ - file::{CaptionContentBlock, EncryptedContent, FileContentBlock}, - message::TextContentBlock, - room::message::Relation, -}; - -/// The payload for an extensible image message. -/// -/// This is the new primary type introduced in [MSC3552] and should only be sent in rooms with a -/// version that supports it. This type replaces both the `m.room.message` type with `msgtype: -/// "m.image"` and the `m.sticker` type. To replace the latter, `sticker` must be set to `true` in -/// `image_details`. See the documentation of the [`message`] module for more information. -/// -/// [MSC3552]: https://github.com/matrix-org/matrix-spec-proposals/pull/3552 -/// [`message`]: super::message -#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[ruma_event(type = "org.matrix.msc1767.image", kind = MessageLike, without_relation)] -pub struct ImageEventContent { - /// The text representation of the message. - #[serde(rename = "org.matrix.msc1767.text")] - pub text: TextContentBlock, - - /// The file content of the message. - #[serde(rename = "org.matrix.msc1767.file")] - pub file: FileContentBlock, - - /// The image details of the message, if any. - #[serde(rename = "org.matrix.msc1767.image_details", skip_serializing_if = "Option::is_none")] - pub image_details: Option, - - /// The thumbnails of the message, if any. - /// - /// This is optional and defaults to an empty array. - #[serde( - rename = "org.matrix.msc1767.thumbnail", - default, - skip_serializing_if = "ThumbnailContentBlock::is_empty" - )] - pub thumbnail: ThumbnailContentBlock, - - /// The caption of the message, if any. - #[serde(rename = "org.matrix.msc1767.caption", skip_serializing_if = "Option::is_none")] - pub caption: Option, - - /// The alternative text of the image, for accessibility considerations, if any. - #[serde(rename = "org.matrix.msc1767.alt_text", skip_serializing_if = "Option::is_none")] - pub alt_text: Option, - - /// Whether this message is automated. - #[cfg(feature = "unstable-msc3955")] - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - rename = "org.matrix.msc1767.automated" - )] - pub automated: bool, - - /// Information about related messages. - #[serde( - flatten, - skip_serializing_if = "Option::is_none", - deserialize_with = "crate::room::message::relation_serde::deserialize_relation" - )] - pub relates_to: Option>, -} - -impl ImageEventContent { - /// Creates a new `ImageEventContent` with the given fallback representation and - /// file. - pub fn new(text: TextContentBlock, file: FileContentBlock) -> Self { - Self { - text, - file, - image_details: None, - thumbnail: Default::default(), - caption: None, - alt_text: None, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } - - /// Creates a new `ImageEventContent` with the given plain text fallback representation and - /// file. - pub fn with_plain_text(plain_text: impl Into, file: FileContentBlock) -> Self { - Self { - text: TextContentBlock::plain(plain_text), - file, - image_details: None, - thumbnail: Default::default(), - caption: None, - alt_text: None, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } -} - -/// A block for details of image content. -#[derive(Default, Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct ImageDetailsContentBlock { - /// The height of the image in pixels. - pub height: UInt, - - /// The width of the image in pixels. - pub width: UInt, - - /// Whether the image should be presented as sticker. - #[serde( - rename = "org.matrix.msc1767.sticker", - default, - skip_serializing_if = "ruma_common::serde::is_default" - )] - pub sticker: bool, -} - -impl ImageDetailsContentBlock { - /// Creates a new `ImageDetailsContentBlock` with the given width and height. - pub fn new(width: UInt, height: UInt) -> Self { - Self { height, width, sticker: Default::default() } - } -} - -/// A block for thumbnail content. -/// -/// This is an array of [`Thumbnail`]. -/// -/// To construct a `ThumbnailContentBlock` convert a `Vec` with -/// `ThumbnailContentBlock::from()` / `.into()`. -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[allow(clippy::exhaustive_structs)] -pub struct ThumbnailContentBlock(Vec); - -impl ThumbnailContentBlock { - /// Whether this content block is empty. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -impl From> for ThumbnailContentBlock { - fn from(thumbnails: Vec) -> Self { - Self(thumbnails) - } -} - -impl FromIterator for ThumbnailContentBlock { - fn from_iter>(iter: T) -> Self { - Self(iter.into_iter().collect()) - } -} - -impl Deref for ThumbnailContentBlock { - type Target = [Thumbnail]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// Thumbnail content. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct Thumbnail { - /// The file info of the thumbnail. - #[serde(rename = "org.matrix.msc1767.file")] - pub file: ThumbnailFileContentBlock, - - /// The image info of the thumbnail. - #[serde(rename = "org.matrix.msc1767.image_details")] - pub image_details: ThumbnailImageDetailsContentBlock, -} - -impl Thumbnail { - /// Creates a `Thumbnail` with the given file and image details. - pub fn new( - file: ThumbnailFileContentBlock, - image_details: ThumbnailImageDetailsContentBlock, - ) -> Self { - Self { file, image_details } - } -} - -/// A block for thumbnail file content. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct ThumbnailFileContentBlock { - /// The URL to the thumbnail. - pub url: OwnedMxcUri, - - /// The mimetype of the file, e.g. "image/png". - pub mimetype: String, - - /// The original filename of the uploaded file. - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - - /// The size of the file in bytes. - #[serde(skip_serializing_if = "Option::is_none")] - pub size: Option, - - /// Information on the encrypted thumbnail. - /// - /// Required if the thumbnail is encrypted. - #[serde(flatten, skip_serializing_if = "Option::is_none")] - pub encryption_info: Option>, -} - -impl ThumbnailFileContentBlock { - /// Creates a new non-encrypted `ThumbnailFileContentBlock` with the given url and mimetype. - pub fn plain(url: OwnedMxcUri, mimetype: String) -> Self { - Self { url, mimetype, name: None, size: None, encryption_info: None } - } - - /// Creates a new encrypted `ThumbnailFileContentBlock` with the given url, mimetype and - /// encryption info. - pub fn encrypted( - url: OwnedMxcUri, - mimetype: String, - encryption_info: EncryptedContent, - ) -> Self { - Self { - url, - mimetype, - name: None, - size: None, - encryption_info: Some(Box::new(encryption_info)), - } - } - - /// Whether the thumbnail file is encrypted. - pub fn is_encrypted(&self) -> bool { - self.encryption_info.is_some() - } -} - -/// A block for details of thumbnail image content. -#[derive(Default, Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct ThumbnailImageDetailsContentBlock { - /// The height of the image in pixels. - pub height: UInt, - - /// The width of the image in pixels. - pub width: UInt, -} - -impl ThumbnailImageDetailsContentBlock { - /// Creates a new `ThumbnailImageDetailsContentBlock` with the given width and height. - pub fn new(width: UInt, height: UInt) -> Self { - Self { height, width } - } -} - -/// A block for alternative text content. -/// -/// The content should only contain plain text messages. Non-plain text messages should be ignored. -/// -/// To construct an `AltTextContentBlock` with a custom [`TextContentBlock`], convert it with -/// `AltTextContentBlock::from()` / `.into()`. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct AltTextContentBlock { - /// The alternative text. - #[serde(rename = "org.matrix.msc1767.text")] - pub text: TextContentBlock, -} - -impl AltTextContentBlock { - /// A convenience constructor to create a plain text alternative text content block. - pub fn plain(body: impl Into) -> Self { - Self { text: TextContentBlock::plain(body) } - } -} - -impl From for AltTextContentBlock { - fn from(text: TextContentBlock) -> Self { - Self { text } - } -} diff --git a/crates/ruma-events/src/lib.rs b/crates/ruma-events/src/lib.rs index 355e34cd..91bb1834 100644 --- a/crates/ruma-events/src/lib.rs +++ b/crates/ruma-events/src/lib.rs @@ -137,33 +137,17 @@ pub mod macros { pub use ruma_macros::{Event, EventContent}; } -#[cfg(feature = "unstable-msc3927")] -pub mod audio; -pub mod call; -pub mod direct; +pub mod attachment; pub mod dummy; -#[cfg(feature = "unstable-msc3954")] -pub mod emote; -#[cfg(feature = "unstable-msc3956")] pub mod encrypted; -#[cfg(feature = "unstable-msc3551")] -pub mod file; pub mod forwarded_room_key; -pub mod fully_read; -pub mod identity_server; pub mod ignored_user_list; -#[cfg(feature = "unstable-msc3552")] -pub mod image; pub mod key; -#[cfg(feature = "unstable-msc3488")] -pub mod location; -#[cfg(feature = "unstable-msc1767")] pub mod message; +pub mod mixins; #[cfg(feature = "unstable-pdu")] pub mod pdu; pub mod policy; -#[cfg(feature = "unstable-msc3381")] -pub mod poll; pub mod presence; pub mod push_rules; pub mod reaction; @@ -176,12 +160,49 @@ pub mod secret; pub mod secret_storage; pub mod space; pub mod sticker; -pub mod tag; +pub mod tag; // may be removed pub mod typing; -#[cfg(feature = "unstable-msc3553")] -pub mod video; -#[cfg(feature = "unstable-msc3245")] -pub mod voice; + +// extensible events removed in favor of attachment relations +// mixins are cool, but shouldn't be the base: tagged enums are better +// #[cfg(feature = "unstable-msc3927")] +// pub mod audio; +// NOTE: perhaps emote could be revived? +// #[cfg(feature = "unstable-msc3954")] +// pub mod emote; +// #[cfg(feature = "unstable-msc3956")] +// pub mod encrypted; +// #[cfg(feature = "unstable-msc3552")] +// pub mod image; +// NOTE: perhaps location could be revived? +// #[cfg(feature = "unstable-msc3488")] +// pub mod location; +// #[cfg(feature = "unstable-msc3551")] +// pub mod file; +// #[cfg(feature = "unstable-msc3553")] +// pub mod video; +// #[cfg(feature = "unstable-msc3245")] +// pub mod voice; + +// removed in favor of poll threads +// #[cfg(feature = "unstable-msc3381")] +// pub mod poll; + +// calls *temporarily* removed until ephemeral events are implemented +// and they will use call threads anyway +// pub mod call; + +// dummy events... maybe keep for forward extremities +// pub mod dummy; + +// removed in favor of private read receipts +// pub mod fully_read; + +// removed in favor of m.room.purpose +// pub mod direct; + +// these never should have existed +// pub mod identity_server; pub use self::{ content::*, diff --git a/crates/ruma-events/src/location.rs b/crates/ruma-events/src/location.rs deleted file mode 100644 index 57d91b6b..00000000 --- a/crates/ruma-events/src/location.rs +++ /dev/null @@ -1,196 +0,0 @@ -//! Types for extensible location message events ([MSC3488]). -//! -//! [MSC3488]: https://github.com/matrix-org/matrix-spec-proposals/pull/3488 - -use js_int::UInt; -use ruma_macros::{EventContent, StringEnum}; -use serde::{Deserialize, Serialize}; - -mod zoomlevel_serde; - -use ruma_common::MilliSecondsSinceUnixEpoch; - -use super::{message::TextContentBlock, room::message::Relation}; -use crate::PrivOwnedStr; - -/// The payload for an extensible location message. -/// -/// This is the new primary type introduced in [MSC3488] and should only be sent in rooms with a -/// version that supports it. See the documentation of the [`message`] module for more information. -/// -/// [MSC3488]: https://github.com/matrix-org/matrix-spec-proposals/pull/3488 -/// [`message`]: super::message -#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[ruma_event(type = "m.location", kind = MessageLike, without_relation)] -pub struct LocationEventContent { - /// The text representation of the message. - #[serde(rename = "org.matrix.msc1767.text")] - pub text: TextContentBlock, - - /// The location info of the message. - #[serde(rename = "m.location")] - pub location: LocationContent, - - /// The asset this message refers to. - #[serde(default, rename = "m.asset", skip_serializing_if = "ruma_common::serde::is_default")] - pub asset: AssetContent, - - /// The timestamp this message refers to. - #[serde(rename = "m.ts", skip_serializing_if = "Option::is_none")] - pub ts: Option, - - /// Whether this message is automated. - #[cfg(feature = "unstable-msc3955")] - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - rename = "org.matrix.msc1767.automated" - )] - pub automated: bool, - - /// Information about related messages. - #[serde( - flatten, - skip_serializing_if = "Option::is_none", - deserialize_with = "crate::room::message::relation_serde::deserialize_relation" - )] - pub relates_to: Option>, -} - -impl LocationEventContent { - /// Creates a new `LocationEventContent` with the given fallback representation and location. - pub fn new(text: TextContentBlock, location: LocationContent) -> Self { - Self { - text, - location, - asset: Default::default(), - ts: None, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } - - /// Creates a new `LocationEventContent` with the given plain text fallback representation and - /// location. - pub fn with_plain_text(plain_text: impl Into, location: LocationContent) -> Self { - Self { - text: TextContentBlock::plain(plain_text), - location, - asset: Default::default(), - ts: None, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } -} - -/// Location content. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct LocationContent { - /// A `geo:` URI representing the location. - /// - /// See [RFC 5870](https://datatracker.ietf.org/doc/html/rfc5870) for more details. - pub uri: String, - - /// The description of the location. - /// - /// It should be used to label the location on a map. - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - - /// A zoom level to specify the displayed area size. - #[serde(skip_serializing_if = "Option::is_none")] - pub zoom_level: Option, -} - -impl LocationContent { - /// Creates a new `LocationContent` with the given geo URI. - pub fn new(uri: String) -> Self { - Self { uri, description: None, zoom_level: None } - } -} - -/// An error encountered when trying to convert to a `ZoomLevel`. -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)] -#[non_exhaustive] -pub enum ZoomLevelError { - /// The value is higher than [`ZoomLevel::MAX`]. - #[error("value too high")] - TooHigh, -} - -/// A zoom level. -/// -/// This is an integer between 0 and 20 as defined in the [OpenStreetMap Wiki]. -/// -/// [OpenStreetMap Wiki]: https://wiki.openstreetmap.org/wiki/Zoom_levels -#[derive(Clone, Debug, Serialize)] -pub struct ZoomLevel(UInt); - -impl ZoomLevel { - /// The smallest value of a `ZoomLevel`, 0. - pub const MIN: u8 = 0; - - /// The largest value of a `ZoomLevel`, 20. - pub const MAX: u8 = 20; - - /// Creates a new `ZoomLevel` with the given value. - pub fn new(value: u8) -> Option { - if value > Self::MAX { - None - } else { - Some(Self(value.into())) - } - } - - /// The value of this `ZoomLevel`. - pub fn get(&self) -> UInt { - self.0 - } -} - -impl TryFrom for ZoomLevel { - type Error = ZoomLevelError; - - fn try_from(value: u8) -> Result { - Self::new(value).ok_or(ZoomLevelError::TooHigh) - } -} - -/// Asset content. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct AssetContent { - /// The type of asset being referred to. - #[serde(rename = "type")] - pub type_: AssetType, -} - -impl AssetContent { - /// Creates a new default `AssetContent`. - pub fn new() -> Self { - Self::default() - } -} - -/// The type of an asset. -#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] -#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, StringEnum)] -#[ruma_enum(rename_all = "m.snake_case")] -#[non_exhaustive] -pub enum AssetType { - /// The asset is the sender of the event. - #[default] - #[ruma_enum(rename = "m.self")] - Self_, - - /// The asset is a location pinned by the sender. - Pin, - - #[doc(hidden)] - _Custom(PrivOwnedStr), -} diff --git a/crates/ruma-events/src/location/zoomlevel_serde.rs b/crates/ruma-events/src/location/zoomlevel_serde.rs deleted file mode 100644 index 32f402da..00000000 --- a/crates/ruma-events/src/location/zoomlevel_serde.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! `Serialize` and `Deserialize` implementations for extensible events (MSC1767). - -use js_int::UInt; -use serde::{de, Deserialize}; - -use super::{ZoomLevel, ZoomLevelError}; - -impl<'de> Deserialize<'de> for ZoomLevel { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let uint = UInt::deserialize(deserializer)?; - if uint > Self::MAX.into() { - Err(de::Error::custom(ZoomLevelError::TooHigh)) - } else { - Ok(Self(uint)) - } - } -} diff --git a/crates/ruma-events/src/message.rs b/crates/ruma-events/src/message.rs index de697515..a8edd227 100644 --- a/crates/ruma-events/src/message.rs +++ b/crates/ruma-events/src/message.rs @@ -1,92 +1,13 @@ -//! Types for extensible text message events ([MSC1767]). -//! -//! # Extensible events -//! -//! [MSC1767] defines a new structure for events that is made of two parts: a type and zero or more -//! reusable content blocks. -//! -//! This allows to construct new event types from a list of known content blocks that allows in turn -//! clients to be able to render unknown event types by using the known content blocks as a -//! fallback. When a new type is defined, all the content blocks it can or must contain are defined -//! too. -//! -//! There are also some content blocks called "mixins" that can apply to any event when they are -//! defined. -//! -//! # MSCs -//! -//! This is a list of MSCs defining the extensible events and deprecating the corresponding legacy -//! types. Note that "primary type" means the `type` field at the root of the event and "message -//! type" means the `msgtype` field in the content of the `m.room.message` primary type. -//! -//! - [MSC1767]: Text messages, where the `m.message` primary type replaces the `m.text` message -//! type. -//! - [MSC3954]: Emotes, where the `m.emote` primary type replaces the `m.emote` message type. -//! - [MSC3955]: Automated events, where the `m.automated` mixin replaces the `m.notice` message -//! type. -//! - [MSC3956]: Encrypted events, where the `m.encrypted` primary type replaces the -//! `m.room.encrypted` primary type. -//! - [MSC3551]: Files, where the `m.file` primary type replaces the `m.file` message type. -//! - [MSC3552]: Images and Stickers, where the `m.image` primary type replaces the `m.image` -//! message type and the `m.sticker` primary type. -//! - [MSC3553]: Videos, where the `m.video` primary type replaces the `m.video` message type. -//! - [MSC3927]: Audio, where the `m.audio` primary type replaces the `m.audio` message type. -//! - [MSC3488]: Location, where the `m.location` primary type replaces the `m.location` message -//! type. -//! -//! There are also the following MSCs that introduce new features with extensible events: -//! -//! - [MSC3245]: Voice Messages. -//! - [MSC3246]: Audio Waveform. -//! - [MSC3381]: Polls. -//! -//! # How to use them in Matrix -//! -//! The extensible events types are meant to be used separately than the legacy types. As such, -//! their use is reserved for room versions that support it. -//! -//! Currently no stable room version supports extensible events so they can only be sent with -//! unstable room versions that support them. -//! -//! An exception is made for some new extensible events types that don't have a legacy type. They -//! can be used with stable room versions without support for extensible types, but they might be -//! ignored by clients that have no support for extensible events. The types that support this must -//! advertise it in their MSC. -//! -//! Note that if a room version supports extensible events, it doesn't support the legacy types -//! anymore and those should be ignored. There is not yet a definition of the deprecated legacy -//! types in extensible events rooms. -//! -//! # How to use them in Ruma -//! -//! First, you can enable the `unstable-extensible-events` feature from the `ruma` crate, that -//! will enable all the MSCs for the extensible events that correspond to the legacy types. It -//! is also possible to enable only the MSCs you want with the `unstable-mscXXXX` features (where -//! `XXXX` is the number of the MSC). When enabling an MSC, all MSC dependencies are enabled at the -//! same time to avoid issues. -//! -//! Currently the extensible events use the unstable prefixes as defined in the corresponding MSCs. -//! -//! [MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767 -//! [MSC3954]: https://github.com/matrix-org/matrix-spec-proposals/pull/3954 -//! [MSC3955]: https://github.com/matrix-org/matrix-spec-proposals/pull/3955 -//! [MSC3956]: https://github.com/matrix-org/matrix-spec-proposals/pull/3956 -//! [MSC3551]: https://github.com/matrix-org/matrix-spec-proposals/pull/3551 -//! [MSC3552]: https://github.com/matrix-org/matrix-spec-proposals/pull/3552 -//! [MSC3553]: https://github.com/matrix-org/matrix-spec-proposals/pull/3553 -//! [MSC3927]: https://github.com/matrix-org/matrix-spec-proposals/pull/3927 -//! [MSC3488]: https://github.com/matrix-org/matrix-spec-proposals/pull/3488 -//! [MSC3245]: https://github.com/matrix-org/matrix-spec-proposals/pull/3245 -//! [MSC3246]: https://github.com/matrix-org/matrix-spec-proposals/pull/3246 -//! [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381 +//! Types for message events (m.message) + use std::ops::Deref; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; -use super::room::message::Relation; - -pub(super) mod historical_serde; +pub use crate::relation::Relation; +use crate::Mentions; +use crate::mixins::MessageHints; /// The payload for an extensible text message. /// @@ -106,22 +27,27 @@ pub struct MessageEventContent { #[serde(rename = "org.matrix.msc1767.text")] pub text: TextContentBlock, - /// Whether this message is automated. - #[cfg(feature = "unstable-msc3955")] - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - rename = "org.matrix.msc1767.automated" - )] - pub automated: bool, - - /// Information about related messages. + /// Information about related events. #[serde( flatten, - skip_serializing_if = "Option::is_none", - deserialize_with = "crate::room::message::relation_serde::deserialize_relation" + skip_serializing_if = "Vec::is_empty", + deserialize_with = "crate::relation::deserialize_relation" )] - pub relates_to: Option>, + pub relations: Vec>, + + /// The [mentions] of this event. + /// + /// This should always be set to avoid triggering the legacy mention push rules. It is + /// recommended to use [`Self::set_mentions()`] to set this field, that will take care of + /// populating the fields correctly if this is a replacement. + /// + /// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions + #[serde(rename = "m.mentions", skip_serializing_if = "Option::is_none")] + pub mentions: Option, + + /// UI hints for this event. + #[serde(rename = "m.hints", skip_serializing_if = "MessageHints::is_empty")] + pub hints: MessageHints, } impl MessageEventContent { @@ -129,9 +55,9 @@ impl MessageEventContent { pub fn plain(body: impl Into) -> Self { Self { text: TextContentBlock::plain(body), - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, + relations: vec![], + mentions: None, + hints: MessageHints::new(), } } @@ -139,9 +65,9 @@ impl MessageEventContent { pub fn html(body: impl Into, html_body: impl Into) -> Self { Self { text: TextContentBlock::html(body, html_body), - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, + relations: vec![], + mentions: None, + hints: MessageHints::new(), } } @@ -153,9 +79,9 @@ impl MessageEventContent { pub fn markdown(body: impl AsRef + Into) -> Self { Self { text: TextContentBlock::markdown(body), - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, + relations: vec![], + mentions: None, + hints: MessageHints::new(), } } } @@ -164,9 +90,9 @@ impl From for MessageEventContent { fn from(text: TextContentBlock) -> Self { Self { text, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, + relations: vec![], + mentions: None, + hints: MessageHints::new(), } } } @@ -176,7 +102,7 @@ impl From for MessageEventContent { /// This is an array of [`TextRepresentation`]. /// /// To construct a `TextContentBlock` with custom MIME types, construct a `Vec` -/// first and use its `::from()` / `.into()` implementation. +/// first and use its `::from()` / `.into()` implemention. #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct TextContentBlock(Vec); diff --git a/crates/ruma-events/src/message/historical_serde.rs b/crates/ruma-events/src/message/historical_serde.rs deleted file mode 100644 index f5812c84..00000000 --- a/crates/ruma-events/src/message/historical_serde.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! Serde for old versions of MSC1767 still used in some types ([spec]). -//! -//! [spec]: https://github.com/matrix-org/matrix-spec-proposals/blob/d6046d8402e7a3c7a4fcbc9da16ea9bad5968992/proposals/1767-extensible-events.md - -use serde::{Deserialize, Serialize}; - -use super::{TextContentBlock, TextRepresentation}; - -/// Historical `m.message` text content block from MSC1767. -#[derive(Clone, Default, Serialize, Deserialize)] -#[serde(try_from = "MessageContentBlockSerDeHelper")] -#[serde(into = "MessageContentBlockSerDeHelper")] -pub(crate) struct MessageContentBlock(Vec); - -impl From for TextContentBlock { - fn from(value: MessageContentBlock) -> Self { - Self(value.0) - } -} - -impl From for MessageContentBlock { - fn from(value: TextContentBlock) -> Self { - Self(value.0) - } -} - -#[derive(Default, Serialize, Deserialize)] -pub(crate) struct MessageContentBlockSerDeHelper { - /// Plain text short form. - #[serde(rename = "org.matrix.msc1767.text", skip_serializing_if = "Option::is_none")] - text: Option, - - /// HTML short form. - #[serde(rename = "org.matrix.msc1767.html", skip_serializing_if = "Option::is_none")] - html: Option, - - /// Long form. - #[serde(rename = "org.matrix.msc1767.message", skip_serializing_if = "Option::is_none")] - message: Option>, -} - -impl TryFrom for Vec { - type Error = &'static str; - - fn try_from(value: MessageContentBlockSerDeHelper) -> Result { - let MessageContentBlockSerDeHelper { text, html, message } = value; - - if let Some(message) = message { - Ok(message) - } else { - let message: Vec<_> = html - .map(TextRepresentation::html) - .into_iter() - .chain(text.map(TextRepresentation::plain)) - .collect(); - if !message.is_empty() { - Ok(message) - } else { - Err("missing at least one of fields `org.matrix.msc1767.text`, `org.matrix.msc1767.html` or `org.matrix.msc1767.message`") - } - } - } -} - -impl TryFrom for MessageContentBlock { - type Error = &'static str; - - fn try_from(value: MessageContentBlockSerDeHelper) -> Result { - Ok(Self(value.try_into()?)) - } -} - -impl From> for MessageContentBlockSerDeHelper { - fn from(value: Vec) -> Self { - let has_shortcut = - |message: &TextRepresentation| matches!(&*message.mimetype, "text/plain" | "text/html"); - - if value.iter().all(has_shortcut) { - let mut helper = Self::default(); - - for message in value.into_iter() { - if message.mimetype == "text/plain" { - helper.text = Some(message.body); - } else if message.mimetype == "text/html" { - helper.html = Some(message.body); - } - } - - helper - } else { - Self { message: Some(value), ..Default::default() } - } - } -} - -impl From for MessageContentBlockSerDeHelper { - fn from(value: MessageContentBlock) -> Self { - value.0.into() - } -} diff --git a/crates/ruma-events/src/mixins.rs b/crates/ruma-events/src/mixins.rs new file mode 100644 index 00000000..7f36175f --- /dev/null +++ b/crates/ruma-events/src/mixins.rs @@ -0,0 +1,31 @@ +use std::collections::HashMap; + +use js_int::UInt; +use serde::{Serialize, Deserialize}; + +/// The source of an embed. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct MessageHints { + /// How many attachments will this event have? + #[serde(skip_serializing_if = "Option::is_none")] + attachments: Option, + + /// Which preset reactions to show? (for bot interactions) + #[serde(skip_serializing_if = "HashMap::is_empty")] + reactions: HashMap, +} + +impl MessageHints { + /// Create a new empty hint set + pub fn new() -> MessageHints { + MessageHints { + attachments: None, + reactions: HashMap::new(), + } + } + + /// Check if a hint set is empty + pub fn is_empty(&self) -> bool { + self.attachments.is_none() && self.reactions.is_empty() + } +} diff --git a/crates/ruma-events/src/poll.rs b/crates/ruma-events/src/poll.rs deleted file mode 100644 index 8974001d..00000000 --- a/crates/ruma-events/src/poll.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! Modules for events in the `m.poll` namespace ([MSC3381]). -//! -//! This module also contains types shared by events in its child namespaces. -//! -//! [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381 - -use std::{ - collections::{BTreeMap, BTreeSet}, - ops::Deref, -}; - -use indexmap::IndexMap; -use js_int::{uint, UInt}; -use ruma_common::{MilliSecondsSinceUnixEpoch, UserId}; - -use self::{start::PollContentBlock, unstable_start::UnstablePollStartContentBlock}; - -pub mod end; -pub mod response; -pub mod start; -pub mod unstable_end; -pub mod unstable_response; -pub mod unstable_start; - -/// The data from a poll response necessary to compile poll results. -#[derive(Debug, Clone, Copy)] -#[allow(clippy::exhaustive_structs)] -pub struct PollResponseData<'a> { - /// The sender of the response. - pub sender: &'a UserId, - - /// The time of creation of the response on the originating server. - pub origin_server_ts: MilliSecondsSinceUnixEpoch, - - /// The selections/answers of the response. - pub selections: &'a [String], -} - -/// Generate the current results with the given poll and responses. -/// -/// If the `end_timestamp` is provided, any response with an `origin_server_ts` after that timestamp -/// is ignored. If it is not provided, `MilliSecondsSinceUnixEpoch::now()` will be used instead. -/// -/// This method will handle invalid responses, or several response from the same user so all -/// responses to the poll should be provided. -/// -/// Returns a map of answer ID to a set of user IDs that voted for them. When using `.iter()` or -/// `.into_iter()` on the map, the results are sorted from the highest number of votes to the -/// lowest. -pub fn compile_poll_results<'a>( - poll: &'a PollContentBlock, - responses: impl IntoIterator>, - end_timestamp: Option, -) -> IndexMap<&'a str, BTreeSet<&'a UserId>> { - let answer_ids = poll.answers.iter().map(|a| a.id.as_str()).collect(); - let users_selections = - filter_selections(answer_ids, poll.max_selections, responses, end_timestamp); - - aggregate_results(poll.answers.iter().map(|a| a.id.as_str()), users_selections) -} - -/// Generate the current results with the given unstable poll and responses. -/// -/// If the `end_timestamp` is provided, any response with an `origin_server_ts` after that timestamp -/// is ignored. If it is not provided, `MilliSecondsSinceUnixEpoch::now()` will be used instead. -/// -/// This method will handle invalid responses, or several response from the same user so all -/// responses to the poll should be provided. -/// -/// Returns a map of answer ID to a set of user IDs that voted for them. When using `.iter()` or -/// `.into_iter()` on the map, the results are sorted from the highest number of votes to the -/// lowest. -pub fn compile_unstable_poll_results<'a>( - poll: &'a UnstablePollStartContentBlock, - responses: impl IntoIterator>, - end_timestamp: Option, -) -> IndexMap<&'a str, BTreeSet<&'a UserId>> { - let answer_ids = poll.answers.iter().map(|a| a.id.as_str()).collect(); - let users_selections = - filter_selections(answer_ids, poll.max_selections, responses, end_timestamp); - - aggregate_results(poll.answers.iter().map(|a| a.id.as_str()), users_selections) -} - -/// Validate the selections of a response. -fn validate_selections<'a>( - answer_ids: &BTreeSet<&str>, - max_selections: UInt, - selections: &'a [String], -) -> Option> { - // Vote is spoiled if any answer is unknown. - if selections.iter().any(|s| !answer_ids.contains(s.as_str())) { - return None; - } - - // Fallback to the maximum value for usize because we can't have more selections than that - // in memory. - let max_selections: usize = max_selections.try_into().unwrap_or(usize::MAX); - - Some(selections.iter().take(max_selections).map(Deref::deref)) -} - -fn filter_selections<'a>( - answer_ids: BTreeSet<&str>, - max_selections: UInt, - responses: impl IntoIterator>, - end_timestamp: Option, -) -> BTreeMap<&'a UserId, (MilliSecondsSinceUnixEpoch, Option>)> { - responses - .into_iter() - .filter(|ev| { - // Filter out responses after the end_timestamp. - end_timestamp.map_or(true, |end_ts| ev.origin_server_ts <= end_ts) - }) - .fold(BTreeMap::new(), |mut acc, data| { - let response = - acc.entry(data.sender).or_insert((MilliSecondsSinceUnixEpoch(uint!(0)), None)); - - // Only keep the latest selections for each user. - if response.0 < data.origin_server_ts { - *response = ( - data.origin_server_ts, - validate_selections(&answer_ids, max_selections, data.selections), - ); - } - - acc - }) -} - -/// Aggregate the given selections by answer. -fn aggregate_results<'a>( - answers: impl Iterator, - users_selections: BTreeMap< - &'a UserId, - (MilliSecondsSinceUnixEpoch, Option>), - >, -) -> IndexMap<&'a str, BTreeSet<&'a UserId>> { - let mut results = IndexMap::from_iter(answers.into_iter().map(|a| (a, BTreeSet::new()))); - - for (user, (_, selections)) in users_selections { - if let Some(selections) = selections { - for selection in selections { - results - .get_mut(selection) - .expect("validated selections should only match possible answers") - .insert(user); - } - } - } - - results.sort_by(|_, a, _, b| b.len().cmp(&a.len())); - - results -} - -/// Generate the fallback text representation of a poll end event. -/// -/// This is a sentence that lists the top answers for the given results, in english. It is used to -/// generate a valid poll end event when using -/// `OriginalSync(Unstable)PollStartEvent::compile_results()`. -/// -/// `answers` is an iterator of `(answer ID, answer plain text representation)` and `results` is an -/// iterator of `(answer ID, count)` ordered in descending order. -fn generate_poll_end_fallback_text<'a>( - answers: &[(&'a str, &'a str)], - results: impl Iterator, -) -> String { - let mut top_answers = Vec::new(); - let mut top_count = 0; - - for (id, count) in results { - if count >= top_count { - top_answers.push(id); - top_count = count; - } else { - break; - } - } - - let top_answers_text = top_answers - .into_iter() - .map(|id| { - answers - .iter() - .find(|(a_id, _)| *a_id == id) - .expect("top answer ID should be a valid answer ID") - .1 - }) - .collect::>(); - - // Construct the plain text representation. - match top_answers_text.len() { - 0 => "The poll has closed with no top answer".to_owned(), - 1 => { - format!("The poll has closed. Top answer: {}", top_answers_text[0]) - } - _ => { - let answers = top_answers_text.join(", "); - format!("The poll has closed. Top answers: {answers}") - } - } -} diff --git a/crates/ruma-events/src/poll/end.rs b/crates/ruma-events/src/poll/end.rs deleted file mode 100644 index a5d04301..00000000 --- a/crates/ruma-events/src/poll/end.rs +++ /dev/null @@ -1,125 +0,0 @@ -//! Types for the `m.poll.end` event. - -use std::{ - collections::{btree_map, BTreeMap}, - ops::Deref, -}; - -use js_int::UInt; -use ruma_common::OwnedEventId; -use ruma_macros::EventContent; -use serde::{Deserialize, Serialize}; - -use crate::{message::TextContentBlock, relation::Reference}; - -/// The payload for a poll end event. -/// -/// This type can be generated from the poll start and poll response events with -/// [`OriginalSyncPollStartEvent::compile_results()`]. -/// -/// This is the event content that should be sent for room versions that support extensible events. -/// As of Matrix 1.7, none of the stable room versions (1 through 10) support extensible events. -/// -/// To send a poll end event for a room version that does not support extensible events, use -/// [`UnstablePollEndEventContent`]. -/// -/// [`OriginalSyncPollStartEvent::compile_results()`]: super::start::OriginalSyncPollStartEvent::compile_results -/// [`UnstablePollEndEventContent`]: super::unstable_end::UnstablePollEndEventContent -#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[ruma_event(type = "m.poll.end", kind = MessageLike)] -pub struct PollEndEventContent { - /// The text representation of the results. - #[serde(rename = "m.text")] - pub text: TextContentBlock, - - /// The sender's perspective of the results. - #[serde(rename = "m.poll.results", skip_serializing_if = "Option::is_none")] - pub poll_results: Option, - - /// Whether this message is automated. - #[cfg(feature = "unstable-msc3955")] - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - rename = "org.matrix.msc1767.automated" - )] - pub automated: bool, - - /// Information about the poll start event this responds to. - #[serde(rename = "m.relates_to")] - pub relates_to: Reference, -} - -impl PollEndEventContent { - /// Creates a new `PollEndEventContent` with the given fallback representation and - /// that responds to the given poll start event ID. - pub fn new(text: TextContentBlock, poll_start_id: OwnedEventId) -> Self { - Self { - text, - poll_results: None, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: Reference::new(poll_start_id), - } - } - - /// Creates a new `PollEndEventContent` with the given plain text fallback representation and - /// that responds to the given poll start event ID. - pub fn with_plain_text(plain_text: impl Into, poll_start_id: OwnedEventId) -> Self { - Self { - text: TextContentBlock::plain(plain_text), - poll_results: None, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: Reference::new(poll_start_id), - } - } -} - -/// A block for the results of a poll. -/// -/// This is a map of answer ID to number of votes. -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct PollResultsContentBlock(BTreeMap); - -impl PollResultsContentBlock { - /// Get these results sorted from the highest number of votes to the lowest. - /// - /// Returns a list of `(answer ID, number of votes)`. - pub fn sorted(&self) -> Vec<(&str, UInt)> { - let mut sorted = self.0.iter().map(|(id, count)| (id.as_str(), *count)).collect::>(); - sorted.sort_by(|(_, a), (_, b)| b.cmp(a)); - sorted - } -} - -impl From> for PollResultsContentBlock { - fn from(value: BTreeMap) -> Self { - Self(value) - } -} - -impl IntoIterator for PollResultsContentBlock { - type Item = (String, UInt); - type IntoIter = btree_map::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl FromIterator<(String, UInt)> for PollResultsContentBlock { - fn from_iter>(iter: T) -> Self { - Self(BTreeMap::from_iter(iter)) - } -} - -impl Deref for PollResultsContentBlock { - type Target = BTreeMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/crates/ruma-events/src/poll/response.rs b/crates/ruma-events/src/poll/response.rs deleted file mode 100644 index 9e135be7..00000000 --- a/crates/ruma-events/src/poll/response.rs +++ /dev/null @@ -1,129 +0,0 @@ -//! Types for the `m.poll.response` event. - -use std::{ops::Deref, vec}; - -use ruma_common::OwnedEventId; -use ruma_macros::EventContent; -use serde::{Deserialize, Serialize}; - -use super::{start::PollContentBlock, validate_selections, PollResponseData}; -use crate::relation::Reference; - -/// The payload for a poll response event. -/// -/// This is the event content that should be sent for room versions that support extensible events. -/// As of Matrix 1.7, none of the stable room versions (1 through 10) support extensible events. -/// -/// To send a poll response event for a room version that does not support extensible events, use -/// [`UnstablePollResponseEventContent`]. -/// -/// [`UnstablePollResponseEventContent`]: super::unstable_response::UnstablePollResponseEventContent -#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[ruma_event(type = "m.poll.response", kind = MessageLike)] -pub struct PollResponseEventContent { - /// The user's selection. - #[serde(rename = "m.selections")] - pub selections: SelectionsContentBlock, - - /// Whether this message is automated. - #[cfg(feature = "unstable-msc3955")] - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - rename = "org.matrix.msc1767.automated" - )] - pub automated: bool, - - /// Information about the poll start event this responds to. - #[serde(rename = "m.relates_to")] - pub relates_to: Reference, -} - -impl PollResponseEventContent { - /// Creates a new `PollResponseEventContent` that responds to the given poll start event ID, - /// with the given poll response content. - pub fn new(selections: SelectionsContentBlock, poll_start_id: OwnedEventId) -> Self { - Self { - selections, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: Reference::new(poll_start_id), - } - } -} - -impl OriginalSyncPollResponseEvent { - /// Get the data from this response necessary to compile poll results. - pub fn data(&self) -> PollResponseData<'_> { - PollResponseData { - sender: &self.sender, - origin_server_ts: self.origin_server_ts, - selections: &self.content.selections, - } - } -} - -impl OriginalPollResponseEvent { - /// Get the data from this response necessary to compile poll results. - pub fn data(&self) -> PollResponseData<'_> { - PollResponseData { - sender: &self.sender, - origin_server_ts: self.origin_server_ts, - selections: &self.content.selections, - } - } -} - -/// A block for selections content. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct SelectionsContentBlock(Vec); - -impl SelectionsContentBlock { - /// Whether this `SelectionsContentBlock` is empty. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Validate these selections against the given `PollContentBlock`. - /// - /// Returns the list of valid selections in this `SelectionsContentBlock`, or `None` if there is - /// no valid selection. - pub fn validate<'a>( - &'a self, - poll: &PollContentBlock, - ) -> Option> { - let answer_ids = poll.answers.iter().map(|a| a.id.as_str()).collect(); - validate_selections(&answer_ids, poll.max_selections, &self.0) - } -} - -impl From> for SelectionsContentBlock { - fn from(value: Vec) -> Self { - Self(value) - } -} - -impl IntoIterator for SelectionsContentBlock { - type Item = String; - type IntoIter = vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl FromIterator for SelectionsContentBlock { - fn from_iter>(iter: T) -> Self { - Self(Vec::from_iter(iter)) - } -} - -impl Deref for SelectionsContentBlock { - type Target = [String]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/crates/ruma-events/src/poll/start.rs b/crates/ruma-events/src/poll/start.rs deleted file mode 100644 index 112b9eb6..00000000 --- a/crates/ruma-events/src/poll/start.rs +++ /dev/null @@ -1,285 +0,0 @@ -//! Types for the `m.poll.start` event. - -use std::ops::Deref; - -use js_int::{uint, UInt}; -use ruma_common::{serde::StringEnum, MilliSecondsSinceUnixEpoch}; -use ruma_macros::EventContent; -use serde::{Deserialize, Serialize}; - -use crate::PrivOwnedStr; - -mod poll_answers_serde; - -use poll_answers_serde::PollAnswersDeHelper; - -use super::{ - compile_poll_results, - end::{PollEndEventContent, PollResultsContentBlock}, - generate_poll_end_fallback_text, PollResponseData, -}; -use crate::{message::TextContentBlock, room::message::Relation}; - -/// The payload for a poll start event. -/// -/// This is the event content that should be sent for room versions that support extensible events. -/// As of Matrix 1.7, none of the stable room versions (1 through 10) support extensible events. -/// -/// To send a poll start event for a room version that does not support extensible events, use -/// [`UnstablePollStartEventContent`]. -/// -/// [`UnstablePollStartEventContent`]: super::unstable_start::UnstablePollStartEventContent -#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[ruma_event(type = "m.poll.start", kind = MessageLike, without_relation)] -pub struct PollStartEventContent { - /// The poll content of the message. - #[serde(rename = "m.poll")] - pub poll: PollContentBlock, - - /// Text representation of the message, for clients that don't support polls. - #[serde(rename = "m.text")] - pub text: TextContentBlock, - - /// Information about related messages. - #[serde( - flatten, - skip_serializing_if = "Option::is_none", - deserialize_with = "crate::room::message::relation_serde::deserialize_relation" - )] - pub relates_to: Option>, - - /// Whether this message is automated. - #[cfg(feature = "unstable-msc3955")] - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - rename = "org.matrix.msc1767.automated" - )] - pub automated: bool, -} - -impl PollStartEventContent { - /// Creates a new `PollStartEventContent` with the given fallback representation and poll - /// content. - pub fn new(text: TextContentBlock, poll: PollContentBlock) -> Self { - Self { - poll, - text, - relates_to: None, - #[cfg(feature = "unstable-msc3955")] - automated: false, - } - } - - /// Creates a new `PollStartEventContent` with the given plain text fallback - /// representation and poll content. - pub fn with_plain_text(plain_text: impl Into, poll: PollContentBlock) -> Self { - Self::new(TextContentBlock::plain(plain_text), poll) - } -} - -impl OriginalSyncPollStartEvent { - /// Compile the results for this poll with the given response into a `PollEndEventContent`. - /// - /// It generates a default text representation of the results in English. - /// - /// This uses [`compile_poll_results()`] internally. - pub fn compile_results<'a>( - &'a self, - responses: impl IntoIterator>, - ) -> PollEndEventContent { - let full_results = compile_poll_results( - &self.content.poll, - responses, - Some(MilliSecondsSinceUnixEpoch::now()), - ); - let results = - full_results.into_iter().map(|(id, users)| (id, users.len())).collect::>(); - - // Construct the results and get the top answer(s). - let poll_results = PollResultsContentBlock::from_iter( - results - .iter() - .map(|(id, count)| ((*id).to_owned(), (*count).try_into().unwrap_or(UInt::MAX))), - ); - - // Get the text representation of the best answers. - let answers = self - .content - .poll - .answers - .iter() - .map(|a| { - let text = a.text.find_plain().unwrap_or(&a.id); - (a.id.as_str(), text) - }) - .collect::>(); - let plain_text = generate_poll_end_fallback_text(&answers, results.into_iter()); - - let mut end = PollEndEventContent::with_plain_text(plain_text, self.event_id.clone()); - end.poll_results = Some(poll_results); - - end - } -} - -/// A block for poll content. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct PollContentBlock { - /// The question of the poll. - pub question: PollQuestion, - - /// The kind of the poll. - #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] - pub kind: PollKind, - - /// The maximum number of responses a user is able to select. - /// - /// Must be greater or equal to `1`. - /// - /// Defaults to `1`. - #[serde( - default = "PollContentBlock::default_max_selections", - skip_serializing_if = "PollContentBlock::max_selections_is_default" - )] - pub max_selections: UInt, - - /// The possible answers to the poll. - pub answers: PollAnswers, -} - -impl PollContentBlock { - /// Creates a new `PollStartContent` with the given question and answers. - pub fn new(question: TextContentBlock, answers: PollAnswers) -> Self { - Self { - question: question.into(), - kind: Default::default(), - max_selections: Self::default_max_selections(), - answers, - } - } - - pub(super) fn default_max_selections() -> UInt { - uint!(1) - } - - fn max_selections_is_default(max_selections: &UInt) -> bool { - max_selections == &Self::default_max_selections() - } -} - -/// The question of a poll. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct PollQuestion { - /// The text representation of the question. - #[serde(rename = "m.text")] - pub text: TextContentBlock, -} - -impl From for PollQuestion { - fn from(text: TextContentBlock) -> Self { - Self { text } - } -} - -/// The kind of poll. -#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] -#[derive(Clone, Default, PartialEq, Eq, StringEnum)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub enum PollKind { - /// The results are revealed once the poll is closed. - #[default] - #[ruma_enum(rename = "m.undisclosed")] - Undisclosed, - - /// The votes are visible up until and including when the poll is closed. - #[ruma_enum(rename = "m.disclosed")] - Disclosed, - - #[doc(hidden)] - _Custom(PrivOwnedStr), -} - -/// The answers to a poll. -/// -/// Must include between 1 and 20 `PollAnswer`s. -/// -/// To build this, use the `TryFrom` implementations. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(try_from = "PollAnswersDeHelper")] -pub struct PollAnswers(Vec); - -impl PollAnswers { - /// The smallest number of values contained in a `PollAnswers`. - pub const MIN_LENGTH: usize = 1; - - /// The largest number of values contained in a `PollAnswers`. - pub const MAX_LENGTH: usize = 20; -} - -/// An error encountered when trying to convert to a `PollAnswers`. -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)] -#[non_exhaustive] -pub enum PollAnswersError { - /// There are more than [`PollAnswers::MAX_LENGTH`] values. - #[error("too many values")] - TooManyValues, - /// There are less that [`PollAnswers::MIN_LENGTH`] values. - #[error("not enough values")] - NotEnoughValues, -} - -impl TryFrom> for PollAnswers { - type Error = PollAnswersError; - - fn try_from(value: Vec) -> Result { - if value.len() < Self::MIN_LENGTH { - Err(PollAnswersError::NotEnoughValues) - } else if value.len() > Self::MAX_LENGTH { - Err(PollAnswersError::TooManyValues) - } else { - Ok(Self(value)) - } - } -} - -impl TryFrom<&[PollAnswer]> for PollAnswers { - type Error = PollAnswersError; - - fn try_from(value: &[PollAnswer]) -> Result { - Self::try_from(value.to_owned()) - } -} - -impl Deref for PollAnswers { - type Target = [PollAnswer]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// Poll answer. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct PollAnswer { - /// The ID of the answer. - /// - /// This must be unique among the answers of a poll. - #[serde(rename = "m.id")] - pub id: String, - - /// The text representation of the answer. - #[serde(rename = "m.text")] - pub text: TextContentBlock, -} - -impl PollAnswer { - /// Creates a new `PollAnswer` with the given id and text representation. - pub fn new(id: String, text: TextContentBlock) -> Self { - Self { id, text } - } -} diff --git a/crates/ruma-events/src/poll/start/poll_answers_serde.rs b/crates/ruma-events/src/poll/start/poll_answers_serde.rs deleted file mode 100644 index 3ee79b4f..00000000 --- a/crates/ruma-events/src/poll/start/poll_answers_serde.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! `Serialize` and `Deserialize` implementations for extensible events (MSC1767). - -use serde::Deserialize; - -use super::{PollAnswer, PollAnswers, PollAnswersError}; - -#[derive(Debug, Default, Deserialize)] -pub(crate) struct PollAnswersDeHelper(Vec); - -impl TryFrom for PollAnswers { - type Error = PollAnswersError; - - fn try_from(helper: PollAnswersDeHelper) -> Result { - let mut answers = helper.0; - answers.truncate(PollAnswers::MAX_LENGTH); - PollAnswers::try_from(answers) - } -} diff --git a/crates/ruma-events/src/poll/unstable_end.rs b/crates/ruma-events/src/poll/unstable_end.rs deleted file mode 100644 index 86bdf85a..00000000 --- a/crates/ruma-events/src/poll/unstable_end.rs +++ /dev/null @@ -1,57 +0,0 @@ -//! Types for the `org.matrix.msc3381.poll.end` event, the unstable version of `m.poll.end`. - -use ruma_common::OwnedEventId; -use ruma_macros::EventContent; -use serde::{Deserialize, Serialize}; - -use crate::relation::Reference; - -/// The payload for an unstable poll end event. -/// -/// This type can be generated from the unstable poll start and poll response events with -/// [`OriginalSyncUnstablePollStartEvent::compile_results()`]. -/// -/// This is the event content that should be sent for room versions that don't support extensible -/// events. As of Matrix 1.7, none of the stable room versions (1 through 10) support extensible -/// events. -/// -/// To send a poll end event for a room version that supports extensible events, use -/// [`PollEndEventContent`]. -/// -/// [`OriginalSyncUnstablePollStartEvent::compile_results()`]: super::unstable_start::OriginalSyncUnstablePollStartEvent::compile_results -/// [`PollEndEventContent`]: super::end::PollEndEventContent -#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[ruma_event(type = "org.matrix.msc3381.poll.end", kind = MessageLike)] -pub struct UnstablePollEndEventContent { - /// The text representation of the results. - #[serde(rename = "org.matrix.msc1767.text")] - pub text: String, - - /// The poll end content. - #[serde(default, rename = "org.matrix.msc3381.poll.end")] - pub poll_end: UnstablePollEndContentBlock, - - /// Information about the poll start event this responds to. - #[serde(rename = "m.relates_to")] - pub relates_to: Reference, -} - -impl UnstablePollEndEventContent { - /// Creates a new `PollEndEventContent` with the given fallback representation and - /// that responds to the given poll start event ID. - pub fn new(text: impl Into, poll_start_id: OwnedEventId) -> Self { - Self { - text: text.into(), - poll_end: UnstablePollEndContentBlock {}, - relates_to: Reference::new(poll_start_id), - } - } -} - -/// A block for the results of a poll. -/// -/// This is currently an empty struct. -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct UnstablePollEndContentBlock {} diff --git a/crates/ruma-events/src/poll/unstable_response.rs b/crates/ruma-events/src/poll/unstable_response.rs deleted file mode 100644 index bf256be9..00000000 --- a/crates/ruma-events/src/poll/unstable_response.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Types for the `org.matrix.msc3381.poll.response` event, the unstable version of -//! `m.poll.response`. - -use ruma_common::OwnedEventId; -use ruma_macros::EventContent; -use serde::{Deserialize, Serialize}; - -use super::{unstable_start::UnstablePollStartContentBlock, validate_selections, PollResponseData}; -use crate::relation::Reference; - -/// The payload for an unstable poll response event. -/// -/// This is the event content that should be sent for room versions that don't support extensible -/// events. As of Matrix 1.7, none of the stable room versions (1 through 10) support extensible -/// events. -/// -/// To send a poll response event for a room version that supports extensible events, use -/// [`PollResponseEventContent`]. -/// -/// [`PollResponseEventContent`]: super::response::PollResponseEventContent -#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[ruma_event(type = "org.matrix.msc3381.poll.response", kind = MessageLike)] -pub struct UnstablePollResponseEventContent { - /// The response's content. - #[serde(rename = "org.matrix.msc3381.poll.response")] - pub poll_response: UnstablePollResponseContentBlock, - - /// Information about the poll start event this responds to. - #[serde(rename = "m.relates_to")] - pub relates_to: Reference, -} - -impl UnstablePollResponseEventContent { - /// Creates a new `UnstablePollResponseEventContent` that responds to the given poll start event - /// ID, with the given answers. - pub fn new(answers: Vec, poll_start_id: OwnedEventId) -> Self { - Self { - poll_response: UnstablePollResponseContentBlock::new(answers), - relates_to: Reference::new(poll_start_id), - } - } -} - -impl OriginalSyncUnstablePollResponseEvent { - /// Get the data from this response necessary to compile poll results. - pub fn data(&self) -> PollResponseData<'_> { - PollResponseData { - sender: &self.sender, - origin_server_ts: self.origin_server_ts, - selections: &self.content.poll_response.answers, - } - } -} - -impl OriginalUnstablePollResponseEvent { - /// Get the data from this response necessary to compile poll results. - pub fn data(&self) -> PollResponseData<'_> { - PollResponseData { - sender: &self.sender, - origin_server_ts: self.origin_server_ts, - selections: &self.content.poll_response.answers, - } - } -} - -/// An unstable block for poll response content. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct UnstablePollResponseContentBlock { - /// The selected answers for the response. - pub answers: Vec, -} - -impl UnstablePollResponseContentBlock { - /// Creates a new `UnstablePollResponseContentBlock` with the given answers. - pub fn new(answers: Vec) -> Self { - Self { answers } - } - - /// Validate these selections against the given `UnstablePollStartContentBlock`. - /// - /// Returns the list of valid selections in this `UnstablePollResponseContentBlock`, or `None` - /// if there is no valid selection. - pub fn validate<'a>( - &'a self, - poll: &UnstablePollStartContentBlock, - ) -> Option> { - let answer_ids = poll.answers.iter().map(|a| a.id.as_str()).collect(); - validate_selections(&answer_ids, poll.max_selections, &self.answers) - } -} - -impl From> for UnstablePollResponseContentBlock { - fn from(value: Vec) -> Self { - Self::new(value) - } -} diff --git a/crates/ruma-events/src/poll/unstable_start.rs b/crates/ruma-events/src/poll/unstable_start.rs deleted file mode 100644 index 16094499..00000000 --- a/crates/ruma-events/src/poll/unstable_start.rs +++ /dev/null @@ -1,377 +0,0 @@ -//! Types for the `org.matrix.msc3381.poll.start` event, the unstable version of `m.poll.start`. - -use std::ops::Deref; - -use js_int::UInt; -use ruma_macros::EventContent; -use serde::{Deserialize, Serialize}; - -mod content_serde; -mod unstable_poll_answers_serde; -mod unstable_poll_kind_serde; - -use ruma_common::{MilliSecondsSinceUnixEpoch, OwnedEventId}; - -use self::unstable_poll_answers_serde::UnstablePollAnswersDeHelper; -use super::{ - compile_unstable_poll_results, generate_poll_end_fallback_text, - start::{PollAnswers, PollAnswersError, PollContentBlock, PollKind}, - unstable_end::UnstablePollEndEventContent, - PollResponseData, -}; -use crate::{ - relation::Replacement, room::message::RelationWithoutReplacement, EventContent, - MessageLikeEventContent, MessageLikeEventType, RedactContent, RedactedMessageLikeEventContent, - StaticEventContent, -}; - -/// The payload for an unstable poll start event. -/// -/// This is the event content that should be sent for room versions that don't support extensible -/// events. As of Matrix 1.7, none of the stable room versions (1 through 10) support extensible -/// events. -/// -/// To send a poll start event for a room version that supports extensible events, use -/// [`PollStartEventContent`]. -/// -/// [`PollStartEventContent`]: super::start::PollStartEventContent -#[derive(Clone, Debug, Serialize, EventContent)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[ruma_event(type = "org.matrix.msc3381.poll.start", kind = MessageLike, custom_redacted)] -#[serde(untagged)] -#[allow(clippy::large_enum_variant)] -pub enum UnstablePollStartEventContent { - /// A new poll start event. - New(NewUnstablePollStartEventContent), - - /// A replacement poll start event. - Replacement(ReplacementUnstablePollStartEventContent), -} - -impl UnstablePollStartEventContent { - /// Get the poll start content of this event content. - pub fn poll_start(&self) -> &UnstablePollStartContentBlock { - match self { - Self::New(c) => &c.poll_start, - Self::Replacement(c) => &c.relates_to.new_content.poll_start, - } - } -} - -impl RedactContent for UnstablePollStartEventContent { - type Redacted = RedactedUnstablePollStartEventContent; - - fn redact(self, _version: &crate::RoomVersionId) -> Self::Redacted { - RedactedUnstablePollStartEventContent::default() - } -} - -impl From for UnstablePollStartEventContent { - fn from(value: NewUnstablePollStartEventContent) -> Self { - Self::New(value) - } -} - -impl From for UnstablePollStartEventContent { - fn from(value: ReplacementUnstablePollStartEventContent) -> Self { - Self::Replacement(value) - } -} - -impl OriginalSyncUnstablePollStartEvent { - /// Compile the results for this poll with the given response into an - /// `UnstablePollEndEventContent`. - /// - /// It generates a default text representation of the results in English. - /// - /// This uses [`compile_unstable_poll_results()`] internally. - pub fn compile_results<'a>( - &'a self, - responses: impl IntoIterator>, - ) -> UnstablePollEndEventContent { - let poll_start = self.content.poll_start(); - - let full_results = compile_unstable_poll_results( - poll_start, - responses, - Some(MilliSecondsSinceUnixEpoch::now()), - ); - let results = - full_results.into_iter().map(|(id, users)| (id, users.len())).collect::>(); - - // Get the text representation of the best answers. - let answers = - poll_start.answers.iter().map(|a| (a.id.as_str(), a.text.as_str())).collect::>(); - let plain_text = generate_poll_end_fallback_text(&answers, results.into_iter()); - - UnstablePollEndEventContent::new(plain_text, self.event_id.clone()) - } -} - -/// A new unstable poll start event. -#[derive(Clone, Debug, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct NewUnstablePollStartEventContent { - /// The poll content of the message. - #[serde(rename = "org.matrix.msc3381.poll.start")] - pub poll_start: UnstablePollStartContentBlock, - - /// Text representation of the message, for clients that don't support polls. - #[serde(rename = "org.matrix.msc1767.text")] - pub text: Option, - - /// Information about related messages. - #[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")] - pub relates_to: Option, -} - -impl NewUnstablePollStartEventContent { - /// Creates a `NewUnstablePollStartEventContent` with the given poll content. - pub fn new(poll_start: UnstablePollStartContentBlock) -> Self { - Self { poll_start, text: None, relates_to: None } - } - - /// Creates a `NewUnstablePollStartEventContent` with the given plain text fallback - /// representation and poll content. - pub fn plain_text(text: impl Into, poll_start: UnstablePollStartContentBlock) -> Self { - Self { poll_start, text: Some(text.into()), relates_to: None } - } -} - -impl EventContent for NewUnstablePollStartEventContent { - type EventType = MessageLikeEventType; - - fn event_type(&self) -> Self::EventType { - MessageLikeEventType::UnstablePollStart - } -} - -impl StaticEventContent for NewUnstablePollStartEventContent { - const TYPE: &'static str = "org.matrix.msc3381.poll.start"; -} - -impl MessageLikeEventContent for NewUnstablePollStartEventContent {} - -/// Form of [`NewUnstablePollStartEventContent`] without relation. -/// -/// To construct this type, construct a [`NewUnstablePollStartEventContent`] and then use one of its -/// `::from()` / `.into()` methods. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct NewUnstablePollStartEventContentWithoutRelation { - /// The poll content of the message. - #[serde(rename = "org.matrix.msc3381.poll.start")] - pub poll_start: UnstablePollStartContentBlock, - - /// Text representation of the message, for clients that don't support polls. - #[serde(rename = "org.matrix.msc1767.text")] - pub text: Option, -} - -impl From for NewUnstablePollStartEventContentWithoutRelation { - fn from(value: NewUnstablePollStartEventContent) -> Self { - let NewUnstablePollStartEventContent { poll_start, text, .. } = value; - Self { poll_start, text } - } -} - -/// A replacement unstable poll start event. -#[derive(Clone, Debug)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct ReplacementUnstablePollStartEventContent { - /// The poll content of the message. - pub poll_start: Option, - - /// Text representation of the message, for clients that don't support polls. - pub text: Option, - - /// Information about related messages. - pub relates_to: Replacement, -} - -impl ReplacementUnstablePollStartEventContent { - /// Creates a `ReplacementUnstablePollStartEventContent` with the given poll content that - /// replaces the event with the given ID. - /// - /// The constructed content does not have a fallback by default. - pub fn new(poll_start: UnstablePollStartContentBlock, replaces: OwnedEventId) -> Self { - Self { - poll_start: None, - text: None, - relates_to: Replacement { - event_id: replaces, - new_content: NewUnstablePollStartEventContent::new(poll_start).into(), - }, - } - } - - /// Creates a `ReplacementUnstablePollStartEventContent` with the given plain text fallback - /// representation and poll content that replaces the event with the given ID. - /// - /// The constructed content does not have a fallback by default. - pub fn plain_text( - text: impl Into, - poll_start: UnstablePollStartContentBlock, - replaces: OwnedEventId, - ) -> Self { - Self { - poll_start: None, - text: None, - relates_to: Replacement { - event_id: replaces, - new_content: NewUnstablePollStartEventContent::plain_text(text, poll_start).into(), - }, - } - } -} - -impl EventContent for ReplacementUnstablePollStartEventContent { - type EventType = MessageLikeEventType; - - fn event_type(&self) -> Self::EventType { - MessageLikeEventType::UnstablePollStart - } -} - -impl StaticEventContent for ReplacementUnstablePollStartEventContent { - const TYPE: &'static str = "org.matrix.msc3381.poll.start"; -} - -impl MessageLikeEventContent for ReplacementUnstablePollStartEventContent {} - -/// Redacted form of UnstablePollStartEventContent -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct RedactedUnstablePollStartEventContent {} - -impl RedactedUnstablePollStartEventContent { - /// Creates an empty RedactedUnstablePollStartEventContent. - pub fn new() -> RedactedUnstablePollStartEventContent { - Self::default() - } -} - -impl EventContent for RedactedUnstablePollStartEventContent { - type EventType = MessageLikeEventType; - - fn event_type(&self) -> Self::EventType { - MessageLikeEventType::UnstablePollStart - } -} - -impl StaticEventContent for RedactedUnstablePollStartEventContent { - const TYPE: &'static str = "org.matrix.msc3381.poll.start"; -} - -impl RedactedMessageLikeEventContent for RedactedUnstablePollStartEventContent {} - -/// An unstable block for poll start content. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct UnstablePollStartContentBlock { - /// The question of the poll. - pub question: UnstablePollQuestion, - - /// The kind of the poll. - #[serde(default, with = "unstable_poll_kind_serde")] - pub kind: PollKind, - - /// The maximum number of responses a user is able to select. - /// - /// Must be greater or equal to `1`. - /// - /// Defaults to `1`. - #[serde(default = "PollContentBlock::default_max_selections")] - pub max_selections: UInt, - - /// The possible answers to the poll. - pub answers: UnstablePollAnswers, -} - -impl UnstablePollStartContentBlock { - /// Creates a new `PollStartContent` with the given question and answers. - pub fn new(question: impl Into, answers: UnstablePollAnswers) -> Self { - Self { - question: UnstablePollQuestion::new(question), - kind: Default::default(), - max_selections: PollContentBlock::default_max_selections(), - answers, - } - } -} - -/// An unstable poll question. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct UnstablePollQuestion { - /// The text representation of the question. - #[serde(rename = "org.matrix.msc1767.text")] - pub text: String, -} - -impl UnstablePollQuestion { - /// Creates a new `UnstablePollQuestion` with the given plain text. - pub fn new(text: impl Into) -> Self { - Self { text: text.into() } - } -} - -/// The unstable answers to a poll. -/// -/// Must include between 1 and 20 `UnstablePollAnswer`s. -/// -/// To build this, use one of the `TryFrom` implementations. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(try_from = "UnstablePollAnswersDeHelper")] -pub struct UnstablePollAnswers(Vec); - -impl TryFrom> for UnstablePollAnswers { - type Error = PollAnswersError; - - fn try_from(value: Vec) -> Result { - if value.len() < PollAnswers::MIN_LENGTH { - Err(PollAnswersError::NotEnoughValues) - } else if value.len() > PollAnswers::MAX_LENGTH { - Err(PollAnswersError::TooManyValues) - } else { - Ok(Self(value)) - } - } -} - -impl TryFrom<&[UnstablePollAnswer]> for UnstablePollAnswers { - type Error = PollAnswersError; - - fn try_from(value: &[UnstablePollAnswer]) -> Result { - Self::try_from(value.to_owned()) - } -} - -impl Deref for UnstablePollAnswers { - type Target = [UnstablePollAnswer]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// Unstable poll answer. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct UnstablePollAnswer { - /// The ID of the answer. - /// - /// This must be unique among the answers of a poll. - pub id: String, - - /// The text representation of the answer. - #[serde(rename = "org.matrix.msc1767.text")] - pub text: String, -} - -impl UnstablePollAnswer { - /// Creates a new `PollAnswer` with the given id and text representation. - pub fn new(id: impl Into, text: impl Into) -> Self { - Self { id: id.into(), text: text.into() } - } -} diff --git a/crates/ruma-events/src/poll/unstable_start/content_serde.rs b/crates/ruma-events/src/poll/unstable_start/content_serde.rs deleted file mode 100644 index 46565bcd..00000000 --- a/crates/ruma-events/src/poll/unstable_start/content_serde.rs +++ /dev/null @@ -1,82 +0,0 @@ -use ruma_common::{serde::from_raw_json_value, EventId}; -use serde::{de, ser::SerializeStruct, Deserialize, Deserializer, Serialize}; -use serde_json::value::RawValue as RawJsonValue; - -use super::{ - NewUnstablePollStartEventContent, NewUnstablePollStartEventContentWithoutRelation, - ReplacementUnstablePollStartEventContent, UnstablePollStartContentBlock, - UnstablePollStartEventContent, -}; -use crate::room::message::{deserialize_relation, Relation}; - -impl<'de> Deserialize<'de> for UnstablePollStartEventContent { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let json = Box::::deserialize(deserializer)?; - - let mut deserializer = serde_json::Deserializer::from_str(json.get()); - let relates_to: Option> = - deserialize_relation(&mut deserializer).map_err(de::Error::custom)?; - let UnstablePollStartEventContentDeHelper { poll_start, text } = - from_raw_json_value(&json)?; - - let c = match relates_to { - Some(Relation::Replacement(relates_to)) => { - ReplacementUnstablePollStartEventContent { poll_start, text, relates_to }.into() - } - rel => { - let poll_start = poll_start - .ok_or_else(|| de::Error::missing_field("org.matrix.msc3381.poll.start"))?; - let relates_to = rel - .map(|r| r.try_into().expect("Relation::Replacement has already been handled")); - NewUnstablePollStartEventContent { poll_start, text, relates_to }.into() - } - }; - - Ok(c) - } -} - -#[derive(Debug, Deserialize)] -struct UnstablePollStartEventContentDeHelper { - #[serde(rename = "org.matrix.msc3381.poll.start")] - poll_start: Option, - - #[serde(rename = "org.matrix.msc1767.text")] - text: Option, -} - -impl Serialize for ReplacementUnstablePollStartEventContent { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let len = 2 + self.poll_start.is_some() as usize + self.text.is_some() as usize; - - let mut state = - serializer.serialize_struct("ReplacementUnstablePollStartEventContent", len)?; - - if let Some(poll_start) = &self.poll_start { - state.serialize_field("org.matrix.msc3381.poll.start", poll_start)?; - } - if let Some(text) = &self.text { - state.serialize_field("org.matrix.msc1767.text", text)?; - } - - state.serialize_field("m.new_content", &self.relates_to.new_content)?; - state.serialize_field( - "m.relates_to", - &ReplacementRelatesTo { event_id: &self.relates_to.event_id }, - )?; - - state.end() - } -} - -#[derive(Debug, Serialize)] -#[serde(tag = "rel_type", rename = "m.replace")] -struct ReplacementRelatesTo<'a> { - event_id: &'a EventId, -} diff --git a/crates/ruma-events/src/poll/unstable_start/unstable_poll_answers_serde.rs b/crates/ruma-events/src/poll/unstable_start/unstable_poll_answers_serde.rs deleted file mode 100644 index 01ad1ab6..00000000 --- a/crates/ruma-events/src/poll/unstable_start/unstable_poll_answers_serde.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! `Deserialize` helpers for unstable poll answers (MSC3381). - -use serde::Deserialize; - -use super::{UnstablePollAnswer, UnstablePollAnswers}; -use crate::poll::start::{PollAnswers, PollAnswersError}; - -#[derive(Debug, Default, Deserialize)] -pub(crate) struct UnstablePollAnswersDeHelper(Vec); - -impl TryFrom for UnstablePollAnswers { - type Error = PollAnswersError; - - fn try_from(helper: UnstablePollAnswersDeHelper) -> Result { - let mut answers = helper.0; - answers.truncate(PollAnswers::MAX_LENGTH); - UnstablePollAnswers::try_from(answers) - } -} diff --git a/crates/ruma-events/src/poll/unstable_start/unstable_poll_kind_serde.rs b/crates/ruma-events/src/poll/unstable_start/unstable_poll_kind_serde.rs deleted file mode 100644 index de0e53d6..00000000 --- a/crates/ruma-events/src/poll/unstable_start/unstable_poll_kind_serde.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! `Serialize` and `Deserialize` helpers for unstable poll kind (MSC3381). - -use std::borrow::Cow; - -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - -use crate::{poll::start::PollKind, PrivOwnedStr}; - -/// Serializes a PollKind using the unstable prefixes. -pub(super) fn serialize(kind: &PollKind, serializer: S) -> Result -where - S: Serializer, -{ - let s = match kind { - PollKind::Undisclosed => "org.matrix.msc3381.poll.undisclosed", - PollKind::Disclosed => "org.matrix.msc3381.poll.disclosed", - PollKind::_Custom(s) => &s.0, - }; - - s.serialize(serializer) -} - -/// Deserializes a PollKind using the unstable prefixes. -pub(super) fn deserialize<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let s = Cow::<'_, str>::deserialize(deserializer)?; - - let kind = match &*s { - "org.matrix.msc3381.poll.undisclosed" => PollKind::Undisclosed, - "org.matrix.msc3381.poll.disclosed" => PollKind::Disclosed, - _ => PollKind::_Custom(PrivOwnedStr(s.into())), - }; - - Ok(kind) -} diff --git a/crates/ruma-events/src/reaction.rs b/crates/ruma-events/src/reaction.rs index ffffdb1d..54e4af1c 100644 --- a/crates/ruma-events/src/reaction.rs +++ b/crates/ruma-events/src/reaction.rs @@ -4,7 +4,7 @@ use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; - +use crate::message::Relation; use super::relation::Annotation; /// The payload for a `m.reaction` event. @@ -14,23 +14,27 @@ use super::relation::Annotation; #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.reaction", kind = MessageLike)] pub struct ReactionEventContent { - /// Information about the related event. - #[serde(rename = "m.relates_to")] - pub relates_to: Annotation, + /// Information about related events. + #[serde( + flatten, + skip_serializing_if = "Vec::is_empty", + deserialize_with = "crate::relation::deserialize_relation" + )] + pub relations: Vec>, } impl ReactionEventContent { /// Creates a new `ReactionEventContent` from the given annotation. /// /// You can also construct a `ReactionEventContent` from an annotation using `From` / `Into`. - pub fn new(relates_to: Annotation) -> Self { - Self { relates_to } + pub fn new(annotation: Annotation) -> Self { + Self { relations: vec![Relation::Annotation(annotation)] } } } impl From for ReactionEventContent { - fn from(relates_to: Annotation) -> Self { - Self::new(relates_to) + fn from(annotation: Annotation) -> Self { + Self::new(annotation) } } diff --git a/crates/ruma-events/src/relation.rs b/crates/ruma-events/src/relation.rs index 75d453d3..805be311 100644 --- a/crates/ruma-events/src/relation.rs +++ b/crates/ruma-events/src/relation.rs @@ -14,29 +14,48 @@ use serde::{Deserialize, Serialize}; use super::AnyMessageLikeEvent; use crate::PrivOwnedStr; -mod rel_serde; +mod relation_serde; +mod bundle_serde; +pub use relation_serde::deserialize_relation; /// Information about the event a [rich reply] is replying to. /// /// [rich reply]: https://spec.matrix.org/latest/client-server-api/#rich-replies -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct InReplyTo { +pub struct Reply { /// The event being replied to. pub event_id: OwnedEventId, } -impl InReplyTo { +impl Reply { /// Creates a new `InReplyTo` with the given event ID. pub fn new(event_id: OwnedEventId) -> Self { Self { event_id } } } +/// An [attachment] for an event. +/// +/// [attachment]: TODO: spec +#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct Attachment { + /// The event that is being attache to. + pub event_id: OwnedEventId, +} + +impl Attachment { + /// Creates a new `Annotation` with the given event ID and key. + pub fn new(event_id: OwnedEventId) -> Self { + Self { event_id } + } +} + /// An [annotation] for an event. /// /// [annotation]: https://spec.matrix.org/latest/client-server-api/#event-annotations-and-reactions -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(tag = "rel_type", rename = "m.annotation")] pub struct Annotation { @@ -88,45 +107,13 @@ impl Replacement { pub struct Thread { /// The ID of the root message in the thread. pub event_id: OwnedEventId, - - /// A reply relation. - /// - /// If this event is a reply and belongs to a thread, this points to the message that is being - /// replied to, and `is_falling_back` must be set to `false`. - /// - /// If this event is not a reply, this is used as a fallback mechanism for clients that do not - /// support threads. This should point to the latest message-like event in the thread and - /// `is_falling_back` must be set to `true`. - #[serde(rename = "m.in_reply_to", skip_serializing_if = "Option::is_none")] - pub in_reply_to: Option, - - /// Whether the `m.in_reply_to` field is a fallback for older clients or a genuine reply in a - /// thread. - #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] - pub is_falling_back: bool, } impl Thread { /// Convenience method to create a regular `Thread` relation with the given root event ID and /// latest message-like event ID. - pub fn plain(event_id: OwnedEventId, latest_event_id: OwnedEventId) -> Self { - Self { event_id, in_reply_to: Some(InReplyTo::new(latest_event_id)), is_falling_back: true } - } - - /// Convenience method to create a regular `Thread` relation with the given root event ID and - /// *without* the recommended reply fallback. - pub fn without_fallback(event_id: OwnedEventId) -> Self { - Self { event_id, in_reply_to: None, is_falling_back: false } - } - - /// Convenience method to create a reply `Thread` relation with the given root event ID and - /// replied-to event ID. - pub fn reply(event_id: OwnedEventId, reply_to_event_id: OwnedEventId) -> Self { - Self { - event_id, - in_reply_to: Some(InReplyTo::new(reply_to_event_id)), - is_falling_back: false, - } + pub fn new(event_id: OwnedEventId) -> Self { + Self { event_id } } } @@ -140,8 +127,32 @@ pub struct BundledThread { /// The number of events in the thread. pub count: UInt, - /// Whether the current logged in user has participated in the thread. - pub current_user_participated: bool, + /// The current logged in user's thread participtaion + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + pub current_user_participation: ThreadParticipation, +} + +/// The current user's thread participation +#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] +#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, StringEnum)] +#[ruma_enum(rename_all = "lowercase")] +#[non_exhaustive] +pub enum ThreadParticipation { + /// The user wants to get updates on this thread. This + /// participation is automatically set to `Watching` when interacting + /// with a thread. + Watching, + + /// The default participation. + #[default] + Default, + + /// The user does not want to see this thread. It is no longer + /// included in responses by default. + Ignoring, + + #[doc(hidden)] + _Custom(PrivOwnedStr), } impl BundledThread { @@ -149,18 +160,17 @@ impl BundledThread { pub fn new( latest_event: Raw, count: UInt, - current_user_participated: bool, + current_user_participation: ThreadParticipation, ) -> Self { - Self { latest_event, count, current_user_participated } + Self { latest_event, count, current_user_participation } } } /// A [reference] to another event. /// /// [reference]: https://spec.matrix.org/latest/client-server-api/#reference-relations -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[serde(tag = "rel_type", rename = "m.reference")] pub struct Reference { /// The ID of the event being referenced. pub event_id: OwnedEventId, @@ -203,6 +213,29 @@ impl ReferenceChunk { } } +/// A bundled attachment. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct BundledAttachment { + /// The ID of the event referencing this event. + pub event_id: OwnedEventId, +} + +/// A chunk of attachments. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct AttachmentChunk { + /// A batch of bundled attachments. + pub chunk: Vec, +} + +impl AttachmentChunk { + /// Creates a new `AttachmentChunk` with the given chunk. + pub fn new(chunk: Vec) -> Self { + Self { chunk } + } +} + /// [Bundled aggregations] of related child events of a message-like event. /// /// [Bundled aggregations]: https://spec.matrix.org/latest/client-server-api/#aggregations-of-child-events @@ -223,6 +256,10 @@ pub struct BundledMessageLikeRelations { #[serde(rename = "m.thread", skip_serializing_if = "Option::is_none")] pub thread: Option>, + /// Attachment relations. + #[serde(rename = "m.attachment", skip_serializing_if = "Option::is_none")] + pub attachments: Option>, + /// Reference relations. #[serde(rename = "m.reference", skip_serializing_if = "Option::is_none")] pub reference: Option>, @@ -231,7 +268,13 @@ pub struct BundledMessageLikeRelations { impl BundledMessageLikeRelations { /// Creates a new empty `BundledMessageLikeRelations`. pub const fn new() -> Self { - Self { replace: None, has_invalid_replacement: false, thread: None, reference: None } + Self { + replace: None, + has_invalid_replacement: false, + thread: None, + reference: None, + attachments: None, + } } /// Whether this bundle contains a replacement relation. @@ -246,15 +289,18 @@ impl BundledMessageLikeRelations { /// Returns `true` if all fields are empty. pub fn is_empty(&self) -> bool { - self.replace.is_none() && self.thread.is_none() && self.reference.is_none() + self.replace.is_none() + && self.thread.is_none() + && self.reference.is_none() + && self.attachments.is_none() } /// Transform `BundledMessageLikeRelations` to `BundledMessageLikeRelations` using the /// given closure to convert the `replace` field if it is `Some(_)`. pub(crate) fn map_replace(self, f: impl FnOnce(E) -> T) -> BundledMessageLikeRelations { - let Self { replace, has_invalid_replacement, thread, reference } = self; + let Self { replace, has_invalid_replacement, thread, reference, attachments } = self; let replace = replace.map(|r| Box::new(f(*r))); - BundledMessageLikeRelations { replace, has_invalid_replacement, thread, reference } + BundledMessageLikeRelations { replace, has_invalid_replacement, thread, reference, attachments } } } @@ -291,12 +337,18 @@ impl BundledStateRelations { } } -/// Relation types as defined in `rel_type` of an `m.relates_to` field. +/// Relation types as defined in `rel_type` of an `m.relations` entry. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "m.snake_case")] #[non_exhaustive] pub enum RelationType { + /// `m.reply`, a reply to a message + Reply, + + /// `m.attachment`, an attachment to a message + Attachment, + /// `m.annotation`, an annotation, principally used by reactions. Annotation, @@ -313,6 +365,32 @@ pub enum RelationType { _Custom(PrivOwnedStr), } +/// A relation +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum Relation { + /// `m.reply`, a reply to a message + Reply(Reply), + + /// `m.attachment`, an attachment to a message + Attachment(Attachment), + + /// `m.annotation`, an annotation, principally used by reactions. + Annotation(Annotation), + + /// `m.replace`, a replacement. + Replacement(Replacement), + + /// `m.thread`, a participant to a thread. + Thread(Thread), + + /// `m.reference`, a reference to another event. + Reference(Reference), + + #[doc(hidden)] + _Custom(CustomRelation), +} + /// The payload for a custom relation. #[doc(hidden)] #[derive(Clone, Debug, Deserialize, Serialize)] @@ -320,7 +398,7 @@ pub enum RelationType { pub struct CustomRelation(pub(super) JsonObject); impl CustomRelation { - pub(super) fn rel_type(&self) -> Option { + pub fn rel_type(&self) -> Option { Some(self.0.get("rel_type")?.as_str()?.into()) } } diff --git a/crates/ruma-events/src/relation/rel_serde.rs b/crates/ruma-events/src/relation/bundle_serde.rs similarity index 85% rename from crates/ruma-events/src/relation/rel_serde.rs rename to crates/ruma-events/src/relation/bundle_serde.rs index 9ac9e477..122b19f3 100644 --- a/crates/ruma-events/src/relation/rel_serde.rs +++ b/crates/ruma-events/src/relation/bundle_serde.rs @@ -1,7 +1,7 @@ use ruma_common::serde::Raw; use serde::{de::DeserializeOwned, Deserialize, Deserializer}; -use super::{BundledMessageLikeRelations, BundledThread, ReferenceChunk}; +use super::{BundledMessageLikeRelations, BundledThread, ReferenceChunk, AttachmentChunk}; #[derive(Deserialize)] struct BundledMessageLikeRelationsJsonRepr { @@ -11,6 +11,8 @@ struct BundledMessageLikeRelationsJsonRepr { thread: Option>, #[serde(rename = "m.reference")] reference: Option>, + #[serde(rename = "m.attachments")] + attachments: Option>, } impl<'de, E> Deserialize<'de> for BundledMessageLikeRelations @@ -21,7 +23,7 @@ where where D: Deserializer<'de>, { - let BundledMessageLikeRelationsJsonRepr { replace, thread, reference } = + let BundledMessageLikeRelationsJsonRepr { replace, thread, reference, attachments } = BundledMessageLikeRelationsJsonRepr::deserialize(deserializer)?; let (replace, has_invalid_replacement) = @@ -30,6 +32,6 @@ where Err(_) => (None, true), }; - Ok(BundledMessageLikeRelations { replace, has_invalid_replacement, thread, reference }) + Ok(BundledMessageLikeRelations { replace, has_invalid_replacement, thread, reference, attachments }) } } diff --git a/crates/ruma-events/src/relation/relation_serde.rs b/crates/ruma-events/src/relation/relation_serde.rs new file mode 100644 index 00000000..a12104ab --- /dev/null +++ b/crates/ruma-events/src/relation/relation_serde.rs @@ -0,0 +1,170 @@ +use ruma_common::OwnedEventId; +use serde::{de, Deserialize, Deserializer, Serialize}; +use super::{Relation, Replacement, Thread, Annotation, Attachment, Reply, Reference}; +use crate::relation::CustomRelation; + +/// Deserialize an event's `m.relations` field. +/// +/// Use it like this: +/// ``` +/// # use serde::{Deserialize, Serialize}; +/// use ruma_events::room::message::{deserialize_relation, MessageType, Relation}; +/// +/// #[derive(Deserialize, Serialize)] +/// struct MyEventContent { +/// #[serde( +/// flatten, +/// skip_serializing_if = "Vec::is_empty", +/// deserialize_with = "deserialize_relation" +/// )] +/// relates_to: Vec>, +/// } +/// ``` +pub fn deserialize_relation<'de, D, C>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, + C: Deserialize<'de>, +{ + let EventWithRelatesToDeHelper { relations, mut new_content } = + EventWithRelatesToDeHelper::::deserialize(deserializer)?; + + relations + .into_iter() + .map(|rel| match rel { + RelationDeHelper::Known(known) => match known { + KnownRelationDeHelper::Replacement(ReplacementJsonRepr { event_id }) => { + match new_content.take() { + Some(new_content) => { + Ok(Relation::Replacement(Replacement { event_id, new_content })) + }, + // maybe not the best error message if there are two m.replace relations + None => Err(de::Error::missing_field("m.new_content")), + } + } + // TODO: move "reply" into its own key, instead of revealing it to the server + KnownRelationDeHelper::Reply(reply) => { + Ok(Relation::Reply(reply)) + }, + KnownRelationDeHelper::Attachment(att) => Ok(Relation::Attachment(att)), + KnownRelationDeHelper::Annotation(ann) => Ok(Relation::Annotation(ann)), + KnownRelationDeHelper::Thread(thread) => Ok(Relation::Thread(thread)), + KnownRelationDeHelper::Reference(reference) => Ok(Relation::Reference(reference)), + } + // RelationDeHelper::Known(unknown) => Ok(Relation::_Custom(c)), + RelationDeHelper::Unknown(unknown) => Ok(Relation::_Custom(unknown)), + }) + .collect::, D::Error>>() + .map_err(de::Error::custom) +} + +impl Serialize for Relation +where + C: Clone + Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let (relations, new_content) = self.clone().into_parts(); + + EventWithRelatesToSerHelper { relations, new_content }.serialize(serializer) + } +} + +#[derive(Deserialize)] +pub(crate) struct EventWithRelatesToDeHelper { + #[serde(rename = "m.relations")] + relations: Vec, + + #[serde(rename = "m.new_content")] + new_content: Option, +} + +#[derive(Deserialize)] +#[serde(untagged)] +pub(crate) enum RelationDeHelper { + Known(KnownRelationDeHelper), + Unknown(CustomRelation), +} + +/// A replacement relation without `m.new_content`. +#[derive(Deserialize, Serialize)] +pub(crate) struct ReplacementJsonRepr { + event_id: OwnedEventId, +} + +#[derive(Deserialize)] +#[serde(tag = "rel_type")] +pub(crate) enum KnownRelationDeHelper { + #[serde(rename = "m.reply")] + Reply(Reply), + + #[serde(rename = "m.attachment")] + Attachment(Attachment), + + #[serde(rename = "m.annotation")] + Annotation(Annotation), + + #[serde(rename = "m.replace")] + Replacement(ReplacementJsonRepr), + + #[serde(rename = "m.thread")] + Thread(Thread), + + #[serde(rename = "m.reference")] + Reference(Reference), +} + +/// A relation, which associates new information to an existing event. +#[derive(Serialize)] +#[serde(tag = "rel_type")] +pub(super) enum RelationSerHelper { + /// An event that replaces another event. + #[serde(rename = "m.replace")] + Replacement(ReplacementJsonRepr), + + #[serde(rename = "m.reply")] + Reply(Reply), + + #[serde(rename = "m.attachment")] + Attachment(Attachment), + + #[serde(rename = "m.annotation")] + Annotation(Annotation), + + #[serde(rename = "m.thread")] + Thread(Thread), + + #[serde(rename = "m.reference")] + Reference(Reference), + + /// An unknown relation type. + #[serde(untagged)] + Custom(CustomRelation), +} + +#[derive(Serialize)] +pub(super) struct EventWithRelatesToSerHelper { + #[serde(rename = "m.relations")] + relations: RelationSerHelper, + + #[serde(rename = "m.new_content", skip_serializing_if = "Option::is_none")] + new_content: Option, +} + +impl Relation { + fn into_parts(self) -> (RelationSerHelper, Option) { + match self { + Relation::Replacement(Replacement { event_id, new_content }) => ( + RelationSerHelper::Replacement(ReplacementJsonRepr { event_id }), + Some(new_content), + ), + Relation::Thread(t) => (RelationSerHelper::Thread(t), None), + Relation::Reply(reply) => (RelationSerHelper::Reply(reply), None), + Relation::Attachment(att) => (RelationSerHelper::Attachment(att), None), + Relation::Annotation(ann) => (RelationSerHelper::Annotation(ann), None), + Relation::Reference(reference) => (RelationSerHelper::Reference(reference), None), + Relation::_Custom(c) => (RelationSerHelper::Custom(c), None), + } + } +} diff --git a/crates/ruma-events/src/room.rs b/crates/ruma-events/src/room.rs index 1017b801..53db3001 100644 --- a/crates/ruma-events/src/room.rs +++ b/crates/ruma-events/src/room.rs @@ -15,20 +15,19 @@ pub mod aliases; pub mod avatar; pub mod canonical_alias; pub mod create; -pub mod encrypted; pub mod encryption; pub mod guest_access; -pub mod history_visibility; +pub mod history_visibility; // TODO: remove pub mod join_rules; pub mod member; -pub mod message; pub mod name; pub mod pinned_events; pub mod power_levels; pub mod redaction; +// pub mod message; pub mod server_acl; -pub mod third_party_invite; -mod thumbnail_source_serde; +pub mod third_party_invite; // TODO: remove +pub(crate) mod thumbnail_source_serde; pub mod tombstone; pub mod topic; diff --git a/crates/ruma-events/src/room/encrypted/relation_serde.rs b/crates/ruma-events/src/room/encrypted/relation_serde.rs deleted file mode 100644 index 89516fab..00000000 --- a/crates/ruma-events/src/room/encrypted/relation_serde.rs +++ /dev/null @@ -1,100 +0,0 @@ -use ruma_common::{ - serde::{from_raw_json_value, JsonObject}, - OwnedEventId, -}; -use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize}; -use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue}; - -use super::{InReplyTo, Relation, Thread}; - -impl<'de> Deserialize<'de> for Relation { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let json = Box::::deserialize(deserializer)?; - - let RelationDeHelper { in_reply_to, rel_type } = from_raw_json_value(&json)?; - - let rel = match (in_reply_to, rel_type.as_deref()) { - (_, Some("m.thread")) => Relation::Thread(from_raw_json_value(&json)?), - (in_reply_to, Some("io.element.thread")) => { - let ThreadUnstableDeHelper { event_id, is_falling_back } = - from_raw_json_value(&json)?; - Relation::Thread(Thread { event_id, in_reply_to, is_falling_back }) - } - (_, Some("m.annotation")) => Relation::Annotation(from_raw_json_value(&json)?), - (_, Some("m.reference")) => Relation::Reference(from_raw_json_value(&json)?), - (_, Some("m.replace")) => Relation::Replacement(from_raw_json_value(&json)?), - (Some(in_reply_to), _) => Relation::Reply { in_reply_to }, - _ => Relation::_Custom(from_raw_json_value(&json)?), - }; - - Ok(rel) - } -} - -#[derive(Default, Deserialize)] -struct RelationDeHelper { - #[serde(rename = "m.in_reply_to")] - in_reply_to: Option, - - rel_type: Option, -} - -/// A thread relation without the reply fallback, with unstable names. -#[derive(Clone, Deserialize)] -struct ThreadUnstableDeHelper { - event_id: OwnedEventId, - - #[serde(rename = "io.element.show_reply", default)] - is_falling_back: bool, -} - -impl Relation { - pub(super) fn serialize_data(&self) -> JsonObject { - match serde_json::to_value(self).expect("relation serialization to succeed") { - JsonValue::Object(mut obj) => { - obj.remove("rel_type"); - obj - } - _ => panic!("all relations must serialize to objects"), - } - } -} - -impl Serialize for Relation { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - Relation::Reply { in_reply_to } => { - let mut st = serializer.serialize_struct("Relation", 1)?; - st.serialize_field("m.in_reply_to", in_reply_to)?; - st.end() - } - Relation::Replacement(data) => { - RelationSerHelper { rel_type: "m.replace", data }.serialize(serializer) - } - Relation::Reference(data) => { - RelationSerHelper { rel_type: "m.reference", data }.serialize(serializer) - } - Relation::Annotation(data) => { - RelationSerHelper { rel_type: "m.annotation", data }.serialize(serializer) - } - Relation::Thread(data) => { - RelationSerHelper { rel_type: "m.thread", data }.serialize(serializer) - } - Relation::_Custom(c) => c.serialize(serializer), - } - } -} - -#[derive(Serialize)] -struct RelationSerHelper<'a, T> { - rel_type: &'a str, - - #[serde(flatten)] - data: &'a T, -} diff --git a/crates/ruma-events/src/room/message.rs b/crates/ruma-events/src/room/message.rs index e5672cae..892556f4 100644 --- a/crates/ruma-events/src/room/message.rs +++ b/crates/ruma-events/src/room/message.rs @@ -19,27 +19,10 @@ use self::reply::OriginalEventData; #[cfg(feature = "html")] use self::sanitize::remove_plain_reply_fallback; use crate::{ - relation::{InReplyTo, Replacement, Thread}, + relation::{Reply, Replacement, Thread}, AnySyncTimelineEvent, Mentions, PrivOwnedStr, }; -mod audio; -mod content_serde; -mod emote; -mod file; -mod image; -mod key_verification_request; -mod location; -mod notice; -mod relation; -pub(crate) mod relation_serde; -mod reply; -pub mod sanitize; -mod server_notice; -mod text; -mod video; -mod without_relation; - #[cfg(feature = "unstable-msc3245-v1-compat")] pub use self::audio::{ UnstableAmplitude, UnstableAudioDetailsContentBlock, UnstableVoiceContentBlock, @@ -237,8 +220,6 @@ impl RoomMessageEventContent { self.relates_to = Some(Relation::Thread(Thread { event_id: thread_root, - in_reply_to: Some(InReplyTo { event_id: previous_message.event_id.clone() }), - is_falling_back: is_reply == ReplyWithinThread::No, })); self diff --git a/crates/ruma-events/src/room/message/audio.rs b/crates/ruma-events/src/room/message/audio.rs deleted file mode 100644 index edaa3bad..00000000 --- a/crates/ruma-events/src/room/message/audio.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::time::Duration; - -use js_int::UInt; -use ruma_common::OwnedMxcUri; -use serde::{Deserialize, Serialize}; - -use crate::room::{EncryptedFile, MediaSource}; - -/// The payload for an audio message. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[serde(tag = "msgtype", rename = "m.audio")] -pub struct AudioMessageEventContent { - /// The textual representation of this message. - pub body: String, - - /// The source of the audio clip. - #[serde(flatten)] - pub source: MediaSource, - - /// Metadata for the audio clip referred to in `source`. - #[serde(skip_serializing_if = "Option::is_none")] - pub info: Option>, - - /// Extensible event fallback data for audio messages, from the - /// [first version of MSC3245][msc]. - /// - /// [msc]: https://github.com/matrix-org/matrix-spec-proposals/blob/83f6c5b469c1d78f714e335dcaa25354b255ffa5/proposals/3245-voice-messages.md - #[cfg(feature = "unstable-msc3245-v1-compat")] - #[serde(rename = "org.matrix.msc1767.audio", skip_serializing_if = "Option::is_none")] - pub audio: Option, - - /// Extensible event fallback data for voice messages, from the - /// [first version of MSC3245][msc]. - /// - /// [msc]: https://github.com/matrix-org/matrix-spec-proposals/blob/83f6c5b469c1d78f714e335dcaa25354b255ffa5/proposals/3245-voice-messages.md - #[cfg(feature = "unstable-msc3245-v1-compat")] - #[serde(rename = "org.matrix.msc3245.voice", skip_serializing_if = "Option::is_none")] - pub voice: Option, -} - -impl AudioMessageEventContent { - /// Creates a new `AudioMessageEventContent` with the given body and source. - pub fn new(body: String, source: MediaSource) -> Self { - Self { - body, - source, - info: None, - #[cfg(feature = "unstable-msc3245-v1-compat")] - audio: None, - #[cfg(feature = "unstable-msc3245-v1-compat")] - voice: None, - } - } - - /// Creates a new non-encrypted `AudioMessageEventContent` with the given bod and url. - pub fn plain(body: String, url: OwnedMxcUri) -> Self { - Self::new(body, MediaSource::Plain(url)) - } - - /// Creates a new encrypted `AudioMessageEventContent` with the given body and encrypted - /// file. - pub fn encrypted(body: String, file: EncryptedFile) -> Self { - Self::new(body, MediaSource::Encrypted(Box::new(file))) - } - - /// Creates a new `AudioMessageEventContent` from `self` with the `info` field set to the given - /// value. - /// - /// Since the field is public, you can also assign to it directly. This method merely acts - /// as a shorthand for that, because it is very common to set this field. - pub fn info(self, info: impl Into>>) -> Self { - Self { info: info.into(), ..self } - } -} - -/// Metadata about an audio clip. -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct AudioInfo { - /// The duration of the audio in milliseconds. - #[serde( - with = "ruma_common::serde::duration::opt_ms", - default, - skip_serializing_if = "Option::is_none" - )] - pub duration: Option, - - /// The mimetype of the audio, e.g. "audio/aac". - #[serde(skip_serializing_if = "Option::is_none")] - pub mimetype: Option, - - /// The size of the audio clip in bytes. - #[serde(skip_serializing_if = "Option::is_none")] - pub size: Option, -} - -impl AudioInfo { - /// Creates an empty `AudioInfo`. - pub fn new() -> Self { - Self::default() - } -} - -/// Extensible event fallback data for audio messages, from the -/// [first version of MSC3245][msc]. -/// -/// [msc]: https://github.com/matrix-org/matrix-spec-proposals/blob/83f6c5b469c1d78f714e335dcaa25354b255ffa5/proposals/3245-voice-messages.md -#[cfg(feature = "unstable-msc3245-v1-compat")] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct UnstableAudioDetailsContentBlock { - /// The duration of the audio in milliseconds. - /// - /// Note that the MSC says this should be in seconds but for compatibility with the Element - /// clients, this uses milliseconds. - #[serde(with = "ruma_common::serde::duration::ms")] - pub duration: Duration, - - /// The waveform representation of the audio content, if any. - /// - /// This is optional and defaults to an empty array. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub waveform: Vec, -} - -#[cfg(feature = "unstable-msc3245-v1-compat")] -impl UnstableAudioDetailsContentBlock { - /// Creates a new `UnstableAudioDetailsContentBlock ` with the given duration and waveform. - pub fn new(duration: Duration, waveform: Vec) -> Self { - Self { duration, waveform } - } -} - -/// Extensible event fallback data for voice messages, from the -/// [first version of MSC3245][msc]. -/// -/// [msc]: https://github.com/matrix-org/matrix-spec-proposals/blob/83f6c5b469c1d78f714e335dcaa25354b255ffa5/proposals/3245-voice-messages.md -#[cfg(feature = "unstable-msc3245-v1-compat")] -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct UnstableVoiceContentBlock {} - -#[cfg(feature = "unstable-msc3245-v1-compat")] -impl UnstableVoiceContentBlock { - /// Creates a new `UnstableVoiceContentBlock`. - pub fn new() -> Self { - Self::default() - } -} - -/// The unstable version of the amplitude of a waveform sample. -/// -/// Must be an integer between 0 and 1024. -#[cfg(feature = "unstable-msc3245-v1-compat")] -#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)] -pub struct UnstableAmplitude(UInt); - -#[cfg(feature = "unstable-msc3245-v1-compat")] -impl UnstableAmplitude { - /// The smallest value that can be represented by this type, 0. - pub const MIN: u16 = 0; - - /// The largest value that can be represented by this type, 1024. - pub const MAX: u16 = 1024; - - /// Creates a new `UnstableAmplitude` with the given value. - /// - /// It will saturate if it is bigger than [`UnstableAmplitude::MAX`]. - pub fn new(value: u16) -> Self { - Self(value.min(Self::MAX).into()) - } - - /// The value of this `UnstableAmplitude`. - pub fn get(&self) -> UInt { - self.0 - } -} - -#[cfg(feature = "unstable-msc3245-v1-compat")] -impl From for UnstableAmplitude { - fn from(value: u16) -> Self { - Self::new(value) - } -} - -#[cfg(feature = "unstable-msc3245-v1-compat")] -impl<'de> Deserialize<'de> for UnstableAmplitude { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let uint = UInt::deserialize(deserializer)?; - Ok(Self(uint.min(Self::MAX.into()))) - } -} diff --git a/crates/ruma-events/src/room/message/content_serde.rs b/crates/ruma-events/src/room/message/content_serde.rs deleted file mode 100644 index cd762fbd..00000000 --- a/crates/ruma-events/src/room/message/content_serde.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! `Deserialize` implementation for RoomMessageEventContent and MessageType. - -use ruma_common::serde::from_raw_json_value; -use serde::{de, Deserialize}; -use serde_json::value::RawValue as RawJsonValue; - -use super::{ - relation_serde::deserialize_relation, MessageType, RoomMessageEventContent, - RoomMessageEventContentWithoutRelation, -}; -use crate::Mentions; - -impl<'de> Deserialize<'de> for RoomMessageEventContent { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - let json = Box::::deserialize(deserializer)?; - - let mut deserializer = serde_json::Deserializer::from_str(json.get()); - let relates_to = deserialize_relation(&mut deserializer).map_err(de::Error::custom)?; - - let MentionsDeHelper { mentions } = from_raw_json_value(&json)?; - - Ok(Self { msgtype: from_raw_json_value(&json)?, relates_to, mentions }) - } -} - -impl<'de> Deserialize<'de> for RoomMessageEventContentWithoutRelation { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - let json = Box::::deserialize(deserializer)?; - - let MentionsDeHelper { mentions } = from_raw_json_value(&json)?; - - Ok(Self { msgtype: from_raw_json_value(&json)?, mentions }) - } -} - -#[derive(Deserialize)] -struct MentionsDeHelper { - #[serde(rename = "m.mentions")] - mentions: Option, -} - -/// Helper struct to determine the msgtype from a `serde_json::value::RawValue` -#[derive(Debug, Deserialize)] -struct MessageTypeDeHelper { - /// The message type field - msgtype: String, -} - -impl<'de> Deserialize<'de> for MessageType { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - let json = Box::::deserialize(deserializer)?; - let MessageTypeDeHelper { msgtype } = from_raw_json_value(&json)?; - - Ok(match msgtype.as_ref() { - "m.audio" => Self::Audio(from_raw_json_value(&json)?), - "m.emote" => Self::Emote(from_raw_json_value(&json)?), - "m.file" => Self::File(from_raw_json_value(&json)?), - "m.image" => Self::Image(from_raw_json_value(&json)?), - "m.location" => Self::Location(from_raw_json_value(&json)?), - "m.notice" => Self::Notice(from_raw_json_value(&json)?), - "m.server_notice" => Self::ServerNotice(from_raw_json_value(&json)?), - "m.text" => Self::Text(from_raw_json_value(&json)?), - "m.video" => Self::Video(from_raw_json_value(&json)?), - "m.key.verification.request" => Self::VerificationRequest(from_raw_json_value(&json)?), - _ => Self::_Custom(from_raw_json_value(&json)?), - }) - } -} - -#[allow(unreachable_pub)] // https://github.com/rust-lang/rust/issues/112615 -#[cfg(feature = "unstable-msc3488")] -pub(in super::super) mod msc3488 { - use ruma_common::MilliSecondsSinceUnixEpoch; - use serde::{Deserialize, Serialize}; - - use crate::{ - location::{AssetContent, LocationContent}, - message::historical_serde::MessageContentBlock, - room::message::{LocationInfo, LocationMessageEventContent}, - }; - - /// Deserialize helper type for `LocationMessageEventContent` with unstable fields from msc3488. - #[derive(Serialize, Deserialize)] - #[serde(tag = "msgtype", rename = "m.location")] - pub(in super::super) struct LocationMessageEventContentSerDeHelper { - pub body: String, - - pub geo_uri: String, - - #[serde(skip_serializing_if = "Option::is_none")] - pub info: Option>, - - #[serde(flatten)] - pub message: Option, - - #[serde(rename = "org.matrix.msc3488.location", skip_serializing_if = "Option::is_none")] - pub location: Option, - - #[serde(rename = "org.matrix.msc3488.asset", skip_serializing_if = "Option::is_none")] - pub asset: Option, - - #[serde(rename = "org.matrix.msc3488.ts", skip_serializing_if = "Option::is_none")] - pub ts: Option, - } - - impl From for LocationMessageEventContentSerDeHelper { - fn from(value: LocationMessageEventContent) -> Self { - let LocationMessageEventContent { body, geo_uri, info, message, location, asset, ts } = - value; - - Self { body, geo_uri, info, message: message.map(Into::into), location, asset, ts } - } - } - - impl From for LocationMessageEventContent { - fn from(value: LocationMessageEventContentSerDeHelper) -> Self { - let LocationMessageEventContentSerDeHelper { - body, - geo_uri, - info, - message, - location, - asset, - ts, - } = value; - - LocationMessageEventContent { - body, - geo_uri, - info, - message: message.map(Into::into), - location, - asset, - ts, - } - } - } -} diff --git a/crates/ruma-events/src/room/message/emote.rs b/crates/ruma-events/src/room/message/emote.rs deleted file mode 100644 index 951e0a24..00000000 --- a/crates/ruma-events/src/room/message/emote.rs +++ /dev/null @@ -1,43 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::FormattedBody; - -/// The payload for an emote message. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[serde(tag = "msgtype", rename = "m.emote")] -pub struct EmoteMessageEventContent { - /// The emote action to perform. - pub body: String, - - /// Formatted form of the message `body`. - #[serde(flatten)] - pub formatted: Option, -} - -impl EmoteMessageEventContent { - /// A convenience constructor to create a plain-text emote. - pub fn plain(body: impl Into) -> Self { - let body = body.into(); - Self { body, formatted: None } - } - - /// A convenience constructor to create an html emote message. - pub fn html(body: impl Into, html_body: impl Into) -> Self { - let body = body.into(); - Self { body, formatted: Some(FormattedBody::html(html_body)) } - } - - /// A convenience constructor to create a markdown emote. - /// - /// Returns an html emote message if some markdown formatting was detected, otherwise returns a - /// plain-text emote. - #[cfg(feature = "markdown")] - pub fn markdown(body: impl AsRef + Into) -> Self { - if let Some(formatted) = FormattedBody::markdown(&body) { - Self::html(body, formatted.body) - } else { - Self::plain(body) - } - } -} diff --git a/crates/ruma-events/src/room/message/file.rs b/crates/ruma-events/src/room/message/file.rs deleted file mode 100644 index e3d18cc4..00000000 --- a/crates/ruma-events/src/room/message/file.rs +++ /dev/null @@ -1,96 +0,0 @@ -use js_int::UInt; -use ruma_common::OwnedMxcUri; -use serde::{Deserialize, Serialize}; - -use crate::room::{EncryptedFile, MediaSource, ThumbnailInfo}; - -/// The payload for a file message. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[serde(tag = "msgtype", rename = "m.file")] -pub struct FileMessageEventContent { - /// A human-readable description of the file. - /// - /// This is recommended to be the filename of the original upload. - pub body: String, - - /// The original filename of the uploaded file. - #[serde(skip_serializing_if = "Option::is_none")] - pub filename: Option, - - /// The source of the file. - #[serde(flatten)] - pub source: MediaSource, - - /// Metadata about the file referred to in `source`. - #[serde(skip_serializing_if = "Option::is_none")] - pub info: Option>, -} - -impl FileMessageEventContent { - /// Creates a new `FileMessageEventContent` with the given body and source. - pub fn new(body: String, source: MediaSource) -> Self { - Self { body, filename: None, source, info: None } - } - - /// Creates a new non-encrypted `FileMessageEventContent` with the given body and url. - pub fn plain(body: String, url: OwnedMxcUri) -> Self { - Self::new(body, MediaSource::Plain(url)) - } - - /// Creates a new encrypted `FileMessageEventContent` with the given body and encrypted - /// file. - pub fn encrypted(body: String, file: EncryptedFile) -> Self { - Self::new(body, MediaSource::Encrypted(Box::new(file))) - } - - /// Creates a new `FileMessageEventContent` from `self` with the `filename` field set to the - /// given value. - /// - /// Since the field is public, you can also assign to it directly. This method merely acts - /// as a shorthand for that, because it is very common to set this field. - pub fn filename(self, filename: impl Into>) -> Self { - Self { filename: filename.into(), ..self } - } - - /// Creates a new `FileMessageEventContent` from `self` with the `info` field set to the given - /// value. - /// - /// Since the field is public, you can also assign to it directly. This method merely acts - /// as a shorthand for that, because it is very common to set this field. - pub fn info(self, info: impl Into>>) -> Self { - Self { info: info.into(), ..self } - } -} - -/// Metadata about a file. -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct FileInfo { - /// The mimetype of the file, e.g. "application/msword". - #[serde(skip_serializing_if = "Option::is_none")] - pub mimetype: Option, - - /// The size of the file in bytes. - #[serde(skip_serializing_if = "Option::is_none")] - pub size: Option, - - /// Metadata about the image referred to in `thumbnail_source`. - #[serde(skip_serializing_if = "Option::is_none")] - pub thumbnail_info: Option>, - - /// The source of the thumbnail of the file. - #[serde( - flatten, - with = "crate::room::thumbnail_source_serde", - skip_serializing_if = "Option::is_none" - )] - pub thumbnail_source: Option, -} - -impl FileInfo { - /// Creates an empty `FileInfo`. - pub fn new() -> Self { - Self::default() - } -} diff --git a/crates/ruma-events/src/room/message/image.rs b/crates/ruma-events/src/room/message/image.rs deleted file mode 100644 index 8ffff508..00000000 --- a/crates/ruma-events/src/room/message/image.rs +++ /dev/null @@ -1,51 +0,0 @@ -use ruma_common::OwnedMxcUri; -use serde::{Deserialize, Serialize}; - -use crate::room::{EncryptedFile, ImageInfo, MediaSource}; - -/// The payload for an image message. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[serde(tag = "msgtype", rename = "m.image")] -pub struct ImageMessageEventContent { - /// A textual representation of the image. - /// - /// Could be the alt text of the image, the filename of the image, or some kind of content - /// description for accessibility e.g. "image attachment". - pub body: String, - - /// The source of the image. - #[serde(flatten)] - pub source: MediaSource, - - /// Metadata about the image referred to in `source`. - #[serde(skip_serializing_if = "Option::is_none")] - pub info: Option>, -} - -impl ImageMessageEventContent { - /// Creates a new `ImageMessageEventContent` with the given body and source. - pub fn new(body: String, source: MediaSource) -> Self { - Self { body, source, info: None } - } - - /// Creates a new non-encrypted `ImageMessageEventContent` with the given body and url. - pub fn plain(body: String, url: OwnedMxcUri) -> Self { - Self::new(body, MediaSource::Plain(url)) - } - - /// Creates a new encrypted `ImageMessageEventContent` with the given body and encrypted - /// file. - pub fn encrypted(body: String, file: EncryptedFile) -> Self { - Self::new(body, MediaSource::Encrypted(Box::new(file))) - } - - /// Creates a new `ImageMessageEventContent` from `self` with the `info` field set to the given - /// value. - /// - /// Since the field is public, you can also assign to it directly. This method merely acts - /// as a shorthand for that, because it is very common to set this field. - pub fn info(self, info: impl Into>>) -> Self { - Self { info: info.into(), ..self } - } -} diff --git a/crates/ruma-events/src/room/message/key_verification_request.rs b/crates/ruma-events/src/room/message/key_verification_request.rs deleted file mode 100644 index 0e32e3f8..00000000 --- a/crates/ruma-events/src/room/message/key_verification_request.rs +++ /dev/null @@ -1,52 +0,0 @@ -use ruma_common::{OwnedDeviceId, OwnedUserId}; -use serde::{Deserialize, Serialize}; - -use super::FormattedBody; -use crate::key::verification::VerificationMethod; - -/// The payload for a key verification request message. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[serde(tag = "msgtype", rename = "m.key.verification.request")] -pub struct KeyVerificationRequestEventContent { - /// A fallback message to alert users that their client does not support the key verification - /// framework. - /// - /// Clients that do support the key verification framework should hide the body and instead - /// present the user with an interface to accept or reject the key verification. - pub body: String, - - /// Formatted form of the `body`. - /// - /// As with the `body`, clients that do support the key verification framework should hide the - /// formatted body and instead present the user with an interface to accept or reject the key - /// verification. - #[serde(flatten)] - pub formatted: Option, - - /// The verification methods supported by the sender. - pub methods: Vec, - - /// The device ID which is initiating the request. - pub from_device: OwnedDeviceId, - - /// The user ID which should receive the request. - /// - /// Users should only respond to verification requests if they are named in this field. Users - /// who are not named in this field and who did not send this event should ignore all other - /// events that have a `m.reference` relationship with this event. - pub to: OwnedUserId, -} - -impl KeyVerificationRequestEventContent { - /// Creates a new `KeyVerificationRequestEventContent` with the given body, method, device - /// and user ID. - pub fn new( - body: String, - methods: Vec, - from_device: OwnedDeviceId, - to: OwnedUserId, - ) -> Self { - Self { body, formatted: None, methods, from_device, to } - } -} diff --git a/crates/ruma-events/src/room/message/location.rs b/crates/ruma-events/src/room/message/location.rs deleted file mode 100644 index ef9f656f..00000000 --- a/crates/ruma-events/src/room/message/location.rs +++ /dev/null @@ -1,137 +0,0 @@ -#[cfg(feature = "unstable-msc3488")] -use ruma_common::MilliSecondsSinceUnixEpoch; -use serde::{Deserialize, Serialize}; - -use crate::room::{MediaSource, ThumbnailInfo}; -#[cfg(feature = "unstable-msc3488")] -use crate::{ - location::{AssetContent, AssetType, LocationContent}, - message::{TextContentBlock, TextRepresentation}, -}; - -/// The payload for a location message. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[serde(tag = "msgtype", rename = "m.location")] -#[cfg_attr( - feature = "unstable-msc3488", - serde( - from = "super::content_serde::msc3488::LocationMessageEventContentSerDeHelper", - into = "super::content_serde::msc3488::LocationMessageEventContentSerDeHelper" - ) -)] -pub struct LocationMessageEventContent { - /// A description of the location e.g. "Big Ben, London, UK", or some kind of content - /// description for accessibility, e.g. "location attachment". - pub body: String, - - /// A geo URI representing the location. - pub geo_uri: String, - - /// Info about the location being represented. - #[serde(skip_serializing_if = "Option::is_none")] - pub info: Option>, - - /// Extensible-event text representation of the message. - /// - /// If present, this should be preferred over the `body` field. - #[cfg(feature = "unstable-msc3488")] - pub message: Option, - - /// Extensible-event location info of the message. - /// - /// If present, this should be preferred over the `geo_uri` field. - #[cfg(feature = "unstable-msc3488")] - pub location: Option, - - /// Extensible-event asset this message refers to. - #[cfg(feature = "unstable-msc3488")] - pub asset: Option, - - /// Extensible-event timestamp this message refers to. - #[cfg(feature = "unstable-msc3488")] - pub ts: Option, -} - -impl LocationMessageEventContent { - /// Creates a new `LocationMessageEventContent` with the given body and geo URI. - pub fn new(body: String, geo_uri: String) -> Self { - Self { - #[cfg(feature = "unstable-msc3488")] - message: Some(vec![TextRepresentation::plain(&body)].into()), - #[cfg(feature = "unstable-msc3488")] - location: Some(LocationContent::new(geo_uri.clone())), - #[cfg(feature = "unstable-msc3488")] - asset: Some(AssetContent::default()), - #[cfg(feature = "unstable-msc3488")] - ts: None, - body, - geo_uri, - info: None, - } - } - - /// Set the asset type of this `LocationMessageEventContent`. - #[cfg(feature = "unstable-msc3488")] - pub fn with_asset_type(mut self, asset: AssetType) -> Self { - self.asset = Some(AssetContent { type_: asset }); - self - } - - /// Set the timestamp of this `LocationMessageEventContent`. - #[cfg(feature = "unstable-msc3488")] - pub fn with_ts(mut self, ts: MilliSecondsSinceUnixEpoch) -> Self { - self.ts = Some(ts); - self - } - - /// Get the `geo:` URI of this `LocationMessageEventContent`. - pub fn geo_uri(&self) -> &str { - #[cfg(feature = "unstable-msc3488")] - if let Some(uri) = self.location.as_ref().map(|l| &l.uri) { - return uri; - } - - &self.geo_uri - } - - /// Get the plain text representation of this `LocationMessageEventContent`. - pub fn plain_text_representation(&self) -> &str { - #[cfg(feature = "unstable-msc3488")] - if let Some(text) = self.message.as_ref().and_then(|m| m.find_plain()) { - return text; - } - - &self.body - } - - /// Get the asset type of this `LocationMessageEventContent`. - #[cfg(feature = "unstable-msc3488")] - pub fn asset_type(&self) -> AssetType { - self.asset.as_ref().map(|a| a.type_.clone()).unwrap_or_default() - } -} - -/// Thumbnail info associated with a location. -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct LocationInfo { - /// The source of a thumbnail of the location. - #[serde( - flatten, - with = "crate::room::thumbnail_source_serde", - skip_serializing_if = "Option::is_none" - )] - pub thumbnail_source: Option, - - /// Metadata about the image referred to in `thumbnail_source. - #[serde(skip_serializing_if = "Option::is_none")] - pub thumbnail_info: Option>, -} - -impl LocationInfo { - /// Creates an empty `LocationInfo`. - pub fn new() -> Self { - Self::default() - } -} diff --git a/crates/ruma-events/src/room/message/notice.rs b/crates/ruma-events/src/room/message/notice.rs deleted file mode 100644 index ff39bec6..00000000 --- a/crates/ruma-events/src/room/message/notice.rs +++ /dev/null @@ -1,43 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::FormattedBody; - -/// The payload for a notice message. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[serde(tag = "msgtype", rename = "m.notice")] -pub struct NoticeMessageEventContent { - /// The notice text. - pub body: String, - - /// Formatted form of the message `body`. - #[serde(flatten)] - pub formatted: Option, -} - -impl NoticeMessageEventContent { - /// A convenience constructor to create a plain text notice. - pub fn plain(body: impl Into) -> Self { - let body = body.into(); - Self { body, formatted: None } - } - - /// A convenience constructor to create an html notice. - pub fn html(body: impl Into, html_body: impl Into) -> Self { - let body = body.into(); - Self { body, formatted: Some(FormattedBody::html(html_body)) } - } - - /// A convenience constructor to create a markdown notice. - /// - /// Returns an html notice if some markdown formatting was detected, otherwise returns a plain - /// text notice. - #[cfg(feature = "markdown")] - pub fn markdown(body: impl AsRef + Into) -> Self { - if let Some(formatted) = FormattedBody::markdown(&body) { - Self::html(body, formatted.body) - } else { - Self::plain(body) - } - } -} diff --git a/crates/ruma-events/src/room/message/relation.rs b/crates/ruma-events/src/room/message/relation.rs deleted file mode 100644 index 58654516..00000000 --- a/crates/ruma-events/src/room/message/relation.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::borrow::Cow; - -use ruma_common::serde::JsonObject; - -use crate::relation::{CustomRelation, InReplyTo, RelationType, Replacement, Thread}; - -/// Message event relationship. -#[derive(Clone, Debug)] -#[allow(clippy::manual_non_exhaustive)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub enum Relation { - /// An `m.in_reply_to` relation indicating that the event is a reply to another event. - Reply { - /// Information about another message being replied to. - in_reply_to: InReplyTo, - }, - - /// An event that replaces another event. - Replacement(Replacement), - - /// An event that belongs to a thread. - Thread(Thread), - - #[doc(hidden)] - _Custom(CustomRelation), -} - -impl Relation { - /// The type of this `Relation`. - /// - /// Returns an `Option` because the `Reply` relation does not have a`rel_type` field. - pub fn rel_type(&self) -> Option { - match self { - Relation::Reply { .. } => None, - Relation::Replacement(_) => Some(RelationType::Replacement), - Relation::Thread(_) => Some(RelationType::Thread), - Relation::_Custom(c) => c.rel_type(), - } - } - - /// The associated data. - /// - /// The returned JSON object holds the contents of `m.relates_to`, including `rel_type` and - /// `event_id` if present, but not things like `m.new_content` for `m.replace` relations that - /// live next to `m.relates_to`. - /// - /// Prefer to use the public variants of `Relation` where possible; this method is meant to - /// be used for custom relations only. - pub fn data(&self) -> Cow<'_, JsonObject> - where - C: Clone, - { - if let Relation::_Custom(CustomRelation(data)) = self { - Cow::Borrowed(data) - } else { - Cow::Owned(self.serialize_data()) - } - } -} - -/// Message event relationship, except a replacement. -#[derive(Clone, Debug)] -#[allow(clippy::manual_non_exhaustive)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub enum RelationWithoutReplacement { - /// An `m.in_reply_to` relation indicating that the event is a reply to another event. - Reply { - /// Information about another message being replied to. - in_reply_to: InReplyTo, - }, - - /// An event that belongs to a thread. - Thread(Thread), - - #[doc(hidden)] - _Custom(CustomRelation), -} - -impl RelationWithoutReplacement { - /// The type of this `Relation`. - /// - /// Returns an `Option` because the `Reply` relation does not have a`rel_type` field. - pub fn rel_type(&self) -> Option { - match self { - Self::Reply { .. } => None, - Self::Thread(_) => Some(RelationType::Thread), - Self::_Custom(c) => c.rel_type(), - } - } - - /// The associated data. - /// - /// The returned JSON object holds the contents of `m.relates_to`, including `rel_type` and - /// `event_id` if present, but not things like `m.new_content` for `m.replace` relations that - /// live next to `m.relates_to`. - /// - /// Prefer to use the public variants of `Relation` where possible; this method is meant to - /// be used for custom relations only. - pub fn data(&self) -> Cow<'_, JsonObject> { - if let Self::_Custom(CustomRelation(data)) = self { - Cow::Borrowed(data) - } else { - Cow::Owned(self.serialize_data()) - } - } -} - -impl TryFrom> for RelationWithoutReplacement { - type Error = Replacement; - - fn try_from(value: Relation) -> Result { - let rel = match value { - Relation::Reply { in_reply_to } => Self::Reply { in_reply_to }, - Relation::Replacement(r) => return Err(r), - Relation::Thread(t) => Self::Thread(t), - Relation::_Custom(c) => Self::_Custom(c), - }; - - Ok(rel) - } -} diff --git a/crates/ruma-events/src/room/message/relation_serde.rs b/crates/ruma-events/src/room/message/relation_serde.rs deleted file mode 100644 index e5bfb657..00000000 --- a/crates/ruma-events/src/room/message/relation_serde.rs +++ /dev/null @@ -1,253 +0,0 @@ -use ruma_common::{serde::JsonObject, OwnedEventId}; -use serde::{de, Deserialize, Deserializer, Serialize}; -use serde_json::Value as JsonValue; - -use super::{InReplyTo, Relation, RelationWithoutReplacement, Replacement, Thread}; -use crate::relation::CustomRelation; - -/// Deserialize an event's `relates_to` field. -/// -/// Use it like this: -/// ``` -/// # use serde::{Deserialize, Serialize}; -/// use ruma_events::room::message::{deserialize_relation, MessageType, Relation}; -/// -/// #[derive(Deserialize, Serialize)] -/// struct MyEventContent { -/// #[serde( -/// flatten, -/// skip_serializing_if = "Option::is_none", -/// deserialize_with = "deserialize_relation" -/// )] -/// relates_to: Option>, -/// } -/// ``` -pub fn deserialize_relation<'de, D, C>(deserializer: D) -> Result>, D::Error> -where - D: Deserializer<'de>, - C: Deserialize<'de>, -{ - let EventWithRelatesToDeHelper { relates_to, new_content } = - EventWithRelatesToDeHelper::deserialize(deserializer)?; - let Some(relates_to) = relates_to else { - return Ok(None); - }; - - let RelatesToDeHelper { in_reply_to, relation } = relates_to; - - let rel = match relation { - RelationDeHelper::Known(relation) => match relation { - KnownRelationDeHelper::Replacement(ReplacementJsonRepr { event_id }) => { - match new_content { - Some(new_content) => { - Relation::Replacement(Replacement { event_id, new_content }) - } - None => return Err(de::Error::missing_field("m.new_content")), - } - } - KnownRelationDeHelper::Thread(ThreadDeHelper { event_id, is_falling_back }) - | KnownRelationDeHelper::ThreadUnstable(ThreadUnstableDeHelper { - event_id, - is_falling_back, - }) => Relation::Thread(Thread { event_id, in_reply_to, is_falling_back }), - }, - RelationDeHelper::Unknown(c) => { - if let Some(in_reply_to) = in_reply_to { - Relation::Reply { in_reply_to } - } else { - Relation::_Custom(c) - } - } - }; - - Ok(Some(rel)) -} - -impl Serialize for Relation -where - C: Clone + Serialize, -{ - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let (relates_to, new_content) = self.clone().into_parts(); - - EventWithRelatesToSerHelper { relates_to, new_content }.serialize(serializer) - } -} - -#[derive(Deserialize)] -pub(crate) struct EventWithRelatesToDeHelper { - #[serde(rename = "m.relates_to")] - relates_to: Option, - - #[serde(rename = "m.new_content")] - new_content: Option, -} - -#[derive(Deserialize)] -pub(crate) struct RelatesToDeHelper { - #[serde(rename = "m.in_reply_to")] - in_reply_to: Option, - - #[serde(flatten)] - relation: RelationDeHelper, -} - -#[derive(Deserialize)] -#[serde(untagged)] -pub(crate) enum RelationDeHelper { - Known(KnownRelationDeHelper), - Unknown(CustomRelation), -} - -#[derive(Deserialize)] -#[serde(tag = "rel_type")] -pub(crate) enum KnownRelationDeHelper { - #[serde(rename = "m.replace")] - Replacement(ReplacementJsonRepr), - - #[serde(rename = "m.thread")] - Thread(ThreadDeHelper), - - #[serde(rename = "io.element.thread")] - ThreadUnstable(ThreadUnstableDeHelper), -} - -/// A replacement relation without `m.new_content`. -#[derive(Deserialize, Serialize)] -pub(crate) struct ReplacementJsonRepr { - event_id: OwnedEventId, -} - -/// A thread relation without the reply fallback, with stable names. -#[derive(Deserialize)] -pub(crate) struct ThreadDeHelper { - event_id: OwnedEventId, - - #[serde(default)] - is_falling_back: bool, -} - -/// A thread relation without the reply fallback, with unstable names. -#[derive(Deserialize)] -pub(crate) struct ThreadUnstableDeHelper { - event_id: OwnedEventId, - - #[serde(rename = "io.element.show_reply", default)] - is_falling_back: bool, -} - -#[derive(Serialize)] -pub(super) struct EventWithRelatesToSerHelper { - #[serde(rename = "m.relates_to")] - relates_to: RelationSerHelper, - - #[serde(rename = "m.new_content", skip_serializing_if = "Option::is_none")] - new_content: Option, -} - -/// A relation, which associates new information to an existing event. -#[derive(Serialize)] -#[serde(tag = "rel_type")] -pub(super) enum RelationSerHelper { - /// An event that replaces another event. - #[serde(rename = "m.replace")] - Replacement(ReplacementJsonRepr), - - /// An event that belongs to a thread, with stable names. - #[serde(rename = "m.thread")] - Thread(Thread), - - /// An unknown relation type. - #[serde(untagged)] - Custom(CustomSerHelper), -} - -impl Relation { - fn into_parts(self) -> (RelationSerHelper, Option) { - match self { - Relation::Replacement(Replacement { event_id, new_content }) => ( - RelationSerHelper::Replacement(ReplacementJsonRepr { event_id }), - Some(new_content), - ), - Relation::Reply { in_reply_to } => { - (RelationSerHelper::Custom(in_reply_to.into()), None) - } - Relation::Thread(t) => (RelationSerHelper::Thread(t), None), - Relation::_Custom(c) => (RelationSerHelper::Custom(c.into()), None), - } - } - - pub(super) fn serialize_data(&self) -> JsonObject - where - C: Clone, - { - let (relates_to, _) = self.clone().into_parts(); - - match serde_json::to_value(relates_to).expect("relation serialization to succeed") { - JsonValue::Object(mut obj) => { - obj.remove("rel_type"); - obj - } - _ => panic!("all relations must serialize to objects"), - } - } -} - -#[derive(Default, Serialize)] -pub(super) struct CustomSerHelper { - #[serde(rename = "m.in_reply_to", skip_serializing_if = "Option::is_none")] - in_reply_to: Option, - - #[serde(flatten, skip_serializing_if = "JsonObject::is_empty")] - data: JsonObject, -} - -impl From for CustomSerHelper { - fn from(value: InReplyTo) -> Self { - Self { in_reply_to: Some(value), data: JsonObject::new() } - } -} - -impl From for CustomSerHelper { - fn from(CustomRelation(data): CustomRelation) -> Self { - Self { in_reply_to: None, data } - } -} - -impl From<&RelationWithoutReplacement> for RelationSerHelper { - fn from(value: &RelationWithoutReplacement) -> Self { - match value.clone() { - RelationWithoutReplacement::Reply { in_reply_to } => { - RelationSerHelper::Custom(in_reply_to.into()) - } - RelationWithoutReplacement::Thread(t) => RelationSerHelper::Thread(t), - RelationWithoutReplacement::_Custom(c) => RelationSerHelper::Custom(c.into()), - } - } -} - -impl Serialize for RelationWithoutReplacement { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - RelationSerHelper::from(self).serialize(serializer) - } -} - -impl RelationWithoutReplacement { - pub(super) fn serialize_data(&self) -> JsonObject { - let helper = RelationSerHelper::from(self); - - match serde_json::to_value(helper).expect("relation serialization to succeed") { - JsonValue::Object(mut obj) => { - obj.remove("rel_type"); - obj - } - _ => panic!("all relations must serialize to objects"), - } - } -} diff --git a/crates/ruma-events/src/room/message/reply.rs b/crates/ruma-events/src/room/message/reply.rs deleted file mode 100644 index 0f08e034..00000000 --- a/crates/ruma-events/src/room/message/reply.rs +++ /dev/null @@ -1,179 +0,0 @@ -use std::fmt::{self, Write}; - -use ruma_common::{EventId, RoomId, UserId}; -#[cfg(feature = "html")] -use ruma_html::Html; - -use super::{ - sanitize::remove_plain_reply_fallback, FormattedBody, MessageType, OriginalRoomMessageEvent, - Relation, -}; - -pub(super) struct OriginalEventData<'a> { - pub(super) body: &'a str, - pub(super) formatted: Option<&'a FormattedBody>, - pub(super) is_emote: bool, - pub(super) is_reply: bool, - pub(super) room_id: &'a RoomId, - pub(super) event_id: &'a EventId, - pub(super) sender: &'a UserId, -} - -impl<'a> From<&'a OriginalRoomMessageEvent> for OriginalEventData<'a> { - fn from(message: &'a OriginalRoomMessageEvent) -> Self { - let OriginalRoomMessageEvent { room_id, event_id, sender, content, .. } = message; - let is_reply = matches!(content.relates_to, Some(Relation::Reply { .. })); - - let (body, formatted, is_emote) = match &content.msgtype { - MessageType::Audio(_) => ("sent an audio file.", None, false), - MessageType::Emote(c) => (&*c.body, c.formatted.as_ref(), true), - MessageType::File(_) => ("sent a file.", None, false), - MessageType::Image(_) => ("sent an image.", None, false), - MessageType::Location(_) => ("sent a location.", None, false), - MessageType::Notice(c) => (&*c.body, c.formatted.as_ref(), false), - MessageType::ServerNotice(c) => (&*c.body, None, false), - MessageType::Text(c) => (&*c.body, c.formatted.as_ref(), false), - MessageType::Video(_) => ("sent a video.", None, false), - MessageType::VerificationRequest(c) => (&*c.body, None, false), - MessageType::_Custom(c) => (&*c.body, None, false), - }; - - Self { body, formatted, is_emote, is_reply, room_id, event_id, sender } - } -} - -fn get_message_quote_fallbacks(original_event: OriginalEventData<'_>) -> (String, String) { - let OriginalEventData { body, formatted, is_emote, is_reply, room_id, event_id, sender } = - original_event; - let emote_sign = is_emote.then_some("* ").unwrap_or_default(); - let body = is_reply.then(|| remove_plain_reply_fallback(body)).unwrap_or(body); - #[cfg(feature = "html")] - let html_body = FormattedOrPlainBody { formatted, body, is_reply }; - #[cfg(not(feature = "html"))] - let html_body = FormattedOrPlainBody { formatted, body }; - - ( - format!("> {emote_sign}<{sender}> {body}").replace('\n', "\n> "), - format!( - "\ -
\ - In reply to \ - {emote_sign}{sender}\ -
\ - {html_body}\ -
\ -
" - ), - ) -} - -struct EscapeHtmlEntities<'a>(&'a str); - -impl fmt::Display for EscapeHtmlEntities<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for c in self.0.chars() { - // Escape reserved HTML entities and new lines. - // - match c { - '&' => f.write_str("&")?, - '<' => f.write_str("<")?, - '>' => f.write_str(">")?, - '"' => f.write_str(""")?, - '\n' => f.write_str("
")?, - _ => f.write_char(c)?, - } - } - - Ok(()) - } -} - -struct FormattedOrPlainBody<'a> { - formatted: Option<&'a FormattedBody>, - body: &'a str, - #[cfg(feature = "html")] - is_reply: bool, -} - -impl fmt::Display for FormattedOrPlainBody<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(formatted_body) = self.formatted { - #[cfg(feature = "html")] - if self.is_reply { - let mut html = Html::parse(&formatted_body.body); - html.sanitize(); - - write!(f, "{html}") - } else { - f.write_str(&formatted_body.body) - } - - #[cfg(not(feature = "html"))] - f.write_str(&formatted_body.body) - } else { - write!(f, "{}", EscapeHtmlEntities(self.body)) - } - } -} - -/// Get the plain and formatted body for a rich reply. -/// -/// Returns a `(plain, html)` tuple. -/// -/// With the `sanitize` feature, [HTML tags and attributes] that are not allowed in the Matrix -/// spec and previous [rich reply fallbacks] are removed from the previous message in the new rich -/// reply fallback. -/// -/// [HTML tags and attributes]: https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes -/// [rich reply fallbacks]: https://spec.matrix.org/latest/client-server-api/#fallbacks-for-rich-replies -pub(super) fn plain_and_formatted_reply_body( - body: &str, - formatted: Option, - original_event: OriginalEventData<'_>, -) -> (String, String) { - let (quoted, quoted_html) = get_message_quote_fallbacks(original_event); - - let plain = format!("{quoted}\n\n{body}"); - let html = match formatted { - Some(formatted) => format!("{quoted_html}{formatted}"), - None => format!("{quoted_html}{}", EscapeHtmlEntities(body)), - }; - - (plain, html) -} - -#[cfg(test)] -mod tests { - use ruma_common::{owned_event_id, owned_room_id, owned_user_id, MilliSecondsSinceUnixEpoch}; - - use super::OriginalRoomMessageEvent; - use crate::{room::message::RoomMessageEventContent, MessageLikeUnsigned}; - - #[test] - fn fallback_multiline() { - let (plain_quote, html_quote) = super::get_message_quote_fallbacks( - (&OriginalRoomMessageEvent { - content: RoomMessageEventContent::text_plain("multi\nline"), - event_id: owned_event_id!("$1598361704261elfgc:localhost"), - sender: owned_user_id!("@alice:example.com"), - origin_server_ts: MilliSecondsSinceUnixEpoch::now(), - room_id: owned_room_id!("!n8f893n9:example.com"), - unsigned: MessageLikeUnsigned::new(), - }) - .into(), - ); - - assert_eq!(plain_quote, "> <@alice:example.com> multi\n> line"); - assert_eq!( - html_quote, - "\ -
\ - In reply to \ - @alice:example.com\ -
\ - multi
line\ -
\ -
", - ); - } -} diff --git a/crates/ruma-events/src/room/message/sanitize.rs b/crates/ruma-events/src/room/message/sanitize.rs deleted file mode 100644 index 27b07a23..00000000 --- a/crates/ruma-events/src/room/message/sanitize.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Convenience methods and types to sanitize text messages. - -/// Remove the [rich reply fallback] of the given plain text string. -/// -/// [rich reply fallback]: https://spec.matrix.org/latest/client-server-api/#fallbacks-for-rich-replies -pub fn remove_plain_reply_fallback(mut s: &str) -> &str { - if !s.starts_with("> ") { - return s; - } - - while s.starts_with("> ") { - if let Some((_line, rest)) = s.split_once('\n') { - s = rest; - } else { - return ""; - } - } - - // Strip the first line after the fallback if it is empty. - if let Some(rest) = s.strip_prefix('\n') { - rest - } else { - s - } -} - -#[cfg(test)] -mod tests { - use super::remove_plain_reply_fallback; - - #[test] - fn remove_plain_reply() { - assert_eq!( - remove_plain_reply_fallback("No reply here\nJust a simple message"), - "No reply here\nJust a simple message" - ); - - assert_eq!( - remove_plain_reply_fallback( - "> <@user:notareal.hs> Replied to on\n\ - > two lines\n\ - \n\ - \n\ - This is my reply" - ), - "\nThis is my reply" - ); - - assert_eq!(remove_plain_reply_fallback("\n> Not on first line"), "\n> Not on first line"); - - assert_eq!( - remove_plain_reply_fallback("> <@user:notareal.hs> Previous message\n\n> New quote"), - "> New quote" - ); - } -} diff --git a/crates/ruma-events/src/room/message/server_notice.rs b/crates/ruma-events/src/room/message/server_notice.rs deleted file mode 100644 index 33af12c6..00000000 --- a/crates/ruma-events/src/room/message/server_notice.rs +++ /dev/null @@ -1,64 +0,0 @@ -use ruma_common::serde::StringEnum; -use serde::{Deserialize, Serialize}; - -use crate::PrivOwnedStr; - -/// The payload for a server notice message. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[serde(tag = "msgtype", rename = "m.server_notice")] -pub struct ServerNoticeMessageEventContent { - /// A human-readable description of the notice. - pub body: String, - - /// The type of notice being represented. - pub server_notice_type: ServerNoticeType, - - /// A URI giving a contact method for the server administrator. - /// - /// Required if the notice type is `m.server_notice.usage_limit_reached`. - #[serde(skip_serializing_if = "Option::is_none")] - pub admin_contact: Option, - - /// The kind of usage limit the server has exceeded. - /// - /// Required if the notice type is `m.server_notice.usage_limit_reached`. - #[serde(skip_serializing_if = "Option::is_none")] - pub limit_type: Option, -} - -impl ServerNoticeMessageEventContent { - /// Creates a new `ServerNoticeMessageEventContent` with the given body and notice type. - pub fn new(body: String, server_notice_type: ServerNoticeType) -> Self { - Self { body, server_notice_type, admin_contact: None, limit_type: None } - } -} - -/// Types of server notices. -#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] -#[derive(Clone, PartialEq, Eq, StringEnum)] -#[non_exhaustive] -pub enum ServerNoticeType { - /// The server has exceeded some limit which requires the server administrator to intervene. - #[ruma_enum(rename = "m.server_notice.usage_limit_reached")] - UsageLimitReached, - - #[doc(hidden)] - _Custom(PrivOwnedStr), -} - -/// Types of usage limits. -#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] -#[derive(Clone, PartialEq, Eq, StringEnum)] -#[ruma_enum(rename_all = "snake_case")] -#[non_exhaustive] -pub enum LimitType { - /// The server's number of active users in the last 30 days has exceeded the maximum. - /// - /// New connections are being refused by the server. What defines "active" is left as an - /// implementation detail, however servers are encouraged to treat syncing users as "active". - MonthlyActiveUser, - - #[doc(hidden)] - _Custom(PrivOwnedStr), -} diff --git a/crates/ruma-events/src/room/message/text.rs b/crates/ruma-events/src/room/message/text.rs deleted file mode 100644 index ca080185..00000000 --- a/crates/ruma-events/src/room/message/text.rs +++ /dev/null @@ -1,43 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::FormattedBody; - -/// The payload for a text message. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[serde(tag = "msgtype", rename = "m.text")] -pub struct TextMessageEventContent { - /// The body of the message. - pub body: String, - - /// Formatted form of the message `body`. - #[serde(flatten)] - pub formatted: Option, -} - -impl TextMessageEventContent { - /// A convenience constructor to create a plain text message. - pub fn plain(body: impl Into) -> Self { - let body = body.into(); - Self { body, formatted: None } - } - - /// A convenience constructor to create an HTML message. - pub fn html(body: impl Into, html_body: impl Into) -> Self { - let body = body.into(); - Self { body, formatted: Some(FormattedBody::html(html_body)) } - } - - /// A convenience constructor to create a Markdown message. - /// - /// Returns an HTML message if some Markdown formatting was detected, otherwise returns a plain - /// text message. - #[cfg(feature = "markdown")] - pub fn markdown(body: impl AsRef + Into) -> Self { - if let Some(formatted) = FormattedBody::markdown(&body) { - Self::html(body, formatted.body) - } else { - Self::plain(body) - } - } -} diff --git a/crates/ruma-events/src/room/message/video.rs b/crates/ruma-events/src/room/message/video.rs deleted file mode 100644 index 28ba338e..00000000 --- a/crates/ruma-events/src/room/message/video.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::time::Duration; - -use js_int::UInt; -use ruma_common::OwnedMxcUri; -use serde::{Deserialize, Serialize}; - -use crate::room::{EncryptedFile, MediaSource, ThumbnailInfo}; - -/// The payload for a video message. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[serde(tag = "msgtype", rename = "m.video")] -pub struct VideoMessageEventContent { - /// A description of the video, e.g. "Gangnam Style", or some kind of content description for - /// accessibility, e.g. "video attachment". - pub body: String, - - /// The source of the video clip. - #[serde(flatten)] - pub source: MediaSource, - - /// Metadata about the video clip referred to in `source`. - #[serde(skip_serializing_if = "Option::is_none")] - pub info: Option>, -} - -impl VideoMessageEventContent { - /// Creates a new `VideoMessageEventContent` with the given body and source. - pub fn new(body: String, source: MediaSource) -> Self { - Self { body, source, info: None } - } - - /// Creates a new non-encrypted `VideoMessageEventContent` with the given body and url. - pub fn plain(body: String, url: OwnedMxcUri) -> Self { - Self::new(body, MediaSource::Plain(url)) - } - - /// Creates a new encrypted `VideoMessageEventContent` with the given body and encrypted - /// file. - pub fn encrypted(body: String, file: EncryptedFile) -> Self { - Self::new(body, MediaSource::Encrypted(Box::new(file))) - } - - /// Creates a new `VideoMessageEventContent` from `self` with the `info` field set to the given - /// value. - /// - /// Since the field is public, you can also assign to it directly. This method merely acts - /// as a shorthand for that, because it is very common to set this field. - pub fn info(self, info: impl Into>>) -> Self { - Self { info: info.into(), ..self } - } -} - -/// Metadata about a video. -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct VideoInfo { - /// The duration of the video in milliseconds. - #[serde( - with = "ruma_common::serde::duration::opt_ms", - default, - skip_serializing_if = "Option::is_none" - )] - pub duration: Option, - - /// The height of the video in pixels. - #[serde(rename = "h", skip_serializing_if = "Option::is_none")] - pub height: Option, - - /// The width of the video in pixels. - #[serde(rename = "w", skip_serializing_if = "Option::is_none")] - pub width: Option, - - /// The mimetype of the video, e.g. "video/mp4". - #[serde(skip_serializing_if = "Option::is_none")] - pub mimetype: Option, - - /// The size of the video in bytes. - #[serde(skip_serializing_if = "Option::is_none")] - pub size: Option, - - /// Metadata about the image referred to in `thumbnail_source`. - #[serde(skip_serializing_if = "Option::is_none")] - pub thumbnail_info: Option>, - - /// The source of the thumbnail of the video clip. - #[serde( - flatten, - with = "crate::room::thumbnail_source_serde", - skip_serializing_if = "Option::is_none" - )] - pub thumbnail_source: Option, - - /// The [BlurHash](https://blurha.sh) for this video. - /// - /// This uses the unstable prefix in - /// [MSC2448](https://github.com/matrix-org/matrix-spec-proposals/pull/2448). - #[cfg(feature = "unstable-msc2448")] - #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")] - pub blurhash: Option, -} - -impl VideoInfo { - /// Creates an empty `VideoInfo`. - pub fn new() -> Self { - Self::default() - } -} diff --git a/crates/ruma-events/src/room/message/without_relation.rs b/crates/ruma-events/src/room/message/without_relation.rs deleted file mode 100644 index 1907dff3..00000000 --- a/crates/ruma-events/src/room/message/without_relation.rs +++ /dev/null @@ -1,255 +0,0 @@ -use as_variant::as_variant; -use ruma_common::{serde::Raw, OwnedEventId, OwnedUserId, RoomId, UserId}; -use serde::{Deserialize, Serialize}; - -use super::{ - AddMentions, ForwardThread, MessageType, OriginalRoomMessageEvent, Relation, - RoomMessageEventContent, -}; -use crate::{ - relation::{InReplyTo, Thread}, - room::message::{reply::OriginalEventData, FormattedBody}, - AnySyncTimelineEvent, Mentions, -}; - -/// Form of [`RoomMessageEventContent`] without relation. -#[derive(Clone, Debug, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct RoomMessageEventContentWithoutRelation { - /// A key which identifies the type of message being sent. - /// - /// This also holds the specific content of each message. - #[serde(flatten)] - pub msgtype: MessageType, - - /// The [mentions] of this event. - /// - /// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions - #[serde(rename = "m.mentions", skip_serializing_if = "Option::is_none")] - pub mentions: Option, -} - -impl RoomMessageEventContentWithoutRelation { - /// Creates a new `RoomMessageEventContentWithoutRelation` with the given `MessageType`. - pub fn new(msgtype: MessageType) -> Self { - Self { msgtype, mentions: None } - } - - /// A constructor to create a plain text message. - pub fn text_plain(body: impl Into) -> Self { - Self::new(MessageType::text_plain(body)) - } - - /// A constructor to create an html message. - pub fn text_html(body: impl Into, html_body: impl Into) -> Self { - Self::new(MessageType::text_html(body, html_body)) - } - - /// A constructor to create a markdown message. - #[cfg(feature = "markdown")] - pub fn text_markdown(body: impl AsRef + Into) -> Self { - Self::new(MessageType::text_markdown(body)) - } - - /// A constructor to create a plain text notice. - pub fn notice_plain(body: impl Into) -> Self { - Self::new(MessageType::notice_plain(body)) - } - - /// A constructor to create an html notice. - pub fn notice_html(body: impl Into, html_body: impl Into) -> Self { - Self::new(MessageType::notice_html(body, html_body)) - } - - /// A constructor to create a markdown notice. - #[cfg(feature = "markdown")] - pub fn notice_markdown(body: impl AsRef + Into) -> Self { - Self::new(MessageType::notice_markdown(body)) - } - - /// A constructor to create a plain text emote. - pub fn emote_plain(body: impl Into) -> Self { - Self::new(MessageType::emote_plain(body)) - } - - /// A constructor to create an html emote. - pub fn emote_html(body: impl Into, html_body: impl Into) -> Self { - Self::new(MessageType::emote_html(body, html_body)) - } - - /// A constructor to create a markdown emote. - #[cfg(feature = "markdown")] - pub fn emote_markdown(body: impl AsRef + Into) -> Self { - Self::new(MessageType::emote_markdown(body)) - } - - /// Transform `self` into a `RoomMessageEventContent` with the given relation. - pub fn with_relation( - self, - relates_to: Option>, - ) -> RoomMessageEventContent { - let Self { msgtype, mentions } = self; - RoomMessageEventContent { msgtype, relates_to, mentions } - } - - /// Turns `self` into a reply to the given message. - /// - /// Takes the `body` / `formatted_body` (if any) in `self` for the main text and prepends a - /// quoted version of `original_message`. Also sets the `in_reply_to` field inside `relates_to`, - /// and optionally the `rel_type` to `m.thread` if the `original_message is in a thread and - /// thread forwarding is enabled. - #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] - /// - /// # Panics - /// - /// Panics if `self` has a `formatted_body` with a format other than HTML. - #[track_caller] - pub fn make_reply_to( - mut self, - original_message: &OriginalRoomMessageEvent, - forward_thread: ForwardThread, - add_mentions: AddMentions, - ) -> RoomMessageEventContent { - self.msgtype.add_reply_fallback(original_message.into()); - let original_event_id = original_message.event_id.clone(); - - let original_thread_id = if forward_thread == ForwardThread::Yes { - original_message - .content - .relates_to - .as_ref() - .and_then(as_variant!(Relation::Thread)) - .map(|thread| thread.event_id.clone()) - } else { - None - }; - - let sender_for_mentions = - (add_mentions == AddMentions::Yes).then_some(&*original_message.sender); - - self.make_reply_tweaks(original_event_id, original_thread_id, sender_for_mentions) - } - - /// Turns `self` into a reply to the given raw event. - /// - /// Takes the `body` / `formatted_body` (if any) in `self` for the main text and prepends a - /// quoted version of the `body` of `original_event` (if any). Also sets the `in_reply_to` field - /// inside `relates_to`, and optionally the `rel_type` to `m.thread` if the - /// `original_message is in a thread and thread forwarding is enabled. - /// - /// It is recommended to use [`Self::make_reply_to()`] for replies to `m.room.message` events, - /// as the generated fallback is better for some `msgtype`s. - /// - /// Note that except for the panic below, this is infallible. Which means that if a field is - /// missing when deserializing the data, the changes that require it will not be applied. It - /// will still at least apply the `m.in_reply_to` relation to this content. - /// - /// # Panics - /// - /// Panics if `self` has a `formatted_body` with a format other than HTML. - #[track_caller] - pub fn make_reply_to_raw( - mut self, - original_event: &Raw, - original_event_id: OwnedEventId, - room_id: &RoomId, - forward_thread: ForwardThread, - add_mentions: AddMentions, - ) -> RoomMessageEventContent { - #[derive(Deserialize)] - struct ContentDeHelper { - body: Option, - #[serde(flatten)] - formatted: Option, - #[cfg(feature = "unstable-msc1767")] - #[serde(rename = "org.matrix.msc1767.text")] - text: Option, - #[serde(rename = "m.relates_to")] - relates_to: Option, - } - - let sender = original_event.get_field::("sender").ok().flatten(); - let content = original_event.get_field::("content").ok().flatten(); - let relates_to = content.as_ref().and_then(|c| c.relates_to.as_ref()); - - let content_body = content.as_ref().and_then(|c| { - let body = c.body.as_deref(); - #[cfg(feature = "unstable-msc1767")] - let body = body.or(c.text.as_deref()); - - Some((c, body?)) - }); - - // Only apply fallback if we managed to deserialize raw event. - if let (Some(sender), Some((content, body))) = (&sender, content_body) { - let is_reply = - matches!(content.relates_to, Some(crate::room::encrypted::Relation::Reply { .. })); - let data = OriginalEventData { - body, - formatted: content.formatted.as_ref(), - is_emote: false, - is_reply, - room_id, - event_id: &original_event_id, - sender, - }; - - self.msgtype.add_reply_fallback(data); - } - - let original_thread_id = if forward_thread == ForwardThread::Yes { - relates_to - .and_then(as_variant!(crate::room::encrypted::Relation::Thread)) - .map(|thread| thread.event_id.clone()) - } else { - None - }; - - let sender_for_mentions = sender.as_deref().filter(|_| add_mentions == AddMentions::Yes); - self.make_reply_tweaks(original_event_id, original_thread_id, sender_for_mentions) - } - - /// Add the given [mentions] to this event. - /// - /// If no [`Mentions`] was set on this events, this sets it. Otherwise, this updates the current - /// mentions by extending the previous `user_ids` with the new ones, and applies a logical OR to - /// the values of `room`. - /// - /// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions - pub fn add_mentions(mut self, mentions: Mentions) -> Self { - self.mentions.get_or_insert_with(Mentions::new).add(mentions); - self - } - - fn make_reply_tweaks( - mut self, - original_event_id: OwnedEventId, - original_thread_id: Option, - sender_for_mentions: Option<&UserId>, - ) -> RoomMessageEventContent { - let relates_to = if let Some(event_id) = original_thread_id { - Relation::Thread(Thread::plain(event_id.to_owned(), original_event_id.to_owned())) - } else { - Relation::Reply { in_reply_to: InReplyTo { event_id: original_event_id.to_owned() } } - }; - - if let Some(sender) = sender_for_mentions { - self.mentions.get_or_insert_with(Mentions::new).user_ids.insert(sender.to_owned()); - } - - self.with_relation(Some(relates_to)) - } -} - -impl From for RoomMessageEventContentWithoutRelation { - fn from(msgtype: MessageType) -> Self { - Self::new(msgtype) - } -} - -impl From for RoomMessageEventContentWithoutRelation { - fn from(value: RoomMessageEventContent) -> Self { - let RoomMessageEventContent { msgtype, mentions, .. } = value; - Self { msgtype, mentions } - } -} diff --git a/crates/ruma-events/src/threads.rs b/crates/ruma-events/src/threads.rs new file mode 100644 index 00000000..4ece9e1c --- /dev/null +++ b/crates/ruma-events/src/threads.rs @@ -0,0 +1,3 @@ +mod message; +mod call; +mod poll; diff --git a/crates/ruma-events/src/threads/call.rs b/crates/ruma-events/src/threads/call.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/ruma-events/src/threads/call.rs @@ -0,0 +1 @@ + diff --git a/crates/ruma-events/src/threads/message.rs b/crates/ruma-events/src/threads/message.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/ruma-events/src/threads/message.rs @@ -0,0 +1 @@ + diff --git a/crates/ruma-events/src/threads/poll.rs b/crates/ruma-events/src/threads/poll.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/ruma-events/src/threads/poll.rs @@ -0,0 +1 @@ + diff --git a/crates/ruma-events/src/video.rs b/crates/ruma-events/src/video.rs deleted file mode 100644 index 756e5fb5..00000000 --- a/crates/ruma-events/src/video.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Types for extensible video message events ([MSC3553]). -//! -//! [MSC3553]: https://github.com/matrix-org/matrix-spec-proposals/pull/3553 - -use std::time::Duration; - -use js_int::UInt; -use ruma_macros::EventContent; -use serde::{Deserialize, Serialize}; - -use super::{ - file::{CaptionContentBlock, FileContentBlock}, - image::ThumbnailContentBlock, - message::TextContentBlock, - room::message::Relation, -}; - -/// The payload for an extensible video message. -/// -/// This is the new primary type introduced in [MSC3553] and should only be sent in rooms with a -/// version that supports it. See the documentation of the [`message`] module for more information. -/// -/// [MSC3553]: https://github.com/matrix-org/matrix-spec-proposals/pull/3553 -/// [`message`]: super::message -#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[ruma_event(type = "org.matrix.msc1767.video", kind = MessageLike, without_relation)] -pub struct VideoEventContent { - /// The text representation of the message. - #[serde(rename = "org.matrix.msc1767.text")] - pub text: TextContentBlock, - - /// The file content of the message. - #[serde(rename = "org.matrix.msc1767.file")] - pub file: FileContentBlock, - - /// The video details of the message, if any. - #[serde(rename = "org.matrix.msc1767.video_details", skip_serializing_if = "Option::is_none")] - pub video_details: Option, - - /// The thumbnails of the message, if any. - /// - /// This is optional and defaults to an empty array. - #[serde( - rename = "org.matrix.msc1767.thumbnail", - default, - skip_serializing_if = "ThumbnailContentBlock::is_empty" - )] - pub thumbnail: ThumbnailContentBlock, - - /// The caption of the message, if any. - #[serde(rename = "org.matrix.msc1767.caption", skip_serializing_if = "Option::is_none")] - pub caption: Option, - - /// Whether this message is automated. - #[cfg(feature = "unstable-msc3955")] - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - rename = "org.matrix.msc1767.automated" - )] - pub automated: bool, - - /// Information about related messages. - #[serde( - flatten, - skip_serializing_if = "Option::is_none", - deserialize_with = "crate::room::message::relation_serde::deserialize_relation" - )] - pub relates_to: Option>, -} - -impl VideoEventContent { - /// Creates a new `VideoEventContent` with the given fallback representation and file. - pub fn new(text: TextContentBlock, file: FileContentBlock) -> Self { - Self { - text, - file, - video_details: None, - thumbnail: Default::default(), - caption: None, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } - - /// Creates a new `VideoEventContent` with the given plain text fallback representation and - /// file. - pub fn with_plain_text(plain_text: impl Into, file: FileContentBlock) -> Self { - Self { - text: TextContentBlock::plain(plain_text), - file, - video_details: None, - thumbnail: Default::default(), - caption: None, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } -} - -/// A block for details of video content. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct VideoDetailsContentBlock { - /// The width of the video in pixels. - pub width: UInt, - - /// The height of the video in pixels. - pub height: UInt, - - /// The duration of the video in seconds. - #[serde( - with = "ruma_common::serde::duration::opt_secs", - default, - skip_serializing_if = "Option::is_none" - )] - pub duration: Option, -} - -impl VideoDetailsContentBlock { - /// Creates a new `VideoDetailsContentBlock` with the given height and width. - pub fn new(width: UInt, height: UInt) -> Self { - Self { width, height, duration: None } - } -} diff --git a/crates/ruma-events/src/voice.rs b/crates/ruma-events/src/voice.rs deleted file mode 100644 index d27ec02f..00000000 --- a/crates/ruma-events/src/voice.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Types for voice message events ([MSC3245]). -//! -//! [MSC3245]: https://github.com/matrix-org/matrix-spec-proposals/pull/3245 - -use std::time::Duration; - -use ruma_macros::EventContent; -use serde::{Deserialize, Serialize}; - -use super::{ - audio::Amplitude, file::FileContentBlock, message::TextContentBlock, room::message::Relation, -}; - -/// The payload for an extensible voice message. -/// -/// This is the new primary type introduced in [MSC3245] and can be sent in rooms with a version -/// that doesn't support extensible events. See the documentation of the [`message`] module for more -/// information. -/// -/// [MSC3245]: https://github.com/matrix-org/matrix-spec-proposals/pull/3245 -/// [`message`]: super::message -#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[ruma_event(type = "org.matrix.msc3245.voice.v2", kind = MessageLike, without_relation)] -pub struct VoiceEventContent { - /// The text representation of the message. - #[serde(rename = "org.matrix.msc1767.text")] - pub text: TextContentBlock, - - /// The file content of the message. - #[serde(rename = "org.matrix.msc1767.file")] - pub file: FileContentBlock, - - /// The audio content of the message. - #[serde(rename = "org.matrix.msc1767.audio_details")] - pub audio_details: VoiceAudioDetailsContentBlock, - - /// Whether this message is automated. - #[cfg(feature = "unstable-msc3955")] - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - rename = "org.matrix.msc1767.automated" - )] - pub automated: bool, - - /// Information about related messages. - #[serde( - flatten, - skip_serializing_if = "Option::is_none", - deserialize_with = "crate::room::message::relation_serde::deserialize_relation" - )] - pub relates_to: Option>, -} - -impl VoiceEventContent { - /// Creates a new `VoiceEventContent` with the given fallback representation, file and audio - /// details. - pub fn new( - text: TextContentBlock, - file: FileContentBlock, - audio_details: VoiceAudioDetailsContentBlock, - ) -> Self { - Self { - text, - file, - audio_details, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } - - /// Creates a new `VoiceEventContent` with the given plain text fallback representation, file - /// and audio details. - pub fn with_plain_text( - plain_text: impl Into, - file: FileContentBlock, - audio_details: VoiceAudioDetailsContentBlock, - ) -> Self { - Self { - text: TextContentBlock::plain(plain_text), - file, - audio_details, - #[cfg(feature = "unstable-msc3955")] - automated: false, - relates_to: None, - } - } -} - -/// A block for details of voice audio content. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct VoiceAudioDetailsContentBlock { - /// The duration of the audio in seconds. - #[serde(with = "ruma_common::serde::duration::secs")] - pub duration: Duration, - - /// The waveform representation of the content. - #[serde(rename = "org.matrix.msc3246.waveform")] - pub waveform: Vec, -} - -impl VoiceAudioDetailsContentBlock { - /// Creates a new `AudioDetailsContentBlock` with the given duration and waveform - /// representation. - pub fn new(duration: Duration, waveform: Vec) -> Self { - Self { duration, waveform } - } -} diff --git a/crates/ruma-events/tests/it/encrypted.rs b/crates/ruma-events/tests/it/encrypted.rs index 8778f5bb..ab19da70 100644 --- a/crates/ruma-events/tests/it/encrypted.rs +++ b/crates/ruma-events/tests/it/encrypted.rs @@ -1,7 +1,7 @@ use assert_matches2::assert_matches; use ruma_common::{owned_device_id, owned_event_id}; use ruma_events::{ - relation::{CustomRelation, InReplyTo, Reference, Thread}, + relation::{CustomRelation, Reply, Reference, Thread}, room::encrypted::{ EncryptedEventScheme, MegolmV1AesSha2ContentInit, Relation, Replacement, RoomEncryptedEventContent, @@ -86,7 +86,7 @@ fn content_no_relation_deserialization() { fn content_reply_serialization() { let content = RoomEncryptedEventContent::new( encrypted_scheme(), - Some(Relation::Reply { in_reply_to: InReplyTo::new(owned_event_id!("$replied_to_event")) }), + Some(Relation::Reply { in_reply_to: Reply::new(owned_event_id!("$replied_to_event")) }), ); assert_eq!( @@ -305,7 +305,6 @@ fn content_thread_serialization() { "m.relates_to": { "rel_type": "m.thread", "event_id": "$thread_root", - "is_falling_back": true, "m.in_reply_to": { "event_id": "$prev_event", }, @@ -353,7 +352,6 @@ fn content_thread_deserialization() { assert_matches!(content.relates_to, Some(Relation::Thread(thread))); assert_eq!(thread.event_id, "$thread_root"); assert_eq!(thread.in_reply_to.unwrap().event_id, "$prev_event"); - assert!(!thread.is_falling_back); } #[test] diff --git a/crates/ruma-events/tests/it/message.rs b/crates/ruma-events/tests/it/message.rs index 05112579..0fb7ab7a 100644 --- a/crates/ruma-events/tests/it/message.rs +++ b/crates/ruma-events/tests/it/message.rs @@ -171,8 +171,6 @@ fn plain_text_content_deserialization() { let content = from_json_value::(json_data).unwrap(); assert_eq!(content.text.find_plain(), Some("This is my body")); assert_eq!(content.text.find_html(), None); - #[cfg(feature = "unstable-msc3955")] - assert!(!content.automated); } #[test] @@ -186,8 +184,6 @@ fn html_content_deserialization() { let content = from_json_value::(json_data).unwrap(); assert_eq!(content.text.find_plain(), None); assert_eq!(content.text.find_html(), Some("Hello, New World!")); - #[cfg(feature = "unstable-msc3955")] - assert!(!content.automated); } #[test] @@ -202,8 +198,6 @@ fn html_and_text_content_deserialization() { let content = from_json_value::(json_data).unwrap(); assert_eq!(content.text.find_plain(), Some("Hello, New World!")); assert_eq!(content.text.find_html(), Some("Hello, New World!")); - #[cfg(feature = "unstable-msc3955")] - assert!(!content.automated); } #[test] @@ -359,37 +353,3 @@ fn lang_deserialization() { assert_eq!(content[1].lang, "de"); assert_eq!(content[2].lang, "en"); } - -#[test] -#[cfg(feature = "unstable-msc3955")] -fn automated_content_serialization() { - let mut message_event_content = - MessageEventContent::plain("> <@test:example.com> test\n\ntest reply"); - message_event_content.automated = true; - - assert_eq!( - to_json_value(&message_event_content).unwrap(), - json!({ - "org.matrix.msc1767.text": [ - { "body": "> <@test:example.com> test\n\ntest reply" }, - ], - "org.matrix.msc1767.automated": true, - }) - ); -} - -#[test] -#[cfg(feature = "unstable-msc3955")] -fn automated_content_deserialization() { - let json_data = json!({ - "org.matrix.msc1767.text": [ - { "mimetype": "text/html", "body": "Hello, New World!" }, - ], - "org.matrix.msc1767.automated": true, - }); - - let content = from_json_value::(json_data).unwrap(); - assert_eq!(content.text.find_plain(), None); - assert_eq!(content.text.find_html(), Some("Hello, New World!")); - assert!(content.automated); -} diff --git a/crates/ruma-events/tests/it/relations.rs b/crates/ruma-events/tests/it/relations.rs index 50b6a0fc..1b1c6b8a 100644 --- a/crates/ruma-events/tests/it/relations.rs +++ b/crates/ruma-events/tests/it/relations.rs @@ -2,7 +2,7 @@ use assert_matches2::assert_matches; use assign::assign; use ruma_common::owned_event_id; use ruma_events::{ - relation::{CustomRelation, InReplyTo, Replacement, Thread}, + relation::{CustomRelation, Reply, Replacement, Thread}, room::message::{MessageType, Relation, RoomMessageEventContent}, }; use serde_json::{ @@ -25,7 +25,7 @@ fn reply_deserialize() { from_json_value::(json), Ok(RoomMessageEventContent { msgtype: MessageType::Text(_), - relates_to: Some(Relation::Reply { in_reply_to: InReplyTo { event_id, .. }, .. }), + relates_to: Some(Relation::Reply { in_reply_to: Reply { event_id, .. }, .. }), .. }) ); @@ -35,7 +35,7 @@ fn reply_deserialize() { #[test] fn reply_serialize() { let content = assign!(RoomMessageEventContent::text_plain("This is a reply"), { - relates_to: Some(Relation::Reply { in_reply_to: InReplyTo::new(owned_event_id!("$1598361704261elfgc")) }), + relates_to: Some(Relation::Reply { in_reply_to: Reply::new(owned_event_id!("$1598361704261elfgc")) }), }); assert_eq!( @@ -136,7 +136,6 @@ fn thread_plain_serialize() { "m.in_reply_to": { "event_id": "$latesteventid", }, - "is_falling_back": true, }, }) ); @@ -193,7 +192,6 @@ fn thread_stable_deserialize() { ); assert_eq!(thread.event_id, "$1598361704261elfgc"); assert_matches!(thread.in_reply_to, None); - assert!(!thread.is_falling_back); } #[test] @@ -220,7 +218,6 @@ fn thread_stable_reply_deserialize() { ); assert_eq!(thread.event_id, "$1598361704261elfgc"); assert_eq!(thread.in_reply_to.unwrap().event_id, "$latesteventid"); - assert!(!thread.is_falling_back); } #[test] @@ -247,7 +244,6 @@ fn thread_unstable_deserialize() { ); assert_eq!(thread.event_id, "$1598361704261elfgc"); assert_eq!(thread.in_reply_to.unwrap().event_id, "$latesteventid"); - assert!(!thread.is_falling_back); } #[test] diff --git a/crates/ruma-events/tests/it/room_message.rs b/crates/ruma-events/tests/it/room_message.rs index c72008e3..e19ab74b 100644 --- a/crates/ruma-events/tests/it/room_message.rs +++ b/crates/ruma-events/tests/it/room_message.rs @@ -481,7 +481,6 @@ fn reply_thread_fallback() { Some(threaded_message.event_id) ); assert_eq!(thread_info.event_id, thread_root.event_id); - assert!(thread_info.is_falling_back); } #[test] diff --git a/crates/ruma-events/tests/it/without_relation.rs b/crates/ruma-events/tests/it/without_relation.rs index ddba246b..2e33d051 100644 --- a/crates/ruma-events/tests/it/without_relation.rs +++ b/crates/ruma-events/tests/it/without_relation.rs @@ -1,7 +1,7 @@ use assert_matches2::assert_matches; use ruma_common::owned_event_id; use ruma_events::{ - relation::InReplyTo, + relation::Reply, room::message::{MessageType, Relation, RoomMessageEventContent}, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; @@ -10,7 +10,7 @@ use serde_json::{from_value as from_json_value, json, to_value as to_json_value} fn serialize_room_message_content_without_relation() { let mut content = RoomMessageEventContent::text_plain("Hello, world!"); content.relates_to = - Some(Relation::Reply { in_reply_to: InReplyTo::new(owned_event_id!("$eventId")) }); + Some(Relation::Reply { in_reply_to: Reply::new(owned_event_id!("$eventId")) }); let without_relation = MessageType::from(content); assert_eq!( @@ -37,7 +37,7 @@ fn deserialize_room_message_content_without_relation() { fn convert_room_message_content_without_relation_to_full() { let mut content = RoomMessageEventContent::text_plain("Hello, world!"); content.relates_to = - Some(Relation::Reply { in_reply_to: InReplyTo::new(owned_event_id!("$eventId")) }); + Some(Relation::Reply { in_reply_to: Reply::new(owned_event_id!("$eventId")) }); let new_content = RoomMessageEventContent::from(MessageType::from(content)); assert_matches!( diff --git a/crates/ruma-macros/src/events/event_content.rs b/crates/ruma-macros/src/events/event_content.rs index b674bc4e..078e60d3 100644 --- a/crates/ruma-macros/src/events/event_content.rs +++ b/crates/ruma-macros/src/events/event_content.rs @@ -675,20 +675,10 @@ fn generate_event_content_without_relation<'a>( ); let without_relation_ident = format_ident!("{ident}WithoutRelation"); - let with_relation_fn_doc = - format!("Transform `self` into a [`{ident}`] with the given relation."); - - let (relates_to, other_fields) = fields.partition::, _>(|f| { - f.ident.as_ref().filter(|ident| *ident == "relates_to").is_some() + let (_relations, other_fields) = fields.partition::, _>(|f| { + f.ident.as_ref().filter(|ident| *ident == "relations").is_some() }); - let relates_to_type = relates_to.into_iter().next().map(|f| &f.ty).ok_or_else(|| { - syn::Error::new( - Span::call_site(), - "`without_relation` can only be used on events with a `relates_to` field", - ) - })?; - let without_relation_fields = other_fields.iter().flat_map(|f| &f.ident).collect::>(); let without_relation_struct = if other_fields.is_empty() { quote! { ; } @@ -713,16 +703,6 @@ fn generate_event_content_without_relation<'a>( #[derive(Clone, Debug, #serde::Deserialize, #serde::Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #vis struct #without_relation_ident #without_relation_struct - - impl #without_relation_ident { - #[doc = #with_relation_fn_doc] - #vis fn with_relation(self, relates_to: #relates_to_type) -> #ident { - #ident { - #( #without_relation_fields: self.#without_relation_fields, )* - relates_to, - } - } - } }) }