armageddon

This commit is contained in:
tezlm 2023-12-03 17:46:16 -08:00
parent 6e2d6ef142
commit c377f8ec91
Signed by: tezlm
GPG key ID: 649733FCD94AFBBA
69 changed files with 803 additions and 5344 deletions

View file

@ -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"

View file

@ -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

View file

@ -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.

View file

@ -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<String>,
/// Limit on the number of events to return in this request.
#[ruma_api(query)]
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<UInt>,
/// Which rooms to include. An empty vec searches all rooms.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub room_ids: Vec<OwnedRoomId>,
/// Allows basic filtering of events returned.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub filter: Vec<InboxFilter>,
}
/// 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<String>,
/// The list of threads mention events are in
pub threads: Vec<Notification>,
/// The list of thread and notification events
pub chunk: Vec<Notification>,
}
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<Notification>) -> 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<AnyTimelineEvent>,
/// Indicates whether the user has sent a read receipt indicating that they have read this
/// message.
pub read: bool,
}
}

View file

@ -2,3 +2,4 @@
pub mod get_threads;
pub mod bulk_threads;
pub mod set_participation;

View file

@ -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<Raw<AnyTimelineEvent>>) -> 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 {

View file

@ -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;

View file

@ -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<String>,
/// A helpful description or summary.
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
/// Where this embed links to.
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
/// An accent color associated with this embed, in the format `#rrggbb`.
#[serde(skip_serializing_if = "Option::is_none")]
color: Option<String>,
/// The source of this embed.
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<EmbedSource>,
/// Information about related events.
#[serde(
flatten,
skip_serializing_if = "Vec::is_empty",
deserialize_with = "crate::relation::deserialize_relation"
)]
pub relations: Vec<Relation<AttachmentEmbedEventContentWithoutRelation>>,
}
/// 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<String>,
/// The canonical url representing this source; the base url for urls
pub url: Option<String>,
/// A small icon representing this source; the favicon for urls
pub icon: Option<File>,
}

View file

@ -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<Relation<AttachmentFileEventContentWithoutRelation>>,
}
/// 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<String>,
/// A textual description of the file's contents.
#[serde(skip_serializing_if = "Option::is_none")]
pub alt: Option<String>,
/// 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<Box<FileInfo>>,
}
/// 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<String>,
/// The size of the file in bytes.
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
/// The width of the {image, video}
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<UInt>,
/// The height of the {image, video}
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<UInt>,
/// The duration of the {audio, video}
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<UInt>,
/// Metadata about the image referred to in `thumbnail_source`.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<Box<ThumbnailInfo>>,
/// 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<MediaSource>,
}
impl FileInfo {
/// Creates an empty `FileInfo`.
pub fn new() -> Self {
Self::default()
}
}

View file

@ -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<AudioDetailsContentBlock>,
/// The caption of the message, if any.
#[serde(rename = "org.matrix.msc1767.caption", skip_serializing_if = "Option::is_none")]
pub caption: Option<CaptionContentBlock>,
/// 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<Relation<AudioEventContentWithoutRelation>>,
}
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<String>, 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<Amplitude>,
}
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<u16> for Amplitude {
fn from(value: u16) -> Self {
Self::new(value)
}
}

View file

@ -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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let uint = UInt::deserialize(deserializer)?;
Ok(Self(uint.min(Self::MAX.into())))
}
}

View file

@ -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<Relation<EmoteEventContentWithoutRelation>>,
}
impl EmoteEventContent {
/// A convenience constructor to create a plain text emote.
pub fn plain(body: impl Into<String>) -> 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<String>, html_body: impl Into<String>) -> 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<str> + Into<String>) -> Self {
Self {
text: TextContentBlock::markdown(body),
#[cfg(feature = "unstable-msc3955")]
automated: false,
relates_to: None,
}
}
}
impl From<TextContentBlock> for EmoteEventContent {
fn from(text: TextContentBlock) -> Self {
Self {
text,
#[cfg(feature = "unstable-msc3955")]
automated: false,
relates_to: None,
}
}
}

View file

@ -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<Relation>,
#[serde(
flatten,
skip_serializing_if = "Vec::is_empty",
deserialize_with = "crate::relation::deserialize_relation"
)]
pub relations: Vec<Relation<EncryptedContentBlock>>,
/// 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<Mentions>,
}
impl EncryptedEventContent {
/// Creates a new `EncryptedEventContent` with the given scheme and relation.
pub fn new(scheme: EncryptedEventScheme, relates_to: Option<Relation>) -> Self {
Self { encrypted: scheme.into(), relates_to }
pub fn new(scheme: EncryptedEventScheme) -> Self {
Self { encrypted: scheme.into(), relations: vec![], mentions: None }
}
}
impl From<EncryptedEventScheme> for EncryptedEventContent {
fn from(scheme: EncryptedEventScheme) -> Self {
Self { encrypted: scheme.into(), relates_to: None }
Self { encrypted: scheme.into(), relations: vec![], mentions: None }
}
}

View file

@ -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<Relation>,
}
impl RoomEncryptedEventContent {
/// Creates a new `RoomEncryptedEventContent` with the given scheme and relation.
pub fn new(scheme: EncryptedEventScheme, relates_to: Option<Relation>) -> Self {
Self { scheme, relates_to }
}
}
impl From<EncryptedEventScheme> 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<RelationType> {
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<C> From<message::Relation<C>> for Relation {
fn from(rel: message::Relation<C>) -> 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)]

View file

@ -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<encrypted::Relation> {
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<Vec<Relation>> {
// 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,
// }
// }
}

View file

@ -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<CaptionContentBlock>,
/// 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<Relation<FileEventContentWithoutRelation>>,
}
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<String>,
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<String>,
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<String>,
/// The size of the file in bytes.
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
/// Information on the encrypted file.
///
/// Required if the file is encrypted.
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub encryption_info: Option<Box<EncryptedContent>>,
}
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<String, Base64>,
/// 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<String, Base64>,
/// Version of the encrypted attachments protocol.
///
/// Must be `v2`.
pub v: String,
}
impl From<EncryptedContentInit> 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<String>) -> Self {
Self { text: TextContentBlock::plain(body) }
}
/// A convenience constructor to create an HTML caption content block.
pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> 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<str> + Into<String>) -> Self {
Self { text: TextContentBlock::markdown(body) }
}
}
impl From<TextContentBlock> for CaptionContentBlock {
fn from(text: TextContentBlock) -> Self {
Self { text }
}
}

View file

@ -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<ImageDetailsContentBlock>,
/// 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<CaptionContentBlock>,
/// 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<AltTextContentBlock>,
/// 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<Relation<ImageEventContentWithoutRelation>>,
}
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<String>, 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<Thumbnail>` with
/// `ThumbnailContentBlock::from()` / `.into()`.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[allow(clippy::exhaustive_structs)]
pub struct ThumbnailContentBlock(Vec<Thumbnail>);
impl ThumbnailContentBlock {
/// Whether this content block is empty.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl From<Vec<Thumbnail>> for ThumbnailContentBlock {
fn from(thumbnails: Vec<Thumbnail>) -> Self {
Self(thumbnails)
}
}
impl FromIterator<Thumbnail> for ThumbnailContentBlock {
fn from_iter<T: IntoIterator<Item = Thumbnail>>(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<String>,
/// The size of the file in bytes.
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
/// Information on the encrypted thumbnail.
///
/// Required if the thumbnail is encrypted.
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub encryption_info: Option<Box<EncryptedContent>>,
}
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<String>) -> Self {
Self { text: TextContentBlock::plain(body) }
}
}
impl From<TextContentBlock> for AltTextContentBlock {
fn from(text: TextContentBlock) -> Self {
Self { text }
}
}

View file

@ -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::*,

View file

@ -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<MilliSecondsSinceUnixEpoch>,
/// 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<Relation<LocationEventContentWithoutRelation>>,
}
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<String>, 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<String>,
/// A zoom level to specify the displayed area size.
#[serde(skip_serializing_if = "Option::is_none")]
pub zoom_level: Option<ZoomLevel>,
}
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<Self> {
if value > Self::MAX {
None
} else {
Some(Self(value.into()))
}
}
/// The value of this `ZoomLevel`.
pub fn get(&self) -> UInt {
self.0
}
}
impl TryFrom<u8> for ZoomLevel {
type Error = ZoomLevelError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
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),
}

View file

@ -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<D>(deserializer: D) -> Result<Self, D::Error>
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))
}
}
}

View file

@ -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<Relation<MessageEventContentWithoutRelation>>,
pub relations: Vec<Relation<MessageEventContentWithoutRelation>>,
/// 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<Mentions>,
/// 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<String>) -> 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<String>, html_body: impl Into<String>) -> 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<str> + Into<String>) -> 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<TextContentBlock> 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<TextContentBlock> for MessageEventContent {
/// This is an array of [`TextRepresentation`].
///
/// To construct a `TextContentBlock` with custom MIME types, construct a `Vec<TextRepresentation>`
/// first and use its `::from()` / `.into()` implementation.
/// first and use its `::from()` / `.into()` implemention.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct TextContentBlock(Vec<TextRepresentation>);

View file

@ -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<TextRepresentation>);
impl From<MessageContentBlock> for TextContentBlock {
fn from(value: MessageContentBlock) -> Self {
Self(value.0)
}
}
impl From<TextContentBlock> 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<String>,
/// HTML short form.
#[serde(rename = "org.matrix.msc1767.html", skip_serializing_if = "Option::is_none")]
html: Option<String>,
/// Long form.
#[serde(rename = "org.matrix.msc1767.message", skip_serializing_if = "Option::is_none")]
message: Option<Vec<TextRepresentation>>,
}
impl TryFrom<MessageContentBlockSerDeHelper> for Vec<TextRepresentation> {
type Error = &'static str;
fn try_from(value: MessageContentBlockSerDeHelper) -> Result<Self, Self::Error> {
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<MessageContentBlockSerDeHelper> for MessageContentBlock {
type Error = &'static str;
fn try_from(value: MessageContentBlockSerDeHelper) -> Result<Self, Self::Error> {
Ok(Self(value.try_into()?))
}
}
impl From<Vec<TextRepresentation>> for MessageContentBlockSerDeHelper {
fn from(value: Vec<TextRepresentation>) -> 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<MessageContentBlock> for MessageContentBlockSerDeHelper {
fn from(value: MessageContentBlock) -> Self {
value.0.into()
}
}

View file

@ -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<UInt>,
/// Which preset reactions to show? (for bot interactions)
#[serde(skip_serializing_if = "HashMap::is_empty")]
reactions: HashMap<String, UInt>,
}
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()
}
}

View file

@ -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<Item = PollResponseData<'a>>,
end_timestamp: Option<MilliSecondsSinceUnixEpoch>,
) -> 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<Item = PollResponseData<'a>>,
end_timestamp: Option<MilliSecondsSinceUnixEpoch>,
) -> 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<impl Iterator<Item = &'a str>> {
// 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<Item = PollResponseData<'a>>,
end_timestamp: Option<MilliSecondsSinceUnixEpoch>,
) -> BTreeMap<&'a UserId, (MilliSecondsSinceUnixEpoch, Option<impl Iterator<Item = &'a str>>)> {
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<Item = &'a str>,
users_selections: BTreeMap<
&'a UserId,
(MilliSecondsSinceUnixEpoch, Option<impl Iterator<Item = &'a str>>),
>,
) -> 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<Item = (&'a str, usize)>,
) -> 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::<Vec<_>>();
// 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}")
}
}
}

View file

@ -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<PollResultsContentBlock>,
/// 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<String>, 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<String, UInt>);
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::<Vec<_>>();
sorted.sort_by(|(_, a), (_, b)| b.cmp(a));
sorted
}
}
impl From<BTreeMap<String, UInt>> for PollResultsContentBlock {
fn from(value: BTreeMap<String, UInt>) -> Self {
Self(value)
}
}
impl IntoIterator for PollResultsContentBlock {
type Item = (String, UInt);
type IntoIter = btree_map::IntoIter<String, UInt>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl FromIterator<(String, UInt)> for PollResultsContentBlock {
fn from_iter<T: IntoIterator<Item = (String, UInt)>>(iter: T) -> Self {
Self(BTreeMap::from_iter(iter))
}
}
impl Deref for PollResultsContentBlock {
type Target = BTreeMap<String, UInt>;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View file

@ -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<String>);
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<impl Iterator<Item = &'a str>> {
let answer_ids = poll.answers.iter().map(|a| a.id.as_str()).collect();
validate_selections(&answer_ids, poll.max_selections, &self.0)
}
}
impl From<Vec<String>> for SelectionsContentBlock {
fn from(value: Vec<String>) -> Self {
Self(value)
}
}
impl IntoIterator for SelectionsContentBlock {
type Item = String;
type IntoIter = vec::IntoIter<String>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl FromIterator<String> for SelectionsContentBlock {
fn from_iter<T: IntoIterator<Item = String>>(iter: T) -> Self {
Self(Vec::from_iter(iter))
}
}
impl Deref for SelectionsContentBlock {
type Target = [String];
fn deref(&self) -> &Self::Target {
&self.0
}
}

View file

@ -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<Relation<PollStartEventContentWithoutRelation>>,
/// 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<String>, 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<Item = PollResponseData<'a>>,
) -> 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::<Vec<_>>();
// 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::<Vec<_>>();
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<TextContentBlock> 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<PollAnswer>);
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<Vec<PollAnswer>> for PollAnswers {
type Error = PollAnswersError;
fn try_from(value: Vec<PollAnswer>) -> Result<Self, Self::Error> {
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, Self::Error> {
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 }
}
}

View file

@ -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<PollAnswer>);
impl TryFrom<PollAnswersDeHelper> for PollAnswers {
type Error = PollAnswersError;
fn try_from(helper: PollAnswersDeHelper) -> Result<Self, Self::Error> {
let mut answers = helper.0;
answers.truncate(PollAnswers::MAX_LENGTH);
PollAnswers::try_from(answers)
}
}

View file

@ -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<String>, 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 {}

View file

@ -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<String>, 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<String>,
}
impl UnstablePollResponseContentBlock {
/// Creates a new `UnstablePollResponseContentBlock` with the given answers.
pub fn new(answers: Vec<String>) -> 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<impl Iterator<Item = &'a str>> {
let answer_ids = poll.answers.iter().map(|a| a.id.as_str()).collect();
validate_selections(&answer_ids, poll.max_selections, &self.answers)
}
}
impl From<Vec<String>> for UnstablePollResponseContentBlock {
fn from(value: Vec<String>) -> Self {
Self::new(value)
}
}

View file

@ -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<NewUnstablePollStartEventContent> for UnstablePollStartEventContent {
fn from(value: NewUnstablePollStartEventContent) -> Self {
Self::New(value)
}
}
impl From<ReplacementUnstablePollStartEventContent> 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<Item = PollResponseData<'a>>,
) -> 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::<Vec<_>>();
// 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::<Vec<_>>();
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<String>,
/// Information about related messages.
#[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")]
pub relates_to: Option<RelationWithoutReplacement>,
}
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<String>, 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<String>,
}
impl From<NewUnstablePollStartEventContent> 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<UnstablePollStartContentBlock>,
/// Text representation of the message, for clients that don't support polls.
pub text: Option<String>,
/// Information about related messages.
pub relates_to: Replacement<NewUnstablePollStartEventContentWithoutRelation>,
}
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<String>,
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<String>, 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<String>) -> 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<UnstablePollAnswer>);
impl TryFrom<Vec<UnstablePollAnswer>> for UnstablePollAnswers {
type Error = PollAnswersError;
fn try_from(value: Vec<UnstablePollAnswer>) -> Result<Self, Self::Error> {
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, Self::Error> {
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<String>, text: impl Into<String>) -> Self {
Self { id: id.into(), text: text.into() }
}
}

View file

@ -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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
let mut deserializer = serde_json::Deserializer::from_str(json.get());
let relates_to: Option<Relation<NewUnstablePollStartEventContentWithoutRelation>> =
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<UnstablePollStartContentBlock>,
#[serde(rename = "org.matrix.msc1767.text")]
text: Option<String>,
}
impl Serialize for ReplacementUnstablePollStartEventContent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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,
}

View file

@ -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<UnstablePollAnswer>);
impl TryFrom<UnstablePollAnswersDeHelper> for UnstablePollAnswers {
type Error = PollAnswersError;
fn try_from(helper: UnstablePollAnswersDeHelper) -> Result<Self, Self::Error> {
let mut answers = helper.0;
answers.truncate(PollAnswers::MAX_LENGTH);
UnstablePollAnswers::try_from(answers)
}
}

View file

@ -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<S>(kind: &PollKind, serializer: S) -> Result<S::Ok, S::Error>
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<PollKind, D::Error>
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)
}

View file

@ -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<Relation<()>>,
}
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<Annotation> for ReactionEventContent {
fn from(relates_to: Annotation) -> Self {
Self::new(relates_to)
fn from(annotation: Annotation) -> Self {
Self::new(annotation)
}
}

View file

@ -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<C> Replacement<C> {
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<InReplyTo>,
/// 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<AnyMessageLikeEvent>,
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<BundledAttachment>,
}
impl AttachmentChunk {
/// Creates a new `AttachmentChunk` with the given chunk.
pub fn new(chunk: Vec<BundledAttachment>) -> 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<E> {
#[serde(rename = "m.thread", skip_serializing_if = "Option::is_none")]
pub thread: Option<Box<BundledThread>>,
/// Attachment relations.
#[serde(rename = "m.attachment", skip_serializing_if = "Option::is_none")]
pub attachments: Option<Box<AttachmentChunk>>,
/// Reference relations.
#[serde(rename = "m.reference", skip_serializing_if = "Option::is_none")]
pub reference: Option<Box<ReferenceChunk>>,
@ -231,7 +268,13 @@ pub struct BundledMessageLikeRelations<E> {
impl<E> BundledMessageLikeRelations<E> {
/// 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<E> BundledMessageLikeRelations<E> {
/// 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<E>` to `BundledMessageLikeRelations<T>` using the
/// given closure to convert the `replace` field if it is `Some(_)`.
pub(crate) fn map_replace<T>(self, f: impl FnOnce(E) -> T) -> BundledMessageLikeRelations<T> {
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<C> {
/// `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<C>),
/// `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<RelationType> {
pub fn rel_type(&self) -> Option<RelationType> {
Some(self.0.get("rel_type")?.as_str()?.into())
}
}

View file

@ -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<E> {
@ -11,6 +11,8 @@ struct BundledMessageLikeRelationsJsonRepr<E> {
thread: Option<Box<BundledThread>>,
#[serde(rename = "m.reference")]
reference: Option<Box<ReferenceChunk>>,
#[serde(rename = "m.attachments")]
attachments: Option<Box<AttachmentChunk>>,
}
impl<'de, E> Deserialize<'de> for BundledMessageLikeRelations<E>
@ -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 })
}
}

View file

@ -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<Relation<MessageType>>,
/// }
/// ```
pub fn deserialize_relation<'de, D, C>(deserializer: D) -> Result<Vec<Relation<C>>, D::Error>
where
D: Deserializer<'de>,
C: Deserialize<'de>,
{
let EventWithRelatesToDeHelper { relations, mut new_content } =
EventWithRelatesToDeHelper::<C>::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::<Result<Vec<_>, D::Error>>()
.map_err(de::Error::custom)
}
impl<C> Serialize for Relation<C>
where
C: Clone + Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let (relations, new_content) = self.clone().into_parts();
EventWithRelatesToSerHelper { relations, new_content }.serialize(serializer)
}
}
#[derive(Deserialize)]
pub(crate) struct EventWithRelatesToDeHelper<C> {
#[serde(rename = "m.relations")]
relations: Vec<RelationDeHelper>,
#[serde(rename = "m.new_content")]
new_content: Option<C>,
}
#[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<C> {
#[serde(rename = "m.relations")]
relations: RelationSerHelper,
#[serde(rename = "m.new_content", skip_serializing_if = "Option::is_none")]
new_content: Option<C>,
}
impl<C> Relation<C> {
fn into_parts(self) -> (RelationSerHelper, Option<C>) {
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),
}
}
}

View file

@ -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;

View file

@ -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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let json = Box::<RawJsonValue>::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<InReplyTo>,
rel_type: Option<String>,
}
/// 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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,
}

View file

@ -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

View file

@ -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<Box<AudioInfo>>,
/// 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<UnstableAudioDetailsContentBlock>,
/// 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<UnstableVoiceContentBlock>,
}
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<Option<Box<AudioInfo>>>) -> 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<Duration>,
/// The mimetype of the audio, e.g. "audio/aac".
#[serde(skip_serializing_if = "Option::is_none")]
pub mimetype: Option<String>,
/// The size of the audio clip in bytes.
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
}
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<UnstableAmplitude>,
}
#[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<UnstableAmplitude>) -> 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<u16> for UnstableAmplitude {
fn from(value: u16) -> Self {
Self::new(value)
}
}
#[cfg(feature = "unstable-msc3245-v1-compat")]
impl<'de> Deserialize<'de> for UnstableAmplitude {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let uint = UInt::deserialize(deserializer)?;
Ok(Self(uint.min(Self::MAX.into())))
}
}

View file

@ -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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
let json = Box::<RawJsonValue>::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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
let json = Box::<RawJsonValue>::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<Mentions>,
}
/// 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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
let json = Box::<RawJsonValue>::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<Box<LocationInfo>>,
#[serde(flatten)]
pub message: Option<MessageContentBlock>,
#[serde(rename = "org.matrix.msc3488.location", skip_serializing_if = "Option::is_none")]
pub location: Option<LocationContent>,
#[serde(rename = "org.matrix.msc3488.asset", skip_serializing_if = "Option::is_none")]
pub asset: Option<AssetContent>,
#[serde(rename = "org.matrix.msc3488.ts", skip_serializing_if = "Option::is_none")]
pub ts: Option<MilliSecondsSinceUnixEpoch>,
}
impl From<LocationMessageEventContent> 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<LocationMessageEventContentSerDeHelper> 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,
}
}
}
}

View file

@ -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<FormattedBody>,
}
impl EmoteMessageEventContent {
/// A convenience constructor to create a plain-text emote.
pub fn plain(body: impl Into<String>) -> Self {
let body = body.into();
Self { body, formatted: None }
}
/// A convenience constructor to create an html emote message.
pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> 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<str> + Into<String>) -> Self {
if let Some(formatted) = FormattedBody::markdown(&body) {
Self::html(body, formatted.body)
} else {
Self::plain(body)
}
}
}

View file

@ -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<String>,
/// 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<Box<FileInfo>>,
}
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<Option<String>>) -> 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<Option<Box<FileInfo>>>) -> 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<String>,
/// The size of the file in bytes.
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
/// Metadata about the image referred to in `thumbnail_source`.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<Box<ThumbnailInfo>>,
/// 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<MediaSource>,
}
impl FileInfo {
/// Creates an empty `FileInfo`.
pub fn new() -> Self {
Self::default()
}
}

View file

@ -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<Box<ImageInfo>>,
}
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<Option<Box<ImageInfo>>>) -> Self {
Self { info: info.into(), ..self }
}
}

View file

@ -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<FormattedBody>,
/// The verification methods supported by the sender.
pub methods: Vec<VerificationMethod>,
/// 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<VerificationMethod>,
from_device: OwnedDeviceId,
to: OwnedUserId,
) -> Self {
Self { body, formatted: None, methods, from_device, to }
}
}

View file

@ -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<Box<LocationInfo>>,
/// Extensible-event text representation of the message.
///
/// If present, this should be preferred over the `body` field.
#[cfg(feature = "unstable-msc3488")]
pub message: Option<TextContentBlock>,
/// 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<LocationContent>,
/// Extensible-event asset this message refers to.
#[cfg(feature = "unstable-msc3488")]
pub asset: Option<AssetContent>,
/// Extensible-event timestamp this message refers to.
#[cfg(feature = "unstable-msc3488")]
pub ts: Option<MilliSecondsSinceUnixEpoch>,
}
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<MediaSource>,
/// Metadata about the image referred to in `thumbnail_source.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<Box<ThumbnailInfo>>,
}
impl LocationInfo {
/// Creates an empty `LocationInfo`.
pub fn new() -> Self {
Self::default()
}
}

View file

@ -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<FormattedBody>,
}
impl NoticeMessageEventContent {
/// A convenience constructor to create a plain text notice.
pub fn plain(body: impl Into<String>) -> Self {
let body = body.into();
Self { body, formatted: None }
}
/// A convenience constructor to create an html notice.
pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> 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<str> + Into<String>) -> Self {
if let Some(formatted) = FormattedBody::markdown(&body) {
Self::html(body, formatted.body)
} else {
Self::plain(body)
}
}
}

View file

@ -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<C> {
/// 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<C>),
/// An event that belongs to a thread.
Thread(Thread),
#[doc(hidden)]
_Custom(CustomRelation),
}
impl<C> Relation<C> {
/// 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<RelationType> {
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<RelationType> {
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<C> TryFrom<Relation<C>> for RelationWithoutReplacement {
type Error = Replacement<C>;
fn try_from(value: Relation<C>) -> Result<Self, Self::Error> {
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)
}
}

View file

@ -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<Relation<MessageType>>,
/// }
/// ```
pub fn deserialize_relation<'de, D, C>(deserializer: D) -> Result<Option<Relation<C>>, 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<C> Serialize for Relation<C>
where
C: Clone + Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<C> {
#[serde(rename = "m.relates_to")]
relates_to: Option<RelatesToDeHelper>,
#[serde(rename = "m.new_content")]
new_content: Option<C>,
}
#[derive(Deserialize)]
pub(crate) struct RelatesToDeHelper {
#[serde(rename = "m.in_reply_to")]
in_reply_to: Option<InReplyTo>,
#[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<C> {
#[serde(rename = "m.relates_to")]
relates_to: RelationSerHelper,
#[serde(rename = "m.new_content", skip_serializing_if = "Option::is_none")]
new_content: Option<C>,
}
/// 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<C> Relation<C> {
fn into_parts(self) -> (RelationSerHelper, Option<C>) {
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<InReplyTo>,
#[serde(flatten, skip_serializing_if = "JsonObject::is_empty")]
data: JsonObject,
}
impl From<InReplyTo> for CustomSerHelper {
fn from(value: InReplyTo) -> Self {
Self { in_reply_to: Some(value), data: JsonObject::new() }
}
}
impl From<CustomRelation> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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"),
}
}
}

View file

@ -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!(
"<mx-reply>\
<blockquote>\
<a href=\"https://matrix.to/#/{room_id}/{event_id}\">In reply to</a> \
{emote_sign}<a href=\"https://matrix.to/#/{sender}\">{sender}</a>\
<br>\
{html_body}\
</blockquote>\
</mx-reply>"
),
)
}
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.
// <https://developer.mozilla.org/en-US/docs/Glossary/Entity#reserved_characters>
match c {
'&' => f.write_str("&amp;")?,
'<' => f.write_str("&lt;")?,
'>' => f.write_str("&gt;")?,
'"' => f.write_str("&quot;")?,
'\n' => f.write_str("<br>")?,
_ => 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<impl fmt::Display>,
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,
"<mx-reply>\
<blockquote>\
<a href=\"https://matrix.to/#/!n8f893n9:example.com/$1598361704261elfgc:localhost\">In reply to</a> \
<a href=\"https://matrix.to/#/@alice:example.com\">@alice:example.com</a>\
<br>\
multi<br>line\
</blockquote>\
</mx-reply>",
);
}
}

View file

@ -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"
);
}
}

View file

@ -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<String>,
/// 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<LimitType>,
}
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),
}

View file

@ -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<FormattedBody>,
}
impl TextMessageEventContent {
/// A convenience constructor to create a plain text message.
pub fn plain(body: impl Into<String>) -> Self {
let body = body.into();
Self { body, formatted: None }
}
/// A convenience constructor to create an HTML message.
pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> 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<str> + Into<String>) -> Self {
if let Some(formatted) = FormattedBody::markdown(&body) {
Self::html(body, formatted.body)
} else {
Self::plain(body)
}
}
}

View file

@ -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<Box<VideoInfo>>,
}
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<Option<Box<VideoInfo>>>) -> 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<Duration>,
/// The height of the video in pixels.
#[serde(rename = "h", skip_serializing_if = "Option::is_none")]
pub height: Option<UInt>,
/// The width of the video in pixels.
#[serde(rename = "w", skip_serializing_if = "Option::is_none")]
pub width: Option<UInt>,
/// The mimetype of the video, e.g. "video/mp4".
#[serde(skip_serializing_if = "Option::is_none")]
pub mimetype: Option<String>,
/// The size of the video in bytes.
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
/// Metadata about the image referred to in `thumbnail_source`.
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<Box<ThumbnailInfo>>,
/// 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<MediaSource>,
/// 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<String>,
}
impl VideoInfo {
/// Creates an empty `VideoInfo`.
pub fn new() -> Self {
Self::default()
}
}

View file

@ -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<Mentions>,
}
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<String>) -> Self {
Self::new(MessageType::text_plain(body))
}
/// A constructor to create an html message.
pub fn text_html(body: impl Into<String>, html_body: impl Into<String>) -> 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<str> + Into<String>) -> Self {
Self::new(MessageType::text_markdown(body))
}
/// A constructor to create a plain text notice.
pub fn notice_plain(body: impl Into<String>) -> Self {
Self::new(MessageType::notice_plain(body))
}
/// A constructor to create an html notice.
pub fn notice_html(body: impl Into<String>, html_body: impl Into<String>) -> 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<str> + Into<String>) -> Self {
Self::new(MessageType::notice_markdown(body))
}
/// A constructor to create a plain text emote.
pub fn emote_plain(body: impl Into<String>) -> Self {
Self::new(MessageType::emote_plain(body))
}
/// A constructor to create an html emote.
pub fn emote_html(body: impl Into<String>, html_body: impl Into<String>) -> 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<str> + Into<String>) -> Self {
Self::new(MessageType::emote_markdown(body))
}
/// Transform `self` into a `RoomMessageEventContent` with the given relation.
pub fn with_relation(
self,
relates_to: Option<Relation<RoomMessageEventContentWithoutRelation>>,
) -> 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<AnySyncTimelineEvent>,
original_event_id: OwnedEventId,
room_id: &RoomId,
forward_thread: ForwardThread,
add_mentions: AddMentions,
) -> RoomMessageEventContent {
#[derive(Deserialize)]
struct ContentDeHelper {
body: Option<String>,
#[serde(flatten)]
formatted: Option<FormattedBody>,
#[cfg(feature = "unstable-msc1767")]
#[serde(rename = "org.matrix.msc1767.text")]
text: Option<String>,
#[serde(rename = "m.relates_to")]
relates_to: Option<crate::room::encrypted::Relation>,
}
let sender = original_event.get_field::<OwnedUserId>("sender").ok().flatten();
let content = original_event.get_field::<ContentDeHelper>("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<OwnedEventId>,
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<MessageType> for RoomMessageEventContentWithoutRelation {
fn from(msgtype: MessageType) -> Self {
Self::new(msgtype)
}
}
impl From<RoomMessageEventContent> for RoomMessageEventContentWithoutRelation {
fn from(value: RoomMessageEventContent) -> Self {
let RoomMessageEventContent { msgtype, mentions, .. } = value;
Self { msgtype, mentions }
}
}

View file

@ -0,0 +1,3 @@
mod message;
mod call;
mod poll;

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -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<VideoDetailsContentBlock>,
/// 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<CaptionContentBlock>,
/// 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<Relation<VideoEventContentWithoutRelation>>,
}
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<String>, 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<Duration>,
}
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 }
}
}

View file

@ -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<Relation<VoiceEventContentWithoutRelation>>,
}
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<String>,
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<Amplitude>,
}
impl VoiceAudioDetailsContentBlock {
/// Creates a new `AudioDetailsContentBlock` with the given duration and waveform
/// representation.
pub fn new(duration: Duration, waveform: Vec<Amplitude>) -> Self {
Self { duration, waveform }
}
}

View file

@ -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]

View file

@ -171,8 +171,6 @@ fn plain_text_content_deserialization() {
let content = from_json_value::<MessageEventContent>(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::<MessageEventContent>(json_data).unwrap();
assert_eq!(content.text.find_plain(), None);
assert_eq!(content.text.find_html(), Some("Hello, <em>New World</em>!"));
#[cfg(feature = "unstable-msc3955")]
assert!(!content.automated);
}
#[test]
@ -202,8 +198,6 @@ fn html_and_text_content_deserialization() {
let content = from_json_value::<MessageEventContent>(json_data).unwrap();
assert_eq!(content.text.find_plain(), Some("Hello, New World!"));
assert_eq!(content.text.find_html(), Some("Hello, <em>New World</em>!"));
#[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, <em>New World</em>!" },
],
"org.matrix.msc1767.automated": true,
});
let content = from_json_value::<MessageEventContent>(json_data).unwrap();
assert_eq!(content.text.find_plain(), None);
assert_eq!(content.text.find_html(), Some("Hello, <em>New World</em>!"));
assert!(content.automated);
}

View file

@ -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::<RoomMessageEventContent>(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]

View file

@ -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]

View file

@ -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!(

View file

@ -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::<Vec<_>, _>(|f| {
f.ident.as_ref().filter(|ident| *ident == "relates_to").is_some()
let (_relations, other_fields) = fields.partition::<Vec<_>, _>(|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::<Vec<_>>();
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,
}
}
}
})
}