diff --git a/crates/ruma-appservice-api/Cargo.toml b/crates/ruma-appservice-api/Cargo.toml index 645dcfdf..3eb869f2 100644 --- a/crates/ruma-appservice-api/Cargo.toml +++ b/crates/ruma-appservice-api/Cargo.toml @@ -19,8 +19,8 @@ client = [] server = [] unstable-exhaustive-types = [] -unstable-msc2409 = [] unstable-msc3202 = [] +unstable-msc4203 = [] [dependencies] js_int = { workspace = true, features = ["serde"] } diff --git a/crates/ruma-appservice-api/src/event/push_events.rs b/crates/ruma-appservice-api/src/event/push_events.rs index a7c9d83e..2c2e7b64 100644 --- a/crates/ruma-appservice-api/src/event/push_events.rs +++ b/crates/ruma-appservice-api/src/event/push_events.rs @@ -7,39 +7,29 @@ pub mod v1 { //! //! [spec]: https://spec.matrix.org/latest/application-service-api/#put_matrixappv1transactionstxnid - #[cfg(any(feature = "unstable-msc2409", feature = "unstable-msc3202"))] + use std::borrow::Cow; + #[cfg(feature = "unstable-msc3202")] use std::collections::BTreeMap; - #[cfg(feature = "unstable-msc2409")] - use std::{ - collections::btree_map, - ops::{Deref, DerefMut}, - }; - #[cfg(any(feature = "unstable-msc2409", feature = "unstable-msc3202"))] + #[cfg(feature = "unstable-msc3202")] use js_int::UInt; - #[cfg(any(feature = "unstable-msc2409", feature = "unstable-msc3202"))] + #[cfg(feature = "unstable-msc3202")] use ruma_common::OwnedUserId; use ruma_common::{ api::{request, response, Metadata}, metadata, - serde::Raw, + serde::{from_raw_json_value, JsonObject, Raw}, OwnedTransactionId, }; - #[cfg(feature = "unstable-msc2409")] - use ruma_common::{ - presence::PresenceState, serde::from_raw_json_value, OwnedEventId, OwnedRoomId, - }; #[cfg(feature = "unstable-msc3202")] use ruma_common::{OneTimeKeyAlgorithm, OwnedDeviceId}; - use ruma_events::AnyTimelineEvent; - #[cfg(feature = "unstable-msc2409")] - use ruma_events::{receipt::Receipt, AnyToDeviceEvent}; - #[cfg(feature = "unstable-msc2409")] - use serde::Deserializer; - #[cfg(any(feature = "unstable-msc2409", feature = "unstable-msc3202"))] - use serde::{Deserialize, Serialize}; - #[cfg(feature = "unstable-msc2409")] - use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue}; + #[cfg(feature = "unstable-msc4203")] + use ruma_events::AnyToDeviceEvent; + use ruma_events::{ + presence::PresenceEvent, receipt::ReceiptEvent, typing::TypingEvent, AnyTimelineEvent, + }; + use serde::{Deserialize, Deserializer, Serialize}; + use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue}; const METADATA: Metadata = metadata! { method: PUT, @@ -93,17 +83,12 @@ pub mod v1 { pub device_unused_fallback_key_types: BTreeMap>>, - /// A list of EDUs. - #[cfg(feature = "unstable-msc2409")] - #[serde( - default, - skip_serializing_if = "<[_]>::is_empty", - rename = "de.sorunome.msc2409.ephemeral" - )] - pub ephemeral: Vec, + /// A list of ephemeral data. + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub ephemeral: Vec, /// A list of to-device messages. - #[cfg(feature = "unstable-msc2409")] + #[cfg(feature = "unstable-msc4203")] #[serde( default, skip_serializing_if = "<[_]>::is_empty", @@ -129,9 +114,8 @@ pub mod v1 { device_one_time_keys_count: BTreeMap::new(), #[cfg(feature = "unstable-msc3202")] device_unused_fallback_key_types: BTreeMap::new(), - #[cfg(feature = "unstable-msc2409")] ephemeral: Vec::new(), - #[cfg(feature = "unstable-msc2409")] + #[cfg(feature = "unstable-msc4203")] to_device: Vec::new(), } } @@ -173,230 +157,118 @@ pub mod v1 { } } - /// Type for passing ephemeral data to homeservers. - #[cfg(feature = "unstable-msc2409")] + /// Type for passing ephemeral data to application services. #[derive(Clone, Debug, Serialize)] - #[non_exhaustive] - pub enum Edu { - /// An EDU representing presence updates for users of the sending homeserver. - Presence(PresenceContent), + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + #[serde(untagged)] + pub enum EphemeralData { + /// A presence update for a user. + Presence(PresenceEvent), - /// An EDU representing receipt updates for users of the sending homeserver. - Receipt(ReceiptContent), + /// A receipt update for a room. + Receipt(ReceiptEvent), - /// A typing notification EDU for a user in a room. - Typing(TypingContent), + /// A typing notification update for a room. + Typing(TypingEvent), #[doc(hidden)] - #[serde(skip)] - _Custom(JsonValue), + _Custom(_CustomEphemeralData), } - #[derive(Debug, Deserialize)] - #[cfg(feature = "unstable-msc2409")] - struct EduDeHelper { - /// The message type field - r#type: String, - content: Box, - } - - #[cfg(feature = "unstable-msc2409")] - impl<'de> Deserialize<'de> for Edu { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let json = Box::::deserialize(deserializer)?; - let EduDeHelper { r#type, content } = from_raw_json_value(&json)?; - - Ok(match r#type.as_ref() { - "m.presence" => Self::Presence(from_raw_json_value(&content)?), - "m.receipt" => Self::Receipt(from_raw_json_value(&content)?), - "m.typing" => Self::Typing(from_raw_json_value(&content)?), - _ => Self::_Custom(from_raw_json_value(&content)?), - }) + impl EphemeralData { + /// A reference to the `type` string of the data. + pub fn data_type(&self) -> &str { + match self { + Self::Presence(_) => "m.presence", + Self::Receipt(_) => "m.receipt", + Self::Typing(_) => "m.typing", + Self::_Custom(c) => &c.data_type, + } } - } - /// The content for "m.presence" Edu. - #[cfg(feature = "unstable-msc2409")] - #[derive(Clone, Debug, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct PresenceContent { - /// A list of presence updates that the receiving server is likely to be interested in. - pub push: Vec, - } - - #[cfg(feature = "unstable-msc2409")] - impl PresenceContent { - /// Creates a new `PresenceContent`. - pub fn new(push: Vec) -> Self { - Self { push } - } - } - - /// An update to the presence of a user. - #[cfg(feature = "unstable-msc2409")] - #[derive(Clone, Debug, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct PresenceUpdate { - /// The user ID this presence EDU is for. - pub user_id: OwnedUserId, - - /// The presence of the user. - pub presence: PresenceState, - - /// An optional description to accompany the presence. - #[serde(skip_serializing_if = "Option::is_none")] - pub status_msg: Option, - - /// The number of milliseconds that have elapsed since the user last did something. - pub last_active_ago: UInt, - - /// Whether or not the user is currently active. + /// The data as a JSON object. /// - /// Defaults to false. - #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] - pub currently_active: bool, - } + /// Prefer to use the public variants of `EphemeralData` where possible; this method is + /// meant to be used for unsupported data types only. + pub fn data(&self) -> Cow<'_, JsonObject> { + fn serialize(obj: &T) -> JsonObject { + match serde_json::to_value(obj).expect("ephemeral data serialization to succeed") { + JsonValue::Object(obj) => obj, + _ => panic!("all ephemeral data types must serialize to objects"), + } + } - #[cfg(feature = "unstable-msc2409")] - impl PresenceUpdate { - /// Creates a new `PresenceUpdate` with the given `user_id`, `presence` and `last_activity`. - pub fn new(user_id: OwnedUserId, presence: PresenceState, last_activity: UInt) -> Self { - Self { - user_id, - presence, - last_active_ago: last_activity, - status_msg: None, - currently_active: false, + match self { + Self::Presence(d) => Cow::Owned(serialize(d)), + Self::Receipt(d) => Cow::Owned(serialize(d)), + Self::Typing(d) => Cow::Owned(serialize(d)), + Self::_Custom(c) => Cow::Borrowed(&c.data), } } } - /// The content for "m.receipt" Edu. - #[cfg(feature = "unstable-msc2409")] - #[derive(Clone, Debug, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - #[serde(transparent)] - pub struct ReceiptContent(pub BTreeMap); - - #[cfg(feature = "unstable-msc2409")] - impl ReceiptContent { - /// Creates a new `ReceiptContent`. - pub fn new(receipts: BTreeMap) -> Self { - Self(receipts) - } - } - - #[cfg(feature = "unstable-msc2409")] - impl Deref for ReceiptContent { - type Target = BTreeMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - #[cfg(feature = "unstable-msc2409")] - impl DerefMut for ReceiptContent { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } - } - - #[cfg(feature = "unstable-msc2409")] - impl IntoIterator for ReceiptContent { - type Item = (OwnedRoomId, ReceiptMap); - type IntoIter = btree_map::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } - } - - #[cfg(feature = "unstable-msc2409")] - impl FromIterator<(OwnedRoomId, ReceiptMap)> for ReceiptContent { - fn from_iter(iter: T) -> Self + impl<'de> Deserialize<'de> for EphemeralData { + fn deserialize(deserializer: D) -> Result where - T: IntoIterator, + D: Deserializer<'de>, { - Self(BTreeMap::from_iter(iter)) + #[derive(Deserialize)] + struct EphemeralDataDeHelper { + /// The data type. + #[serde(rename = "type")] + data_type: String, + } + + let json = Box::::deserialize(deserializer)?; + let EphemeralDataDeHelper { data_type } = from_raw_json_value(&json)?; + + Ok(match data_type.as_ref() { + "m.presence" => Self::Presence(from_raw_json_value(&json)?), + "m.receipt" => Self::Receipt(from_raw_json_value(&json)?), + "m.typing" => Self::Typing(from_raw_json_value(&json)?), + _ => Self::_Custom(_CustomEphemeralData { + data_type, + data: from_raw_json_value(&json)?, + }), + }) } } - /// Mapping between user and `ReceiptData`. - #[cfg(feature = "unstable-msc2409")] - #[derive(Clone, Debug, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct ReceiptMap { - /// Read receipts for users in the room. - #[serde(rename = "m.read")] - pub read: BTreeMap, + /// Ephemeral data with an unknown type. + #[doc(hidden)] + #[derive(Debug, Clone)] + pub struct _CustomEphemeralData { + /// The type of the data. + data_type: String, + /// The data. + data: JsonObject, } - #[cfg(feature = "unstable-msc2409")] - impl ReceiptMap { - /// Creates a new `ReceiptMap`. - pub fn new(read: BTreeMap) -> Self { - Self { read } + impl Serialize for _CustomEphemeralData { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.data.serialize(serializer) } } - /// Metadata about the event that was last read and when. - #[cfg(feature = "unstable-msc2409")] - #[derive(Clone, Debug, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct ReceiptData { - /// Metadata for the read receipt. - pub data: Receipt, - - /// The extremity event ID the user has read up to. - pub event_ids: Vec, - } - - #[cfg(feature = "unstable-msc2409")] - impl ReceiptData { - /// Creates a new `ReceiptData`. - pub fn new(data: Receipt, event_ids: Vec) -> Self { - Self { data, event_ids } - } - } - - /// The content for "m.typing" Edu. - #[cfg(feature = "unstable-msc2409")] - #[derive(Clone, Debug, Deserialize, Serialize)] - #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] - pub struct TypingContent { - /// The room where the user's typing status has been updated. - pub room_id: OwnedRoomId, - - /// The user ID that has had their typing status changed. - pub user_id: OwnedUserId, - - /// Whether the user is typing in the room or not. - pub typing: bool, - } - - #[cfg(feature = "unstable-msc2409")] - impl TypingContent { - /// Creates a new `TypingContent`. - pub fn new(room_id: OwnedRoomId, user_id: OwnedUserId, typing: bool) -> Self { - Self { room_id, user_id, typing } - } - } - - #[cfg(feature = "server")] #[cfg(test)] mod tests { - use ruma_common::api::{OutgoingRequest, SendAccessToken}; - use serde_json::json; + use assert_matches2::assert_matches; + use js_int::uint; + use ruma_common::{event_id, room_id, user_id, MilliSecondsSinceUnixEpoch}; + use ruma_events::receipt::ReceiptType; + use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; - use super::Request; + use super::EphemeralData; + #[cfg(feature = "client")] #[test] - fn decode_request_contains_events_field() { - let dummy_event = serde_json::from_value(json!({ + fn request_contains_events_field() { + use ruma_common::api::{OutgoingRequest, SendAccessToken}; + + let dummy_event_json = json!({ "type": "m.room.message", "event_id": "$143273582443PhrSn:example.com", "origin_server_ts": 1, @@ -406,11 +278,11 @@ pub mod v1 { "body": "test", "msgtype": "m.text", }, - })) - .unwrap(); + }); + let dummy_event = from_json_value(dummy_event_json.clone()).unwrap(); let events = vec![dummy_event]; - let req = Request::new("any_txn_id".into(), events) + let req = super::Request::new("any_txn_id".into(), events) .try_into_http_request::>( "https://homeserver.tld", SendAccessToken::IfRequired("auth_tok"), @@ -420,9 +292,98 @@ pub mod v1 { let json_body: serde_json::Value = serde_json::from_slice(req.body()).unwrap(); assert_eq!( - 1, - json_body.as_object().unwrap().get("events").unwrap().as_array().unwrap().len() + json_body, + json!({ + "events": [ + dummy_event_json, + ] + }) ); } + + #[test] + fn serde_ephemeral_data() { + let room_id = room_id!("!jEsUZKDJdhlrceRyVU:server.local"); + let user_id = user_id!("@alice:server.local"); + let event_id = event_id!("$1435641916114394fHBL"); + + // Test m.typing serde. + let typing_json = json!({ + "type": "m.typing", + "room_id": room_id, + "content": { + "user_ids": [user_id], + }, + }); + + let data = from_json_value::(typing_json.clone()).unwrap(); + assert_matches!(&data, EphemeralData::Typing(typing)); + assert_eq!(typing.room_id, room_id); + assert_eq!(typing.content.user_ids, &[user_id.to_owned()]); + + let serialized_data = to_json_value(data).unwrap(); + assert_eq!(serialized_data, typing_json); + + // Test m.receipt serde. + let receipt_json = json!({ + "type": "m.receipt", + "room_id": room_id, + "content": { + event_id: { + "m.read": { + user_id: { + "ts": 453, + }, + }, + }, + }, + }); + + let data = from_json_value::(receipt_json.clone()).unwrap(); + assert_matches!(&data, EphemeralData::Receipt(receipt)); + assert_eq!(receipt.room_id, room_id); + let event_receipts = receipt.content.get(event_id).unwrap(); + let event_read_receipts = event_receipts.get(&ReceiptType::Read).unwrap(); + let event_user_read_receipt = event_read_receipts.get(user_id).unwrap(); + assert_eq!(event_user_read_receipt.ts, Some(MilliSecondsSinceUnixEpoch(uint!(453)))); + + let serialized_data = to_json_value(data).unwrap(); + assert_eq!(serialized_data, receipt_json); + + // Test m.presence serde. + let presence_json = json!({ + "type": "m.presence", + "sender": user_id, + "content": { + "avatar_url": "mxc://localhost/wefuiwegh8742w", + "currently_active": false, + "last_active_ago": 785, + "presence": "online", + "status_msg": "Making cupcakes", + }, + }); + + let data = from_json_value::(presence_json.clone()).unwrap(); + assert_matches!(&data, EphemeralData::Presence(presence)); + assert_eq!(presence.sender, user_id); + assert_eq!(presence.content.currently_active, Some(false)); + + let serialized_data = to_json_value(data).unwrap(); + assert_eq!(serialized_data, presence_json); + + // Test custom serde. + let custom_json = json!({ + "type": "dev.ruma.custom", + "key": "value", + "content": { + "foo": "bar", + }, + }); + + let data = from_json_value::(custom_json.clone()).unwrap(); + + let serialized_data = to_json_value(data).unwrap(); + assert_eq!(serialized_data, custom_json); + } } } diff --git a/crates/ruma-appservice-api/src/lib.rs b/crates/ruma-appservice-api/src/lib.rs index 4de0309d..1a431ca7 100644 --- a/crates/ruma-appservice-api/src/lib.rs +++ b/crates/ruma-appservice-api/src/lib.rs @@ -96,14 +96,15 @@ pub struct Registration { #[serde(skip_serializing_if = "Option::is_none")] pub rate_limited: Option, - /// Whether the homeserver should send EDUs to the appservice, as per MSC2409 - #[cfg(feature = "unstable-msc2409")] - #[serde(skip_serializing_if = "Option::is_none", alias = "de.sorunome.msc2409.push_ephemeral")] - pub receive_ephemeral: Option, - /// The external protocols which the application service provides (e.g. IRC). #[serde(skip_serializing_if = "Option::is_none")] pub protocols: Option>, + + /// Whether the application service wants to receive ephemeral data. + /// + /// Defaults to `false`. + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default", alias = "de.sorunome.msc2409.push_ephemeral")] + pub receive_ephemeral: bool, } /// Initial set of fields of `Registration`. @@ -140,9 +141,6 @@ pub struct RegistrationInit { /// The sender is excluded. pub rate_limited: Option, - /// Whether the homeserver should send EDUs to the appservice, as per MSC2409 - #[cfg(feature = "unstable-msc2409")] - pub receive_ephemeral: Option, /// The external protocols which the application service provides (e.g. IRC). pub protocols: Option>, @@ -158,8 +156,6 @@ impl From for Registration { sender_localpart, namespaces, rate_limited, - #[cfg(feature = "unstable-msc2409")] - receive_ephemeral, protocols, } = init; Self { @@ -170,8 +166,7 @@ impl From for Registration { sender_localpart, namespaces, rate_limited, - #[cfg(feature = "unstable-msc2409")] - receive_ephemeral, + receive_ephemeral: false, protocols, } } diff --git a/crates/ruma/Cargo.toml b/crates/ruma/Cargo.toml index 79302707..a4f827b2 100644 --- a/crates/ruma/Cargo.toml +++ b/crates/ruma/Cargo.toml @@ -220,7 +220,7 @@ unstable-extensible-events = [ "unstable-msc3955", ] unstable-msc1767 = ["ruma-events?/unstable-msc1767"] -unstable-msc2409 = ["ruma-appservice-api?/unstable-msc2409"] +unstable-msc4203 = ["ruma-appservice-api?/unstable-msc4203"] unstable-msc2448 = [ "ruma-client-api?/unstable-msc2448", "ruma-events?/unstable-msc2448", @@ -282,7 +282,6 @@ unstable-unspecified = [ # Private features, only used in test / benchmarking code __unstable-mscs = [ "unstable-msc1767", - "unstable-msc2409", "unstable-msc2448", "unstable-msc2654", "unstable-msc2666",