diff --git a/crates/ruma-client-api/src/r0/room/create_room.rs b/crates/ruma-client-api/src/r0/room/create_room.rs index 57adaf85..df1194bc 100644 --- a/crates/ruma-client-api/src/r0/room/create_room.rs +++ b/crates/ruma-client-api/src/r0/room/create_room.rs @@ -2,6 +2,8 @@ use assign::assign; use ruma_api::ruma_api; +#[cfg(feature = "unstable-pre-spec")] +use ruma_events::room::create::RoomType; use ruma_events::{ room::{ create::{CreateEventContent, PreviousRoom}, @@ -129,27 +131,60 @@ pub struct CreationContent { /// A reference to the room this room replaces, if the previous room was upgraded. #[serde(skip_serializing_if = "Option::is_none")] pub predecessor: Option, + + /// The room type. + /// + /// This is currently only used for spaces. + #[cfg(feature = "unstable-pre-spec")] + #[serde(skip_serializing_if = "Option::is_none", rename = "type")] + pub room_type: Option, } impl CreationContent { /// Creates a new `CreationContent` with all fields defaulted. pub fn new() -> Self { - Self { federate: true, predecessor: None } + Self { + federate: true, + predecessor: None, + #[cfg(feature = "unstable-pre-spec")] + room_type: None, + } } /// Given a `CreationContent` and the other fields that a homeserver has to fill, construct /// a `CreateEventContent`. pub fn into_event_content( - Self { federate, predecessor }: Self, + self, creator: UserId, room_version: RoomVersionId, ) -> CreateEventContent { - assign!(CreateEventContent::new(creator), { federate, room_version, predecessor }) + #[allow(unused_mut)] + let mut content = assign!(CreateEventContent::new(creator), { + federate: self.federate, + room_version: room_version, + predecessor: self.predecessor, + }); + + #[cfg(feature = "unstable-pre-spec")] + { + content.room_type = self.room_type; + } + + content } /// Returns whether all fields have their default value. pub fn is_empty(&self) -> bool { - self.federate && self.predecessor.is_none() + let stable_fields = self.federate && self.predecessor.is_none(); + + #[cfg(feature = "unstable-pre-spec")] + { + stable_fields && self.room_type.is_none() + } + #[cfg(not(feature = "unstable-pre-spec"))] + { + stable_fields + } } } diff --git a/crates/ruma-events/src/enums.rs b/crates/ruma-events/src/enums.rs index a14e4773..0b80f3f0 100644 --- a/crates/ruma-events/src/enums.rs +++ b/crates/ruma-events/src/enums.rs @@ -85,6 +85,12 @@ event_enum! { "m.room.third_party_invite", "m.room.tombstone", "m.room.topic", + #[cfg(feature = "unstable-pre-spec")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] + "m.space.child", + #[cfg(feature = "unstable-pre-spec")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] + "m.space.parent", } /// Any to-device event. diff --git a/crates/ruma-events/src/lib.rs b/crates/ruma-events/src/lib.rs index b233e294..8e1a6197 100644 --- a/crates/ruma-events/src/lib.rs +++ b/crates/ruma-events/src/lib.rs @@ -176,6 +176,9 @@ pub mod relation; pub mod room; pub mod room_key; pub mod room_key_request; +#[cfg(feature = "unstable-pre-spec")] +#[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] +pub mod space; pub mod sticker; pub mod tag; pub mod typing; diff --git a/crates/ruma-events/src/room/create.rs b/crates/ruma-events/src/room/create.rs index 39914f05..fa587eba 100644 --- a/crates/ruma-events/src/room/create.rs +++ b/crates/ruma-events/src/room/create.rs @@ -2,6 +2,7 @@ use ruma_events_macros::EventContent; use ruma_identifiers::{EventId, RoomId, RoomVersionId, UserId}; +use ruma_serde::StringEnum; use serde::{Deserialize, Serialize}; use crate::StateEvent; @@ -34,15 +35,40 @@ pub struct CreateEventContent { /// A reference to the room this room replaces, if the previous room was upgraded. #[serde(skip_serializing_if = "Option::is_none")] pub predecessor: Option, + + /// The room type. + /// + /// This is currently only used for spaces. + #[cfg(feature = "unstable-pre-spec")] + #[serde(skip_serializing_if = "Option::is_none", rename = "type")] + pub room_type: Option, } impl CreateEventContent { /// Creates a new `CreateEventContent` with the given creator. pub fn new(creator: UserId) -> Self { - Self { creator, federate: true, room_version: default_room_version_id(), predecessor: None } + Self { + creator, + federate: true, + room_version: default_room_version_id(), + predecessor: None, + #[cfg(feature = "unstable-pre-spec")] + room_type: None, + } } } +/// An enum of possible room types. +#[derive(Clone, Debug, PartialEq, Eq, StringEnum)] +pub enum RoomType { + /// Defines the room as a space. + #[ruma_enum(rename = "m.space")] + Space, + /// Defines the room as a custom type. + #[doc(hidden)] + _Custom(String), +} + /// A reference to an old room replaced during a room version upgrade. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] @@ -75,6 +101,9 @@ mod tests { use super::CreateEventContent; + #[cfg(feature = "unstable-pre-spec")] + use super::RoomType; + #[test] fn serialization() { let content = CreateEventContent { @@ -82,6 +111,8 @@ mod tests { federate: false, room_version: RoomVersionId::Version4, predecessor: None, + #[cfg(feature = "unstable-pre-spec")] + room_type: None, }; let json = json!({ @@ -93,6 +124,27 @@ mod tests { assert_eq!(to_json_value(&content).unwrap(), json); } + #[cfg(feature = "unstable-pre-spec")] + #[test] + fn space_serialization() { + let content = CreateEventContent { + creator: user_id!("@carl:example.com"), + federate: false, + room_version: RoomVersionId::Version4, + predecessor: None, + room_type: Some(RoomType::Space), + }; + + let json = json!({ + "creator": "@carl:example.com", + "m.federate": false, + "room_version": "4", + "type": "m.space" + }); + + assert_eq!(to_json_value(&content).unwrap(), json); + } + #[test] fn deserialization() { let json = json!({ @@ -111,7 +163,34 @@ mod tests { federate: true, room_version: RoomVersionId::Version4, predecessor: None, + #[cfg(feature = "unstable-pre-spec")] + room_type: None, } if creator == "@carl:example.com" ); } + + #[cfg(feature = "unstable-pre-spec")] + #[test] + fn space_deserialization() { + let json = json!({ + "creator": "@carl:example.com", + "m.federate": true, + "room_version": "4", + "type": "m.space" + }); + + assert_matches!( + from_json_value::>(json) + .unwrap() + .deserialize() + .unwrap(), + CreateEventContent { + creator, + federate: true, + room_version: RoomVersionId::Version4, + predecessor: None, + room_type + } if creator == "@carl:example.com" && room_type == Some(RoomType::Space) + ); + } } diff --git a/crates/ruma-events/src/space.rs b/crates/ruma-events/src/space.rs new file mode 100644 index 00000000..1f083ca7 --- /dev/null +++ b/crates/ruma-events/src/space.rs @@ -0,0 +1,8 @@ +//! Types for the *m.space* events. +//! +//! See [MSC2758] and [MSC1772]. +//! [MSC2758]: https://github.com/matrix-org/matrix-doc/blob/master/proposals/2758-textual-id-grammar.md +//! [MSC1772]: https://github.com/matrix-org/matrix-doc/blob/master/proposals/1772-groups-as-rooms.md + +pub mod child; +pub mod parent; diff --git a/crates/ruma-events/src/space/child.rs b/crates/ruma-events/src/space/child.rs new file mode 100644 index 00000000..9dea94de --- /dev/null +++ b/crates/ruma-events/src/space/child.rs @@ -0,0 +1,78 @@ +//! Types for the *m.space.child* event. + +use ruma_events_macros::EventContent; +use ruma_identifiers::ServerNameBox; +use serde::{Deserialize, Serialize}; + +use crate::StateEvent; + +/// The admins of a space can advertise rooms and subspaces for their space by setting +/// `m.space.child` state events. +/// +/// The `state_key` is the ID of a child room or space, and the content must contain a `via` key +/// which gives a list of candidate servers that can be used to join the room. +pub type ChildEvent = StateEvent; + +/// The payload for `ChildEvent`. +#[derive(Clone, Debug, Deserialize, Serialize, EventContent)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[ruma_event(type = "m.space.child", kind = State)] +pub struct ChildEventContent { + /// List of candidate servers that can be used to join the room. + #[serde(skip_serializing_if = "Option::is_none")] + pub via: Option>, + + /// Provide a default ordering of siblings in the room list. + /// + /// Rooms are sorted based on a lexicographic ordering of the Unicode codepoints of the + /// characters in `order` values. Rooms with no `order` come last, in ascending numeric order + /// of the origin_server_ts of their m.room.create events, or ascending lexicographic order of + /// their room_ids in case of equal `origin_server_ts`. `order`s which are not strings, or do + /// not consist solely of ascii characters in the range `\x20` (space) to `\x7E` (`~`), or + /// consist of more than 50 characters, are forbidden and the field should be ignored if + /// received. + #[serde(skip_serializing_if = "Option::is_none")] + pub order: Option, + + /// Space admins can mark particular children of a space as "suggested". + /// + /// This mainly serves as a hint to clients that that they can be displayed differently, for + /// example by showing them eagerly in the room list. A child which is missing the `suggested` + /// property is treated identically to a child with `"suggested": false`. A suggested child may + /// be a room or a subspace. + #[serde(skip_serializing_if = "Option::is_none")] + pub suggested: Option, +} + +#[cfg(test)] +mod tests { + use super::ChildEventContent; + use ruma_identifiers::server_name; + use serde_json::{json, to_value as to_json_value}; + + #[test] + fn space_child_serialization() { + let content = ChildEventContent { + via: Some(vec![server_name!("example.com")]), + order: Some("uwu".to_owned()), + suggested: Some(false), + }; + + let json = json!({ + "via": ["example.com"], + "order": "uwu", + "suggested": false, + }); + + assert_eq!(to_json_value(&content).unwrap(), json); + } + + #[test] + fn space_child_empty_serialization() { + let content = ChildEventContent { via: None, order: None, suggested: None }; + + let json = json!({}); + + assert_eq!(to_json_value(&content).unwrap(), json); + } +} diff --git a/crates/ruma-events/src/space/parent.rs b/crates/ruma-events/src/space/parent.rs new file mode 100644 index 00000000..d8344193 --- /dev/null +++ b/crates/ruma-events/src/space/parent.rs @@ -0,0 +1,64 @@ +//! Types for the *m.space.child* event. + +use ruma_events_macros::EventContent; +use ruma_identifiers::ServerNameBox; +use serde::{Deserialize, Serialize}; + +use crate::StateEvent; + +/// Rooms can claim parents via the `m.space.parent` state event. +/// +/// Similar to `m.space.child`, the `state_key` is the ID of the parent space, and the content must +/// contain a `via` key which gives a list of candidate servers that can be used to join the +/// parent. +pub type ParentEvent = StateEvent; + +/// The payload for `ParentEvent`. +#[derive(Clone, Debug, Deserialize, Serialize, EventContent)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[ruma_event(type = "m.space.child", kind = State)] +pub struct ParentEventContent { + /// List of candidate servers that can be used to join the room. + #[serde(skip_serializing_if = "Option::is_none")] + pub via: Option>, + + /// Determines whether this is the main parent for the space. + /// + /// When a user joins a room with a canonical parent, clients may switch to view the room in + /// the context of that space, peeking into it in order to find other rooms and group them + /// together. In practice, well behaved rooms should only have one `canonical` parent, but + /// given this is not enforced: if multiple are present the client should select the one with + /// the lowest room ID, as determined via a lexicographic ordering of the Unicode code-points. + pub canonical: bool, +} + +#[cfg(test)] +mod tests { + use super::ParentEventContent; + use ruma_identifiers::server_name; + use serde_json::{json, to_value as to_json_value}; + + #[test] + fn space_parent_serialization() { + let content = + ParentEventContent { via: Some(vec![server_name!("example.com")]), canonical: true }; + + let json = json!({ + "via": ["example.com"], + "canonical": true, + }); + + assert_eq!(to_json_value(&content).unwrap(), json); + } + + #[test] + fn space_parent_empty_serialization() { + let content = ParentEventContent { via: None, canonical: true }; + + let json = json!({ + "canonical": true, + }); + + assert_eq!(to_json_value(&content).unwrap(), json); + } +}