armageddon
This commit is contained in:
parent
6e2d6ef142
commit
c377f8ec91
69 changed files with 803 additions and 5344 deletions
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
118
crates/ruma-client-api/src/push/get_inbox.rs
Normal file
118
crates/ruma-client-api/src/push/get_inbox.rs
Normal 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,
|
||||
}
|
||||
}
|
|
@ -2,3 +2,4 @@
|
|||
|
||||
pub mod get_threads;
|
||||
pub mod bulk_threads;
|
||||
pub mod set_participation;
|
||||
|
|
|
@ -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 {
|
||||
|
|
6
crates/ruma-events/src/attachment.rs
Normal file
6
crates/ruma-events/src/attachment.rs
Normal 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;
|
59
crates/ruma-events/src/attachment/embed.rs
Normal file
59
crates/ruma-events/src/attachment/embed.rs
Normal 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>,
|
||||
}
|
89
crates/ruma-events/src/attachment/file.rs
Normal file
89
crates/ruma-events/src/attachment/file.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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())))
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)]
|
|
@ -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,
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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::*,
|
||||
|
|
|
@ -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),
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>);
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
31
crates/ruma-events/src/mixins.rs
Normal file
31
crates/ruma-events/src/mixins.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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() }
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
170
crates/ruma-events/src/relation/relation_serde.rs
Normal file
170
crates/ruma-events/src/relation/relation_serde.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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())))
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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("&")?,
|
||||
'<' => f.write_str("<")?,
|
||||
'>' => f.write_str(">")?,
|
||||
'"' => f.write_str(""")?,
|
||||
'\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>",
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
3
crates/ruma-events/src/threads.rs
Normal file
3
crates/ruma-events/src/threads.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
mod message;
|
||||
mod call;
|
||||
mod poll;
|
1
crates/ruma-events/src/threads/call.rs
Normal file
1
crates/ruma-events/src/threads/call.rs
Normal file
|
@ -0,0 +1 @@
|
|||
|
1
crates/ruma-events/src/threads/message.rs
Normal file
1
crates/ruma-events/src/threads/message.rs
Normal file
|
@ -0,0 +1 @@
|
|||
|
1
crates/ruma-events/src/threads/poll.rs
Normal file
1
crates/ruma-events/src/threads/poll.rs
Normal file
|
@ -0,0 +1 @@
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue