events: Add support for extensible file events
As defined in MSC3551
This commit is contained in:
parent
c1e1665808
commit
c37a514a89
@ -6,7 +6,9 @@ Breaking changes:
|
||||
|
||||
Improvements:
|
||||
|
||||
* Add unstable support for extensible text, notice and emote message events ([MSC1767](https://github.com/matrix-org/matrix-spec-proposals/pull/1767))
|
||||
* Add unstable support for extensible message events:
|
||||
* text, notice and emote ([MSC1767](https://github.com/matrix-org/matrix-spec-proposals/pull/1767))
|
||||
* file ([MSC3551](https://github.com/matrix-org/matrix-spec-proposals/pull/3551))
|
||||
|
||||
# 0.26.0
|
||||
|
||||
|
@ -26,6 +26,7 @@ unstable-msc2448 = []
|
||||
unstable-msc2675 = []
|
||||
unstable-msc2676 = []
|
||||
unstable-msc2677 = []
|
||||
unstable-msc3551 = ["unstable-msc1767"]
|
||||
|
||||
[dependencies]
|
||||
criterion = { version = "0.3.3", optional = true }
|
||||
|
@ -39,6 +39,8 @@ event_enum! {
|
||||
"m.call.candidates",
|
||||
#[cfg(feature = "unstable-msc1767")]
|
||||
"m.emote",
|
||||
#[cfg(feature = "unstable-msc3551")]
|
||||
"m.file",
|
||||
"m.key.verification.ready",
|
||||
"m.key.verification.start",
|
||||
"m.key.verification.cancel",
|
||||
@ -364,6 +366,8 @@ impl AnyMessageLikeEventContent {
|
||||
Self::Notice(ev) => ev.relates_to.clone().map(Into::into),
|
||||
#[cfg(feature = "unstable-msc1767")]
|
||||
Self::Emote(ev) => ev.relates_to.clone().map(Into::into),
|
||||
#[cfg(feature = "unstable-msc3551")]
|
||||
Self::File(ev) => ev.relates_to.clone().map(Into::into),
|
||||
Self::CallAnswer(_)
|
||||
| Self::CallInvite(_)
|
||||
| Self::CallHangup(_)
|
||||
|
206
crates/ruma-events/src/file.rs
Normal file
206
crates/ruma-events/src/file.rs
Normal file
@ -0,0 +1,206 @@
|
||||
//! 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_identifiers::MxcUri;
|
||||
use ruma_macros::EventContent;
|
||||
use ruma_serde::Base64;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
message::MessageContent,
|
||||
room::{message::Relation, JsonWebKey},
|
||||
};
|
||||
|
||||
/// 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 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a file content.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct FileContentInfo {
|
||||
/// The original filename of the uploaded file.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<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>,
|
||||
}
|
||||
|
||||
impl FileContentInfo {
|
||||
/// Creates an empty `FileContentInfo`.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// File content.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct FileContent {
|
||||
/// The URL to the file.
|
||||
pub url: Box<MxcUri>,
|
||||
|
||||
/// Information about the uploaded file.
|
||||
#[serde(flatten, skip_serializing_if = "Option::is_none")]
|
||||
pub info: Option<Box<FileContentInfo>>,
|
||||
|
||||
/// Information on the encrypted file.
|
||||
///
|
||||
/// Required if file is encrypted.
|
||||
#[serde(flatten, skip_serializing_if = "Option::is_none")]
|
||||
pub encryption_info: Option<Box<EncryptedContent>>,
|
||||
}
|
||||
|
||||
impl FileContent {
|
||||
/// Creates a new non-encrypted `FileContent` with the given url and file info.
|
||||
pub fn plain(url: Box<MxcUri>, info: Option<Box<FileContentInfo>>) -> Self {
|
||||
Self { url, info, encryption_info: None }
|
||||
}
|
||||
|
||||
/// Creates a new encrypted `FileContent` with the given url, encryption info and file info.
|
||||
pub fn encrypted(
|
||||
url: Box<MxcUri>,
|
||||
encryption_info: EncryptedContent,
|
||||
info: Option<Box<FileContentInfo>>,
|
||||
) -> Self {
|
||||
Self { url, info, encryption_info: Some(Box::new(encryption_info)) }
|
||||
}
|
||||
|
||||
/// Whether the file is encrypted.
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.encryption_info.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// The payload for an extensible text message.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
#[ruma_event(type = "m.file", kind = MessageLike)]
|
||||
pub struct FileEventContent {
|
||||
/// The text representation of the message.
|
||||
#[serde(flatten)]
|
||||
pub message: MessageContent,
|
||||
|
||||
/// The file content of the message.
|
||||
#[serde(rename = "org.matrix.msc1767.file")]
|
||||
pub file: FileContent,
|
||||
|
||||
/// Information about related messages for [rich replies].
|
||||
///
|
||||
/// [rich replies]: https://spec.matrix.org/v1.2/client-server-api/#rich-replies
|
||||
#[serde(flatten, skip_serializing_if = "Option::is_none")]
|
||||
pub relates_to: Option<Relation>,
|
||||
}
|
||||
|
||||
impl FileEventContent {
|
||||
/// Creates a new non-encrypted `FileEventContent` with the given plain text message, url and
|
||||
/// file info.
|
||||
pub fn plain(
|
||||
message: impl Into<String>,
|
||||
url: Box<MxcUri>,
|
||||
info: Option<Box<FileContentInfo>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
message: MessageContent::plain(message),
|
||||
file: FileContent::plain(url, info),
|
||||
relates_to: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new non-encrypted `FileEventContent` with the given message, url and
|
||||
/// file info.
|
||||
pub fn plain_message(
|
||||
message: MessageContent,
|
||||
url: Box<MxcUri>,
|
||||
info: Option<Box<FileContentInfo>>,
|
||||
) -> Self {
|
||||
Self { message, file: FileContent::plain(url, info), relates_to: None }
|
||||
}
|
||||
|
||||
/// Creates a new encrypted `FileEventContent` with the given plain text message, url,
|
||||
/// encryption info and file info.
|
||||
pub fn encrypted(
|
||||
message: impl Into<String>,
|
||||
url: Box<MxcUri>,
|
||||
encryption_info: EncryptedContent,
|
||||
info: Option<Box<FileContentInfo>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
message: MessageContent::plain(message),
|
||||
file: FileContent::encrypted(url, encryption_info, info),
|
||||
relates_to: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new encrypted `FileEventContent` with the given message, url,
|
||||
/// encryption info and file info.
|
||||
pub fn encrypted_message(
|
||||
message: MessageContent,
|
||||
url: Box<MxcUri>,
|
||||
encryption_info: EncryptedContent,
|
||||
info: Option<Box<FileContentInfo>>,
|
||||
) -> Self {
|
||||
Self { message, file: FileContent::encrypted(url, encryption_info, info), relates_to: None }
|
||||
}
|
||||
}
|
@ -174,6 +174,8 @@ pub mod direct;
|
||||
pub mod dummy;
|
||||
#[cfg(feature = "unstable-msc1767")]
|
||||
pub mod emote;
|
||||
#[cfg(feature = "unstable-msc3551")]
|
||||
pub mod file;
|
||||
pub mod forwarded_room_key;
|
||||
pub mod fully_read;
|
||||
pub mod ignored_user_list;
|
||||
|
251
crates/ruma-events/tests/file.rs
Normal file
251
crates/ruma-events/tests/file.rs
Normal file
@ -0,0 +1,251 @@
|
||||
#![cfg(feature = "unstable-msc3551")]
|
||||
|
||||
use assign::assign;
|
||||
use js_int::uint;
|
||||
use matches::assert_matches;
|
||||
use ruma_common::MilliSecondsSinceUnixEpoch;
|
||||
use ruma_events::{
|
||||
file::{EncryptedContentInit, FileContent, FileContentInfo, FileEventContent},
|
||||
message::MessageContent,
|
||||
room::{
|
||||
message::{InReplyTo, Relation},
|
||||
JsonWebKeyInit,
|
||||
},
|
||||
AnyMessageLikeEvent, MessageLikeEvent, Unsigned,
|
||||
};
|
||||
use ruma_identifiers::{event_id, mxc_uri, room_id, user_id};
|
||||
use ruma_serde::Base64;
|
||||
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
|
||||
|
||||
#[test]
|
||||
fn plain_content_serialization() {
|
||||
let event_content = FileEventContent::plain(
|
||||
"Upload: my_file.txt",
|
||||
mxc_uri!("mxc://notareal.hs/abcdef").to_owned(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
to_json_value(&event_content).unwrap(),
|
||||
json!({
|
||||
"org.matrix.msc1767.text": "Upload: my_file.txt",
|
||||
"org.matrix.msc1767.file": {
|
||||
"url": "mxc://notareal.hs/abcdef",
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_content_serialization() {
|
||||
let event_content = FileEventContent::encrypted(
|
||||
"Upload: my_file.txt",
|
||||
mxc_uri!("mxc://notareal.hs/abcdef").to_owned(),
|
||||
EncryptedContentInit {
|
||||
key: JsonWebKeyInit {
|
||||
kty: "oct".to_owned(),
|
||||
key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()],
|
||||
alg: "A256CTR".to_owned(),
|
||||
k: Base64::parse("TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A").unwrap(),
|
||||
ext: true,
|
||||
}
|
||||
.into(),
|
||||
iv: Base64::parse("S22dq3NAX8wAAAAAAAAAAA").unwrap(),
|
||||
hashes: [(
|
||||
"sha256".to_owned(),
|
||||
Base64::parse("aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q").unwrap(),
|
||||
)]
|
||||
.into(),
|
||||
v: "v2".to_owned(),
|
||||
}
|
||||
.into(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
to_json_value(&event_content).unwrap(),
|
||||
json!({
|
||||
"org.matrix.msc1767.text": "Upload: my_file.txt",
|
||||
"org.matrix.msc1767.file": {
|
||||
"url": "mxc://notareal.hs/abcdef",
|
||||
"key": {
|
||||
"kty": "oct",
|
||||
"key_ops": ["encrypt", "decrypt"],
|
||||
"alg": "A256CTR",
|
||||
"k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A",
|
||||
"ext": true
|
||||
},
|
||||
"iv": "S22dq3NAX8wAAAAAAAAAAA",
|
||||
"hashes": {
|
||||
"sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q"
|
||||
},
|
||||
"v": "v2"
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_event_serialization() {
|
||||
let event = MessageLikeEvent {
|
||||
content: assign!(
|
||||
FileEventContent::plain_message(
|
||||
MessageContent::html(
|
||||
"Upload: my_file.txt",
|
||||
"Upload: <strong>my_file.txt</strong>",
|
||||
),
|
||||
mxc_uri!("mxc://notareal.hs/abcdef").to_owned(),
|
||||
Some(Box::new(assign!(
|
||||
FileContentInfo::new(),
|
||||
{
|
||||
name: Some("my_file.txt".to_owned()),
|
||||
mimetype: Some("text/plain".to_owned()),
|
||||
size: Some(uint!(774)),
|
||||
}
|
||||
))),
|
||||
),
|
||||
{
|
||||
relates_to: Some(Relation::Reply {
|
||||
in_reply_to: InReplyTo::new(event_id!("$replyevent:example.com").to_owned()),
|
||||
}),
|
||||
}
|
||||
),
|
||||
event_id: event_id!("$event:notareal.hs").to_owned(),
|
||||
sender: user_id!("@user:notareal.hs").to_owned(),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(134_829_848)),
|
||||
room_id: room_id!("!roomid:notareal.hs").to_owned(),
|
||||
unsigned: Unsigned::default(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
to_json_value(&event).unwrap(),
|
||||
json!({
|
||||
"content": {
|
||||
"org.matrix.msc1767.message": [
|
||||
{ "body": "Upload: <strong>my_file.txt</strong>", "mimetype": "text/html"},
|
||||
{ "body": "Upload: my_file.txt", "mimetype": "text/plain"},
|
||||
],
|
||||
"org.matrix.msc1767.file": {
|
||||
"url": "mxc://notareal.hs/abcdef",
|
||||
"name": "my_file.txt",
|
||||
"mimetype": "text/plain",
|
||||
"size": 774,
|
||||
},
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$replyevent:example.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"event_id": "$event:notareal.hs",
|
||||
"origin_server_ts": 134_829_848,
|
||||
"room_id": "!roomid:notareal.hs",
|
||||
"sender": "@user:notareal.hs",
|
||||
"type": "m.file",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_content_deserialization() {
|
||||
let json_data = json!({
|
||||
"org.matrix.msc1767.text": "Upload: my_file.txt",
|
||||
"org.matrix.msc1767.file": {
|
||||
"url": "mxc://notareal.hs/abcdef",
|
||||
}
|
||||
});
|
||||
|
||||
assert_matches!(
|
||||
from_json_value::<FileEventContent>(json_data)
|
||||
.unwrap(),
|
||||
FileEventContent { message, file, .. }
|
||||
if message.find_plain() == Some("Upload: my_file.txt")
|
||||
&& message.find_html().is_none()
|
||||
&& file.url.as_str() == "mxc://notareal.hs/abcdef"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_content_deserialization() {
|
||||
let json_data = json!({
|
||||
"org.matrix.msc1767.text": "Upload: my_file.txt",
|
||||
"org.matrix.msc1767.file": {
|
||||
"url": "mxc://notareal.hs/abcdef",
|
||||
"key": {
|
||||
"kty": "oct",
|
||||
"key_ops": ["encrypt", "decrypt"],
|
||||
"alg": "A256CTR",
|
||||
"k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A",
|
||||
"ext": true
|
||||
},
|
||||
"iv": "S22dq3NAX8wAAAAAAAAAAA",
|
||||
"hashes": {
|
||||
"sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q"
|
||||
},
|
||||
"v": "v2"
|
||||
}
|
||||
});
|
||||
|
||||
assert_matches!(
|
||||
from_json_value::<FileEventContent>(json_data)
|
||||
.unwrap(),
|
||||
FileEventContent { message, file, .. }
|
||||
if message.find_plain() == Some("Upload: my_file.txt")
|
||||
&& message.find_html().is_none()
|
||||
&& file.url.as_str() == "mxc://notareal.hs/abcdef"
|
||||
&& file.encryption_info.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_event_deserialization() {
|
||||
let json_data = json!({
|
||||
"content": {
|
||||
"org.matrix.msc1767.message": [
|
||||
{ "body": "Upload: <strong>my_file.txt</strong>", "mimetype": "text/html"},
|
||||
{ "body": "Upload: my_file.txt", "mimetype": "text/plain"},
|
||||
],
|
||||
"org.matrix.msc1767.file": {
|
||||
"url": "mxc://notareal.hs/abcdef",
|
||||
"name": "my_file.txt",
|
||||
"mimetype": "text/plain",
|
||||
"size": 774,
|
||||
},
|
||||
},
|
||||
"event_id": "$event:notareal.hs",
|
||||
"origin_server_ts": 134_829_848,
|
||||
"room_id": "!roomid:notareal.hs",
|
||||
"sender": "@user:notareal.hs",
|
||||
"type": "m.file",
|
||||
});
|
||||
|
||||
assert_matches!(
|
||||
from_json_value::<AnyMessageLikeEvent>(json_data).unwrap(),
|
||||
AnyMessageLikeEvent::File(MessageLikeEvent {
|
||||
content: FileEventContent {
|
||||
message,
|
||||
file: FileContent {
|
||||
url,
|
||||
info: Some(info),
|
||||
..
|
||||
},
|
||||
..
|
||||
},
|
||||
event_id,
|
||||
origin_server_ts,
|
||||
room_id,
|
||||
sender,
|
||||
unsigned
|
||||
}) if event_id == event_id!("$event:notareal.hs")
|
||||
&& message.find_plain() == Some("Upload: my_file.txt")
|
||||
&& message.find_html() == Some("Upload: <strong>my_file.txt</strong>")
|
||||
&& url.as_str() == "mxc://notareal.hs/abcdef"
|
||||
&& info.name.as_deref() == Some("my_file.txt")
|
||||
&& info.mimetype.as_deref() == Some("text/plain")
|
||||
&& info.size == Some(uint!(774))
|
||||
&& origin_server_ts == MilliSecondsSinceUnixEpoch(uint!(134_829_848))
|
||||
&& room_id == room_id!("!roomid:notareal.hs")
|
||||
&& sender == user_id!("@user:notareal.hs")
|
||||
&& unsigned.is_empty()
|
||||
);
|
||||
}
|
@ -124,6 +124,7 @@ unstable-msc2677 = [
|
||||
"ruma-common/unstable-msc2677",
|
||||
"ruma-events/unstable-msc2677",
|
||||
]
|
||||
unstable-msc3551 = ["ruma-events/unstable-msc3551"]
|
||||
unstable-msc3618 = ["ruma-federation-api/unstable-msc3618"]
|
||||
|
||||
# Private feature, only used in test / benchmarking code
|
||||
@ -135,6 +136,7 @@ __ci = [
|
||||
"unstable-msc2675",
|
||||
"unstable-msc2676",
|
||||
"unstable-msc2677",
|
||||
"unstable-msc3551",
|
||||
"unstable-msc3618",
|
||||
"ruma-state-res/__ci",
|
||||
]
|
||||
|
Loading…
x
Reference in New Issue
Block a user