diff --git a/.clippy.toml b/.clippy.toml index a32867abcd..12aef4f7a3 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1,3 +1,4 @@ +enum-variant-size-threshold = 512 avoid-breaking-exported-api = false disallowed-methods = [ # https://github.com/serde-rs/json/issues/160 diff --git a/Cargo.lock b/Cargo.lock index e323ff2e17..943a320d80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1874,6 +1874,7 @@ dependencies = [ "ruma-events", "ruma-federation-api", "ruma-html", + "ruma-identifiers-validation", "ruma-identity-service-api", "ruma-push-gateway-api", "ruma-signatures", @@ -1913,6 +1914,8 @@ dependencies = [ "serde", "serde_html_form", "serde_json", + "smallstr", + "smallvec", "thiserror", "url", "web-time", @@ -1944,6 +1947,8 @@ dependencies = [ "serde", "serde_html_form", "serde_json", + "smallstr", + "smallvec", "smol-macros", "thiserror", "time", @@ -1979,6 +1984,8 @@ dependencies = [ "ruma-macros", "serde", "serde_json", + "smallstr", + "smallvec", "thiserror", "tracing", "trybuild", @@ -2006,6 +2013,7 @@ dependencies = [ "ruma-events", "serde", "serde_json", + "smallstr", "thiserror", "tracing", ] @@ -2039,6 +2047,7 @@ dependencies = [ "ruma-events", "serde", "serde_json", + "smallstr", ] [[package]] @@ -2388,11 +2397,24 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "smallstr" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862077b1e764f04c251fe82a2ef562fd78d7cadaeb072ca7c2bcaf7217b1ff3b" +dependencies = [ + "serde", + "smallvec", +] + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smol-macros" diff --git a/Cargo.toml b/Cargo.toml index c9e1c5f729..76e2cee57c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,9 @@ serde = { version = "1.0.164", features = ["derive"] } serde_html_form = "0.2.0" serde_json = "1.0.87" similar = "2.6.0" +smallstr = { version = "0.3", features = ["ffi", "serde", "std", "union"] } +smallvec = { version = "1.15", features = ["const_generics", "const_new", "serde", "union", "write"] } + thiserror = "2.0.0" toml = { version = "0.9.6", default-features = false, features = ["parse", "serde"] } tracing = { version = "0.1.37", default-features = false, features = ["std"] } @@ -54,6 +57,7 @@ unexpected_cfgs = { level = "warn", check-cfg = [ unreachable_pub = "warn" unused_import_braces = "warn" unused_qualifications = "warn" +deprecated = "allow" [workspace.lints.clippy] branches_sharing_code = "warn" diff --git a/crates/ruma-appservice-api/src/lib.rs b/crates/ruma-appservice-api/src/lib.rs index 4388d9e31d..f298208692 100644 --- a/crates/ruma-appservice-api/src/lib.rs +++ b/crates/ruma-appservice-api/src/lib.rs @@ -104,8 +104,14 @@ pub struct Registration { /// Whether the application service wants to receive ephemeral data. /// /// Defaults to `false`. - #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + #[serde(default)] pub receive_ephemeral: bool, + + /// Whether the application service wants to do device management, as part of MSC4190. + /// + /// Defaults to `false` + #[serde(default, rename = "io.element.msc4190")] + pub device_management: bool, } /// Initial set of fields of `Registration`. @@ -168,6 +174,7 @@ impl From for Registration { rate_limited, protocols, receive_ephemeral: false, + device_management: false, } } } diff --git a/crates/ruma-client-api/CHANGELOG.md b/crates/ruma-client-api/CHANGELOG.md index 9d37f03f82..b022f42856 100644 --- a/crates/ruma-client-api/CHANGELOG.md +++ b/crates/ruma-client-api/CHANGELOG.md @@ -36,6 +36,8 @@ Breaking changes: - Allow specifying the event format for `state::get_state_event_for_key`, meaning the response may either be `Raw` or `Raw`, depending on the format specified in the request. +- Use `StrippedState` instead of `AnyStrippedStateEvent`, to allow non-stripped events to be + represented for `sync_events`. - `sync_events::v3::State` is now an enum, to prepare for the stabilization of MSC4222. The state before the timeline, corresponding to the `state` field in the Matrix specification, is available in the `Before` variant, and the struct representing its content was renamed to `StateEvents`. @@ -47,9 +49,16 @@ Breaking changes: should always be identical. - Add `set_presence` field to `sync_events::v5::Request` as per MSC4186; specified identically to the field appearing in prior sync versions. +- Add `tags`, `not_tags`, and `spaces` fields to `sync_events::v5::request::ListFilters` as + specified in MSC4186 `SlidingRoomFilter` properties. Improvements: +- Add `M_INVITE_BLOCKED` candidate error code proposed by + [MSC4380](https://github.com/matrix-org/matrix-spec-proposals/pull/4380) + sharing an unstable prefix with the preceding + [MSC4155](https://github.com/matrix-org/matrix-spec-proposals/pull/4155). + - Added support for the sliding sync extension for thread subscriptions, as well as the accompanying endpoint, both from experimental MSC4308. - Added support for the experiment MSC4306 thread subscription endpoints. @@ -210,6 +219,7 @@ Improvements: - Allow constructing `error::ErrorBody::NotJson` outside of this crate. - Add function for checking if a `Content-Type` is considered "safe" for `inline` rendering, according to MSC2702 / Matrix 1.12. +- Allow constructing `error::ErrorBody::NotJson` outside of this crate. Bug fixes: diff --git a/crates/ruma-client-api/Cargo.toml b/crates/ruma-client-api/Cargo.toml index cbbf959754..9b00400382 100644 --- a/crates/ruma-client-api/Cargo.toml +++ b/crates/ruma-client-api/Cargo.toml @@ -39,6 +39,7 @@ compat-upload-signatures = [] unstable-msc2666 = ["ruma-common/unstable-msc2666"] unstable-msc2448 = [] unstable-msc2654 = [] +unstable-msc2815 = [] unstable-msc2967 = [] unstable-msc3488 = [] unstable-msc3814 = [] @@ -52,10 +53,12 @@ unstable-msc4140 = ["ruma-common/unstable-msc4140"] unstable-msc4143 = [] unstable-msc4186 = ["ruma-common/unstable-msc4186"] unstable-msc4191 = [] +unstable-msc4195 = ["unstable-msc4143"] unstable-msc4222 = [] # Thread subscription support. unstable-msc4306 = [] unstable-msc4308 = [] +unstable-msc4380 = ["ruma-common/unstable-msc4380", "ruma-events/unstable-msc4380"] [dependencies] as_variant = { workspace = true } @@ -71,6 +74,8 @@ ruma-events = { workspace = true } serde = { workspace = true } serde_html_form = { workspace = true } serde_json = { workspace = true } +smallstr = { workspace = true } +smallvec = { workspace = true } thiserror = { workspace = true } url = { workspace = true, features = ["serde"] } web-time = { workspace = true } diff --git a/crates/ruma-client-api/src/authenticated_media/get_content.rs b/crates/ruma-client-api/src/authenticated_media/get_content.rs index e52e04fc77..4c98e2e106 100644 --- a/crates/ruma-client-api/src/authenticated_media/get_content.rs +++ b/crates/ruma-client-api/src/authenticated_media/get_content.rs @@ -7,15 +7,17 @@ pub mod v1 { //! //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1mediadownloadservernamemediaid - use std::time::Duration; + use std::{borrow::Cow, time::Duration}; - use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE}; + use http::header::{CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_TYPE}; use ruma_common::{ api::{request, response, Metadata}, http_headers::ContentDisposition, - metadata, IdParseError, MxcUri, OwnedServerName, + metadata, IdParseError, Mxc, MxcUri, OwnedServerName, }; + use crate::http_headers::CROSS_ORIGIN_RESOURCE_POLICY; + const METADATA: Metadata = metadata! { method: GET, rate_limited: true, @@ -59,12 +61,32 @@ pub mod v1 { /// The content type of the file that was previously uploaded. #[ruma_api(header = CONTENT_TYPE)] - pub content_type: Option, + pub content_type: Option>, /// The value of the `Content-Disposition` HTTP header, possibly containing the name of the /// file that was previously uploaded. #[ruma_api(header = CONTENT_DISPOSITION)] pub content_disposition: Option, + + /// The value of the `Cross-Origin-Resource-Policy` HTTP header. + /// + /// See [MDN] for the syntax. + /// + /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy#syntax + /// + /// TODO: make this use Cow static str's + #[ruma_api(header = CROSS_ORIGIN_RESOURCE_POLICY)] + pub cross_origin_resource_policy: Option>, + + /// The value of the `Cache-Control` HTTP header. + /// + /// See [MDN] for the syntax. + /// + /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#syntax + /// + /// TODO: make this use Cow static str's + #[ruma_api(header = CACHE_CONTROL)] + pub cache_control: Option>, } impl Request { @@ -79,7 +101,7 @@ pub mod v1 { /// Creates a new `Request` with the given URI. pub fn from_uri(uri: &MxcUri) -> Result { - let (server_name, media_id) = uri.parts()?; + let Mxc { server_name, media_id, .. } = uri.parts()?; Ok(Self::new(media_id.to_owned(), server_name.to_owned())) } @@ -89,13 +111,15 @@ pub mod v1 { /// Creates a new `Response` with the given file contents. pub fn new( file: Vec, - content_type: String, + content_type: Cow<'static, str>, content_disposition: ContentDisposition, ) -> Self { Self { file, content_type: Some(content_type), content_disposition: Some(content_disposition), + cross_origin_resource_policy: None, + cache_control: None, } } } diff --git a/crates/ruma-client-api/src/authenticated_media/get_content_as_filename.rs b/crates/ruma-client-api/src/authenticated_media/get_content_as_filename.rs index 94dd5f1c58..79c379fda5 100644 --- a/crates/ruma-client-api/src/authenticated_media/get_content_as_filename.rs +++ b/crates/ruma-client-api/src/authenticated_media/get_content_as_filename.rs @@ -7,15 +7,17 @@ pub mod v1 { //! //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1mediadownloadservernamemediaidfilename - use std::time::Duration; + use std::{borrow::Cow, time::Duration}; - use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE}; + use http::header::{CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_TYPE}; use ruma_common::{ api::{request, response, Metadata}, http_headers::ContentDisposition, - metadata, IdParseError, MxcUri, OwnedServerName, + metadata, IdParseError, Mxc, MxcUri, OwnedServerName, }; + use crate::http_headers::CROSS_ORIGIN_RESOURCE_POLICY; + const METADATA: Metadata = metadata! { method: GET, rate_limited: true, @@ -63,12 +65,32 @@ pub mod v1 { /// The content type of the file that was previously uploaded. #[ruma_api(header = CONTENT_TYPE)] - pub content_type: Option, + pub content_type: Option>, /// The value of the `Content-Disposition` HTTP header, possibly containing the name of the /// file that was previously uploaded. #[ruma_api(header = CONTENT_DISPOSITION)] pub content_disposition: Option, + + /// The value of the `Cross-Origin-Resource-Policy` HTTP header. + /// + /// See [MDN] for the syntax. + /// + /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy#syntax + /// + /// TODO: make this use Cow static str's + #[ruma_api(header = CROSS_ORIGIN_RESOURCE_POLICY)] + pub cross_origin_resource_policy: Option>, + + /// The value of the `Cache-Control` HTTP header. + /// + /// See [MDN] for the syntax. + /// + /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#syntax + /// + /// TODO: make this use Cow static str's + #[ruma_api(header = CACHE_CONTROL)] + pub cache_control: Option>, } impl Request { @@ -84,7 +106,7 @@ pub mod v1 { /// Creates a new `Request` with the given URI and filename. pub fn from_uri(uri: &MxcUri, filename: String) -> Result { - let (server_name, media_id) = uri.parts()?; + let Mxc { server_name, media_id, .. } = uri.parts()?; Ok(Self::new(media_id.to_owned(), server_name.to_owned(), filename)) } @@ -94,13 +116,15 @@ pub mod v1 { /// Creates a new `Response` with the given file. pub fn new( file: Vec, - content_type: String, + content_type: Cow<'static, str>, content_disposition: ContentDisposition, ) -> Self { Self { file, content_type: Some(content_type), content_disposition: Some(content_disposition), + cross_origin_resource_policy: None, + cache_control: None, } } } diff --git a/crates/ruma-client-api/src/authenticated_media/get_content_thumbnail.rs b/crates/ruma-client-api/src/authenticated_media/get_content_thumbnail.rs index 6c592c1257..fa00ff903b 100644 --- a/crates/ruma-client-api/src/authenticated_media/get_content_thumbnail.rs +++ b/crates/ruma-client-api/src/authenticated_media/get_content_thumbnail.rs @@ -7,17 +7,19 @@ pub mod v1 { //! //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1mediathumbnailservernamemediaid - use std::time::Duration; + use std::{borrow::Cow, time::Duration}; - use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE}; + use http::header::{CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_TYPE}; use js_int::UInt; use ruma_common::{ api::{request, response, Metadata}, http_headers::ContentDisposition, media::Method, - metadata, IdParseError, MxcUri, OwnedServerName, + metadata, IdParseError, Mxc, MxcUri, OwnedServerName, }; + use crate::http_headers::CROSS_ORIGIN_RESOURCE_POLICY; + const METADATA: Metadata = metadata! { method: GET, rate_limited: true, @@ -87,7 +89,7 @@ pub mod v1 { /// The content type of the thumbnail. #[ruma_api(header = CONTENT_TYPE)] - pub content_type: Option, + pub content_type: Option>, /// The value of the `Content-Disposition` HTTP header, possibly containing the name of the /// file that was previously uploaded. @@ -97,6 +99,22 @@ pub mod v1 { /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#Syntax #[ruma_api(header = CONTENT_DISPOSITION)] pub content_disposition: Option, + + /// The value of the `Cross-Origin-Resource-Policy` HTTP header. + /// + /// See [MDN] for the syntax. + /// + /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy#syntax + #[ruma_api(header = CROSS_ORIGIN_RESOURCE_POLICY)] + pub cross_origin_resource_policy: Option>, + + /// The value of the `Cache-Control` HTTP header. + /// + /// See [MDN] for the syntax. + /// + /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#syntax + #[ruma_api(header = CACHE_CONTROL)] + pub cache_control: Option>, } impl Request { @@ -122,7 +140,7 @@ pub mod v1 { /// Creates a new `Request` with the given URI, desired thumbnail width and /// desired thumbnail height. pub fn from_uri(uri: &MxcUri, width: UInt, height: UInt) -> Result { - let (server_name, media_id) = uri.parts()?; + let Mxc { server_name, media_id, .. } = uri.parts()?; Ok(Self::new(media_id.to_owned(), server_name.to_owned(), width, height)) } @@ -132,13 +150,15 @@ pub mod v1 { /// Creates a new `Response` with the given thumbnail. pub fn new( file: Vec, - content_type: String, + content_type: Cow<'static, str>, content_disposition: ContentDisposition, ) -> Self { Self { file, content_type: Some(content_type), content_disposition: Some(content_disposition), + cross_origin_resource_policy: None, + cache_control: None, } } } diff --git a/crates/ruma-client-api/src/backup/add_backup_keys.rs b/crates/ruma-client-api/src/backup/add_backup_keys.rs index 67c7f92c9f..ef0a153d11 100644 --- a/crates/ruma-client-api/src/backup/add_backup_keys.rs +++ b/crates/ruma-client-api/src/backup/add_backup_keys.rs @@ -23,6 +23,7 @@ pub mod v3 { authentication: AccessToken, history: { unstable => "/_matrix/client/unstable/room_keys/keys", + 1.0 => "/_matrix/client/r0/room_keys/keys", 1.1 => "/_matrix/client/v3/room_keys/keys", } }; diff --git a/crates/ruma-client-api/src/backup/create_backup_version.rs b/crates/ruma-client-api/src/backup/create_backup_version.rs index 60f055facc..5bc95a3dac 100644 --- a/crates/ruma-client-api/src/backup/create_backup_version.rs +++ b/crates/ruma-client-api/src/backup/create_backup_version.rs @@ -21,6 +21,7 @@ pub mod v3 { authentication: AccessToken, history: { unstable => "/_matrix/client/unstable/room_keys/version", + 1.0 => "/_matrix/client/r0/room_keys/version", 1.1 => "/_matrix/client/v3/room_keys/version", } }; diff --git a/crates/ruma-client-api/src/backup/get_backup_info.rs b/crates/ruma-client-api/src/backup/get_backup_info.rs index 5958dc3fc5..2065d1e053 100644 --- a/crates/ruma-client-api/src/backup/get_backup_info.rs +++ b/crates/ruma-client-api/src/backup/get_backup_info.rs @@ -25,6 +25,7 @@ pub mod v3 { authentication: AccessToken, history: { unstable => "/_matrix/client/unstable/room_keys/version/{version}", + 1.0 => "/_matrix/client/r0/room_keys/version/{version}", 1.1 => "/_matrix/client/v3/room_keys/version/{version}", } }; diff --git a/crates/ruma-client-api/src/backup/update_backup_version.rs b/crates/ruma-client-api/src/backup/update_backup_version.rs index 64b9644ef7..6a5a2ddfff 100644 --- a/crates/ruma-client-api/src/backup/update_backup_version.rs +++ b/crates/ruma-client-api/src/backup/update_backup_version.rs @@ -21,6 +21,7 @@ pub mod v3 { authentication: AccessToken, history: { unstable => "/_matrix/client/unstable/room_keys/version/{version}", + 1.0 => "/_matrix/client/r0/room_keys/version/{version}", 1.1 => "/_matrix/client/v3/room_keys/version/{version}", } }; diff --git a/crates/ruma-client-api/src/context/get_context.rs b/crates/ruma-client-api/src/context/get_context.rs index 7dcca6b5a4..e22eeaa380 100644 --- a/crates/ruma-client-api/src/context/get_context.rs +++ b/crates/ruma-client-api/src/context/get_context.rs @@ -73,7 +73,7 @@ pub mod v3 { /// A list of room events that happened just before the requested event, /// in reverse-chronological order. - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde(default)] pub events_before: Vec>, /// Details of the requested event. diff --git a/crates/ruma-client-api/src/device.rs b/crates/ruma-client-api/src/device.rs index 27d87d9cee..8371aca440 100644 --- a/crates/ruma-client-api/src/device.rs +++ b/crates/ruma-client-api/src/device.rs @@ -2,6 +2,7 @@ use ruma_common::{MilliSecondsSinceUnixEpoch, OwnedDeviceId}; use serde::{Deserialize, Serialize}; +use smallstr::SmallString; pub mod delete_device; pub mod delete_devices; @@ -17,16 +18,19 @@ pub struct Device { pub device_id: OwnedDeviceId, /// Public display name of the device. - pub display_name: Option, + pub display_name: Option, /// Most recently seen IP address of the session. - pub last_seen_ip: Option, + pub last_seen_ip: Option, /// Unix timestamp that the session was last active. #[serde(skip_serializing_if = "Option::is_none")] pub last_seen_ts: Option, } +type DisplayName = SmallString<[u8; 40]>; +type LastSeenIp = SmallString<[u8; 48]>; + impl Device { /// Creates a new `Device` with the given device ID. pub fn new(device_id: OwnedDeviceId) -> Self { diff --git a/crates/ruma-client-api/src/device/update_device.rs b/crates/ruma-client-api/src/device/update_device.rs index c6ac19ab6c..e6d4f426c4 100644 --- a/crates/ruma-client-api/src/device/update_device.rs +++ b/crates/ruma-client-api/src/device/update_device.rs @@ -12,6 +12,8 @@ pub mod v3 { metadata, OwnedDeviceId, }; + use super::super::DisplayName; + const METADATA: Metadata = metadata! { method: PUT, rate_limited: false, @@ -33,7 +35,7 @@ pub mod v3 { /// /// If this is `None`, the display name won't be changed. #[serde(skip_serializing_if = "Option::is_none")] - pub display_name: Option, + pub display_name: Option, } /// Response type for the `update_device` endpoint. diff --git a/crates/ruma-client-api/src/discovery/discover_homeserver.rs b/crates/ruma-client-api/src/discovery/discover_homeserver.rs index 6de50b558d..fd86d1a96e 100644 --- a/crates/ruma-client-api/src/discovery/discover_homeserver.rs +++ b/crates/ruma-client-api/src/discovery/discover_homeserver.rs @@ -4,20 +4,14 @@ //! //! Get discovery information about the domain. -#[cfg(feature = "unstable-msc4143")] -use std::borrow::Cow; - -#[cfg(feature = "unstable-msc4143")] -use ruma_common::serde::JsonObject; use ruma_common::{ api::{request, response, Metadata}, metadata, }; -#[cfg(feature = "unstable-msc4143")] -use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; + #[cfg(feature = "unstable-msc4143")] -use serde_json::Value as JsonValue; +use crate::rtc::RtcTransport; const METADATA: Metadata = metadata! { method: GET, @@ -61,7 +55,7 @@ pub struct Response { default, skip_serializing_if = "Vec::is_empty" )] - pub rtc_foci: Vec, + pub rtc_foci: Vec, } impl Request { @@ -133,185 +127,3 @@ impl TileServerInfo { Self { map_style_url } } } - -/// Information about a specific MatrixRTC focus. -#[cfg(feature = "unstable-msc4143")] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] -#[serde(tag = "type")] -pub enum RtcFocusInfo { - /// A LiveKit RTC focus. - #[serde(rename = "livekit")] - LiveKit(LiveKitRtcFocusInfo), - - /// A custom RTC focus. - #[doc(hidden)] - #[serde(untagged)] - _Custom(CustomRtcFocusInfo), -} - -#[cfg(feature = "unstable-msc4143")] -impl RtcFocusInfo { - /// A constructor to create a custom RTC focus. - /// - /// Prefer to use the public variants of `RtcFocusInfo` where possible; this constructor is - /// meant to be used for unsupported focus types only and does not allow setting arbitrary data - /// for supported ones. - /// - /// # Errors - /// - /// Returns an error if the `focus_type` is known and serialization of `data` to the - /// corresponding `RtcFocusInfo` variant fails. - pub fn new(focus_type: &str, data: JsonObject) -> serde_json::Result { - fn deserialize_variant(obj: JsonObject) -> serde_json::Result { - serde_json::from_value(JsonValue::Object(obj)) - } - - Ok(match focus_type { - "livekit" => Self::LiveKit(deserialize_variant(data)?), - _ => Self::_Custom(CustomRtcFocusInfo { focus_type: focus_type.to_owned(), data }), - }) - } - - /// Creates a new `RtcFocusInfo::LiveKit`. - pub fn livekit(service_url: String) -> Self { - Self::LiveKit(LiveKitRtcFocusInfo { service_url }) - } - - /// Returns a reference to the focus type of this RTC focus. - pub fn focus_type(&self) -> &str { - match self { - Self::LiveKit(_) => "livekit", - Self::_Custom(custom) => &custom.focus_type, - } - } - - /// Returns the associated data. - /// - /// The returned JSON object won't contain the `focus_type` field, please use - /// [`.focus_type()`][Self::focus_type] to access that. - /// - /// Prefer to use the public variants of `RtcFocusInfo` where possible; this method is meant to - /// be used for custom focus types only. - pub fn data(&self) -> Cow<'_, JsonObject> { - fn serialize(object: &T) -> JsonObject { - match serde_json::to_value(object).expect("rtc focus type serialization to succeed") { - JsonValue::Object(object) => object, - _ => panic!("all rtc focus types must serialize to objects"), - } - } - - match self { - Self::LiveKit(info) => Cow::Owned(serialize(info)), - Self::_Custom(info) => Cow::Borrowed(&info.data), - } - } -} - -/// Information about a LiveKit RTC focus. -#[cfg(feature = "unstable-msc4143")] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] -pub struct LiveKitRtcFocusInfo { - /// The URL for the LiveKit service. - #[serde(rename = "livekit_service_url")] - pub service_url: String, -} - -/// Information about a custom RTC focus type. -#[doc(hidden)] -#[cfg(feature = "unstable-msc4143")] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct CustomRtcFocusInfo { - /// The type of RTC focus. - #[serde(rename = "type")] - focus_type: String, - - /// Remaining RTC focus data. - #[serde(flatten)] - data: JsonObject, -} - -#[cfg(test)] -mod tests { - #[cfg(feature = "unstable-msc4143")] - use assert_matches2::assert_matches; - #[cfg(feature = "unstable-msc4143")] - use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; - - #[cfg(feature = "unstable-msc4143")] - use super::RtcFocusInfo; - - #[test] - #[cfg(feature = "unstable-msc4143")] - fn test_livekit_rtc_focus_deserialization() { - // Given the JSON for a LiveKit RTC focus. - let json = json!({ - "type": "livekit", - "livekit_service_url": "https://livekit.example.com" - }); - - // When deserializing it into an RtcFocusInfo. - let focus: RtcFocusInfo = from_json_value(json).unwrap(); - - // Then it should be recognized as a LiveKit focus with the correct service URL. - assert_matches!(focus, RtcFocusInfo::LiveKit(info)); - assert_eq!(info.service_url, "https://livekit.example.com"); - } - - #[test] - #[cfg(feature = "unstable-msc4143")] - fn test_livekit_rtc_focus_serialization() { - // Given a LiveKit RTC focus info. - let focus = RtcFocusInfo::livekit("https://livekit.example.com".to_owned()); - - // When serializing it to JSON. - let json = to_json_value(&focus).unwrap(); - - // Then it should match the expected JSON structure. - assert_eq!( - json, - json!({ - "type": "livekit", - "livekit_service_url": "https://livekit.example.com" - }) - ); - } - - #[test] - #[cfg(feature = "unstable-msc4143")] - fn test_custom_rtc_focus_serialization() { - // Given the JSON for a custom RTC focus type with additional fields. - let json = json!({ - "type": "some-focus-type", - "additional-type-specific-field": "https://my_focus.domain", - "another-additional-type-specific-field": ["with", "Array", "type"] - }); - - // When deserializing it into an RtcFocusInfo. - let focus: RtcFocusInfo = from_json_value(json.clone()).unwrap(); - - // Then it should be recognized as a custom focus type, with all the additional fields - // included. - assert_eq!(focus.focus_type(), "some-focus-type"); - - let data = &focus.data(); - assert_eq!(data["additional-type-specific-field"], "https://my_focus.domain"); - - let array_values: Vec<&str> = data["another-additional-type-specific-field"] - .as_array() - .unwrap() - .iter() - .map(|v| v.as_str().unwrap()) - .collect(); - assert_eq!(array_values, vec!["with", "Array", "type"]); - - assert!(!data.contains_key("type")); - - // When serializing it back to JSON. - let serialized = to_json_value(&focus).unwrap(); - - // Then it should match the original JSON. - assert_eq!(serialized, json); - } -} diff --git a/crates/ruma-client-api/src/discovery/get_capabilities.rs b/crates/ruma-client-api/src/discovery/get_capabilities.rs index bb56644c0e..e4affa6367 100644 --- a/crates/ruma-client-api/src/discovery/get_capabilities.rs +++ b/crates/ruma-client-api/src/discovery/get_capabilities.rs @@ -75,53 +75,29 @@ pub mod v3 { #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub struct Capabilities { /// Capability to indicate if the user can change their password. - #[serde( - rename = "m.change_password", - default, - skip_serializing_if = "ChangePasswordCapability::is_default" - )] + #[serde(rename = "m.change_password", default)] pub change_password: ChangePasswordCapability, /// The room versions the server supports. - #[serde( - rename = "m.room_versions", - default, - skip_serializing_if = "RoomVersionsCapability::is_default" - )] + #[serde(rename = "m.room_versions", default)] pub room_versions: RoomVersionsCapability, /// Capability to indicate if the user can change their display name. - #[serde( - rename = "m.set_displayname", - default, - skip_serializing_if = "SetDisplayNameCapability::is_default" - )] + #[serde(rename = "m.set_displayname", default)] pub set_displayname: SetDisplayNameCapability, /// Capability to indicate if the user can change their avatar. - #[serde( - rename = "m.set_avatar_url", - default, - skip_serializing_if = "SetAvatarUrlCapability::is_default" - )] + #[serde(rename = "m.set_avatar_url", default)] pub set_avatar_url: SetAvatarUrlCapability, /// Capability to indicate if the user can change the third-party identifiers associated /// with their account. - #[serde( - rename = "m.3pid_changes", - default, - skip_serializing_if = "ThirdPartyIdChangesCapability::is_default" - )] + #[serde(rename = "m.3pid_changes", default)] pub thirdparty_id_changes: ThirdPartyIdChangesCapability, /// Capability to indicate if the user can generate tokens to log further clients into /// their account. - #[serde( - rename = "m.get_login_token", - default, - skip_serializing_if = "GetLoginTokenCapability::is_default" - )] + #[serde(rename = "m.get_login_token", default)] pub get_login_token: GetLoginTokenCapability, /// Capability to indicate if the user can set extended profile fields. diff --git a/crates/ruma-client-api/src/discovery/get_supported_versions.rs b/crates/ruma-client-api/src/discovery/get_supported_versions.rs index 8fe8f050a9..0b0be02ada 100644 --- a/crates/ruma-client-api/src/discovery/get_supported_versions.rs +++ b/crates/ruma-client-api/src/discovery/get_supported_versions.rs @@ -10,6 +10,7 @@ use ruma_common::{ api::{request, response, Metadata, SupportedVersions}, metadata, }; +use smallstr::SmallString; const METADATA: Metadata = metadata! { method: GET, @@ -29,16 +30,22 @@ pub struct Request {} #[response(error = crate::Error)] pub struct Response { /// A list of Matrix client API protocol versions supported by the homeserver. - pub versions: Vec, + pub versions: Vec, /// Experimental features supported by the server. /// /// Servers can enable some unstable features only for some users, so this /// list might differ when an access token is provided. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub unstable_features: BTreeMap, + pub unstable_features: BTreeMap, } +/// Opinionated optimized Version String type. +pub type Version = SmallString<[u8; 16]>; + +/// Opinionated optimized Feature String type. +pub type Feature = SmallString<[u8; 48]>; + impl Request { /// Creates an empty `Request`. pub fn new() -> Self { @@ -48,7 +55,7 @@ impl Request { impl Response { /// Creates a new `Response` with the given `versions`. - pub fn new(versions: Vec) -> Self { + pub fn new(versions: Vec) -> Self { Self { versions, unstable_features: BTreeMap::new() } } @@ -58,6 +65,9 @@ impl Response { /// Matrix versions that can't be parsed to a `MatrixVersion`, and features with the boolean /// value set to `false` are discarded. pub fn as_supported_versions(&self) -> SupportedVersions { - SupportedVersions::from_parts(&self.versions, &self.unstable_features) + SupportedVersions::from_parts( + self.versions.iter().map(Version::as_str), + self.unstable_features.iter().map(|(k, v)| (k.as_str(), v)), + ) } } diff --git a/crates/ruma-client-api/src/error.rs b/crates/ruma-client-api/src/error.rs index 34f53b8ea0..68627929ab 100644 --- a/crates/ruma-client-api/src/error.rs +++ b/crates/ruma-client-api/src/error.rs @@ -122,6 +122,9 @@ pub enum ErrorKind { /// service making the request has not created the resource. Exclusive, + /// M_FEATURE_DISABLED + FeatureDisabled, + /// `M_FORBIDDEN` /// /// Forbidden access, e.g. joining a room without permission, failed login. @@ -166,6 +169,13 @@ pub enum ErrorKind { /// The desired user name is not valid. InvalidUsername, + /// `M_INVITE_BLOCKED` + /// + /// The invite was interdicted by moderation tools or configured access controls without having + /// been witnessed by the invitee. + #[cfg(feature = "unstable-msc4380")] + InviteBlocked, + /// `M_LIMIT_EXCEEDED` /// /// The request has been refused due to [rate limiting]: too many requests have been sent in a @@ -194,6 +204,9 @@ pub enum ErrorKind { /// No resource was found for this request. NotFound, + /// M_NOT_IMPLEMENTED + NotImplemented, + /// `M_NOT_IN_THREAD` /// /// Part of [MSC4306]: an automatic thread subscription was set to an event ID that isn't part @@ -440,16 +453,20 @@ impl ErrorKind { ErrorKind::ConnectionTimeout => ErrorCode::ConnectionTimeout, ErrorKind::DuplicateAnnotation => ErrorCode::DuplicateAnnotation, ErrorKind::Exclusive => ErrorCode::Exclusive, + ErrorKind::FeatureDisabled => ErrorCode::FeatureDisabled, ErrorKind::Forbidden { .. } => ErrorCode::Forbidden, ErrorKind::GuestAccessForbidden => ErrorCode::GuestAccessForbidden, ErrorKind::IncompatibleRoomVersion { .. } => ErrorCode::IncompatibleRoomVersion, ErrorKind::InvalidParam => ErrorCode::InvalidParam, ErrorKind::InvalidRoomState => ErrorCode::InvalidRoomState, ErrorKind::InvalidUsername => ErrorCode::InvalidUsername, + #[cfg(feature = "unstable-msc4380")] + ErrorKind::InviteBlocked => ErrorCode::InviteBlocked, ErrorKind::LimitExceeded { .. } => ErrorCode::LimitExceeded, ErrorKind::MissingParam => ErrorCode::MissingParam, ErrorKind::MissingToken => ErrorCode::MissingToken, ErrorKind::NotFound => ErrorCode::NotFound, + ErrorKind::NotImplemented => ErrorCode::NotImplemented, #[cfg(feature = "unstable-msc4306")] ErrorKind::NotInThread => ErrorCode::NotInThread, ErrorKind::NotJson => ErrorCode::NotJson, @@ -486,6 +503,12 @@ impl ErrorKind { } } +impl fmt::Display for ErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.errcode()) + } +} + #[doc(hidden)] #[derive(Clone, Debug, PartialEq, Eq)] pub struct Extra(BTreeMap); @@ -580,6 +603,9 @@ pub enum ErrorCode { /// service making the request has not created the resource. Exclusive, + /// M_FEATURE_DISABLED + FeatureDisabled, + /// `M_FORBIDDEN` /// /// Forbidden access, e.g. joining a room without permission, failed login. @@ -616,6 +642,16 @@ pub enum ErrorCode { /// The desired user name is not valid. InvalidUsername, + /// `M_INVITE_BLOCKED` + /// + /// The invite was interdicted by moderation tools or configured access controls without having + /// been witnessed by the invitee. + /// + /// Unstable prefix intentionally shared with MSC4155 for compatibility. + #[cfg(feature = "unstable-msc4380")] + #[ruma_enum(rename = "ORG.MATRIX.MSC4155.INVITE_BLOCKED")] + InviteBlocked, + /// `M_LIMIT_EXCEEDED` /// /// The request has been refused due to [rate limiting]: too many requests have been sent in a @@ -641,6 +677,9 @@ pub enum ErrorCode { /// No resource was found for this request. NotFound, + /// M_NOT_IMPLEMENTED + NotImplemented, + /// `M_NOT_IN_THREAD` /// /// Part of [MSC4306]: an automatic thread subscription was set to an event ID that isn't part diff --git a/crates/ruma-client-api/src/error/kind_serde.rs b/crates/ruma-client-api/src/error/kind_serde.rs index fa0251e7bf..c0b6052101 100644 --- a/crates/ruma-client-api/src/error/kind_serde.rs +++ b/crates/ruma-client-api/src/error/kind_serde.rs @@ -187,6 +187,7 @@ impl<'de> Visitor<'de> for ErrorKindVisitor { ErrorCode::ConnectionTimeout => ErrorKind::ConnectionTimeout, ErrorCode::DuplicateAnnotation => ErrorKind::DuplicateAnnotation, ErrorCode::Exclusive => ErrorKind::Exclusive, + ErrorCode::FeatureDisabled => ErrorKind::FeatureDisabled, ErrorCode::Forbidden => ErrorKind::forbidden(), ErrorCode::GuestAccessForbidden => ErrorKind::GuestAccessForbidden, ErrorCode::IncompatibleRoomVersion => ErrorKind::IncompatibleRoomVersion { @@ -198,6 +199,8 @@ impl<'de> Visitor<'de> for ErrorKindVisitor { ErrorCode::InvalidParam => ErrorKind::InvalidParam, ErrorCode::InvalidRoomState => ErrorKind::InvalidRoomState, ErrorCode::InvalidUsername => ErrorKind::InvalidUsername, + #[cfg(feature = "unstable-msc4380")] + ErrorCode::InviteBlocked => ErrorKind::InviteBlocked, ErrorCode::LimitExceeded => ErrorKind::LimitExceeded { retry_after: retry_after_ms .map(from_json_value::) @@ -210,6 +213,7 @@ impl<'de> Visitor<'de> for ErrorKindVisitor { ErrorCode::MissingParam => ErrorKind::MissingParam, ErrorCode::MissingToken => ErrorKind::MissingToken, ErrorCode::NotFound => ErrorKind::NotFound, + ErrorCode::NotImplemented => ErrorKind::NotImplemented, #[cfg(feature = "unstable-msc4306")] ErrorCode::NotInThread => ErrorKind::NotInThread, ErrorCode::NotJson => ErrorKind::NotJson, diff --git a/crates/ruma-client-api/src/keys/claim_keys/v3.rs b/crates/ruma-client-api/src/keys/claim_keys/v3.rs index 590f871dae..7fffa18fe3 100644 --- a/crates/ruma-client-api/src/keys/claim_keys/v3.rs +++ b/crates/ruma-client-api/src/keys/claim_keys/v3.rs @@ -18,6 +18,7 @@ const METADATA: Metadata = metadata! { rate_limited: false, authentication: AccessToken, history: { + unstable => "/_matrix/client/unstable/keys/claim", 1.0 => "/_matrix/client/r0/keys/claim", 1.1 => "/_matrix/client/v3/keys/claim", } @@ -45,7 +46,7 @@ pub struct Response { /// If any remote homeservers could not be reached, they are recorded here. /// /// The names of the properties are the names of the unreachable servers. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default)] pub failures: BTreeMap, /// One-time keys for the queried devices. diff --git a/crates/ruma-client-api/src/keys/claim_keys/v4.rs b/crates/ruma-client-api/src/keys/claim_keys/v4.rs index b655d89731..ea691ac931 100644 --- a/crates/ruma-client-api/src/keys/claim_keys/v4.rs +++ b/crates/ruma-client-api/src/keys/claim_keys/v4.rs @@ -44,7 +44,7 @@ pub struct Response { /// If any remote homeservers could not be reached, they are recorded here. /// /// The names of the properties are the names of the unreachable servers. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default)] pub failures: BTreeMap, /// One-time keys for the queried devices. diff --git a/crates/ruma-client-api/src/keys/get_key_changes.rs b/crates/ruma-client-api/src/keys/get_key_changes.rs index 2564e6bf7d..cd11957d01 100644 --- a/crates/ruma-client-api/src/keys/get_key_changes.rs +++ b/crates/ruma-client-api/src/keys/get_key_changes.rs @@ -17,6 +17,7 @@ pub mod v3 { rate_limited: false, authentication: AccessToken, history: { + unstable => "/_matrix/client/unstable/keys/changes", 1.0 => "/_matrix/client/r0/keys/changes", 1.1 => "/_matrix/client/v3/keys/changes", } diff --git a/crates/ruma-client-api/src/keys/get_keys.rs b/crates/ruma-client-api/src/keys/get_keys.rs index 3ddfd91e6d..9a7eee1aaa 100644 --- a/crates/ruma-client-api/src/keys/get_keys.rs +++ b/crates/ruma-client-api/src/keys/get_keys.rs @@ -23,6 +23,7 @@ pub mod v3 { rate_limited: false, authentication: AccessToken, history: { + unstable => "/_matrix/client/unstable/keys/query", 1.0 => "/_matrix/client/r0/keys/query", 1.1 => "/_matrix/client/v3/keys/query", } @@ -55,7 +56,7 @@ pub mod v3 { /// If any remote homeservers could not be reached, they are recorded here. /// /// The names of the properties are the names of the unreachable servers. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default)] pub failures: BTreeMap, /// Information on the queried devices. diff --git a/crates/ruma-client-api/src/keys/upload_keys.rs b/crates/ruma-client-api/src/keys/upload_keys.rs index ef509ed412..3b1c0aefc2 100644 --- a/crates/ruma-client-api/src/keys/upload_keys.rs +++ b/crates/ruma-client-api/src/keys/upload_keys.rs @@ -23,6 +23,7 @@ pub mod v3 { rate_limited: false, authentication: AccessToken, history: { + unstable => "/_matrix/client/unstable/keys/upload", 1.0 => "/_matrix/client/r0/keys/upload", 1.1 => "/_matrix/client/v3/keys/upload", } diff --git a/crates/ruma-client-api/src/keys/upload_signatures.rs b/crates/ruma-client-api/src/keys/upload_signatures.rs index 7279667861..75cfafed30 100644 --- a/crates/ruma-client-api/src/keys/upload_signatures.rs +++ b/crates/ruma-client-api/src/keys/upload_signatures.rs @@ -28,6 +28,7 @@ pub mod v3 { authentication: AccessToken, history: { unstable => "/_matrix/client/unstable/keys/signatures/upload", + 1.0 => "/_matrix/client/r0/keys/signatures/upload", 1.1 => "/_matrix/client/v3/keys/signatures/upload", } }; @@ -45,7 +46,7 @@ pub mod v3 { #[derive(Default)] pub struct Response { /// Signature processing failures. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default)] pub failures: BTreeMap>, } @@ -96,13 +97,14 @@ pub mod v3 { /// A failure to process a signed key. #[derive(Clone, Debug, Deserialize, Serialize)] + #[non_exhaustive] pub struct Failure { /// Machine-readable error code. - errcode: FailureErrorCode, + pub errcode: FailureErrorCode, /// Human-readable error message. #[cfg_attr(feature = "compat-upload-signatures", serde(alias = "message"))] - error: String, + pub error: String, } /// Error code for signed key processing failures. diff --git a/crates/ruma-client-api/src/keys/upload_signing_keys.rs b/crates/ruma-client-api/src/keys/upload_signing_keys.rs index 1412150e77..38f21b6da5 100644 --- a/crates/ruma-client-api/src/keys/upload_signing_keys.rs +++ b/crates/ruma-client-api/src/keys/upload_signing_keys.rs @@ -22,16 +22,18 @@ pub mod v3 { authentication: AccessToken, history: { unstable => "/_matrix/client/unstable/keys/device_signing/upload", + 1.0 => "/_matrix/client/r0/keys/device_signing/upload", 1.1 => "/_matrix/client/v3/keys/device_signing/upload", } }; - /// Request type for the `upload_signing_keys` endpoint. #[request(error = UiaaResponse)] #[derive(Default)] pub struct Request { /// Additional authentication information for the user-interactive authentication API. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(deserialize_with = "ruma_common::serde::or_empty")] + #[serde(default)] pub auth: Option, /// The user's master key. diff --git a/crates/ruma-client-api/src/knock/knock_room.rs b/crates/ruma-client-api/src/knock/knock_room.rs index b85f96d20a..c892ce1346 100644 --- a/crates/ruma-client-api/src/knock/knock_room.rs +++ b/crates/ruma-client-api/src/knock/knock_room.rs @@ -18,6 +18,7 @@ pub mod v3 { authentication: AccessToken, history: { unstable => "/_matrix/client/unstable/xyz.amorgan.knock/knock/{room_id_or_alias}", + 1.0 => "/_matrix/client/r0/knock/{room_id_or_alias}", 1.1 => "/_matrix/client/v3/knock/{room_id_or_alias}", } }; diff --git a/crates/ruma-client-api/src/lib.rs b/crates/ruma-client-api/src/lib.rs index 341eb1dac9..da99767823 100644 --- a/crates/ruma-client-api/src/lib.rs +++ b/crates/ruma-client-api/src/lib.rs @@ -6,7 +6,7 @@ //! [client-api]: https://spec.matrix.org/latest/client-server-api/ #![cfg(any(feature = "client", feature = "server"))] -#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![warn(missing_docs)] pub mod account; @@ -43,6 +43,8 @@ pub mod relations; pub mod rendezvous; pub mod reporting; pub mod room; +#[cfg(feature = "unstable-msc4143")] +pub mod rtc; pub mod search; pub mod server; pub mod session; diff --git a/crates/ruma-client-api/src/media/create_content_async.rs b/crates/ruma-client-api/src/media/create_content_async.rs index 9a15b981d4..6eceb95cac 100644 --- a/crates/ruma-client-api/src/media/create_content_async.rs +++ b/crates/ruma-client-api/src/media/create_content_async.rs @@ -10,7 +10,7 @@ pub mod v3 { use http::header::CONTENT_TYPE; use ruma_common::{ api::{request, response, Metadata}, - metadata, IdParseError, MxcUri, OwnedServerName, + metadata, IdParseError, Mxc, MxcUri, OwnedServerName, }; const METADATA: Metadata = metadata! { @@ -61,7 +61,7 @@ pub mod v3 { /// Creates a new `Request` with the given url and file contents. pub fn from_url(url: &MxcUri, file: Vec) -> Result { - let (server_name, media_id) = url.parts()?; + let Mxc { server_name, media_id, .. } = url.parts()?; Ok(Self::new(media_id.to_owned(), server_name.to_owned(), file)) } } diff --git a/crates/ruma-client-api/src/media/get_content.rs b/crates/ruma-client-api/src/media/get_content.rs index d5aee5e21d..953179bb33 100644 --- a/crates/ruma-client-api/src/media/get_content.rs +++ b/crates/ruma-client-api/src/media/get_content.rs @@ -7,13 +7,13 @@ pub mod v3 { //! //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixmediav3downloadservernamemediaid - use std::time::Duration; + use std::{borrow::Cow, time::Duration}; - use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE}; + use http::header::{CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_TYPE}; use ruma_common::{ api::{request, response, Metadata}, http_headers::ContentDisposition, - metadata, IdParseError, MxcUri, OwnedServerName, + metadata, IdParseError, Mxc, MxcUri, OwnedServerName, }; use crate::http_headers::CROSS_ORIGIN_RESOURCE_POLICY; @@ -84,7 +84,7 @@ pub mod v3 { /// The content type of the file that was previously uploaded. #[ruma_api(header = CONTENT_TYPE)] - pub content_type: Option, + pub content_type: Option>, /// The value of the `Content-Disposition` HTTP header, possibly containing the name of the /// file that was previously uploaded. @@ -97,7 +97,15 @@ pub mod v3 { /// /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy#syntax #[ruma_api(header = CROSS_ORIGIN_RESOURCE_POLICY)] - pub cross_origin_resource_policy: Option, + pub cross_origin_resource_policy: Option>, + + /// The value of the `Cache-Control` HTTP header. + /// + /// See [MDN] for the syntax. + /// + /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#syntax + #[ruma_api(header = CACHE_CONTROL)] + pub cache_control: Option>, } #[allow(deprecated)] @@ -115,7 +123,7 @@ pub mod v3 { /// Creates a new `Request` with the given url. pub fn from_url(url: &MxcUri) -> Result { - let (server_name, media_id) = url.parts()?; + let Mxc { server_name, media_id, .. } = url.parts()?; Ok(Self::new(media_id.to_owned(), server_name.to_owned())) } @@ -127,14 +135,15 @@ pub mod v3 { /// The Cross-Origin Resource Policy defaults to `cross-origin`. pub fn new( file: Vec, - content_type: String, + content_type: Cow<'static, str>, content_disposition: ContentDisposition, ) -> Self { Self { file, content_type: Some(content_type), content_disposition: Some(content_disposition), - cross_origin_resource_policy: Some("cross-origin".to_owned()), + cross_origin_resource_policy: Some("cross-origin".into()), + cache_control: None, } } } diff --git a/crates/ruma-client-api/src/media/get_content_as_filename.rs b/crates/ruma-client-api/src/media/get_content_as_filename.rs index c0d0ab5f1b..b7d541d0f0 100644 --- a/crates/ruma-client-api/src/media/get_content_as_filename.rs +++ b/crates/ruma-client-api/src/media/get_content_as_filename.rs @@ -7,13 +7,13 @@ pub mod v3 { //! //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixmediav3downloadservernamemediaidfilename - use std::time::Duration; + use std::{borrow::Cow, time::Duration}; - use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE}; + use http::header::{CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_TYPE}; use ruma_common::{ api::{request, response, Metadata}, http_headers::ContentDisposition, - metadata, IdParseError, MxcUri, OwnedServerName, + metadata, IdParseError, Mxc, MxcUri, OwnedServerName, }; use crate::http_headers::CROSS_ORIGIN_RESOURCE_POLICY; @@ -88,7 +88,7 @@ pub mod v3 { /// The content type of the file that was previously uploaded. #[ruma_api(header = CONTENT_TYPE)] - pub content_type: Option, + pub content_type: Option>, /// The value of the `Content-Disposition` HTTP header, possibly containing the name of the /// file that was previously uploaded. @@ -101,7 +101,15 @@ pub mod v3 { /// /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy#syntax #[ruma_api(header = CROSS_ORIGIN_RESOURCE_POLICY)] - pub cross_origin_resource_policy: Option, + pub cross_origin_resource_policy: Option>, + + /// The value of the `Cache-Control` HTTP header. + /// + /// See [MDN] for the syntax. + /// + /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#syntax + #[ruma_api(header = CACHE_CONTROL)] + pub cache_control: Option>, } #[allow(deprecated)] @@ -120,7 +128,7 @@ pub mod v3 { /// Creates a new `Request` with the given url and filename. pub fn from_url(url: &MxcUri, filename: String) -> Result { - let (server_name, media_id) = url.parts()?; + let Mxc { server_name, media_id, .. } = url.parts()?; Ok(Self::new(media_id.to_owned(), server_name.to_owned(), filename)) } @@ -132,14 +140,15 @@ pub mod v3 { /// The Cross-Origin Resource Policy defaults to `cross-origin`. pub fn new( file: Vec, - content_type: String, + content_type: Cow<'static, str>, content_disposition: ContentDisposition, ) -> Self { Self { file, content_type: Some(content_type), content_disposition: Some(content_disposition), - cross_origin_resource_policy: Some("cross-origin".to_owned()), + cross_origin_resource_policy: Some("cross-origin".into()), + cache_control: None, } } } diff --git a/crates/ruma-client-api/src/media/get_content_thumbnail.rs b/crates/ruma-client-api/src/media/get_content_thumbnail.rs index f6ca12190b..ddc12e314b 100644 --- a/crates/ruma-client-api/src/media/get_content_thumbnail.rs +++ b/crates/ruma-client-api/src/media/get_content_thumbnail.rs @@ -7,15 +7,15 @@ pub mod v3 { //! //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixmediav3thumbnailservernamemediaid - use std::time::Duration; + use std::{borrow::Cow, time::Duration}; - use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE}; + use http::header::{CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_TYPE}; use js_int::UInt; pub use ruma_common::media::Method; use ruma_common::{ api::{request, response, Metadata}, http_headers::ContentDisposition, - metadata, IdParseError, MxcUri, OwnedServerName, + metadata, IdParseError, Mxc, MxcUri, OwnedServerName, }; use crate::http_headers::CROSS_ORIGIN_RESOURCE_POLICY; @@ -112,7 +112,7 @@ pub mod v3 { /// The content type of the thumbnail. #[ruma_api(header = CONTENT_TYPE)] - pub content_type: Option, + pub content_type: Option>, /// The value of the `Content-Disposition` HTTP header, possibly containing the name of the /// file that was previously uploaded. @@ -129,7 +129,15 @@ pub mod v3 { /// /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy#syntax #[ruma_api(header = CROSS_ORIGIN_RESOURCE_POLICY)] - pub cross_origin_resource_policy: Option, + pub cross_origin_resource_policy: Option>, + + /// The value of the `Cache-Control` HTTP header. + /// + /// See [MDN] for the syntax. + /// + /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#syntax + #[ruma_api(header = CACHE_CONTROL)] + pub cache_control: Option>, } #[allow(deprecated)] @@ -158,7 +166,7 @@ pub mod v3 { /// Creates a new `Request` with the given url, desired thumbnail width and /// desired thumbnail height. pub fn from_url(url: &MxcUri, width: UInt, height: UInt) -> Result { - let (server_name, media_id) = url.parts()?; + let Mxc { server_name, media_id, .. } = url.parts()?; Ok(Self::new(media_id.to_owned(), server_name.to_owned(), width, height)) } @@ -170,14 +178,15 @@ pub mod v3 { /// The Cross-Origin Resource Policy defaults to `cross-origin`. pub fn new( file: Vec, - content_type: String, + content_type: Cow<'static, str>, content_disposition: ContentDisposition, ) -> Self { Self { file, content_type: Some(content_type), content_disposition: Some(content_disposition), - cross_origin_resource_policy: Some("cross-origin".to_owned()), + cross_origin_resource_policy: Some("cross-origin".into()), + cache_control: None, } } } diff --git a/crates/ruma-client-api/src/membership/invite_user.rs b/crates/ruma-client-api/src/membership/invite_user.rs index a96b1937e1..4b59bf7f7a 100644 --- a/crates/ruma-client-api/src/membership/invite_user.rs +++ b/crates/ruma-client-api/src/membership/invite_user.rs @@ -5,7 +5,6 @@ pub mod v3 { //! `/v3/` ([spec (MXID)][spec-mxid], [spec (3PID)][spec-3pid]) //! - //! This endpoint has two forms: one to invite a user //! [by their Matrix identifier][spec-mxid], and one to invite a user //! [by their third party identifier][spec-3pid]. //! diff --git a/crates/ruma-client-api/src/membership/joined_members.rs b/crates/ruma-client-api/src/membership/joined_members.rs index 13fcc1cfc7..878fbd4f23 100644 --- a/crates/ruma-client-api/src/membership/joined_members.rs +++ b/crates/ruma-client-api/src/membership/joined_members.rs @@ -61,14 +61,12 @@ pub mod v3 { #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub struct RoomMember { /// The display name of the user. - #[serde(skip_serializing_if = "Option::is_none")] pub display_name: Option, /// The mxc avatar url of the user. /// /// If you activate the `compat-empty-string-null` feature, this field being an empty /// string in JSON will result in `None` here during deserialization. - #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr( feature = "compat-empty-string-null", serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none") diff --git a/crates/ruma-client-api/src/profile.rs b/crates/ruma-client-api/src/profile.rs index 10c79251fe..b299509c91 100644 --- a/crates/ruma-client-api/src/profile.rs +++ b/crates/ruma-client-api/src/profile.rs @@ -15,17 +15,20 @@ use serde_json::{from_value as from_json_value, to_value as to_json_value, Value #[cfg(feature = "unstable-msc4133")] pub mod delete_profile_field; +pub mod delete_timezone_key; pub mod get_avatar_url; pub mod get_display_name; pub mod get_profile; #[cfg(feature = "unstable-msc4133")] pub mod get_profile_field; +pub mod get_timezone_key; #[cfg(feature = "unstable-msc4133")] mod profile_field_serde; pub mod set_avatar_url; pub mod set_display_name; #[cfg(feature = "unstable-msc4133")] pub mod set_profile_field; +pub mod set_timezone_key; /// Trait implemented by types representing a field in a user's profile having a statically-known /// name. diff --git a/crates/ruma-client-api/src/profile/delete_profile_field.rs b/crates/ruma-client-api/src/profile/delete_profile_field.rs index d600d501c1..ce934329d7 100644 --- a/crates/ruma-client-api/src/profile/delete_profile_field.rs +++ b/crates/ruma-client-api/src/profile/delete_profile_field.rs @@ -20,7 +20,7 @@ pub mod v3 { authentication: AccessToken, history: { unstable("uk.tcpip.msc4133") => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}", - // 1.15 => "/_matrix/client/v3/profile/{user_id}/{field}", + 1.15 => "/_matrix/client/v3/profile/{user_id}/{field}", } }; diff --git a/crates/ruma-client-api/src/profile/delete_profile_key.rs b/crates/ruma-client-api/src/profile/delete_profile_key.rs new file mode 100644 index 0000000000..ce5f8804d4 --- /dev/null +++ b/crates/ruma-client-api/src/profile/delete_profile_key.rs @@ -0,0 +1,60 @@ +#![allow(missing_docs)] +//! `DELETE /_matrix/client/*/profile/{user_id}/{key_name}` +//! +//! Deletes a custom profile key from the user + +pub mod unstable { + //! `msc4133` ([MSC]) + //! + //! [MSC]: https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md + + use std::collections::BTreeMap; + + use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, + }; + use serde_json::Value as JsonValue; + + const METADATA: Metadata = metadata! { + method: DELETE, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{key}", + 1.12 => "/_matrix/client/v3/profile/{user_id}/{key}", + } + }; + + #[request(error = crate::Error)] + pub struct Request { + #[ruma_api(path)] + pub user_id: OwnedUserId, + + #[ruma_api(path)] + pub key: String, + + #[ruma_api(body)] + pub kv_pair: BTreeMap, + } + + #[response(error = crate::Error)] + #[derive(Default)] + pub struct Response {} + + impl Request { + pub fn new( + user_id: OwnedUserId, + key: String, + kv_pair: BTreeMap, + ) -> Self { + Self { user_id, key, kv_pair } + } + } + + impl Response { + pub fn new() -> Self { + Self {} + } + } +} diff --git a/crates/ruma-client-api/src/profile/delete_timezone_key.rs b/crates/ruma-client-api/src/profile/delete_timezone_key.rs new file mode 100644 index 0000000000..792a6f0b82 --- /dev/null +++ b/crates/ruma-client-api/src/profile/delete_timezone_key.rs @@ -0,0 +1,48 @@ +#![allow(missing_docs)] +//! `DELETE /_matrix/client/*/profile/{userId}/m.tz` +//! +//! Deletes the timezone key of the user. + +pub mod unstable { + use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, + }; + + const METADATA: Metadata = metadata! { + method: DELETE, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/us.cloke.msc4175.tz", + 1.12 => "/_matrix/client/v3/profile/{user_id}/m.tz", + } + }; + + /// Request type for the `delete_timezone_key` endpoint. + #[request(error = crate::Error)] + pub struct Request { + /// The user whose timezone will be deleted. + #[ruma_api(path)] + pub user_id: OwnedUserId, + } + + /// Response type for the `delete_timezone_key` endpoint. + #[response(error = crate::Error)] + #[derive(Default)] + pub struct Response {} + + impl Request { + /// Creates a new `Request` with the given user ID. + pub fn new(user_id: OwnedUserId) -> Self { + Self { user_id } + } + } + + impl Response { + /// Creates an empty `Response`. + pub fn new() -> Self { + Self {} + } + } +} diff --git a/crates/ruma-client-api/src/profile/get_profile.rs b/crates/ruma-client-api/src/profile/get_profile.rs index fdb34d3e7b..4a5b2b01c7 100644 --- a/crates/ruma-client-api/src/profile/get_profile.rs +++ b/crates/ruma-client-api/src/profile/get_profile.rs @@ -6,6 +6,10 @@ pub mod v3 { //! `/v3/` ([spec]) //! //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3profileuserid + //! + //! also see: `msc4133` ([MSC]) + //! + //! [MSC]: https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md use std::collections::{btree_map, BTreeMap}; @@ -23,6 +27,7 @@ pub mod v3 { rate_limited: false, authentication: None, history: { + unstable => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}", 1.0 => "/_matrix/client/r0/profile/{user_id}", 1.1 => "/_matrix/client/v3/profile/{user_id}", } diff --git a/crates/ruma-client-api/src/profile/get_profile_field.rs b/crates/ruma-client-api/src/profile/get_profile_field.rs index ad47795ae0..61fb8a2d13 100644 --- a/crates/ruma-client-api/src/profile/get_profile_field.rs +++ b/crates/ruma-client-api/src/profile/get_profile_field.rs @@ -25,7 +25,7 @@ pub mod v3 { authentication: None, history: { unstable("uk.tcpip.msc4133") => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}", - // 1.15 => "/_matrix/client/v3/profile/{user_id}/{field}", + 1.15 => "/_matrix/client/v3/profile/{user_id}/{field}", } }; diff --git a/crates/ruma-client-api/src/profile/get_profile_key.rs b/crates/ruma-client-api/src/profile/get_profile_key.rs new file mode 100644 index 0000000000..80b0e9dff6 --- /dev/null +++ b/crates/ruma-client-api/src/profile/get_profile_key.rs @@ -0,0 +1,55 @@ +#![allow(missing_docs)] +//! `GET /_matrix/client/*/profile/{user_id}/{key_name}` +//! +//! Gets a custom profile key from the user + +//! `msc4133` ([MSC]) +//! +//! [MSC]: https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md +pub mod unstable { + use std::collections::BTreeMap; + + use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, + }; + use serde_json::Value as JsonValue; + + const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: None, + history: { + unstable => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{key}", + 1.12 => "/_matrix/client/v3/profile/{user_id}/{key}", + } + }; + + #[request(error = crate::Error)] + pub struct Request { + #[ruma_api(path)] + pub user_id: OwnedUserId, + + #[ruma_api(path)] + pub key: String, + } + + #[response(error = crate::Error)] + #[derive(Default)] + pub struct Response { + #[ruma_api(body)] + pub value: BTreeMap, + } + + impl Request { + pub fn new(user_id: OwnedUserId, key: String) -> Self { + Self { user_id, key } + } + } + + impl Response { + pub fn new(value: BTreeMap) -> Self { + Self { value } + } + } +} diff --git a/crates/ruma-client-api/src/profile/get_timezone_key.rs b/crates/ruma-client-api/src/profile/get_timezone_key.rs new file mode 100644 index 0000000000..fd772bf286 --- /dev/null +++ b/crates/ruma-client-api/src/profile/get_timezone_key.rs @@ -0,0 +1,58 @@ +//! `GET /_matrix/client/*/profile/{userId}/m.tz` +//! +//! Get the timezone key of a user. + +/// Get the timezone key of a user. +pub mod unstable { + use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, + }; + + const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: None, + history: { + unstable => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/us.cloke.msc4175.tz", + 1.12 => "/_matrix/client/v3/profile/{user_id}/m.tz", + } + }; + + /// Request type for the `get_timezone` endpoint. + #[request(error = crate::Error)] + pub struct Request { + /// The user whose timezone will be retrieved. + #[ruma_api(path)] + pub user_id: OwnedUserId, + } + + /// Response type for the `get_timezone` endpoint. + #[response(error = crate::Error)] + #[derive(Default)] + pub struct Response { + /// [MSC4175][msc]: `m.tz` field for specifying a timezone the user is in + /// + /// [msc]: https://github.com/matrix-org/matrix-spec-proposals/blob/clokep/profile-tz/proposals/4175-profile-field-time-zone.md + #[serde( + rename = "m.tz", + alias = "us.cloke.msc4175.tz", + skip_serializing_if = "Option::is_none" + )] + pub tz: Option, + } + + impl Request { + /// Creates a new `Request` with the given user ID. + pub fn new(user_id: OwnedUserId) -> Self { + Self { user_id } + } + } + + impl Response { + /// Creates a new `Response` with the given timezone. + pub fn new(tz: Option) -> Self { + Self { tz } + } + } +} diff --git a/crates/ruma-client-api/src/profile/set_profile_field.rs b/crates/ruma-client-api/src/profile/set_profile_field.rs index 117dbb9c78..72d5538396 100644 --- a/crates/ruma-client-api/src/profile/set_profile_field.rs +++ b/crates/ruma-client-api/src/profile/set_profile_field.rs @@ -20,7 +20,7 @@ pub mod v3 { authentication: AccessToken, history: { unstable("uk.tcpip.msc4133") => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}", - // 1.15 => "/_matrix/client/v3/profile/{user_id}/{field}", + 1.15 => "/_matrix/client/v3/profile/{user_id}/{field}", } }; diff --git a/crates/ruma-client-api/src/profile/set_profile_key.rs b/crates/ruma-client-api/src/profile/set_profile_key.rs new file mode 100644 index 0000000000..45b690d38f --- /dev/null +++ b/crates/ruma-client-api/src/profile/set_profile_key.rs @@ -0,0 +1,59 @@ +#![allow(missing_docs)] +//! `PUT /_matrix/client/*/profile/{user_id}/{key_name}` +//! +//! Sets a custom profile key from the user + +//! `msc4133` ([MSC]) +//! +//! [MSC]: https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md +pub mod unstable { + use std::collections::BTreeMap; + + use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, + }; + use serde_json::Value as JsonValue; + + const METADATA: Metadata = metadata! { + method: PUT, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{key}", + 1.12 => "/_matrix/client/v3/profile/{user_id}/{key}", + } + }; + + #[request(error = crate::Error)] + pub struct Request { + #[ruma_api(path)] + pub user_id: OwnedUserId, + + #[ruma_api(path)] + pub key: String, + + #[ruma_api(body)] + pub kv_pair: BTreeMap, + } + + #[response(error = crate::Error)] + #[derive(Default)] + pub struct Response {} + + impl Request { + pub fn new( + user_id: OwnedUserId, + key: String, + kv_pair: BTreeMap, + ) -> Self { + Self { user_id, key, kv_pair } + } + } + + impl Response { + pub fn new() -> Self { + Self {} + } + } +} diff --git a/crates/ruma-client-api/src/profile/set_timezone_key.rs b/crates/ruma-client-api/src/profile/set_timezone_key.rs new file mode 100644 index 0000000000..d5a2770d37 --- /dev/null +++ b/crates/ruma-client-api/src/profile/set_timezone_key.rs @@ -0,0 +1,58 @@ +//! `PUT /_matrix/client/*/profile/{userId}/m.tz` +//! +//! Set the timezone key of the user. + +/// Set the timezone key of the user. +pub mod unstable { + use ruma_common::{ + api::{request, response, Metadata}, + metadata, OwnedUserId, + }; + + const METADATA: Metadata = metadata! { + method: PUT, + rate_limited: true, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/us.cloke.msc4175.tz", + 1.12 => "/_matrix/client/v3/profile/{user_id}/m.tz", + } + }; + + /// Request type for the `set_timezone_key` endpoint. + #[request(error = crate::Error)] + pub struct Request { + /// The user whose timezone will be set. + #[ruma_api(path)] + pub user_id: OwnedUserId, + + /// [MSC4175][msc]: `m.tz` field for specifying a timezone the user is in + /// + /// [msc]: https://github.com/matrix-org/matrix-spec-proposals/blob/clokep/profile-tz/proposals/4175-profile-field-time-zone.md + #[serde( + rename = "m.tz", + alias = "us.cloke.msc4175.tz", + skip_serializing_if = "Option::is_none" + )] + pub tz: Option, + } + + /// Response type for the `set_timezone_key` endpoint. + #[response(error = crate::Error)] + #[derive(Default)] + pub struct Response {} + + impl Request { + /// Creates a new `Request` with the given user ID and timezone. + pub fn new(user_id: OwnedUserId, tz: Option) -> Self { + Self { user_id, tz } + } + } + + impl Response { + /// Creates an empty `Response`. + pub fn new() -> Self { + Self {} + } + } +} diff --git a/crates/ruma-client-api/src/push.rs b/crates/ruma-client-api/src/push.rs index a64e1d1184..8f73f91faa 100644 --- a/crates/ruma-client-api/src/push.rs +++ b/crates/ruma-client-api/src/push.rs @@ -4,13 +4,14 @@ use std::{error::Error, fmt}; pub use ruma_common::push::RuleKind; use ruma_common::{ push::{ - Action, AnyPushRule, AnyPushRuleRef, ConditionalPushRule, ConditionalPushRuleInit, - HttpPusherData, PatternedPushRule, PatternedPushRuleInit, PushCondition, SimplePushRule, - SimplePushRuleInit, + Actions, AnyPushRule, AnyPushRuleRef, ConditionalPushRule, ConditionalPushRuleInit, + HttpPusherData, Pattern, PatternedPushRule, PatternedPushRuleInit, PushConditions, RuleId, + SimplePushRule, SimplePushRuleInit, }, serde::JsonObject, }; use serde::{Deserialize, Serialize}; +use smallstr::SmallString; pub mod delete_pushrule; pub mod get_notifications; @@ -34,7 +35,7 @@ pub mod set_pushrule_enabled; #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub struct PushRule { /// The actions to perform when this rule is matched. - pub actions: Vec, + pub actions: Actions, /// Whether this is a default rule, or has been set explicitly. pub default: bool, @@ -43,22 +44,31 @@ pub struct PushRule { pub enabled: bool, /// The ID of this rule. - pub rule_id: String, + pub rule_id: RuleId, /// The conditions that must hold true for an event in order for a rule to be applied to an /// event. /// /// A rule with no conditions always matches. Only applicable to underride and override rules. #[serde(skip_serializing_if = "Option::is_none")] - pub conditions: Option>, + pub conditions: Option, /// The glob-style pattern to match against. /// /// Only applicable to content rules. #[serde(skip_serializing_if = "Option::is_none")] - pub pattern: Option, + pub pattern: Option, } +/// This string determines which set of device specific rules this pusher executes. +pub type ProfileTag = SmallString<[u8; 24]>; + +/// Tuned string type for displayname and app_display_name. +pub type DisplayName = SmallString<[u8; 40]>; + +/// The preferred language for receiving notifications (e.g. 'en' or 'en-US'). +pub type Lang = SmallString<[u8; 8]>; + impl From> for PushRule where T: Into, @@ -66,7 +76,7 @@ where fn from(push_rule: SimplePushRule) -> Self { let SimplePushRule { actions, default, enabled, rule_id, .. } = push_rule; let rule_id = rule_id.into(); - Self { actions, default, enabled, rule_id, conditions: None, pattern: None } + Self { actions, default, enabled, rule_id: rule_id.into(), conditions: None, pattern: None } } } @@ -91,7 +101,7 @@ where fn from(init: SimplePushRuleInit) -> Self { let SimplePushRuleInit { actions, default, enabled, rule_id } = init; let rule_id = rule_id.into(); - Self { actions, default, enabled, rule_id, pattern: None, conditions: None } + Self { actions, default, enabled, rule_id: rule_id.into(), pattern: None, conditions: None } } } @@ -139,7 +149,7 @@ where fn try_from(push_rule: PushRule) -> Result { let PushRule { actions, default, enabled, rule_id, .. } = push_rule; - let rule_id = T::try_from(rule_id)?; + let rule_id = T::try_from(rule_id.as_str().into())?; Ok(SimplePushRuleInit { actions, default, enabled, rule_id }.into()) } } @@ -217,17 +227,17 @@ pub struct Pusher { pub kind: PusherKind, /// A string that will allow the user to identify what application owns this pusher. - pub app_display_name: String, + pub app_display_name: DisplayName, /// A string that will allow the user to identify what device owns this pusher. - pub device_display_name: String, + pub device_display_name: DisplayName, /// Determines which set of device specific rules this pusher executes. #[serde(skip_serializing_if = "Option::is_none")] - pub profile_tag: Option, + pub profile_tag: Option, /// The preferred language for receiving notifications (e.g. 'en' or 'en-US') - pub lang: String, + pub lang: Lang, } /// Initial set of fields of `Pusher`. @@ -244,16 +254,16 @@ pub struct PusherInit { pub kind: PusherKind, /// A string that will allow the user to identify what application owns this pusher. - pub app_display_name: String, + pub app_display_name: DisplayName, /// A string that will allow the user to identify what device owns this pusher. - pub device_display_name: String, + pub device_display_name: DisplayName, /// Determines which set of device-specific rules this pusher executes. - pub profile_tag: Option, + pub profile_tag: Option, /// The preferred language for receiving notifications (e.g. 'en' or 'en-US'). - pub lang: String, + pub lang: Lang, } impl From for Pusher { diff --git a/crates/ruma-client-api/src/push/get_notifications.rs b/crates/ruma-client-api/src/push/get_notifications.rs index 8b8c4dd447..7a869d6532 100644 --- a/crates/ruma-client-api/src/push/get_notifications.rs +++ b/crates/ruma-client-api/src/push/get_notifications.rs @@ -11,12 +11,13 @@ pub mod v3 { use ruma_common::{ api::{request, response, Metadata}, metadata, - push::Action, + push::Actions, serde::Raw, MilliSecondsSinceUnixEpoch, OwnedRoomId, }; use ruma_events::AnySyncTimelineEvent; use serde::{Deserialize, Serialize}; + use smallstr::SmallString; const METADATA: Metadata = metadata! { method: GET, @@ -28,6 +29,15 @@ pub mod v3 { } }; + /// Pagination token tuned string type. + pub type BatchToken = SmallString<[u8; 16]>; + + /// Only-filter tuned string type. + pub type OnlyFilter = SmallString<[u8; 24]>; + + /// Only-filter tuned string type. + pub type ProfileTag = SmallString<[u8; 24]>; + /// Request type for the `get_notifications` endpoint. #[request(error = crate::Error)] #[derive(Default)] @@ -35,7 +45,7 @@ pub mod v3 { /// Pagination token given to retrieve the next set of events. #[ruma_api(query)] #[serde(skip_serializing_if = "Option::is_none")] - pub from: Option, + pub from: Option, /// Limit on the number of events to return in this request. #[ruma_api(query)] @@ -48,7 +58,7 @@ pub mod v3 { /// tweak set. #[ruma_api(query)] #[serde(skip_serializing_if = "Option::is_none")] - pub only: Option, + pub only: Option, } /// Response type for the `get_notifications` endpoint. @@ -59,7 +69,7 @@ pub mod v3 { /// /// If this is absent, there are no more results. #[serde(skip_serializing_if = "Option::is_none")] - pub next_token: Option, + pub next_token: Option, /// The list of events that triggered notifications. pub notifications: Vec, @@ -84,14 +94,14 @@ pub mod v3 { #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub struct Notification { /// The actions to perform when the conditions for this rule are met. - pub actions: Vec, + pub actions: Actions, /// The event that triggered the notification. pub event: Raw, /// The profile tag of the rule that matched this event. #[serde(skip_serializing_if = "Option::is_none")] - pub profile_tag: Option, + pub profile_tag: Option, /// Indicates whether the user has sent a read receipt indicating that they have read this /// message. @@ -108,7 +118,7 @@ pub mod v3 { /// Creates a new `Notification` with the given actions, event, read flag, room ID and /// timestamp. pub fn new( - actions: Vec, + actions: Actions, event: Raw, read: bool, room_id: OwnedRoomId, diff --git a/crates/ruma-client-api/src/push/pusher_serde.rs b/crates/ruma-client-api/src/push/pusher_serde.rs index 5b49d1595a..3712fdad8a 100644 --- a/crates/ruma-client-api/src/push/pusher_serde.rs +++ b/crates/ruma-client-api/src/push/pusher_serde.rs @@ -1,17 +1,18 @@ use ruma_common::serde::from_raw_json_value; use serde::{de, ser::SerializeStruct, Deserialize, Serialize}; use serde_json::value::RawValue as RawJsonValue; +use smallstr::SmallString; -use super::{Pusher, PusherIds, PusherKind}; +use super::{DisplayName, Lang, ProfileTag, Pusher, PusherIds, PusherKind}; #[derive(Debug, Deserialize)] struct PusherDeHelper { #[serde(flatten)] ids: PusherIds, - app_display_name: String, - device_display_name: String, - profile_tag: Option, - lang: String, + app_display_name: DisplayName, + device_display_name: DisplayName, + profile_tag: Option, + lang: Lang, } impl<'de> Deserialize<'de> for Pusher { @@ -23,6 +24,7 @@ impl<'de> Deserialize<'de> for Pusher { let PusherDeHelper { ids, app_display_name, device_display_name, profile_tag, lang } = from_raw_json_value(&json)?; + let kind = from_raw_json_value(&json)?; Ok(Self { ids, kind, app_display_name, device_display_name, profile_tag, lang }) @@ -57,10 +59,12 @@ impl Serialize for PusherKind { #[derive(Debug, Deserialize)] struct PusherKindDeHelper { - kind: String, + kind: PushKind, data: Box, } +type PushKind = SmallString<[u8; 16]>; + impl<'de> Deserialize<'de> for PusherKind { fn deserialize(deserializer: D) -> Result where diff --git a/crates/ruma-client-api/src/push/set_pusher/set_pusher_serde.rs b/crates/ruma-client-api/src/push/set_pusher/set_pusher_serde.rs index b09686e059..08517ef0b0 100644 --- a/crates/ruma-client-api/src/push/set_pusher/set_pusher_serde.rs +++ b/crates/ruma-client-api/src/push/set_pusher/set_pusher_serde.rs @@ -81,10 +81,10 @@ mod tests { pusher: Pusher { ids: PusherIds::new("abcdef".to_owned(), "my.matrix.app".to_owned()), kind: PusherKind::Email(EmailPusherData::new()), - app_display_name: "My Matrix App".to_owned(), - device_display_name: "My Phone".to_owned(), + app_display_name: "My Matrix App".into(), + device_display_name: "My Phone".into(), profile_tag: None, - lang: "en".to_owned(), + lang: "en".into(), }, append: false, }); diff --git a/crates/ruma-client-api/src/push/set_pushrule.rs b/crates/ruma-client-api/src/push/set_pushrule.rs index a5e589b927..a4ab3955ae 100644 --- a/crates/ruma-client-api/src/push/set_pushrule.rs +++ b/crates/ruma-client-api/src/push/set_pushrule.rs @@ -10,7 +10,7 @@ pub mod v3 { use ruma_common::{ api::{response, Metadata}, metadata, - push::{Action, NewPushRule, PushCondition}, + push::{Actions, NewPushRule, Pattern, PushConditions}, }; use serde::{Deserialize, Serialize}; @@ -122,7 +122,7 @@ pub mod v3 { S: AsRef, { use ruma_common::push::{ - NewConditionalPushRule, NewPatternedPushRule, NewSimplePushRule, + NewConditionalPushRule, NewPatternedPushRule, NewSimplePushRule, RuleId, }; // Exhaustive enum to fail deserialization on unknown variants. @@ -142,7 +142,7 @@ pub mod v3 { after: Option, } - let (kind, rule_id): (RuleKind, String) = + let (kind, rule_id): (RuleKind, RuleId) = Deserialize::deserialize(serde::de::value::SeqDeserializer::< _, serde::de::value::Error, @@ -169,13 +169,13 @@ pub mod v3 { RuleKind::Sender => { let SimpleRequestBody { actions } = serde_json::from_slice(request.body().as_ref())?; - let rule_id = rule_id.try_into()?; + let rule_id = rule_id.as_str().try_into()?; NewPushRule::Sender(NewSimplePushRule::new(rule_id, actions)) } RuleKind::Room => { let SimpleRequestBody { actions } = serde_json::from_slice(request.body().as_ref())?; - let rule_id = rule_id.try_into()?; + let rule_id = rule_id.as_str().try_into()?; NewPushRule::Room(NewSimplePushRule::new(rule_id, actions)) } RuleKind::Content => { @@ -210,21 +210,21 @@ pub mod v3 { #[derive(Debug, Serialize, Deserialize)] struct SimpleRequestBody { - actions: Vec, + actions: Actions, } #[derive(Debug, Serialize, Deserialize)] struct PatternedRequestBody { - actions: Vec, + actions: Actions, - pattern: String, + pattern: Pattern, } #[derive(Debug, Serialize, Deserialize)] struct ConditionalRequestBody { - actions: Vec, + actions: Actions, - conditions: Vec, + conditions: PushConditions, } impl From for RequestBody { diff --git a/crates/ruma-client-api/src/room.rs b/crates/ruma-client-api/src/room.rs index a1bbd3f1dd..9a4b0b6c10 100644 --- a/crates/ruma-client-api/src/room.rs +++ b/crates/ruma-client-api/src/room.rs @@ -5,6 +5,7 @@ pub mod create_room; pub mod get_event_by_timestamp; pub mod get_room_event; pub mod get_summary; +pub mod initial_sync; pub mod report_content; pub mod report_room; pub mod upgrade_room; diff --git a/crates/ruma-client-api/src/room/create_room.rs b/crates/ruma-client-api/src/room/create_room.rs index fa5c2732e4..65977967d1 100644 --- a/crates/ruma-client-api/src/room/create_room.rs +++ b/crates/ruma-client-api/src/room/create_room.rs @@ -81,6 +81,10 @@ pub mod v3 { #[serde(skip_serializing_if = "Option::is_none")] pub room_alias_name: Option, + /// The desired custom room ID, local part or fully qualified. + #[serde(alias = "fi.mau.room_id", skip_serializing_if = "Option::is_none")] + pub room_id: Option, + /// Room version to set for the room. /// /// Defaults to homeserver's default if not specified. diff --git a/crates/ruma-client-api/src/room/get_room_event.rs b/crates/ruma-client-api/src/room/get_room_event.rs index 638b186725..057e753ed5 100644 --- a/crates/ruma-client-api/src/room/get_room_event.rs +++ b/crates/ruma-client-api/src/room/get_room_event.rs @@ -35,6 +35,12 @@ pub mod v3 { /// The ID of the event. #[ruma_api(path)] pub event_id: OwnedEventId, + + #[cfg(feature = "unstable-msc2815")] + #[ruma_api(query)] + #[serde(default, rename = "fi.mau.msc2815.include_unredacted_content")] + /// Whether respond with the original event, even if it has been redacted. + pub include_unredacted_content: bool, } /// Response type for the `get_room_event` endpoint. @@ -48,7 +54,12 @@ pub mod v3 { impl Request { /// Creates a new `Request` with the given room ID and event ID. pub fn new(room_id: OwnedRoomId, event_id: OwnedEventId) -> Self { - Self { room_id, event_id } + Self { + room_id, + event_id, + #[cfg(feature = "unstable-msc2815")] + include_unredacted_content: false, + } } } diff --git a/crates/ruma-client-api/src/room/get_summary.rs b/crates/ruma-client-api/src/room/get_summary.rs index 065c6d700f..418b9f4601 100644 --- a/crates/ruma-client-api/src/room/get_summary.rs +++ b/crates/ruma-client-api/src/room/get_summary.rs @@ -20,7 +20,7 @@ pub mod v1 { rate_limited: false, authentication: AccessTokenOptional, history: { - unstable => "/_matrix/client/unstable/im.nheko.summary/rooms/{room_id_or_alias}/summary", + unstable => "/_matrix/client/unstable/im.nheko.summary/summary/{room_id_or_alias}", 1.15 => "/_matrix/client/v1/room_summary/{room_id_or_alias}", } }; diff --git a/crates/ruma-client-api/src/room/initial_sync.rs b/crates/ruma-client-api/src/room/initial_sync.rs new file mode 100644 index 0000000000..00970abe04 --- /dev/null +++ b/crates/ruma-client-api/src/room/initial_sync.rs @@ -0,0 +1,95 @@ +//! `GET /_matrix/client/*/rooms/{roomId}/initialSync` +//! +//! DEPRECATED + +pub mod v3 { + //! `/v3/` ([spec]) + //! + //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3roomsroomidinitialsync + + use ruma_common::{ + api::{request, response, Metadata}, + metadata, + serde::Raw, + OwnedRoomId, + }; + use ruma_events::{ + room::member::MembershipState, AnyRoomAccountDataEvent, AnyStateEvent, AnyTimelineEvent, + }; + use serde::{Deserialize, Serialize}; + + use crate::room::Visibility; + + const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + 1.0 => "/_matrix/client/r0/rooms/{room_id}/initialSync", + 1.1 => "/_matrix/client/v3/rooms/{room_id}/initialSync", + } + }; + + /// Request type for the `get_room_event` endpoint. + #[request(error = crate::Error)] + pub struct Request { + /// The ID of the room. + #[ruma_api(path)] + pub room_id: OwnedRoomId, + + /// Limit messages chunks size + #[ruma_api(query)] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + } + + /// Response type for the `get_room_event` endpoint. + #[response(error = crate::Error)] + pub struct Response { + /// The private data that this user has attached to this room. + #[serde(skip_serializing_if = "Option::is_none")] + pub account_data: Option>>, + + /// The user’s membership state in this room. One of: [invite, join, leave, ban]. + #[serde(skip_serializing_if = "Option::is_none")] + pub membership: Option, + + /// The pagination chunk for this room. + #[serde(skip_serializing_if = "Option::is_none")] + pub messages: Option, + + /// The ID of this room. + pub room_id: OwnedRoomId, + + /// If the user is a member of the room this will be the current state of the room as a + /// list of events. If the user has left the room this will be the state of the room when + /// they left it. + #[serde(skip_serializing_if = "Option::is_none")] + pub state: Option>>, + + /// Whether this room is visible to the /publicRooms API or not. + /// One of: [private, public]. + #[serde(skip_serializing_if = "Option::is_none")] + pub visibility: Option, + } + + /// Page of timeline events + #[derive(Clone, Debug, Default, Deserialize, Serialize)] + #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] + pub struct PaginationChunk { + /// If the user is a member of the room this will be a list of the most recent messages + /// for this room. If the user has left the room this will be the messages that preceded + /// them leaving. This array will consist of at most limit elements. + pub chunk: Vec>, + + /// A token which correlates to the end of chunk. Can be passed to + /// /rooms//messages to retrieve later events. + pub end: String, + + /// A token which correlates to the start of chunk. Can be passed to + /// /rooms//messages to retrieve earlier events. If no earlier events are + /// available, this property may be omitted from the response. + #[serde(skip_serializing_if = "Option::is_none")] + pub start: Option, + } +} diff --git a/crates/ruma-client-api/src/room/report_room.rs b/crates/ruma-client-api/src/room/report_room.rs index 824754668b..b612841604 100644 --- a/crates/ruma-client-api/src/room/report_room.rs +++ b/crates/ruma-client-api/src/room/report_room.rs @@ -18,6 +18,7 @@ pub mod v3 { authentication: AccessToken, history: { unstable => "/_matrix/client/unstable/org.matrix.msc4151/rooms/{room_id}/report", + 1.0 => "/_matrix/client/r0/rooms/{room_id}/report", 1.13 => "/_matrix/client/v3/rooms/{room_id}/report", } }; diff --git a/crates/ruma-client-api/src/rtc.rs b/crates/ruma-client-api/src/rtc.rs new file mode 100644 index 0000000000..ab7f192894 --- /dev/null +++ b/crates/ruma-client-api/src/rtc.rs @@ -0,0 +1,169 @@ +//! [MatrixRTC] endpoints. +//! +//! [MatrixRTC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4143 + +use std::borrow::Cow; + +use ruma_common::serde::JsonObject; +#[cfg(feature = "unstable-msc4195")] +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; + +pub mod transports; + +/// Information about a specific MatrixRTC focus. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] +#[serde(tag = "type")] +pub enum RtcTransport { + /// A LiveKit RTC focus. + #[cfg(feature = "unstable-msc4195")] + #[serde(rename = "livekit")] + LiveKit(LiveKitRtcTransport), + + /// A custom RTC focus. + #[doc(hidden)] + #[serde(untagged)] + _Custom(CustomRtcTransport), +} + +impl RtcTransport { + /// A constructor to create a custom RTC focus. + /// + /// Prefer to use the public variants of `RtcFocusInfo` where possible; this constructor is + /// meant to be used for unsupported focus types only and does not allow setting arbitrary data + /// for supported ones. + /// + /// # Errors + /// + /// Returns an error if the `focus_type` is known and serialization of `data` to the + /// corresponding `RtcFocusInfo` variant fails. + pub fn new(transport_type: &str, data: JsonObject) -> serde_json::Result { + #[cfg(feature = "unstable-msc4195")] + fn deserialize_variant(obj: JsonObject) -> serde_json::Result { + use serde_json::Value; + + serde_json::from_value(Value::Object(obj)) + } + + Ok(match transport_type { + #[cfg(feature = "unstable-msc4195")] + "livekit" => Self::LiveKit(deserialize_variant(data)?), + _ => Self::_Custom(CustomRtcTransport { + transport_type: transport_type.to_owned(), + data, + }), + }) + } + + #[cfg(feature = "unstable-msc4195")] + /// Creates a new `RtcTransportInfo::LiveKit`. + pub fn livekit(service_url: String) -> Self { + Self::LiveKit(LiveKitRtcTransport { service_url }) + } + + /// Returns a reference to the transport type of this RTC transport. + pub fn transport_type(&self) -> &str { + match self { + #[cfg(feature = "unstable-msc4195")] + Self::LiveKit(_) => "livekit", + Self::_Custom(custom) => &custom.transport_type, + } + } + + /// Returns the associated data. + /// + /// The returned JSON object won't contain the `focus_type` field, please use + /// [`.focus_type()`][Self::focus_type] to access that. + /// + /// Prefer to use the public variants of `RtcFocusInfo` where possible; this method is meant to + /// be used for custom focus types only. + pub fn data(&self) -> Cow<'_, JsonObject> { + #[cfg(feature = "unstable-msc4195")] + fn serialize(object: &T) -> JsonObject { + use serde_json::Value; + + match serde_json::to_value(object).expect("rtc focus type serialization to succeed") { + Value::Object(object) => object, + _ => panic!("all rtc focus types must serialize to objects"), + } + } + + match self { + #[cfg(feature = "unstable-msc4195")] + Self::LiveKit(info) => Cow::Owned(serialize(info)), + Self::_Custom(info) => Cow::Borrowed(&info.data), + } + } +} + +/// Information about a LiveKit RTC transport. +#[cfg(feature = "unstable-msc4195")] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] +pub struct LiveKitRtcTransport { + /// The URL for the LiveKit service. + #[serde(rename = "livekit_service_url")] + pub service_url: String, +} + +/// Information about a custom RTC transport. +#[doc(hidden)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct CustomRtcTransport { + /// The type of RTC focus. + #[serde(rename = "type")] + transport_type: String, + + /// Remaining RTC focus data. + #[serde(flatten)] + data: JsonObject, +} + +#[cfg(test)] +mod tests { + use assert_matches2::assert_matches; + use serde_json::{ + from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue, + }; + + use super::RtcTransport; + + #[test] + fn serialize_roundtrip_custom_rtc_transport() { + let transport_type = "local.custom.transport"; + assert_matches!( + json!({ + "foo": "bar", + "baz": true, + }), + JsonValue::Object(transport_data) + ); + let transport = RtcTransport::new(transport_type, transport_data.clone()).unwrap(); + let json = json!({ + "type": transport_type, + "foo": "bar", + "baz": true, + }); + + assert_eq!(transport.transport_type(), transport_type); + assert_eq!(*transport.data().as_ref(), transport_data); + assert_eq!(to_json_value(&transport).unwrap(), json); + assert_eq!(from_json_value::(json).unwrap(), transport); + } + + #[test] + fn serialize_roundtrip_livekit_sfu_transport() { + let transport_type = "livekit"; + let livekit_service_url = "http://livekit.local/"; + let transport = RtcTransport::livekit(livekit_service_url.to_owned()); + let json = json!({ + "type": transport_type, + "livekit_service_url": livekit_service_url, + }); + + assert_eq!(transport.transport_type(), transport_type); + assert_eq!(to_json_value(&transport).unwrap(), json); + assert_eq!(from_json_value::(json).unwrap(), transport); + } +} diff --git a/crates/ruma-client-api/src/rtc/transports.rs b/crates/ruma-client-api/src/rtc/transports.rs new file mode 100644 index 0000000000..251463bd86 --- /dev/null +++ b/crates/ruma-client-api/src/rtc/transports.rs @@ -0,0 +1,52 @@ +//! `GET /_matrix/client/*/rtc/transports` +//! +//! Discover the RTC transports advertised by the homeserver. + +pub mod v1 { + //! `/v1/` ([MSC]) + //! + //! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4143 + + use ruma_common::{ + api::{request, response, Metadata}, + metadata, + }; + + use crate::rtc::RtcTransport; + + const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/unstable/org.matrix.msc4143/rtc/transports", + } + }; + + /// Request type for the `transports` endpoint. + #[request(error = crate::Error)] + #[derive(Default)] + pub struct Request {} + + impl Request { + /// Creates a new empty `Request`. + pub fn new() -> Self { + Self {} + } + } + + /// Response type for the `transports` endpoint. + #[response(error = crate::Error)] + #[derive(Default)] + pub struct Response { + /// The RTC transports advertised by the homeserver. + pub rtc_transports: Vec, + } + + impl Response { + /// Creates a `Response` with the given RTC transports. + pub fn new(rtc_transports: Vec) -> Self { + Self { rtc_transports } + } + } +} diff --git a/crates/ruma-client-api/src/session.rs b/crates/ruma-client-api/src/session.rs index 152231da12..3964685c7a 100644 --- a/crates/ruma-client-api/src/session.rs +++ b/crates/ruma-client-api/src/session.rs @@ -13,6 +13,7 @@ pub mod login_fallback; pub mod logout; pub mod logout_all; pub mod refresh_token; +pub mod sso_callback; pub mod sso_login; pub mod sso_login_with_provider; diff --git a/crates/ruma-client-api/src/session/get_login_types.rs b/crates/ruma-client-api/src/session/get_login_types.rs index 6cd2fe2d4d..ee88cd8b72 100644 --- a/crates/ruma-client-api/src/session/get_login_types.rs +++ b/crates/ruma-client-api/src/session/get_login_types.rs @@ -68,6 +68,9 @@ pub mod v3 { /// Token-based login. Token(TokenLoginType), + /// JSON Web Token type. + Jwt(JwtLoginType), + /// SSO-based login. Sso(SsoLoginType), @@ -93,6 +96,7 @@ pub mod v3 { Ok(match login_type { "m.login.password" => Self::Password(from_json_object(data)?), "m.login.token" => Self::Token(from_json_object(data)?), + "org.matrix.login.jwt" => Self::Jwt(from_json_object(data)?), "m.login.sso" => Self::Sso(from_json_object(data)?), "m.login.application_service" => Self::ApplicationService(from_json_object(data)?), _ => { @@ -106,6 +110,7 @@ pub mod v3 { match self { Self::Password(_) => "m.login.password", Self::Token(_) => "m.login.token", + Self::Jwt(_) => "org.matrix.login.jwt", Self::Sso(_) => "m.login.sso", Self::ApplicationService(_) => "m.login.application_service", Self::_Custom(c) => &c.type_, @@ -127,6 +132,7 @@ pub mod v3 { match self { Self::Password(d) => Cow::Owned(serialize(d)), Self::Token(d) => Cow::Owned(serialize(d)), + Self::Jwt(d) => Cow::Owned(serialize(d)), Self::Sso(d) => Cow::Owned(serialize(d)), Self::ApplicationService(d) => Cow::Owned(serialize(d)), Self::_Custom(c) => Cow::Borrowed(&c.data), @@ -164,6 +170,19 @@ pub mod v3 { } } + /// The payload for JWT-based login. + #[derive(Clone, Debug, Default, Deserialize, Serialize)] + #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] + #[serde(tag = "type", rename = "org.matrix.login.jwt")] + pub struct JwtLoginType {} + + impl JwtLoginType { + /// Creates a new `JwtLoginType`. + pub fn new() -> Self { + Self {} + } + } + /// The payload for SSO login. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] @@ -225,7 +244,7 @@ pub mod v3 { /// /// [matrix-spec-proposals]: https://github.com/matrix-org/matrix-spec-proposals/blob/v1.1/informal/idp-brands.md #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] - #[derive(Clone, PartialEq, Eq, StringEnum)] + #[derive(Clone, PartialEq, Eq, PartialOrd, StringEnum)] #[ruma_enum(rename_all = "lowercase")] #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub enum IdentityProviderBrand { @@ -312,6 +331,7 @@ pub mod v3 { Ok(match type_.as_ref() { "m.login.password" => Self::Password(from_raw_json_value(&json)?), "m.login.token" => Self::Token(from_raw_json_value(&json)?), + "org.matrix.login.jwt" => Self::Jwt(from_raw_json_value(&json)?), "m.login.sso" => Self::Sso(from_raw_json_value(&json)?), "m.login.application_service" => { Self::ApplicationService(from_raw_json_value(&json)?) diff --git a/crates/ruma-client-api/src/session/login.rs b/crates/ruma-client-api/src/session/login.rs index 5585705fb3..425725afc7 100644 --- a/crates/ruma-client-api/src/session/login.rs +++ b/crates/ruma-client-api/src/session/login.rs @@ -150,6 +150,9 @@ pub mod v3 { /// Token-based login. Token(Token), + /// JSON Web Token + Jwt(Token), + /// Application Service-specific login. ApplicationService(ApplicationService), @@ -174,6 +177,9 @@ pub mod v3 { Self::Password(serde_json::from_value(JsonValue::Object(data))?) } "m.login.token" => Self::Token(serde_json::from_value(JsonValue::Object(data))?), + "org.matrix.login.jwt" => { + Self::Jwt(serde_json::from_value(JsonValue::Object(data))?) + } "m.login.application_service" => { Self::ApplicationService(serde_json::from_value(JsonValue::Object(data))?) } @@ -188,6 +194,7 @@ pub mod v3 { match self { Self::Password(inner) => inner.fmt(f), Self::Token(inner) => inner.fmt(f), + Self::Jwt(inner) => inner.fmt(f), Self::ApplicationService(inner) => inner.fmt(f), Self::_Custom(inner) => inner.fmt(f), } @@ -212,6 +219,7 @@ pub mod v3 { match login_type { "m.login.password" => from_json_value(json).map(Self::Password), "m.login.token" => from_json_value(json).map(Self::Token), + "org.matrix.login.jwt" => from_json_value(json).map(Self::Jwt), "m.login.application_service" => { from_json_value(json).map(Self::ApplicationService) } @@ -354,7 +362,7 @@ pub mod v3 { pub homeserver: HomeserverInfo, /// Information about the identity server to connect to. - #[serde(rename = "m.identity_server")] + #[serde(rename = "m.identity_server", skip_serializing_if = "Option::is_none")] pub identity_server: Option, } @@ -385,6 +393,7 @@ pub mod v3 { #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub struct IdentityServerInfo { /// The base URL for the identity server for client-server connections. + #[serde(default)] pub base_url: String, } diff --git a/crates/ruma-client-api/src/session/sso_callback.rs b/crates/ruma-client-api/src/session/sso_callback.rs new file mode 100644 index 0000000000..4b50b647b6 --- /dev/null +++ b/crates/ruma-client-api/src/session/sso_callback.rs @@ -0,0 +1,54 @@ +//! `GET /_matrix/client/unstable/login/sso/callback + +pub mod unstable { + //! `/unstable/` ([spec]) + //! + //! [spec]: none + + use std::borrow::Cow; + + use http::header::{LOCATION, SET_COOKIE}; + use ruma_common::{ + api::{request, response, Metadata}, + metadata, + }; + + const METADATA: Metadata = metadata! { + method: GET, + rate_limited: false, + authentication: None, + history: { + unstable => "/_matrix/client/unstable/login/sso/callback/{idp_id}", + } + }; + + /// Request type for the `sso_callback` endpoint. + #[request(error = crate::Error)] + pub struct Request { + /// Identity Provider ID + #[ruma_api(path)] + pub idp_id: String, + + /// Callback code + #[ruma_api(query)] + #[serde(default)] + pub code: Option, + + /// Callback state + #[ruma_api(query)] + #[serde(default)] + pub state: Option, + } + + /// Response type for the `sso_callback` endpoint. + #[response(error = crate::Error, status = FOUND)] + pub struct Response { + /// Redirect URL to the SSO identity provider. + #[ruma_api(header = LOCATION)] + pub location: String, + + /// Cookie storing state to secure the SSO process. + #[ruma_api(header = SET_COOKIE)] + pub cookie: Option>, + } +} diff --git a/crates/ruma-client-api/src/session/sso_login.rs b/crates/ruma-client-api/src/session/sso_login.rs index 2047831108..f6d1ebf64f 100644 --- a/crates/ruma-client-api/src/session/sso_login.rs +++ b/crates/ruma-client-api/src/session/sso_login.rs @@ -5,6 +5,8 @@ pub mod v3 { //! //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3loginssoredirect + use std::borrow::Cow; + use http::header::{LOCATION, SET_COOKIE}; use ruma_common::{ api::{request, response, Metadata}, @@ -53,7 +55,7 @@ pub mod v3 { /// Cookie storing state to secure the SSO process. #[ruma_api(header = SET_COOKIE)] - pub cookie: Option, + pub cookie: Option>, } impl Request { diff --git a/crates/ruma-client-api/src/session/sso_login_with_provider.rs b/crates/ruma-client-api/src/session/sso_login_with_provider.rs index 07b363f0d7..8d85b31beb 100644 --- a/crates/ruma-client-api/src/session/sso_login_with_provider.rs +++ b/crates/ruma-client-api/src/session/sso_login_with_provider.rs @@ -7,6 +7,8 @@ pub mod v3 { //! //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3loginssoredirectidpid + use std::borrow::Cow; + use http::header::{LOCATION, SET_COOKIE}; use ruma_common::{ api::{request, response, Metadata}, @@ -21,8 +23,8 @@ pub mod v3 { rate_limited: false, authentication: None, history: { - unstable => "/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect/{idp_id}", - 1.1 => "/_matrix/client/v3/login/sso/redirect/{idp_id}", + 1.0 => "/_matrix/client/r0/login/sso/redirect/{idp_id}", + 1.1 => "/_matrix/client/v3/login/sso/redirect/{idp_id}", } }; @@ -48,6 +50,12 @@ pub mod v3 { #[ruma_api(query)] #[serde(skip_serializing_if = "Option::is_none", rename = "org.matrix.msc3824.action")] pub action: Option, + + /// Login token which we use to resolve to an existing user_id. This is used as a method + /// for existing-account association. + #[ruma_api(query)] + #[serde(rename = "loginToken")] + pub login_token: Option, } /// Response type for the `sso_login_with_provider` endpoint. @@ -59,7 +67,7 @@ pub mod v3 { /// Cookie storing state to secure the SSO process. #[ruma_api(header = SET_COOKIE)] - pub cookie: Option, + pub cookie: Option>, } impl Request { @@ -70,6 +78,7 @@ pub mod v3 { redirect_url, #[cfg(feature = "unstable-msc3824")] action: None, + login_token: None, } } } diff --git a/crates/ruma-client-api/src/state/get_state_event_for_key.rs b/crates/ruma-client-api/src/state/get_state_event_for_key.rs index 14e12a76d2..a073cb38bf 100644 --- a/crates/ruma-client-api/src/state/get_state_event_for_key.rs +++ b/crates/ruma-client-api/src/state/get_state_event_for_key.rs @@ -9,12 +9,9 @@ pub mod v3 { use ruma_common::{ api::{response, Metadata}, - metadata, - serde::Raw, - OwnedRoomId, + metadata, OwnedRoomId, }; - use ruma_events::{AnyStateEvent, AnyStateEventContent, StateEventType}; - use serde_json::value::RawValue as RawJsonValue; + use ruma_events::StateEventType; const METADATA: Metadata = metadata! { method: GET, @@ -39,83 +36,40 @@ pub mod v3 { /// The key of the state to look up. pub state_key: String, - /// The format to use for the returned data. - pub format: StateEventFormat, + /// Optional parameter to return the event content + /// or the full state event. + pub format: Option, } impl Request { /// Creates a new `Request` with the given room ID, event type and state key. pub fn new(room_id: OwnedRoomId, event_type: StateEventType, state_key: String) -> Self { - Self { room_id, event_type, state_key, format: StateEventFormat::default() } + Self { room_id, event_type, state_key, format: None } } } - /// The format to use for the returned data. - #[cfg_attr(feature = "client", derive(serde::Serialize))] - #[cfg_attr(feature = "server", derive(serde::Deserialize))] - #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] - #[derive(Default, Debug, PartialEq, Clone, Copy)] - #[serde(rename_all = "lowercase")] - pub enum StateEventFormat { - /// Will return only the content of the state event. - /// - /// This is the default value if the format is unspecified in the request. - #[default] - Content, - - /// Will return the entire event in the usual format suitable for clients, including fields - /// like event ID, sender and timestamp. - Event, - } - - /// Response type for the `get_state_events_for_key` endpoint, either the `Raw` `AnyStateEvent` - /// or `AnyStateEventContent`. - /// - /// While it's possible to access the raw value directly, it's recommended you use the - /// provided helper methods to access it, and `From` to create it. + /// Response type for the `get_state_events_for_key` endpoint. #[response(error = crate::Error)] pub struct Response { - /// The full event (content) of the state event. - #[ruma_api(body)] - pub event_or_content: Box, - } - - impl From> for Response { - fn from(value: Raw) -> Self { - Self { event_or_content: value.into_json() } - } - } + /// The content of the state event. + /// + /// This is `serde_json::Value` due to complexity issues with returning only the + /// actual JSON content without a top level key. + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub content: Option, - impl From> for Response { - fn from(value: Raw) -> Self { - Self { event_or_content: value.into_json() } - } + /// The full state event + /// + /// This is `serde_json::Value` due to complexity issues with returning only the + /// actual JSON content without a top level key. + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub event: Option, } impl Response { - /// Creates a new `Response` with the given event (content). - pub fn new(event_or_content: Box) -> Self { - Self { event_or_content } - } - - /// Returns an unchecked `Raw`. - /// - /// This method should only be used if you specified the `format` in the request to be - /// `StateEventFormat::Event` - pub fn into_event(self) -> Raw { - Raw::from_json(self.event_or_content) - } - - /// Returns an unchecked `Raw`. - /// - /// This method should only be used if you did not specify the `format` in the request, or - /// set it to be `StateEventFormat::Content` - /// - /// Since the inner type of the `Raw` does not implement `Deserialize`, you need to use - /// `.deserialize_as_unchecked::()` or - /// `.cast_ref_unchecked::().deserialize_with_type()` to deserialize it. - pub fn into_content(self) -> Raw { - Raw::from_json(self.event_or_content) + /// Creates a new `Response` with the given content. + pub fn new(content: serde_json::Value, event: serde_json::Value) -> Self { + Self { content: Some(content), event: Some(event) } } } @@ -196,10 +150,10 @@ pub mod v3 { (a, b, "".into()) }; - let RequestQuery { format } = + let request_query: RequestQuery = serde_html_form::from_str(request.uri().query().unwrap_or(""))?; - Ok(Self { room_id, event_type, state_key, format }) + Ok(Self { room_id, event_type, state_key, format: request_query.format }) } } @@ -208,31 +162,9 @@ pub mod v3 { #[cfg_attr(feature = "client", derive(serde::Serialize))] #[cfg_attr(feature = "server", derive(serde::Deserialize))] struct RequestQuery { - /// Timestamp to use for the `origin_server_ts` of the event. - #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] - format: StateEventFormat, - } -} - -#[cfg(all(test, feature = "client"))] -mod tests { - use ruma_common::api::IncomingResponse; - use ruma_events::room::name::RoomNameEventContent; - use serde_json::{json, to_vec as to_json_vec}; - - use super::v3::Response; - - #[test] - fn deserialize_response() { - let body = json!({ - "name": "Nice room 🙂" - }); - let response = http::Response::new(to_json_vec(&body).unwrap()); - - let response = Response::try_from_http_response(response).unwrap(); - let content = - response.into_content().deserialize_as_unchecked::().unwrap(); - - assert_eq!(&content.name, "Nice room 🙂"); + /// Optional parameter to return the event content + /// or the full state event. + #[serde(skip_serializing_if = "Option::is_none")] + format: Option, } } diff --git a/crates/ruma-client-api/src/sync/sync_events/v3.rs b/crates/ruma-client-api/src/sync/sync_events/v3.rs index 6a11e75691..a0a85d1e94 100644 --- a/crates/ruma-client-api/src/sync/sync_events/v3.rs +++ b/crates/ruma-client-api/src/sync/sync_events/v3.rs @@ -19,6 +19,7 @@ use ruma_events::{ AnyToDeviceEvent, }; use serde::{Deserialize, Serialize}; +use smallstr::SmallString; mod response_serde; @@ -35,6 +36,9 @@ const METADATA: Metadata = metadata! { } }; +/// Since string type for batch tokens. +pub type Since = SmallString<[u8; 16]>; + /// Request type for the `sync` endpoint. #[request(error = crate::Error)] #[derive(Default)] @@ -50,7 +54,7 @@ pub struct Request { /// request. #[serde(skip_serializing_if = "Option::is_none")] #[ruma_api(query)] - pub since: Option, + pub since: Option, /// Controls whether to include the full state for all rooms the user is a member of. #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] @@ -89,7 +93,7 @@ pub struct Request { #[response(error = crate::Error)] pub struct Response { /// The batch token to supply in the `since` param of the next `/sync` request. - pub next_batch: String, + pub next_batch: Since, /// Updates to rooms. #[serde(default, skip_serializing_if = "Rooms::is_empty")] @@ -135,7 +139,7 @@ impl Request { impl Response { /// Creates a new `Response` with the given batch token. - pub fn new(next_batch: String) -> Self { + pub fn new(next_batch: Since) -> Self { Self { next_batch, rooms: Default::default(), @@ -756,8 +760,8 @@ mod client_tests { features: Default::default(), }; let req: http::Request> = Request { - filter: Some(Filter::FilterId("66696p746572".to_owned())), - since: Some("s72594_4483_1934".to_owned()), + filter: Some(Filter::FilterId("66696p746572".into())), + since: Some("s72594_4483_1934".into()), full_state: true, set_presence: PresenceState::Offline, timeout: Some(Duration::from_millis(30000)), @@ -1132,7 +1136,7 @@ mod server_tests { let left_room_id = owned_room_id!("!left:localhost"); let event = sync_state_event(); - let mut response = Response::new("aaa".to_owned()); + let mut response = Response::new("aaa".into()); let mut joined_room = JoinedRoom::new(); joined_room.timeline.events.push(event.clone().cast()); @@ -1178,7 +1182,7 @@ mod server_tests { let left_room_id = owned_room_id!("!left:localhost"); let event = sync_state_event(); - let mut response = Response::new("aaa".to_owned()); + let mut response = Response::new("aaa".into()); let mut joined_room = JoinedRoom::new(); joined_room.state = State::Before(vec![event.clone()].into()); @@ -1224,7 +1228,7 @@ mod server_tests { let joined_room_id = owned_room_id!("!joined:localhost"); let left_room_id = owned_room_id!("!left:localhost"); - let mut response = Response::new("aaa".to_owned()); + let mut response = Response::new("aaa".into()); let mut joined_room = JoinedRoom::new(); joined_room.state = State::After(Default::default()); @@ -1263,7 +1267,7 @@ mod server_tests { let left_room_id = owned_room_id!("!left:localhost"); let event = sync_state_event(); - let mut response = Response::new("aaa".to_owned()); + let mut response = Response::new("aaa".into()); let mut joined_room = JoinedRoom::new(); joined_room.state = State::After(vec![event.clone()].into()); diff --git a/crates/ruma-client-api/src/sync/sync_events/v5.rs b/crates/ruma-client-api/src/sync/sync_events/v5.rs index 1c1d458838..32271f2ba4 100644 --- a/crates/ruma-client-api/src/sync/sync_events/v5.rs +++ b/crates/ruma-client-api/src/sync/sync_events/v5.rs @@ -20,6 +20,8 @@ use ruma_common::{ }; use ruma_events::{AnySyncStateEvent, AnySyncTimelineEvent, StateEventType}; use serde::{Deserialize, Serialize}; +use smallstr::SmallString; +use smallvec::SmallVec; use super::UnreadNotificationsCount; @@ -33,6 +35,30 @@ const METADATA: Metadata = metadata! { } }; +/// Connection ID string type for connections. +pub type ConnId = SmallString<[u8; 16]>; + +/// List ID string type for list names. +pub type ListId = SmallString<[u8; 16]>; + +/// Transaction ID string type used by `Request::txn_id`. +pub type TxnId = SmallString<[u8; 32]>; + +/// Since string type for batch tokens i.e. `pos`. +pub type Since = SmallString<[u8; 16]>; + +/// Opinionated room or hero name. +pub type DisplayName = SmallString<[u8; 32]>; + +/// Opinionated vector of ListId's. +pub type ListIds = SmallVec<[ListId; 1]>; + +/// Opinionated vector of Ranges. +pub type Ranges = SmallVec<[Range; 1]>; + +/// Window range. +pub type Range = (UInt, UInt); + /// Request type for the `/sync` endpoint. #[request(error = crate::Error)] #[derive(Default)] @@ -44,7 +70,7 @@ pub struct Request { /// it can be costly) #[serde(skip_serializing_if = "Option::is_none")] #[ruma_api(query)] - pub pos: Option, + pub pos: Option, /// A unique string identifier for this connection to the server. /// @@ -57,12 +83,12 @@ pub struct Request { /// Limitation: it must not contain more than 16 chars, due to it being /// required with every request. #[serde(skip_serializing_if = "Option::is_none")] - pub conn_id: Option, + pub conn_id: Option, /// Allows clients to know what request params reached the server, /// functionally similar to txn IDs on `/send` for events. #[serde(skip_serializing_if = "Option::is_none")] - pub txn_id: Option, + pub txn_id: Option, /// The maximum time to poll before responding to this request. /// @@ -75,19 +101,18 @@ pub struct Request { /// /// Defaults to `PresenceState::Online`. #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] - #[ruma_api(query)] pub set_presence: PresenceState, /// Lists of rooms we are interested by, represented by ranges. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub lists: BTreeMap, + pub lists: BTreeMap, /// Specific rooms we are interested by. /// /// It is useful to receive updates from rooms that are possibly /// out-of-range of all the lists (see [`Self::lists`]). #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub room_subscriptions: BTreeMap, + pub room_subscriptions: BTreeMap, /// Extensions. #[serde(default, skip_serializing_if = "request::Extensions::is_empty")] @@ -104,20 +129,23 @@ impl Request { /// HTTP types related to a [`Request`]. pub mod request { use ruma_common::{directory::RoomTypeFilter, serde::deserialize_cow_str, RoomId}; + use ruma_events::{tag::TagName, StateKey}; use serde::de::Error as _; - use super::{BTreeMap, Deserialize, OwnedRoomId, Serialize, StateEventType, UInt}; + use super::{ + BTreeMap, Deserialize, ListIds, OwnedRoomId, Ranges, Serialize, Since, StateEventType, UInt, + }; /// A sliding sync list request (see [`super::Request::lists`]). #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub struct List { /// The ranges of rooms we're interested in. - pub ranges: Vec<(UInt, UInt)>, + pub ranges: Ranges, /// The details to be included per room. #[serde(flatten)] - pub room_details: RoomDetails, + pub room_details: ListConfig, /// Filters to apply to the list before sorting. #[serde(skip_serializing_if = "Option::is_none")] @@ -158,28 +186,36 @@ pub mod request { /// `not_room_types` wins and the corresponding rooms are not included. #[serde(default, skip_serializing_if = "<[_]>::is_empty")] pub not_room_types: Vec, - } - /// Sliding sync request room subscription (see [`super::Request::room_subscriptions`]). - #[derive(Clone, Debug, Default, Serialize, Deserialize)] - #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] - pub struct RoomSubscription { - /// Required state for each returned room. An array of event type and - /// state key tuples. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub required_state: Vec<(StateEventType, String)>, + /// Filter a room based on its room tags. + /// + /// If multiple tags are present, a room can match any of the tags. + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub tags: Vec, - /// The maximum number of timeline events to return per room. - pub timeline_limit: UInt, + /// Filter a room based on its room tags. + /// + /// Takes priority over `tags`. If a tag is listed in both `tags` and `not_tags`, + /// `not_tags` wins and the corresponding rooms are not included. + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub not_tags: Vec, + + /// Filter a room based on the space it belongs to according to m.space.child events. + /// + /// If multiple spaces are present, a room can be part of any one of the listed spaces. The + /// server will not navigate subspaces. The client must give a complete list of spaces to + /// navigate. + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub spaces: Vec, } - /// Sliding sync request room details (see [`List::room_details`]). + /// Sliding sync request room config (see [`List::room_details`]). #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] - pub struct RoomDetails { + pub struct ListConfig { /// Required state for each returned room. An array of event type and state key tuples. #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub required_state: Vec<(StateEventType, String)>, + pub required_state: Vec<(StateEventType, StateKey)>, /// The maximum number of timeline events to return per room. pub timeline_limit: UInt, @@ -286,7 +322,7 @@ pub mod request { /// Give messages since this token only. #[serde(skip_serializing_if = "Option::is_none")] - pub since: Option, + pub since: Option, } impl ToDevice { @@ -332,7 +368,7 @@ pub mod request { /// If not defined, will be enabled for *all* the lists appearing in the /// request. If defined and empty, will be disabled for all the lists. #[serde(skip_serializing_if = "Option::is_none")] - pub lists: Option>, + pub lists: Option, /// List of room names for which account data should be enabled. /// @@ -367,7 +403,7 @@ pub mod request { /// If not defined, will be enabled for *all* the lists appearing in the /// request. If defined and empty, will be disabled for all the lists. #[serde(skip_serializing_if = "Option::is_none")] - pub lists: Option>, + pub lists: Option, /// List of room names for which receipts should be enabled. /// @@ -401,7 +437,7 @@ pub mod request { /// If not defined, will be enabled for *all* the lists appearing in the /// request. If defined and empty, will be disabled for all the lists. #[serde(skip_serializing_if = "Option::is_none")] - pub lists: Option>, + pub lists: Option, /// List of room names for which typing notifications should be enabled. /// @@ -452,15 +488,15 @@ pub mod request { pub struct Response { /// Matches the `txn_id` sent by the request (see [`Request::txn_id`]). #[serde(skip_serializing_if = "Option::is_none")] - pub txn_id: Option, + pub txn_id: Option, /// The token to supply in the `pos` parameter of the next `/sync` request /// (see [`Request::pos`]). - pub pos: String, + pub pos: Since, /// Resulting details of the lists. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub lists: BTreeMap, + pub lists: BTreeMap, /// The updated rooms. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] @@ -473,7 +509,7 @@ pub struct Response { impl Response { /// Creates a new `Response` with the given `pos`. - pub fn new(pos: String) -> Self { + pub fn new(pos: Since) -> Self { Self { txn_id: None, pos, @@ -490,20 +526,27 @@ pub mod response { #[cfg(feature = "unstable-msc4308")] use ruma_common::OwnedEventId; use ruma_events::{ - receipt::SyncReceiptEvent, typing::SyncTypingEvent, AnyGlobalAccountDataEvent, - AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnyToDeviceEvent, + receipt::SyncReceiptEvent, room::member::MembershipState, typing::SyncTypingEvent, + AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent, + AnyToDeviceEvent, }; use super::{ super::DeviceLists, AnySyncStateEvent, AnySyncTimelineEvent, BTreeMap, Deserialize, - JsOption, OwnedMxcUri, OwnedRoomId, OwnedUserId, Raw, Serialize, UInt, - UnreadNotificationsCount, + DisplayName, JsOption, ListIds, OwnedMxcUri, OwnedRoomId, OwnedUserId, Raw, Serialize, + Since, SmallVec, UInt, UnreadNotificationsCount, }; #[cfg(feature = "unstable-msc4308")] use crate::threads::get_thread_subscriptions_changes::unstable::{ ThreadSubscription, ThreadUnsubscription, }; + /// Optimistic vector of heroes. + pub type Heroes = SmallVec<[Hero; 2]>; + + /// Optimistic vector of timeline events. + pub type Timeline = SmallVec<[Raw; 1]>; + /// A sliding sync response updates to joiend rooms (see /// [`super::Response::lists`]). #[derive(Clone, Debug, Default, Deserialize, Serialize)] @@ -519,7 +562,7 @@ pub mod response { pub struct Room { /// The name as calculated by the server. #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, /// The avatar. #[serde(default, skip_serializing_if = "JsOption::is_undefined")] @@ -543,8 +586,8 @@ pub mod response { pub unread_notifications: UnreadNotificationsCount, /// Message-like events and live state events. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub timeline: Vec>, + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub timeline: Timeline, /// State events as configured by the request. #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -553,7 +596,7 @@ pub mod response { /// The `prev_batch` allowing you to paginate through the messages /// before the given ones. #[serde(skip_serializing_if = "Option::is_none")] - pub prev_batch: Option, + pub prev_batch: Option, /// True if the number of events returned was limited by the limit on /// the filter. @@ -585,7 +628,16 @@ pub mod response { /// Heroes of the room. #[serde(skip_serializing_if = "Option::is_none")] - pub heroes: Option>, + pub heroes: Option, + + /// The current membership of the user, or omitted if user not in room (for peeking). + #[serde(skip_serializing_if = "Option::is_none")] + pub membership: Option, + + /// The name of the lists that match this room. The field is omitted if it doesn't match + /// any list and is included only due to a subscription. + #[serde(default, skip_serializing_if = "ListIds::is_empty")] + pub lists: ListIds, } impl Room { @@ -604,7 +656,7 @@ pub mod response { /// The name. #[serde(rename = "displayname", skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, /// The avatar. #[serde(rename = "avatar_url", skip_serializing_if = "Option::is_none")] @@ -672,7 +724,7 @@ pub mod response { #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub struct ToDevice { /// Fetch the next batch from this entry. - pub next_batch: String, + pub next_batch: Since, /// The to-device events. #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -792,7 +844,7 @@ pub mod response { /// /// Only set when there are more changes to fetch. #[serde(skip_serializing_if = "Option::is_none")] - pub prev_batch: Option, + pub prev_batch: Option, } #[cfg(feature = "unstable-msc4308")] diff --git a/crates/ruma-client-api/src/threads/get_threads.rs b/crates/ruma-client-api/src/threads/get_threads.rs index e1046affaf..b66b04ffec 100644 --- a/crates/ruma-client-api/src/threads/get_threads.rs +++ b/crates/ruma-client-api/src/threads/get_threads.rs @@ -43,7 +43,7 @@ pub mod v1 { pub from: Option, /// Which thread roots are of interest to the caller. - #[serde(skip_serializing_if = "ruma_common::serde::is_default")] + #[serde(default)] #[ruma_api(query)] pub include: IncludeThreads, diff --git a/crates/ruma-client-api/src/uiaa.rs b/crates/ruma-client-api/src/uiaa.rs index 42bdf6c9a9..603d6080b9 100644 --- a/crates/ruma-client-api/src/uiaa.rs +++ b/crates/ruma-client-api/src/uiaa.rs @@ -50,6 +50,9 @@ pub enum AuthData { /// Registration token-based authentication (`m.login.registration_token`). RegistrationToken(RegistrationToken), + /// JSON Web Token authentication (`org.matrix.login.jwt`) + Jwt(Jwt), + /// Fallback acknowledgement. FallbackAcknowledgement(FallbackAcknowledgement), @@ -95,6 +98,7 @@ impl AuthData { "m.login.msisdn" => Self::Msisdn(deserialize_variant(session, data)?), "m.login.dummy" => Self::Dummy(deserialize_variant(session, data)?), "m.registration_token" => Self::RegistrationToken(deserialize_variant(session, data)?), + "org.matrix.login.jwt" => Self::Jwt(deserialize_variant(session, data)?), "m.login.terms" => Self::Terms(deserialize_variant(session, data)?), _ => { Self::_Custom(CustomAuthData { auth_type: auth_type.into(), session, extra: data }) @@ -117,6 +121,7 @@ impl AuthData { Self::Dummy(_) => Some(AuthType::Dummy), Self::RegistrationToken(_) => Some(AuthType::RegistrationToken), Self::FallbackAcknowledgement(_) => None, + Self::Jwt(_) => Some(AuthType::Jwt), Self::Terms(_) => Some(AuthType::Terms), Self::_Custom(c) => Some(AuthType::_Custom(PrivOwnedStr(c.auth_type.as_str().into()))), } @@ -132,6 +137,7 @@ impl AuthData { Self::Dummy(x) => x.session.as_deref(), Self::RegistrationToken(x) => x.session.as_deref(), Self::FallbackAcknowledgement(x) => Some(&x.session), + Self::Jwt(x) => x.session.as_deref(), Self::Terms(x) => x.session.as_deref(), Self::_Custom(x) => x.session.as_deref(), } @@ -157,6 +163,7 @@ impl AuthData { identifier: x.identifier.clone(), password: x.password.clone(), session: None, + user: None, })), Self::ReCaptcha(x) => { Cow::Owned(serialize(ReCaptcha { response: x.response.clone(), session: None })) @@ -172,6 +179,7 @@ impl AuthData { Self::RegistrationToken(x) => { Cow::Owned(serialize(RegistrationToken { token: x.token.clone(), session: None })) } + Self::Jwt(x) => Cow::Owned(serialize(Jwt { token: x.token.clone(), session: None })), // Dummy, fallback acknowledgement, and terms of service have no associated data Self::Dummy(_) | Self::FallbackAcknowledgement(_) | Self::Terms(_) => { Cow::Owned(JsonObject::default()) @@ -191,6 +199,7 @@ impl fmt::Debug for AuthData { Self::Msisdn(inner) => inner.fmt(f), Self::Dummy(inner) => inner.fmt(f), Self::RegistrationToken(inner) => inner.fmt(f), + Self::Jwt(inner) => inner.fmt(f), Self::FallbackAcknowledgement(inner) => inner.fmt(f), Self::Terms(inner) => inner.fmt(f), Self::_Custom(inner) => inner.fmt(f), @@ -224,6 +233,7 @@ impl<'de> Deserialize<'de> for AuthData { Some("m.login.registration_token") => { from_raw_json_value(&json).map(Self::RegistrationToken) } + Some("org.matrix.login.jwt") => from_raw_json_value(&json).map(Self::Jwt), Some("m.login.terms") => from_raw_json_value(&json).map(Self::Terms), None => from_raw_json_value(&json).map(Self::FallbackAcknowledgement), Some(_) => from_raw_json_value(&json).map(Self::_Custom), @@ -264,6 +274,10 @@ pub enum AuthType { #[ruma_enum(rename = "m.login.registration_token")] RegistrationToken, + /// JSON Web Token authentication (`org.matrix.login.jwt`) + #[ruma_enum(rename = "org.matrix.login.jwt")] + Jwt, + /// Terms of service (`m.login.terms`). /// /// This type is only valid during account registration. @@ -284,28 +298,36 @@ pub enum AuthType { #[serde(tag = "type", rename = "m.login.password")] pub struct Password { /// One of the user's identifiers. - pub identifier: UserIdentifier, + #[serde(skip_serializing_if = "Option::is_none")] + pub identifier: Option, /// The plaintext password. pub password: String, /// The value of the session key given by the homeserver, if any. pub session: Option, + + /// Username for the user. (Legacy Element Android/iOS hack, do not use!) + /// + /// See + #[serde(skip_serializing_if = "Option::is_none")] + pub user: Option, } impl Password { /// Creates a new `Password` with the given identifier and password. pub fn new(identifier: UserIdentifier, password: String) -> Self { - Self { identifier, password, session: None } + Self { identifier: Some(identifier), password, session: None, user: None } } } impl fmt::Debug for Password { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { identifier, password: _, session } = self; + let Self { identifier, password: _, session, user } = self; f.debug_struct("Password") .field("identifier", identifier) .field("session", session) + .field("user", user) .finish_non_exhaustive() } } @@ -424,6 +446,25 @@ impl fmt::Debug for RegistrationToken { } } +/// The payload for JWT-based login. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] +#[serde(tag = "type", rename = "org.matrix.login.jwt")] +pub struct Jwt { + /// The JSON Web Token + pub token: String, + + /// The value of the session key given by the homeserver, if any. + pub session: Option, +} + +impl Jwt { + /// Creates a new `Jwt` login token`. + pub fn new(token: String) -> Self { + Self { token, session: None } + } +} + /// Data for UIAA fallback acknowledgement. /// /// See [the spec] for how to use this. @@ -617,7 +658,7 @@ impl fmt::Debug for ThirdpartyIdCredentials { /// Information about available authentication flows and status for User-Interactive Authenticiation /// API. -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub struct UiaaInfo { /// List of authentication flows available for this endpoint. @@ -721,3 +762,38 @@ impl OutgoingResponse for UiaaResponse { } } } + +#[cfg(test)] +mod tests { + use assert_matches2::assert_matches; + use serde; + + #[test] + fn test_empty_uiaa_serialization() { + let input = "{}"; + let result = serde_json::from_str::(input); + assert!(result.is_err()); + } + + #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] + #[serde()] + struct Request { + /// Additional authentication information for the user-interactive authentication API. + #[serde(deserialize_with = "ruma_common::serde::or_empty")] + pub auth: Option, + } + + #[test] + fn test_option_uiaa_serialization() { + let input = r#"{"auth": {}}"#; + let result = serde_json::from_str::(input).unwrap(); + assert_matches!(result.auth, None); + } + + #[test] + fn test_fail_uiaa_serialization() { + let input = r#"{"auth": "aaw"}"#; + let result = serde_json::from_str::(input); + assert!(result.is_err()); + } +} diff --git a/crates/ruma-common/CHANGELOG.md b/crates/ruma-common/CHANGELOG.md index 030377badc..ab44915ba0 100644 --- a/crates/ruma-common/CHANGELOG.md +++ b/crates/ruma-common/CHANGELOG.md @@ -2,6 +2,8 @@ Improvements: +- Add `M_INVITE_BLOCKED` candidate error code proposed by + [MSC4380](https://github.com/matrix-org/matrix-spec-proposals/pull/4380). - Add `MatrixVersion::V1_16` - Remove support for the `org.matrix.hydra.11` room version and the corresponding `unstable-hydra` cargo feature. It should only have been used @@ -87,6 +89,8 @@ Bug fix: Improvements: +- Add `org.matrix.msc4380` unstable feature support to `/versions`. + - Implement the `Zeroize` trait for the `Base64` type. - `ProtocolInstance` has an `instance_id` field, due to a clarification in the spec. diff --git a/crates/ruma-common/Cargo.toml b/crates/ruma-common/Cargo.toml index 227f085f10..82b76c1dff 100644 --- a/crates/ruma-common/Cargo.toml +++ b/crates/ruma-common/Cargo.toml @@ -28,6 +28,7 @@ rand = ["dep:rand", "dep:getrandom", "dep:uuid"] unstable-msc2666 = [] unstable-msc2870 = [] unstable-msc3768 = [] +unstable-msc3026 = [] unstable-msc3930 = [] unstable-msc3931 = [] unstable-msc3932 = ["unstable-msc3931"] @@ -36,9 +37,13 @@ unstable-msc4140 = [] unstable-msc4186 = [] # Thread subscriptions. unstable-msc4306 = [] +unstable-msc4361 = [] +unstable-msc4380 = [] # Allow IDs to exceed 255 bytes. -compat-arbitrary-length-ids = ["ruma-identifiers-validation/compat-arbitrary-length-ids"] +compat-arbitrary-length-ids = [ + "ruma-identifiers-validation/compat-arbitrary-length-ids", +] # Don't validate `ServerSigningKeyVersion`. compat-server-signing-key-version = ["ruma-identifiers-validation/compat-server-signing-key-version"] @@ -64,7 +69,11 @@ getrandom = { version = "0.2.6", optional = true } http = { workspace = true, optional = true } indexmap = { version = "2.0.0", features = ["serde"] } js_int = { workspace = true, features = ["serde"] } -konst = { version = "0.3.5", default-features = false, features = ["cmp", "iter", "parsing"], optional = true } +konst = { version = "0.3.5", default-features = false, features = [ + "cmp", + "iter", + "parsing", +], optional = true } percent-encoding = "2.1.0" rand = { workspace = true, optional = true } regex = { version = "1.5.6", default-features = false, features = ["std", "perf"] } @@ -73,6 +82,8 @@ ruma-macros = { workspace = true } serde = { workspace = true } serde_html_form = { workspace = true } serde_json = { workspace = true, features = ["raw_value"] } +smallstr = { workspace = true } +smallvec = { workspace = true } thiserror = { workspace = true } time = "0.3.34" tracing = { workspace = true, features = ["attributes"] } diff --git a/crates/ruma-common/src/api/metadata.rs b/crates/ruma-common/src/api/metadata.rs index 38e6457042..0870ad24fa 100644 --- a/crates/ruma-common/src/api/metadata.rs +++ b/crates/ruma-common/src/api/metadata.rs @@ -1,6 +1,6 @@ use std::{ cmp::Ordering, - collections::{BTreeMap, BTreeSet}, + collections::BTreeSet, fmt::{Display, Write}, str::FromStr, }; @@ -533,33 +533,34 @@ impl VersionHistory { VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved( self.removed.expect("VersioningDecision::Removed implies metadata.removed"), )), + VersioningDecision::Version { any_deprecated, all_deprecated, any_removed } => { - if any_removed { - if all_deprecated { - warn!( - "endpoint is removed in some (and deprecated in ALL) \ + if cfg!(debug_assertions) { + if any_removed { + if all_deprecated { + warn!( + "endpoint is removed in some (and deprecated in ALL) \ of the following versions: {:?}", + considering.versions + ); + } else if any_deprecated { + warn!( + "endpoint is removed (and deprecated) in some of the \ + following versions: {:?}", + considering.versions + ); + } + } else if all_deprecated { + warn!( + "endpoint is deprecated in ALL of the following versions: {:?}", considering.versions ); } else if any_deprecated { warn!( - "endpoint is removed (and deprecated) in some of the \ - following versions: {:?}", + "endpoint is deprecated in some of the following versions: {:?}", considering.versions ); - } else { - unreachable!("any_removed implies *_deprecated"); } - } else if all_deprecated { - warn!( - "endpoint is deprecated in ALL of the following versions: {:?}", - considering.versions - ); - } else if any_deprecated { - warn!( - "endpoint is deprecated in some of the following versions: {:?}", - considering.versions - ); } Ok(self @@ -1143,13 +1144,19 @@ impl SupportedVersions { /// /// Matrix versions that can't be parsed to a `MatrixVersion`, and features with the boolean /// value set to `false` are discarded. - pub fn from_parts(versions: &[String], unstable_features: &BTreeMap) -> Self { + pub fn from_parts<'a, Versions, Features>( + versions: Versions, + unstable_features: Features, + ) -> Self + where + Versions: Iterator, + Features: Iterator, + { Self { - versions: versions.iter().flat_map(|s| s.parse::()).collect(), + versions: versions.flat_map(|s| s.parse::()).collect(), features: unstable_features - .iter() .filter(|(_, enabled)| **enabled) - .map(|(feature, _)| feature.as_str().into()) + .map(|(feature, _)| feature.into()) .collect(), } } @@ -1264,6 +1271,15 @@ pub enum FeatureFlag { #[ruma_enum(rename = "org.matrix.simplified_msc3575")] Msc4186, + /// `org.matrix.msc4380_invite_permission_config` ([MSC]) + /// + /// Invite Blocking. + /// + /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4380 + #[cfg(feature = "unstable-msc4380")] + #[ruma_enum(rename = "org.matrix.msc4380")] + Msc4380, + #[doc(hidden)] _Custom(PrivOwnedStr), } diff --git a/crates/ruma-common/src/canonical_json.rs b/crates/ruma-common/src/canonical_json.rs index eb5d0d9861..862c14af6f 100644 --- a/crates/ruma-common/src/canonical_json.rs +++ b/crates/ruma-common/src/canonical_json.rs @@ -5,9 +5,10 @@ use std::{fmt, mem}; use serde::Serialize; use serde_json::Value as JsonValue; +mod object; mod value; -pub use self::value::{CanonicalJsonObject, CanonicalJsonValue}; +pub use self::{object::*, value::*}; use crate::{room_version_rules::RedactionRules, serde::Raw}; /// The set of possible errors when serializing to canonical JSON. @@ -103,7 +104,7 @@ pub enum JsonType { pub fn try_from_json_map( json: serde_json::Map, ) -> Result { - json.into_iter().map(|(k, v)| Ok((k, v.try_into()?))).collect() + json.into_iter().map(|(k, v)| Ok((k.into(), v.try_into()?))).collect() } /// Fallible conversion from any value that impl's `Serialize` to a `CanonicalJsonValue`. @@ -199,10 +200,10 @@ pub fn redact_in_place( if let Some(redacted_because) = redacted_because { let unsigned = CanonicalJsonObject::from_iter([( - "redacted_because".to_owned(), + "redacted_because".into(), redacted_because.0.into(), )]); - event.insert("unsigned".to_owned(), unsigned.into()); + event.insert("unsigned".into(), unsigned.into()); } Ok(()) diff --git a/crates/ruma-common/src/canonical_json/object.rs b/crates/ruma-common/src/canonical_json/object.rs new file mode 100644 index 0000000000..b96f3ec962 --- /dev/null +++ b/crates/ruma-common/src/canonical_json/object.rs @@ -0,0 +1,210 @@ +#![allow(clippy::exhaustive_structs)] + +use std::collections::BTreeMap; + +use serde::{ser::Serializer, Serialize}; +use smallstr::SmallString; + +use super::CanonicalJsonValue; +use crate::serde::{JsonCastable, JsonObject}; + +/// The inner type of `CanonicalJsonValue::Object`. +pub type CanonicalJsonObject = BTreeMap; + +/// Strong type for serializing slices of CanonicalJsonMember +#[derive(Clone, Debug, Default)] +pub struct CanonicalJsonMembers<'a, K>(pub &'a [CanonicalJsonMember]); + +/// Strong type for serializing slices of CanonicalJsonMember +#[derive(Clone, Debug, Default)] +pub struct CanonicalJsonMembersRef<'a, K>(pub &'a [CanonicalJsonMemberRef<'a, K>]); + +/// Strong type for serializing slices of CanonicalJsonMemberOptional +#[derive(Clone, Debug, Default)] +pub struct CanonicalJsonMembersOptional<'a, K>(pub &'a [CanonicalJsonMemberOptional]); + +/// Strong type for serializing slices of CanonicalJsonMemberOptional +#[derive(Clone, Debug, Default)] +pub struct CanonicalJsonMembersRefOptional<'a, K>(pub &'a [CanonicalJsonMemberRefOptional<'a, K>]); + +/// Inner type component for CanonicalJsonMembers with AsRef keys and Into Values +#[allow(type_alias_bounds)] +pub type CanonicalJsonMember> = (K, CanonicalJsonValue); + +/// Inner type component for CanonicalJsonMembers with AsRef keys and Into Values +#[allow(type_alias_bounds)] +pub type CanonicalJsonMemberRef<'a, K: AsRef> = (K, &'a CanonicalJsonValue); + +/// Inner type component for CanonicalJsonMembers with AsRef keys and Optional Into Values +#[allow(type_alias_bounds)] +pub type CanonicalJsonMemberOptional> = (K, Option); + +/// Inner type component for CanonicalJsonMembers with AsRef keys and Optional Into Values +#[allow(type_alias_bounds)] +pub type CanonicalJsonMemberRefOptional<'a, K: AsRef> = (K, Option<&'a CanonicalJsonValue>); + +/// Property name (or key) for an Object. This is a string-like but typographically distinct for +/// optimization purposes. +pub type CanonicalJsonName = SmallString<[u8; NAME_INLINE_CAP]>; + +/// Opinionated buffer size of the CanonicalJsonName type. +const NAME_INLINE_CAP: usize = 32; + +impl JsonCastable for T where T: JsonCastable {} + +impl<'a, K> Serialize for CanonicalJsonMembers<'a, K> +where + K: AsRef + Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + use serde::ser::SerializeMap; + debug_assert!( + self.0.iter().map(|(k, _)| k).map(AsRef::::as_ref).is_sorted(), + "Input members must be sorted to be canonical." + ); + + let len = self.0.len(); + serializer.serialize_map(Some(len)).and_then(|mut map| { + self.0 + .iter() + .try_for_each(|(k, v)| map.serialize_entry(k, v)) + .and_then(move |()| map.end()) + }) + } +} + +impl<'a, K> Serialize for CanonicalJsonMembersRef<'a, K> +where + K: AsRef + Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + use serde::ser::SerializeMap; + debug_assert!( + self.0.iter().map(|(k, _)| k).map(AsRef::::as_ref).is_sorted(), + "Input members must be sorted to be canonical." + ); + + let len = self.0.len(); + serializer.serialize_map(Some(len)).and_then(|mut map| { + self.0 + .iter() + .try_for_each(|(k, v)| map.serialize_entry(k, *v)) + .and_then(move |()| map.end()) + }) + } +} + +impl<'a, K> Serialize for CanonicalJsonMembersOptional<'a, K> +where + K: AsRef + Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + use serde::ser::SerializeMap; + debug_assert!( + self.0.iter().map(|(k, _)| k).map(AsRef::::as_ref).is_sorted(), + "Input members must be sorted to be canonical." + ); + + let len = self.0.iter().filter(|(_, v)| v.is_some()).count(); + serializer.serialize_map(Some(len)).and_then(|mut map| { + self.0 + .iter() + .filter_map(|(k, v)| v.as_ref().map(move |v| (k, v))) + .try_for_each(|(k, v)| map.serialize_entry(k, v)) + .and_then(move |()| map.end()) + }) + } +} + +impl<'a, K> Serialize for CanonicalJsonMembersRefOptional<'a, K> +where + K: AsRef + Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + use serde::ser::SerializeMap; + debug_assert!( + self.0.iter().map(|(k, _)| k).map(AsRef::::as_ref).is_sorted(), + "Input members must be sorted to be canonical." + ); + + let len = self.0.iter().filter(|(_, v)| v.is_some()).count(); + serializer.serialize_map(Some(len)).and_then(|mut map| { + self.0 + .iter() + .filter_map(|(k, v)| v.map(move |v| (k, v))) + .try_for_each(|(k, v)| map.serialize_entry(k, v)) + .and_then(move |()| map.end()) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn members_to_string() { + const CANONICAL_STR: &str = r#"{"city":"London","street":"10 Downing Street"}"#; + + let members: [CanonicalJsonMember<_>; _] = + [("city", "London".into()), ("street", "10 Downing Street".into())]; + + let json = serde_json::to_string(&CanonicalJsonMembers(&members)).unwrap(); + assert_eq!(format!("{json}"), CANONICAL_STR); + } + + #[test] + fn remembers_to_string() { + const CANONICAL_STR: &str = r#"{"city":"London","occupant":{"office":"prime minister"},"street":"10 Downing Street"}"#; + const SUB_OBJECT: &str = r#"{"office":"prime minister"}"#; + + let sub_value: CanonicalJsonValue = serde_json::from_str(SUB_OBJECT).unwrap(); + let sub_raw = serde_json::value::to_raw_value(&sub_value).unwrap(); + let members: [CanonicalJsonMember<_>; _] = [ + ("city", "London".into()), + ("occupant", sub_raw.into()), + ("street", "10 Downing Street".into()), + ]; + + let json = serde_json::to_string(&CanonicalJsonMembers(&members)).unwrap(); + assert_eq!(format!("{json}"), CANONICAL_STR); + } + + #[test] + fn optional_members_to_string() { + const CANONICAL_STR: &str = r#"{"city":"London","street":"10 Downing Street"}"#; + + let members: [CanonicalJsonMemberOptional<_>; _] = [ + ("city", Some("London".into())), + ("occupant", None), + ("street", Some("10 Downing Street".into())), + ]; + + let json = serde_json::to_string(&CanonicalJsonMembersOptional(&members)).unwrap(); + assert_eq!(format!("{json}"), CANONICAL_STR); + } + + #[test] + #[should_panic = "Input members must be sorted"] + fn members_unsorted() { + const CANONICAL_STR: &str = r#"{"city":"London","street":"10 Downing Street"}"#; + + let members: [CanonicalJsonMember<_>; _] = + [("street", "10 Downing Street".into()), ("city", "London".into())]; + + let json = serde_json::to_string(&CanonicalJsonMembers(&members)).unwrap(); + assert_eq!(format!("{json}"), CANONICAL_STR); + } +} diff --git a/crates/ruma-common/src/canonical_json/value.rs b/crates/ruma-common/src/canonical_json/value.rs index 255918e6ad..491c90b8ef 100644 --- a/crates/ruma-common/src/canonical_json/value.rs +++ b/crates/ruma-common/src/canonical_json/value.rs @@ -1,17 +1,14 @@ -use std::{collections::BTreeMap, fmt}; +use std::fmt; use as_variant::as_variant; use js_int::{Int, UInt}; use serde::{de::Deserializer, ser::Serializer, Deserialize, Serialize}; -use serde_json::{to_string as to_json_string, Value as JsonValue}; +use serde_json::{ + to_string as to_json_string, value::RawValue as RawJsonValue, Value as JsonValue, +}; -use super::CanonicalJsonError; -use crate::serde::{JsonCastable, JsonObject}; - -/// The inner type of `CanonicalJsonValue::Object`. -pub type CanonicalJsonObject = BTreeMap; - -impl JsonCastable for T where T: JsonCastable {} +use super::{CanonicalJsonError, CanonicalJsonObject}; +use crate::serde::JsonCastable; /// Represents a canonical JSON value as per the Matrix specification. #[derive(Clone, Default, Eq, PartialEq)] @@ -188,7 +185,7 @@ impl TryFrom for CanonicalJsonValue { JsonValue::String(string) => Self::String(string), JsonValue::Object(obj) => Self::Object( obj.into_iter() - .map(|(k, v)| Ok((k, v.try_into()?))) + .map(|(k, v)| Ok((k.into(), v.try_into()?))) .collect::>()?, ), JsonValue::Null => Self::Null, @@ -206,13 +203,36 @@ impl From for JsonValue { Self::Array(vec.into_iter().map(Into::into).collect()) } CanonicalJsonValue::Object(obj) => { - Self::Object(obj.into_iter().map(|(k, v)| (k, v.into())).collect()) + Self::Object(obj.into_iter().map(|(k, v)| (k.into_string(), v.into())).collect()) } CanonicalJsonValue::Null => Self::Null, } } } +impl From> for CanonicalJsonValue { + #[inline] + fn from(val: Box) -> Self { + (&val).into() + } +} + +impl<'a> From<&'a Box> for CanonicalJsonValue { + #[inline] + fn from(val: &'a Box) -> Self { + (&**val).into() + } +} + +impl<'a> From<&'a RawJsonValue> for CanonicalJsonValue { + fn from(val: &'a RawJsonValue) -> Self { + serde_json::from_str::(val.get()) + .map_err(CanonicalJsonError::SerDe) + .and_then(TryInto::try_into) + .expect("Conversion from RawJsonValue to CanonicalJsonValue failed.") + } +} + impl JsonCastable for T {} macro_rules! variant_impls { @@ -257,7 +277,6 @@ impl From for CanonicalJsonValue { } impl Serialize for CanonicalJsonValue { - #[inline] fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -281,7 +300,6 @@ impl Serialize for CanonicalJsonValue { } impl<'de> Deserialize<'de> for CanonicalJsonValue { - #[inline] fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/crates/ruma-common/src/directory.rs b/crates/ruma-common/src/directory.rs index 67a81149f1..29d30e6919 100644 --- a/crates/ruma-common/src/directory.rs +++ b/crates/ruma-common/src/directory.rs @@ -61,7 +61,7 @@ pub struct PublicRoomsChunk { pub avatar_url: Option, /// The join rule of the room. - #[serde(default, skip_serializing_if = "crate::serde::is_default")] + #[serde(default)] pub join_rule: JoinRuleKind, /// The type of room from `m.room.create`, if any. @@ -201,7 +201,7 @@ impl Filter { /// Returns `true` if the filter is empty. pub fn is_empty(&self) -> bool { - self.generic_search_term.is_none() + self.generic_search_term.is_none() && self.room_types.is_empty() } } diff --git a/crates/ruma-common/src/encryption.rs b/crates/ruma-common/src/encryption.rs index f21be1907a..dfcd6b5349 100644 --- a/crates/ruma-common/src/encryption.rs +++ b/crates/ruma-common/src/encryption.rs @@ -9,7 +9,8 @@ use serde::{Deserialize, Serialize}; use crate::{ serde::{Base64, StringEnum}, CrossSigningOrDeviceSignatures, DeviceSignatures, EventEncryptionAlgorithm, - OwnedCrossSigningKeyId, OwnedDeviceId, OwnedDeviceKeyId, OwnedUserId, PrivOwnedStr, + MilliSecondsSinceUnixEpoch, OwnedCrossSigningKeyId, OwnedDeviceId, OwnedDeviceKeyId, + OwnedUserId, PrivOwnedStr, }; /// Identity keys for a device. @@ -33,12 +34,19 @@ pub struct DeviceKeys { pub keys: BTreeMap, /// Signatures for the device key object. + /// + /// serde default is because synapse doesn't seem to mandate this field + #[serde(default)] pub signatures: CrossSigningOrDeviceSignatures, /// Additional data added to the device key information by intermediate servers, and /// not covered by the signatures. #[serde(default, skip_serializing_if = "UnsignedDeviceInfo::is_empty")] pub unsigned: UnsignedDeviceInfo, + + /// unspecced legacy synapse/client field? + #[serde(skip_serializing_if = "Option::is_none")] + pub valid_until_ts: Option, } impl DeviceKeys { @@ -50,8 +58,17 @@ impl DeviceKeys { algorithms: Vec, keys: BTreeMap, signatures: CrossSigningOrDeviceSignatures, + valid_until_ts: Option, ) -> Self { - Self { user_id, device_id, algorithms, keys, signatures, unsigned: Default::default() } + Self { + user_id, + device_id, + algorithms, + keys, + signatures, + unsigned: Default::default(), + valid_until_ts, + } } } @@ -118,7 +135,7 @@ pub enum OneTimeKey { /// A [cross-signing] key. /// /// [cross-signing]: https://spec.matrix.org/latest/client-server-api/#cross-signing -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub struct CrossSigningKey { /// The ID of the user the key belongs to. diff --git a/crates/ruma-common/src/http_headers/content_disposition.rs b/crates/ruma-common/src/http_headers/content_disposition.rs index 915b3763db..e4bccdb073 100644 --- a/crates/ruma-common/src/http_headers/content_disposition.rs +++ b/crates/ruma-common/src/http_headers/content_disposition.rs @@ -2,9 +2,11 @@ use std::{fmt, ops::Deref, str::FromStr}; +use http::header::{HeaderValue, InvalidHeaderValue}; use ruma_macros::{ AsRefStr, AsStrAsRefStr, DebugAsRefStr, DisplayAsRefStr, OrdAsRefStr, PartialOrdAsRefStr, }; +use serde::{Serialize, Serializer}; use super::{ is_tchar, is_token, quote_ascii_string_if_required, rfc8187, sanitize_for_ascii_quoted_string, @@ -70,6 +72,20 @@ impl fmt::Display for ContentDisposition { } } +impl Serialize for ContentDisposition { + fn serialize(&self, s: S) -> Result { + s.serialize_str(self.to_string().as_str()) + } +} + +impl TryFrom<&ContentDisposition> for HeaderValue { + type Error = InvalidHeaderValue; + + fn try_from(cd: &ContentDisposition) -> Result { + HeaderValue::from_str(cd.to_string().as_str()) + } +} + impl TryFrom<&[u8]> for ContentDisposition { type Error = ContentDispositionParseError; diff --git a/crates/ruma-common/src/identifiers.rs b/crates/ruma-common/src/identifiers.rs index a3856b90db..3debf196c0 100644 --- a/crates/ruma-common/src/identifiers.rs +++ b/crates/ruma-common/src/identifiers.rs @@ -33,7 +33,7 @@ pub use self::{ ServerSigningKeyId, SigningKeyId, }, matrix_uri::{MatrixToUri, MatrixUri}, - mxc_uri::{MxcUri, OwnedMxcUri}, + mxc_uri::{Mxc, MxcUri, OwnedMxcUri}, one_time_key_name::{OneTimeKeyName, OwnedOneTimeKeyName}, room_alias_id::{OwnedRoomAliasId, RoomAliasId}, room_id::{OwnedRoomId, RoomId}, diff --git a/crates/ruma-common/src/identifiers/event_id.rs b/crates/ruma-common/src/identifiers/event_id.rs index b4d2dee68d..db2ca175b1 100644 --- a/crates/ruma-common/src/identifiers/event_id.rs +++ b/crates/ruma-common/src/identifiers/event_id.rs @@ -37,7 +37,7 @@ use super::ServerName; /// [room versions]: https://spec.matrix.org/latest/rooms/#complete-list-of-room-versions #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdDst)] -#[ruma_id(validate = ruma_identifiers_validation::event_id::validate)] +#[ruma_id(validate = ruma_identifiers_validation::event_id::validate, inline_bytes = 48)] pub struct EventId(str); impl EventId { @@ -105,6 +105,15 @@ mod tests { ); } + #[test] + fn valid_url_safe_base64_event_id_from_parts() { + assert_eq!( + OwnedEventId::from_parts('$', "Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg", None) + .expect("Failed to create OwnedEventId."), + "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg" + ); + } + #[cfg(feature = "rand")] #[test] fn generate_random_valid_event_id() { diff --git a/crates/ruma-common/src/identifiers/key_id.rs b/crates/ruma-common/src/identifiers/key_id.rs index 9e0f461611..3c30cb31d7 100644 --- a/crates/ruma-common/src/identifiers/key_id.rs +++ b/crates/ruma-common/src/identifiers/key_id.rs @@ -58,12 +58,8 @@ impl KeyId { let algorithm = algorithm.as_ref(); let key_name = key_name.as_ref(); - let mut res = String::with_capacity(algorithm.len() + 1 + key_name.len()); - res.push_str(algorithm); - res.push(':'); - res.push_str(key_name); - - Self::from_borrowed(&res).to_owned() + OwnedKeyId::from_iov(&[algorithm, ":", key_name]) + .expect("Failed to construct KeyId from KeyAlgorithm and KeyName") } /// Returns key algorithm of the key ID - the part that comes before the colon. @@ -99,7 +95,7 @@ impl KeyId { } fn colon_idx(&self) -> usize { - self.as_str().find(':').unwrap() + self.as_str().find(':').expect("Missing ':' in identifier.") } } @@ -226,27 +222,27 @@ mod tests { #[test] fn algorithm_and_key_name_are_correctly_extracted() { - let key_id = DeviceKeyId::parse("ed25519:MYDEVICE").expect("Should parse correctly"); + let key_id = DeviceKeyId::parse_ref("ed25519:MYDEVICE").expect("Should parse correctly"); assert_eq!(key_id.algorithm().as_str(), "ed25519"); assert_eq!(key_id.key_name(), "MYDEVICE"); } #[test] fn empty_key_name_is_correctly_extracted() { - let key_id = DeviceKeyId::parse("ed25519:").expect("Should parse correctly"); + let key_id = DeviceKeyId::parse_ref("ed25519:").expect("Should parse correctly"); assert_eq!(key_id.algorithm().as_str(), "ed25519"); assert_eq!(key_id.key_name(), ""); } #[test] fn missing_colon_fails_to_parse() { - let error = DeviceKeyId::parse("ed25519_MYDEVICE").expect_err("Should fail to parse"); + let error = DeviceKeyId::parse_ref("ed25519_MYDEVICE").expect_err("Should fail to parse"); assert_matches!(error, Error::MissingColon); } #[test] fn empty_algorithm_fails_to_parse() { - let error = DeviceKeyId::parse(":MYDEVICE").expect_err("Should fail to parse"); + let error = DeviceKeyId::parse_ref(":MYDEVICE").expect_err("Should fail to parse"); // Weirdly, this also reports MissingColon assert_matches!(error, Error::MissingColon); } diff --git a/crates/ruma-common/src/identifiers/matrix_uri.rs b/crates/ruma-common/src/identifiers/matrix_uri.rs index 3b4a6fc17f..aac574db51 100644 --- a/crates/ruma-common/src/identifiers/matrix_uri.rs +++ b/crates/ruma-common/src/identifiers/matrix_uri.rs @@ -13,7 +13,7 @@ use super::{ EventId, OwnedEventId, OwnedRoomAliasId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomAliasId, RoomId, RoomOrAliasId, UserId, }; -use crate::{percent_encode::PATH_PERCENT_ENCODE_SET, PrivOwnedStr, ServerName}; +use crate::{percent_encode::PATH_PERCENT_ENCODE_SET, PrivOwnedStr}; const MATRIX_TO_BASE_URL: &str = "https://matrix.to/#/"; const MATRIX_SCHEME: &str = "matrix"; @@ -315,7 +315,7 @@ impl MatrixToUri { query_parts .map(|(key, value)| { if key == "via" { - ServerName::parse(&value) + value.parse::() } else { Err(MatrixToError::UnknownArgument.into()) } diff --git a/crates/ruma-common/src/identifiers/mxc_uri.rs b/crates/ruma-common/src/identifiers/mxc_uri.rs index fe36a1ad13..3613724059 100644 --- a/crates/ruma-common/src/identifiers/mxc_uri.rs +++ b/crates/ruma-common/src/identifiers/mxc_uri.rs @@ -2,10 +2,11 @@ //! //! [MXC URI]: https://spec.matrix.org/latest/client-server-api/#matrix-content-mxc-uris -use std::num::NonZeroU8; +use std::{fmt, num::NonZeroU8}; use ruma_identifiers_validation::{error::MxcUriError, mxc_uri::validate}; use ruma_macros::IdDst; +use serde::{Serialize, Serializer}; use super::ServerName; @@ -18,25 +19,34 @@ type Result = std::result::Result; #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdDst)] pub struct MxcUri(str); +/// Structured MXC URI which may reference strings from separate sources without serialization +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] +pub struct Mxc<'a> { + /// ServerName part of the MXC URI + pub server_name: &'a ServerName, + + /// MediaId part of the MXC URI + pub media_id: &'a str, +} + impl MxcUri { /// If this is a valid MXC URI, returns the media ID. pub fn media_id(&self) -> Result<&str> { - self.parts().map(|(_, s)| s) + self.parts().map(|mxc| mxc.media_id) } /// If this is a valid MXC URI, returns the server name. pub fn server_name(&self) -> Result<&ServerName> { - self.parts().map(|(s, _)| s) + self.parts().map(|mxc| mxc.server_name) } /// If this is a valid MXC URI, returns a `(server_name, media_id)` tuple, else it returns the /// error. - pub fn parts(&self) -> Result<(&ServerName, &str)> { - self.extract_slash_idx().map(|idx| { - ( - ServerName::from_borrowed(&self.as_str()[6..idx.get() as usize]), - &self.as_str()[idx.get() as usize + 1..], - ) + pub fn parts(&self) -> Result> { + self.extract_slash_idx().map(|idx| Mxc::<'_> { + server_name: ServerName::from_borrowed(&self.as_str()[6..idx.get() as usize]), + media_id: &self.as_str()[idx.get() as usize + 1..], }) } @@ -58,11 +68,49 @@ impl MxcUri { } } +impl fmt::Display for Mxc<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "mxc://{}/{}", self.server_name, self.media_id) + } +} + +impl<'a> TryFrom<&'a MxcUri> for Mxc<'a> { + type Error = MxcUriError; + + fn try_from(s: &'a MxcUri) -> Result { + s.parts() + } +} + +impl<'a> TryFrom<&'a str> for Mxc<'a> { + type Error = MxcUriError; + + fn try_from(s: &'a str) -> Result { + let s: &MxcUri = s.into(); + s.try_into() + } +} + +impl<'a> TryFrom<&'a OwnedMxcUri> for Mxc<'a> { + type Error = MxcUriError; + + fn try_from(s: &'a OwnedMxcUri) -> Result { + let s: &MxcUri = s.as_ref(); + s.try_into() + } +} + +impl Serialize for Mxc<'_> { + fn serialize(&self, s: S) -> Result { + s.serialize_str(self.to_string().as_str()) + } +} + #[cfg(test)] mod tests { use ruma_identifiers_validation::error::MxcUriError; - use super::{MxcUri, OwnedMxcUri}; + use super::{Mxc, MxcUri, OwnedMxcUri}; #[test] fn parse_mxc_uri() { @@ -71,7 +119,10 @@ mod tests { assert!(mxc.is_valid()); assert_eq!( mxc.parts(), - Ok(("127.0.0.1".try_into().expect("Failed to create ServerName"), "asd32asdfasdsd")) + Ok(Mxc { + server_name: "127.0.0.1".try_into().expect("Failed to create ServerName"), + media_id: "asd32asdfasdsd", + }) ); } @@ -106,7 +157,10 @@ mod tests { assert!(mxc.is_valid()); assert_eq!( mxc.parts(), - Ok(("server".try_into().expect("Failed to create ServerName"), "1234id")) + Ok(Mxc { + server_name: "server".try_into().expect("Failed to create ServerName"), + media_id: "1234id" + }) ); } } diff --git a/crates/ruma-common/src/identifiers/room_alias_id.rs b/crates/ruma-common/src/identifiers/room_alias_id.rs index 1b24af3d2b..91accbfa77 100644 --- a/crates/ruma-common/src/identifiers/room_alias_id.rs +++ b/crates/ruma-common/src/identifiers/room_alias_id.rs @@ -77,6 +77,15 @@ mod tests { ); } + #[test] + fn valid_room_alias_id_from_parts() { + assert_eq!( + OwnedRoomAliasId::from_parts('#', "ruma", Some("example.com")) + .expect("Failed to create OwnedRoomAliasId."), + "#ruma:example.com" + ); + } + #[test] fn empty_localpart() { assert_eq!( diff --git a/crates/ruma-common/src/identifiers/room_id.rs b/crates/ruma-common/src/identifiers/room_id.rs index b1a2852ce0..e379ac8ac7 100644 --- a/crates/ruma-common/src/identifiers/room_id.rs +++ b/crates/ruma-common/src/identifiers/room_id.rs @@ -21,7 +21,7 @@ use crate::RoomOrAliasId; /// [room ID]: https://spec.matrix.org/latest/appendices/#room-ids #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdDst)] -#[ruma_id(validate = ruma_identifiers_validation::room_id::validate)] +#[ruma_id(validate = ruma_identifiers_validation::room_id::validate, inline_bytes = 48)] pub struct RoomId(str); impl RoomId { @@ -36,6 +36,7 @@ impl RoomId { /// [`RoomIdFormatVersion::V2`]: crate::room_version_rules::RoomIdFormatVersion::V2 /// [`RoomVersionRules`]: crate::room_version_rules::RoomVersionRules #[cfg(feature = "rand")] + #[inline] pub fn new_v1(server_name: &ServerName) -> OwnedRoomId { Self::from_borrowed(&format!("!{}:{server_name}", super::generate_localpart(18))).to_owned() } @@ -52,8 +53,9 @@ impl RoomId { /// [`RoomIdFormatVersion::V1`]: crate::room_version_rules::RoomIdFormatVersion::V1 /// [`RoomIdFormatVersion::V2`]: crate::room_version_rules::RoomIdFormatVersion::V2 /// [`RoomVersionRules`]: crate::room_version_rules::RoomVersionRules + #[inline] pub fn new_v2(room_create_reference_hash: &str) -> Result { - Self::parse(format!("!{room_create_reference_hash}")) + Self::parse(&format!("!{room_create_reference_hash}")).map(ToOwned::to_owned) } /// Returns the room ID without the initial `!` sigil. @@ -62,6 +64,7 @@ impl RoomId { /// `m.room.create` event of the room. /// /// [`RoomIdFormatVersion::V2`]: crate::room_version_rules::RoomIdFormatVersion::V2 + #[inline] pub fn strip_sigil(&self) -> &str { self.as_str().strip_prefix('!').expect("sigil should be checked during construction") } @@ -71,10 +74,18 @@ impl RoomId { /// This should only return `Some(_)` for room versions using [`RoomIdFormatVersion::V1`]. /// /// [`RoomIdFormatVersion::V1`]: crate::room_version_rules::RoomIdFormatVersion::V1 + /// Returns the server name of the room ID. + #[inline] pub fn server_name(&self) -> Option<&ServerName> { <&RoomOrAliasId>::from(self).server_name() } + /// Returns the `RoomId` as an `OwnedEventId` for ['RoomIdFormatVersion::V2']. + #[inline] + pub fn as_event_id(&self) -> Result { + OwnedEventId::from_parts('$', self.strip_sigil(), None) + } + /// Create a `matrix.to` URI for this room ID. /// /// Note that it is recommended to provide servers that should know the room to be able to find diff --git a/crates/ruma-common/src/identifiers/room_or_alias_id.rs b/crates/ruma-common/src/identifiers/room_or_alias_id.rs index 60c971b20c..1ddfc9364e 100644 --- a/crates/ruma-common/src/identifiers/room_or_alias_id.rs +++ b/crates/ruma-common/src/identifiers/room_or_alias_id.rs @@ -31,7 +31,7 @@ use super::{server_name::ServerName, OwnedRoomAliasId, OwnedRoomId, RoomAliasId, /// [room alias ID]: https://spec.matrix.org/latest/appendices/#room-aliases #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdDst)] -#[ruma_id(validate = ruma_identifiers_validation::room_id_or_alias_id::validate)] +#[ruma_id(validate = ruma_identifiers_validation::room_id_or_alias_id::validate, inline_bytes = 48)] pub struct RoomOrAliasId(str); impl RoomOrAliasId { diff --git a/crates/ruma-common/src/identifiers/room_version_id.rs b/crates/ruma-common/src/identifiers/room_version_id.rs index 1ba1ac3940..ae4b9e4022 100644 --- a/crates/ruma-common/src/identifiers/room_version_id.rs +++ b/crates/ruma-common/src/identifiers/room_version_id.rs @@ -73,6 +73,12 @@ pub enum RoomVersionId { #[cfg(feature = "unstable-msc2870")] MSC2870, + /// `org.matrix.msc4361` ([MSC4361]). + /// + /// [MSC4361]: https://github.com/matrix-org/matrix-spec-proposals/pull/4361 + #[cfg(feature = "unstable-msc4361")] + MSC4361, + #[doc(hidden)] _Custom(CustomRoomVersion), } @@ -97,6 +103,8 @@ impl RoomVersionId { Self::V12 => "12", #[cfg(feature = "unstable-msc2870")] Self::MSC2870 => "org.matrix.msc2870", + #[cfg(feature = "unstable-msc4361")] + Self::MSC4361 => "org.matrix.msc4361", Self::_Custom(version) => version.as_str(), } } @@ -126,6 +134,8 @@ impl RoomVersionId { Self::V12 => RoomVersionRules::V12, #[cfg(feature = "unstable-msc2870")] Self::MSC2870 => RoomVersionRules::MSC2870, + #[cfg(feature = "unstable-msc4361")] + Self::MSC4361 => RoomVersionRules::MSC4361, Self::_Custom(_) => return None, }) } @@ -212,6 +222,8 @@ where "12" => RoomVersionId::V12, #[cfg(feature = "unstable-msc2870")] "org.matrix.msc2870" => RoomVersionId::MSC2870, + #[cfg(feature = "unstable-msc4361")] + "org.matrix.msc4361" => RoomVersionId::MSC4361, custom => { ruma_identifiers_validation::room_version_id::validate(custom)?; RoomVersionId::_Custom(CustomRoomVersion(room_version_id.into())) diff --git a/crates/ruma-common/src/identifiers/server_signing_key_version.rs b/crates/ruma-common/src/identifiers/server_signing_key_version.rs index 0f6b6283f9..a7d7f92c69 100644 --- a/crates/ruma-common/src/identifiers/server_signing_key_version.rs +++ b/crates/ruma-common/src/identifiers/server_signing_key_version.rs @@ -15,6 +15,7 @@ use super::{IdParseError, KeyName}; #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdDst)] #[ruma_id( validate = ruma_identifiers_validation::server_signing_key_version::validate, + inline_bytes = 16 )] pub struct ServerSigningKeyVersion(str); diff --git a/crates/ruma-common/src/identifiers/signatures.rs b/crates/ruma-common/src/identifiers/signatures.rs index 9b39dc6fa7..b75c22ef08 100644 --- a/crates/ruma-common/src/identifiers/signatures.rs +++ b/crates/ruma-common/src/identifiers/signatures.rs @@ -27,7 +27,7 @@ pub type EntitySignatures = BTreeMap, String>; /// "YbJva03ihSj5mPk+CHMJKUKlCXCPFXjXOK6VqBnN9nA2evksQcTGn6hwQfrgRHIDDXO2le49x7jnWJHMJrJoBQ"; /// signatures.insert_signature(server_name, key_identifier, signature.into()); /// ``` -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, PartialEq)] #[serde( transparent, bound(serialize = "E: Serialize", deserialize = "E: serde::de::DeserializeOwned") diff --git a/crates/ruma-common/src/identifiers/space_child_order.rs b/crates/ruma-common/src/identifiers/space_child_order.rs index b29d2882a3..aa50ac4308 100644 --- a/crates/ruma-common/src/identifiers/space_child_order.rs +++ b/crates/ruma-common/src/identifiers/space_child_order.rs @@ -11,7 +11,7 @@ use ruma_macros::IdDst; /// [`m.space.child`]: https://spec.matrix.org/latest/client-server-api/#mspacechild #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdDst)] -#[ruma_id(validate = ruma_identifiers_validation::space_child_order::validate)] +#[ruma_id(validate = ruma_identifiers_validation::space_child_order::validate, inline_bytes = 50)] pub struct SpaceChildOrder(str); #[cfg(test)] diff --git a/crates/ruma-common/src/identifiers/user_id.rs b/crates/ruma-common/src/identifiers/user_id.rs index 83861a5ab9..b7e981ae3e 100644 --- a/crates/ruma-common/src/identifiers/user_id.rs +++ b/crates/ruma-common/src/identifiers/user_id.rs @@ -21,7 +21,7 @@ use super::{matrix_uri::UriAction, IdParseError, MatrixToUri, MatrixUri, ServerN /// [user ID]: https://spec.matrix.org/latest/appendices/#user-identifiers #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdDst)] -#[ruma_id(validate = ruma_identifiers_validation::user_id::validate)] +#[ruma_id(validate = ruma_identifiers_validation::user_id::validate, inline_bytes = 40)] pub struct UserId(str); impl UserId { @@ -48,13 +48,13 @@ impl UserId { /// localpart, not the localpart plus the `@` prefix, or the localpart plus server name without /// the `@` prefix. pub fn parse_with_server_name( - id: impl AsRef + Into>, + id: impl AsRef + Into, server_name: &ServerName, ) -> Result { let id_str = id.as_ref(); if id_str.starts_with('@') { - Self::parse(id) + Self::parse_into_owned(id.into()) } else { localpart_is_backwards_compatible(id_str)?; Ok(Self::from_borrowed(&format!("@{id_str}:{server_name}")).to_owned()) @@ -96,11 +96,13 @@ impl UserId { } /// Returns the user's localpart. + #[inline] pub fn localpart(&self) -> &str { &self.as_str()[1..self.colon_idx()] } /// Returns the server name of the user ID. + #[inline] pub fn server_name(&self) -> &ServerName { ServerName::from_borrowed(&self.as_str()[self.colon_idx() + 1..]) } @@ -125,6 +127,7 @@ impl UserId { /// deprecated. /// /// [strict grammar]: https://spec.matrix.org/latest/appendices/#user-identifiers + #[inline] pub fn validate_strict(&self) -> Result<(), IdParseError> { let is_fully_conforming = self.validate_fully_conforming()?; @@ -144,6 +147,7 @@ impl UserId { /// the latest grammar. /// /// [historical grammar]: https://spec.matrix.org/latest/appendices/#historical-user-ids + #[inline] pub fn validate_historical(&self) -> Result<(), IdParseError> { self.validate_fully_conforming()?; Ok(()) @@ -155,6 +159,7 @@ impl UserId { /// ID grammar but is still accepted because it was previously allowed. /// /// [historical user ID]: https://spec.matrix.org/latest/appendices/#historical-user-ids + #[inline] pub fn is_historical(&self) -> bool { self.validate_fully_conforming().is_ok_and(|is_fully_conforming| !is_fully_conforming) } @@ -172,6 +177,7 @@ impl UserId { /// display_name = "jplatte", /// ); /// ``` + #[inline] pub fn matrix_to_uri(&self) -> MatrixToUri { MatrixToUri::new(self.into(), Vec::new()) } @@ -192,6 +198,7 @@ impl UserId { /// display_name = "jplatte", /// ); /// ``` + #[inline] pub fn matrix_uri(&self, chat: bool) -> MatrixUri { MatrixUri::new(self.into(), Vec::new(), Some(UriAction::Chat).filter(|_| chat)) } diff --git a/crates/ruma-common/src/lib.rs b/crates/ruma-common/src/lib.rs index c158a943be..d38cfd045c 100644 --- a/crates/ruma-common/src/lib.rs +++ b/crates/ruma-common/src/lib.rs @@ -2,7 +2,6 @@ #![doc(html_logo_url = "https://ruma.dev/images/logo.png")] //! Common types for the Ruma crates. -#![recursion_limit = "1024"] #![warn(missing_docs)] // https://github.com/rust-lang/rust-clippy/issues/9029 #![allow(clippy::derive_partial_eq_without_eq)] @@ -42,7 +41,12 @@ pub mod to_device; use std::fmt; #[cfg(feature = "canonical-json")] -pub use self::canonical_json::{CanonicalJsonError, CanonicalJsonObject, CanonicalJsonValue}; +pub use self::canonical_json::{ + CanonicalJsonError, CanonicalJsonMember, CanonicalJsonMemberOptional, CanonicalJsonMemberRef, + CanonicalJsonMemberRefOptional, CanonicalJsonMembers, CanonicalJsonMembersOptional, + CanonicalJsonMembersRef, CanonicalJsonMembersRefOptional, CanonicalJsonName, + CanonicalJsonObject, CanonicalJsonValue, +}; pub use self::{ identifiers::*, time::{MilliSecondsSinceUnixEpoch, SecondsSinceUnixEpoch}, diff --git a/crates/ruma-common/src/presence.rs b/crates/ruma-common/src/presence.rs index b9886d2fbb..592a4f8e6e 100644 --- a/crates/ruma-common/src/presence.rs +++ b/crates/ruma-common/src/presence.rs @@ -17,6 +17,10 @@ pub enum PresenceState { #[default] Online, + /// Connected to the service and active but not available for chat. + #[cfg(feature = "unstable-msc3026")] + Busy, + /// Connected to the service but not available for chat. Unavailable, diff --git a/crates/ruma-common/src/push.rs b/crates/ruma-common/src/push.rs index a35e3f4b73..05e1c46d62 100644 --- a/crates/ruma-common/src/push.rs +++ b/crates/ruma-common/src/push.rs @@ -18,6 +18,8 @@ use std::hash::{Hash, Hasher}; use indexmap::{Equivalent, IndexSet}; use serde::{Deserialize, Serialize}; +use smallstr::SmallString; +use smallvec::SmallVec; use thiserror::Error; use tracing::instrument; @@ -47,6 +49,18 @@ pub use self::{ }, }; +/// Push rule ID. +pub type RuleId = SmallString<[u8; 32]>; + +/// Opinionated vector of push actions. +pub type Actions = SmallVec<[Action; 3]>; + +/// Opinionated vector of push conditions. +pub type PushConditions = SmallVec<[PushCondition; 3]>; + +/// String type for patterns in some condition types. +pub type Pattern = SmallString<[u8; 32]>; + /// A push ruleset scopes a set of rules according to some criteria. /// /// For example, some rules may only be applied for messages from a particular sender, a particular @@ -268,7 +282,7 @@ impl Ruleset { &mut self, kind: RuleKind, rule_id: impl AsRef, - actions: Vec, + actions: Actions, ) -> Result<(), RuleNotFoundError> { let rule_id = rule_id.as_ref(); @@ -316,7 +330,7 @@ impl Ruleset { /// /// * `event` - The raw JSON of a room message event. /// * `context` - The context of the message and room at the time of the event. - #[instrument(skip_all, fields(context.room_id = %context.room_id))] + #[instrument(level = "debug", skip_all, fields(context.room_id = %context.room_id))] pub async fn get_match( &self, event: &Raw, @@ -346,7 +360,7 @@ impl Ruleset { /// /// * `event` - The raw JSON of a room message event. /// * `context` - The context of the message and room at the time of the event. - #[instrument(skip_all, fields(context.room_id = %context.room_id))] + #[instrument(level = "debug", skip_all, fields(context.room_id = %context.room_id))] pub async fn get_actions( &self, event: &Raw, @@ -413,7 +427,7 @@ impl Ruleset { #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub struct SimplePushRule { /// Actions to determine if and how a notification is delivered for events matching this rule. - pub actions: Vec, + pub actions: Actions, /// Whether this is a default rule, or has been set explicitly. pub default: bool, @@ -435,7 +449,7 @@ pub struct SimplePushRule { #[allow(clippy::exhaustive_structs)] pub struct SimplePushRuleInit { /// Actions to determine if and how a notification is delivered for events matching this rule. - pub actions: Vec, + pub actions: Actions, /// Whether this is a default rule, or has been set explicitly. pub default: bool, @@ -498,7 +512,7 @@ where #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub struct ConditionalPushRule { /// Actions to determine if and how a notification is delivered for events matching this rule. - pub actions: Vec, + pub actions: Actions, /// Whether this is a default rule, or has been set explicitly. pub default: bool, @@ -507,14 +521,14 @@ pub struct ConditionalPushRule { pub enabled: bool, /// The ID of this rule. - pub rule_id: String, + pub rule_id: RuleId, /// The conditions that must hold true for an event in order for a rule to be applied to an /// event. /// /// A rule with no conditions always matches. #[serde(default)] - pub conditions: Vec, + pub conditions: PushConditions, } impl ConditionalPushRule { @@ -578,7 +592,7 @@ impl ConditionalPushRule { #[allow(clippy::exhaustive_structs)] pub struct ConditionalPushRuleInit { /// Actions to determine if and how a notification is delivered for events matching this rule. - pub actions: Vec, + pub actions: Actions, /// Whether this is a default rule, or has been set explicitly. pub default: bool, @@ -587,13 +601,13 @@ pub struct ConditionalPushRuleInit { pub enabled: bool, /// The ID of this rule. - pub rule_id: String, + pub rule_id: RuleId, /// The conditions that must hold true for an event in order for a rule to be applied to an /// event. /// /// A rule with no conditions always matches. - pub conditions: Vec, + pub conditions: PushConditions, } impl From for ConditionalPushRule { @@ -622,7 +636,7 @@ impl Eq for ConditionalPushRule {} impl Equivalent for str { fn equivalent(&self, key: &ConditionalPushRule) -> bool { - self == key.rule_id + self == &*key.rule_id } } @@ -636,7 +650,7 @@ impl Equivalent for str { #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub struct PatternedPushRule { /// Actions to determine if and how a notification is delivered for events matching this rule. - pub actions: Vec, + pub actions: Actions, /// Whether this is a default rule, or has been set explicitly. pub default: bool, @@ -645,10 +659,10 @@ pub struct PatternedPushRule { pub enabled: bool, /// The ID of this rule. - pub rule_id: String, + pub rule_id: RuleId, /// The glob-style pattern to match against. - pub pattern: String, + pub pattern: Pattern, } impl PatternedPushRule { @@ -688,7 +702,7 @@ impl PatternedPushRule { #[allow(clippy::exhaustive_structs)] pub struct PatternedPushRuleInit { /// Actions to determine if and how a notification is delivered for events matching this rule. - pub actions: Vec, + pub actions: Actions, /// Whether this is a default rule, or has been set explicitly. pub default: bool, @@ -697,10 +711,10 @@ pub struct PatternedPushRuleInit { pub enabled: bool, /// The ID of this rule. - pub rule_id: String, + pub rule_id: RuleId, /// The glob-style pattern to match against. - pub pattern: String, + pub pattern: Pattern, } impl From for PatternedPushRule { @@ -729,7 +743,7 @@ impl Eq for PatternedPushRule {} impl Equivalent for str { fn equivalent(&self, key: &PatternedPushRule) -> bool { - self == key.rule_id + self == &*key.rule_id } } @@ -867,12 +881,12 @@ pub struct NewSimplePushRule { /// Actions to determine if and how a notification is delivered for events matching this /// rule. - pub actions: Vec, + pub actions: Actions, } impl NewSimplePushRule { /// Creates a `NewSimplePushRule` with the given ID and actions. - pub fn new(rule_id: T, actions: Vec) -> Self { + pub fn new(rule_id: T, actions: Actions) -> Self { Self { rule_id, actions } } } @@ -889,19 +903,19 @@ impl From> for SimplePushRule { #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub struct NewPatternedPushRule { /// The ID of this rule. - pub rule_id: String, + pub rule_id: RuleId, /// The glob-style pattern to match against. - pub pattern: String, + pub pattern: Pattern, /// Actions to determine if and how a notification is delivered for events matching this /// rule. - pub actions: Vec, + pub actions: Actions, } impl NewPatternedPushRule { /// Creates a `NewPatternedPushRule` with the given ID, pattern and actions. - pub fn new(rule_id: String, pattern: String, actions: Vec) -> Self { + pub fn new(rule_id: RuleId, pattern: Pattern, actions: Actions) -> Self { Self { rule_id, pattern, actions } } } @@ -918,23 +932,23 @@ impl From for PatternedPushRule { #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub struct NewConditionalPushRule { /// The ID of this rule. - pub rule_id: String, + pub rule_id: RuleId, /// The conditions that must hold true for an event in order for a rule to be applied to an /// event. /// /// A rule with no conditions always matches. #[serde(default)] - pub conditions: Vec, + pub conditions: PushConditions, /// Actions to determine if and how a notification is delivered for events matching this /// rule. - pub actions: Vec, + pub actions: Actions, } impl NewConditionalPushRule { /// Creates a `NewConditionalPushRule` with the given ID, conditions and actions. - pub fn new(rule_id: String, conditions: Vec, actions: Vec) -> Self { + pub fn new(rule_id: RuleId, conditions: PushConditions, actions: Actions) -> Self { Self { rule_id, conditions, actions } } } @@ -1064,8 +1078,9 @@ mod tests { conditions: vec![PushCondition::EventMatch { key: "type".into(), pattern: "m.call.invite".into(), - }], - actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))], + }] + .into(), + actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))].into(), rule_id: ".m.rule.call".into(), enabled: true, default: true, @@ -1113,8 +1128,9 @@ mod tests { conditions: vec![PushCondition::EventMatch { key: "room_id".into(), pattern: "!roomid:matrix.org".into(), - }], - actions: vec![], + }] + .into(), + actions: Default::default(), rule_id: "!roomid:matrix.org".into(), enabled: true, default: false, @@ -1122,8 +1138,8 @@ mod tests { assert!(added); let added = set.override_.insert(ConditionalPushRule { - conditions: vec![], - actions: vec![], + conditions: Default::default(), + actions: Default::default(), rule_id: ".m.rule.suppress_notices".into(), enabled: false, default: true, @@ -1162,7 +1178,7 @@ mod tests { #[test] fn serialize_conditional_push_rule() { let rule = ConditionalPushRule { - actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))], + actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))].into(), default: true, enabled: true, rule_id: ".m.rule.call".into(), @@ -1171,7 +1187,8 @@ mod tests { PushCondition::ContainsDisplayName, PushCondition::RoomMemberCount { is: RoomMemberCountIs::gt(uint!(2)) }, PushCondition::SenderNotificationPermission { key: "room".into() }, - ], + ] + .into(), }; let rule_value: JsonValue = to_json_value(rule).unwrap(); @@ -1212,7 +1229,7 @@ mod tests { #[test] fn serialize_simple_push_rule() { let rule = SimplePushRule { - actions: vec![Action::Notify], + actions: vec![Action::Notify].into(), default: false, enabled: false, rule_id: owned_room_id!("!roomid:server.name"), @@ -1242,7 +1259,8 @@ mod tests { name: "dance".into(), value: RawJsonValue::from_string("true".into()).unwrap(), }), - ], + ] + .into(), default: true, enabled: true, pattern: "user_id".into(), @@ -1280,12 +1298,14 @@ mod tests { conditions: vec![ PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)) }, PushCondition::EventMatch { key: "type".into(), pattern: "m.room.message".into() }, - ], + ] + .into(), actions: vec![ Action::Notify, Action::SetTweak(Tweak::Sound("default".into())), Action::SetTweak(Tweak::Highlight(false)), - ], + ] + .into(), rule_id: ".m.rule.room_one_to_one".into(), enabled: true, default: true, @@ -1295,7 +1315,8 @@ mod tests { Action::Notify, Action::SetTweak(Tweak::Sound("default".into())), Action::SetTweak(Tweak::Highlight(true)), - ], + ] + .into(), rule_id: ".m.rule.contains_user_name".into(), pattern: "user_id".into(), enabled: true, @@ -1607,13 +1628,14 @@ mod tests { let mut set = Ruleset::new(); let disabled = ConditionalPushRule { - actions: vec![Action::Notify], + actions: vec![Action::Notify].into(), default: false, enabled: false, rule_id: "disabled".into(), conditions: vec![PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)), - }], + }] + .into(), }; set.underride.insert(disabled); @@ -1621,11 +1643,11 @@ mod tests { assert_matches!(test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await, []); let no_conditions = ConditionalPushRule { - actions: vec![Action::SetTweak(Tweak::Highlight(true))], + actions: vec![Action::SetTweak(Tweak::Highlight(true))].into(), default: false, enabled: true, rule_id: "no.conditions".into(), - conditions: vec![], + conditions: vec![].into(), }; set.underride.insert(no_conditions); @@ -1636,7 +1658,7 @@ mod tests { ); let sender = SimplePushRule { - actions: vec![Action::Notify], + actions: vec![Action::Notify].into(), default: false, enabled: true, rule_id: owned_user_id!("@rantanplan:server.name"), @@ -1650,7 +1672,7 @@ mod tests { ); let room = SimplePushRule { - actions: vec![Action::SetTweak(Tweak::Highlight(true))], + actions: vec![Action::SetTweak(Tweak::Highlight(true))].into(), default: false, enabled: true, rule_id: owned_room_id!("!dm:server.name"), @@ -1664,7 +1686,7 @@ mod tests { ); let content = PatternedPushRule { - actions: vec![Action::SetTweak(Tweak::Sound("content".into()))], + actions: vec![Action::SetTweak(Tweak::Sound("content".into()))].into(), default: false, enabled: true, rule_id: "content".into(), @@ -1680,7 +1702,7 @@ mod tests { assert_eq!(sound, "content"); let three_conditions = ConditionalPushRule { - actions: vec![Action::SetTweak(Tweak::Sound("three".into()))], + actions: vec![Action::SetTweak(Tweak::Sound("three".into()))].into(), default: false, enabled: true, rule_id: "three.conditions".into(), @@ -1691,7 +1713,8 @@ mod tests { key: "room_id".into(), pattern: "!dm:server.name".into(), }, - ], + ] + .into(), }; set.override_.insert(three_conditions); diff --git a/crates/ruma-common/src/push/condition.rs b/crates/ruma-common/src/push/condition.rs index f615763a10..57e1a73def 100644 --- a/crates/ruma-common/src/push/condition.rs +++ b/crates/ruma-common/src/push/condition.rs @@ -68,6 +68,8 @@ impl RoomVersionFeature { | RoomVersionId::_Custom(_) => vec![], #[cfg(feature = "unstable-msc2870")] RoomVersionId::MSC2870 => vec![], + #[cfg(feature = "unstable-msc4361")] + RoomVersionId::MSC4361 => vec![], } } } @@ -501,13 +503,16 @@ impl StrExt for str { } fn matches_pattern(&self, pattern: &str, match_words: bool) -> bool { - let value = &self.to_lowercase(); - let pattern = &pattern.to_lowercase(); - if match_words { + if self.eq_ignore_ascii_case(pattern) { + return true; + } + + let value = &self.to_lowercase(); + let pattern = &pattern.to_lowercase(); value.matches_word(pattern) } else { - WildMatch::new(pattern).matches(value) + WildMatch::new_case_insensitive(pattern).matches(self) } } diff --git a/crates/ruma-common/src/push/condition/flattened_json.rs b/crates/ruma-common/src/push/condition/flattened_json.rs index cd5c62e8aa..8f55750ab7 100644 --- a/crates/ruma-common/src/push/condition/flattened_json.rs +++ b/crates/ruma-common/src/push/condition/flattened_json.rs @@ -29,7 +29,7 @@ impl FlattenedJson { } /// Flatten and insert the `value` at `path`. - #[instrument(skip(self, value))] + #[instrument(level = "debug", skip(self, value))] fn flatten_value(&mut self, value: JsonValue, path: String) { match value { JsonValue::Object(fields) => { diff --git a/crates/ruma-common/src/push/predefined.rs b/crates/ruma-common/src/push/predefined.rs index 645ea26fed..5b27bf3118 100644 --- a/crates/ruma-common/src/push/predefined.rs +++ b/crates/ruma-common/src/push/predefined.rs @@ -144,55 +144,59 @@ impl ConditionalPushRule { /// generated by override rules set by the user. pub fn master() -> Self { Self { - actions: vec![], + actions: Default::default(), default: true, enabled: false, - rule_id: PredefinedOverrideRuleId::Master.to_string(), - conditions: vec![], + rule_id: PredefinedOverrideRuleId::Master.to_string().into(), + conditions: Default::default(), } } /// Matches messages with a `msgtype` of `notice`. pub fn suppress_notices() -> Self { Self { - actions: vec![], + actions: Default::default(), default: true, enabled: true, - rule_id: PredefinedOverrideRuleId::SuppressNotices.to_string(), + rule_id: PredefinedOverrideRuleId::SuppressNotices.to_string().into(), conditions: vec![EventMatch { key: "content.msgtype".into(), pattern: "m.notice".into(), - }], + }] + .into(), } } /// Matches any invites to a new room for this user. pub fn invite_for_me(user_id: &UserId) -> Self { Self { - actions: vec![ + actions: [ Notify, SetTweak(Tweak::Sound("default".into())), SetTweak(Tweak::Highlight(false)), - ], + ] + .into(), default: true, enabled: true, - rule_id: PredefinedOverrideRuleId::InviteForMe.to_string(), + rule_id: PredefinedOverrideRuleId::InviteForMe.as_str().into(), conditions: vec![ EventMatch { key: "type".into(), pattern: "m.room.member".into() }, EventMatch { key: "content.membership".into(), pattern: "invite".into() }, EventMatch { key: "state_key".into(), pattern: user_id.to_string() }, - ], + ] + .into(), } } /// Matches any `m.room.member_event`. pub fn member_event() -> Self { Self { - actions: vec![], + actions: Default::default(), default: true, enabled: true, - rule_id: PredefinedOverrideRuleId::MemberEvent.to_string(), - conditions: vec![EventMatch { key: "type".into(), pattern: "m.room.member".into() }], + rule_id: PredefinedOverrideRuleId::MemberEvent.as_str().into(), + conditions: vec![EventMatch { key: "type".into(), pattern: "m.room.member".into() }] + .into(), } } @@ -204,14 +208,16 @@ impl ConditionalPushRule { Notify, SetTweak(Tweak::Sound("default".to_owned())), SetTweak(Tweak::Highlight(true)), - ], + ] + .into(), default: true, enabled: true, - rule_id: PredefinedOverrideRuleId::IsUserMention.to_string(), + rule_id: PredefinedOverrideRuleId::IsUserMention.as_str().into(), conditions: vec![EventPropertyContains { key: r"content.m\.mentions.user_ids".to_owned(), value: user_id.as_str().into(), - }], + }] + .into(), } } @@ -228,11 +234,12 @@ impl ConditionalPushRule { Notify, SetTweak(Tweak::Sound("default".into())), SetTweak(Tweak::Highlight(true)), - ], + ] + .into(), default: true, enabled: true, - rule_id: PredefinedOverrideRuleId::ContainsDisplayName.to_string(), - conditions: vec![ContainsDisplayName], + rule_id: PredefinedOverrideRuleId::ContainsDisplayName.as_str().into(), + conditions: vec![ContainsDisplayName].into(), } } @@ -241,14 +248,15 @@ impl ConditionalPushRule { /// similar to what an `@room` notification would accomplish. pub fn tombstone() -> Self { Self { - actions: vec![Notify, SetTweak(Tweak::Highlight(true))], + actions: vec![Notify, SetTweak(Tweak::Highlight(true))].into(), default: true, enabled: true, - rule_id: PredefinedOverrideRuleId::Tombstone.to_string(), + rule_id: PredefinedOverrideRuleId::Tombstone.as_str().into(), conditions: vec![ EventMatch { key: "type".into(), pattern: "m.room.tombstone".into() }, EventMatch { key: "state_key".into(), pattern: "".into() }, - ], + ] + .into(), } } @@ -256,14 +264,15 @@ impl ConditionalPushRule { /// the `m.mentions` property set to `true`. pub fn is_room_mention() -> Self { Self { - actions: vec![Notify, SetTweak(Tweak::Highlight(true))], + actions: vec![Notify, SetTweak(Tweak::Highlight(true))].into(), default: true, enabled: true, - rule_id: PredefinedOverrideRuleId::IsRoomMention.to_string(), + rule_id: PredefinedOverrideRuleId::IsRoomMention.as_str().into(), conditions: vec![ EventPropertyIs { key: r"content.m\.mentions.room".to_owned(), value: true.into() }, SenderNotificationPermission { key: NotificationPowerLevelsKey::Room }, - ], + ] + .into(), } } @@ -276,14 +285,15 @@ impl ConditionalPushRule { pub fn roomnotif() -> Self { #[allow(deprecated)] Self { - actions: vec![Notify, SetTweak(Tweak::Highlight(true))], + actions: vec![Notify, SetTweak(Tweak::Highlight(true))].into(), default: true, enabled: true, - rule_id: PredefinedOverrideRuleId::RoomNotif.to_string(), + rule_id: PredefinedOverrideRuleId::RoomNotif.as_str().into(), conditions: vec![ EventMatch { key: "content.body".into(), pattern: "@room".into() }, SenderNotificationPermission { key: "room".into() }, - ], + ] + .into(), } } @@ -292,11 +302,12 @@ impl ConditionalPushRule { /// [reactions]: https://spec.matrix.org/latest/client-server-api/#event-annotations-and-reactions pub fn reaction() -> Self { Self { - actions: vec![], + actions: Default::default(), default: true, enabled: true, - rule_id: PredefinedOverrideRuleId::Reaction.to_string(), - conditions: vec![EventMatch { key: "type".into(), pattern: "m.reaction".into() }], + rule_id: PredefinedOverrideRuleId::Reaction.as_str().into(), + conditions: vec![EventMatch { key: "type".into(), pattern: "m.reaction".into() }] + .into(), } } @@ -305,14 +316,15 @@ impl ConditionalPushRule { /// [room server ACLs]: https://spec.matrix.org/latest/client-server-api/#server-access-control-lists-acls-for-rooms pub fn server_acl() -> Self { Self { - actions: vec![], + actions: Default::default(), default: true, enabled: true, - rule_id: PredefinedOverrideRuleId::RoomServerAcl.to_string(), + rule_id: PredefinedOverrideRuleId::RoomServerAcl.as_str().into(), conditions: vec![ EventMatch { key: "type".into(), pattern: "m.room.server_acl".into() }, EventMatch { key: "state_key".into(), pattern: "".into() }, - ], + ] + .into(), } } @@ -321,14 +333,15 @@ impl ConditionalPushRule { /// [event replacements]: https://spec.matrix.org/latest/client-server-api/#event-replacements pub fn suppress_edits() -> Self { Self { - actions: vec![], + actions: Default::default(), default: true, enabled: true, - rule_id: PredefinedOverrideRuleId::SuppressEdits.to_string(), + rule_id: PredefinedOverrideRuleId::SuppressEdits.as_str().into(), conditions: vec![EventPropertyIs { key: r"content.m\.relates_to.rel_type".to_owned(), value: "m.replace".into(), - }], + }] + .into(), } } @@ -341,14 +354,15 @@ impl ConditionalPushRule { #[cfg(feature = "unstable-msc3930")] pub fn poll_response() -> Self { Self { - rule_id: PredefinedOverrideRuleId::PollResponse.to_string(), + rule_id: PredefinedOverrideRuleId::PollResponse.as_str().into(), default: true, enabled: true, conditions: vec![EventPropertyIs { key: "type".to_owned(), value: "org.matrix.msc3381.poll.response".into(), - }], - actions: vec![], + }] + .into(), + actions: Default::default(), } } } @@ -364,7 +378,7 @@ impl PatternedPushRule { pub fn contains_user_name(user_id: &UserId) -> Self { #[allow(deprecated)] Self { - rule_id: PredefinedContentRuleId::ContainsUserName.to_string(), + rule_id: PredefinedContentRuleId::ContainsUserName.as_str().into(), enabled: true, default: true, pattern: user_id.localpart().into(), @@ -372,7 +386,8 @@ impl PatternedPushRule { Notify, SetTweak(Tweak::Sound("default".into())), SetTweak(Tweak::Highlight(true)), - ], + ] + .into(), } } } @@ -382,15 +397,17 @@ impl ConditionalPushRule { /// Matches any incoming VOIP call. pub fn call() -> Self { Self { - rule_id: PredefinedUnderrideRuleId::Call.to_string(), + rule_id: PredefinedUnderrideRuleId::Call.as_str().into(), default: true, enabled: true, - conditions: vec![EventMatch { key: "type".into(), pattern: "m.call.invite".into() }], + conditions: vec![EventMatch { key: "type".into(), pattern: "m.call.invite".into() }] + .into(), actions: vec![ Notify, SetTweak(Tweak::Sound("ring".into())), SetTweak(Tweak::Highlight(false)), - ], + ] + .into(), } } @@ -401,47 +418,52 @@ impl ConditionalPushRule { /// either matches all events that are encrypted (in 1:1 rooms) or none. pub fn encrypted_room_one_to_one() -> Self { Self { - rule_id: PredefinedUnderrideRuleId::EncryptedRoomOneToOne.to_string(), + rule_id: PredefinedUnderrideRuleId::EncryptedRoomOneToOne.as_str().into(), default: true, enabled: true, conditions: vec![ RoomMemberCount { is: RoomMemberCountIs::from(js_int::uint!(2)) }, EventMatch { key: "type".into(), pattern: "m.room.encrypted".into() }, - ], + ] + .into(), actions: vec![ Notify, SetTweak(Tweak::Sound("default".into())), SetTweak(Tweak::Highlight(false)), - ], + ] + .into(), } } /// Matches any message sent in a room with exactly two members. pub fn room_one_to_one() -> Self { Self { - rule_id: PredefinedUnderrideRuleId::RoomOneToOne.to_string(), + rule_id: PredefinedUnderrideRuleId::RoomOneToOne.as_str().into(), default: true, enabled: true, conditions: vec![ RoomMemberCount { is: RoomMemberCountIs::from(js_int::uint!(2)) }, EventMatch { key: "type".into(), pattern: "m.room.message".into() }, - ], + ] + .into(), actions: vec![ Notify, SetTweak(Tweak::Sound("default".into())), SetTweak(Tweak::Highlight(false)), - ], + ] + .into(), } } /// Matches all chat messages. pub fn message() -> Self { Self { - rule_id: PredefinedUnderrideRuleId::Message.to_string(), + rule_id: PredefinedUnderrideRuleId::Message.as_str().into(), default: true, enabled: true, - conditions: vec![EventMatch { key: "type".into(), pattern: "m.room.message".into() }], - actions: vec![Notify, SetTweak(Tweak::Highlight(false))], + conditions: vec![EventMatch { key: "type".into(), pattern: "m.room.message".into() }] + .into(), + actions: vec![Notify, SetTweak(Tweak::Highlight(false))].into(), } } @@ -452,11 +474,12 @@ impl ConditionalPushRule { /// either matches all events that are encrypted (in group rooms) or none. pub fn encrypted() -> Self { Self { - rule_id: PredefinedUnderrideRuleId::Encrypted.to_string(), + rule_id: PredefinedUnderrideRuleId::Encrypted.as_str().into(), default: true, enabled: true, - conditions: vec![EventMatch { key: "type".into(), pattern: "m.room.encrypted".into() }], - actions: vec![Notify, SetTweak(Tweak::Highlight(false))], + conditions: vec![EventMatch { key: "type".into(), pattern: "m.room.encrypted".into() }] + .into(), + actions: vec![Notify, SetTweak(Tweak::Highlight(false))].into(), } } @@ -469,7 +492,7 @@ impl ConditionalPushRule { #[cfg(feature = "unstable-msc3930")] pub fn poll_start_one_to_one() -> Self { Self { - rule_id: PredefinedUnderrideRuleId::PollStartOneToOne.to_string(), + rule_id: PredefinedUnderrideRuleId::PollStartOneToOne.as_str().into(), default: true, enabled: true, conditions: vec![ @@ -478,8 +501,9 @@ impl ConditionalPushRule { key: "type".to_owned(), value: "org.matrix.msc3381.poll.start".into(), }, - ], - actions: vec![Notify, SetTweak(Tweak::Sound("default".into()))], + ] + .into(), + actions: vec![Notify, SetTweak(Tweak::Sound("default".into()))].into(), } } @@ -492,14 +516,15 @@ impl ConditionalPushRule { #[cfg(feature = "unstable-msc3930")] pub fn poll_start() -> Self { Self { - rule_id: PredefinedUnderrideRuleId::PollStart.to_string(), + rule_id: PredefinedUnderrideRuleId::PollStart.as_str().into(), default: true, enabled: true, conditions: vec![EventPropertyIs { key: "type".to_owned(), value: "org.matrix.msc3381.poll.start".into(), - }], - actions: vec![Notify], + }] + .into(), + actions: vec![Notify].into(), } } @@ -512,7 +537,7 @@ impl ConditionalPushRule { #[cfg(feature = "unstable-msc3930")] pub fn poll_end_one_to_one() -> Self { Self { - rule_id: PredefinedUnderrideRuleId::PollEndOneToOne.to_string(), + rule_id: PredefinedUnderrideRuleId::PollEndOneToOne.as_str().into(), default: true, enabled: true, conditions: vec![ @@ -521,8 +546,9 @@ impl ConditionalPushRule { key: "type".to_owned(), value: "org.matrix.msc3381.poll.end".into(), }, - ], - actions: vec![Notify, SetTweak(Tweak::Sound("default".into()))], + ] + .into(), + actions: vec![Notify, SetTweak(Tweak::Sound("default".into()))].into(), } } @@ -535,14 +561,15 @@ impl ConditionalPushRule { #[cfg(feature = "unstable-msc3930")] pub fn poll_end() -> Self { Self { - rule_id: PredefinedUnderrideRuleId::PollEnd.to_string(), + rule_id: PredefinedUnderrideRuleId::PollEnd.as_str().into(), default: true, enabled: true, conditions: vec![EventPropertyIs { key: "type".to_owned(), value: "org.matrix.msc3381.poll.end".into(), - }], - actions: vec![Notify], + }] + .into(), + actions: vec![Notify].into(), } } @@ -554,11 +581,11 @@ impl ConditionalPushRule { #[cfg(feature = "unstable-msc4306")] pub fn unsubscribed_thread() -> Self { Self { - rule_id: PredefinedUnderrideRuleId::UnsubscribedThread.to_string(), + rule_id: PredefinedUnderrideRuleId::UnsubscribedThread.as_str().into(), default: true, enabled: true, - conditions: vec![ThreadSubscription { subscribed: false }], - actions: vec![], + conditions: vec![ThreadSubscription { subscribed: false }].into(), + actions: Default::default(), } } @@ -570,11 +597,11 @@ impl ConditionalPushRule { #[cfg(feature = "unstable-msc4306")] pub fn subscribed_thread() -> Self { Self { - rule_id: PredefinedUnderrideRuleId::SubscribedThread.to_string(), + rule_id: PredefinedUnderrideRuleId::SubscribedThread.as_str().into(), default: true, enabled: true, - conditions: vec![ThreadSubscription { subscribed: true }], - actions: vec![Notify, SetTweak(Tweak::Sound("default".into()))], + conditions: vec![ThreadSubscription { subscribed: true }].into(), + actions: vec![Notify, SetTweak(Tweak::Sound("default".into()))].into(), } } } @@ -810,23 +837,23 @@ mod tests { let override_ = [ // Default `.m.rule.master` push rule with non-default state. - assign!(ConditionalPushRule::master(), { enabled: true, actions: vec![Action::Notify]}), + assign!(ConditionalPushRule::master(), { enabled: true, actions: vec![Action::Notify].into()}), // User-defined push rule. ConditionalPushRuleInit { - actions: vec![], + actions: Default::default(), default: false, enabled: false, - rule_id: user_rule_id.to_owned(), - conditions: vec![], + rule_id: user_rule_id.into(), + conditions: Default::default(), } .into(), // Old server-default push rule. ConditionalPushRuleInit { - actions: vec![], + actions: Default::default(), default: true, enabled: true, - rule_id: default_rule_id.to_owned(), - conditions: vec![], + rule_id: default_rule_id.into(), + conditions: Default::default(), } .into(), ] diff --git a/crates/ruma-common/src/room.rs b/crates/ruma-common/src/room.rs index bd01266183..e8cbd18ff7 100644 --- a/crates/ruma-common/src/room.rs +++ b/crates/ruma-common/src/room.rs @@ -8,8 +8,8 @@ use serde::{de, Deserialize, Serialize}; use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue}; use crate::{ - serde::{from_raw_json_value, StringEnum}, - EventEncryptionAlgorithm, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, PrivOwnedStr, + serde::{from_raw_json_value, JsonObject, StringEnum}, + EventEncryptionAlgorithm, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, PrivOwnedStr, RoomId, RoomVersionId, }; @@ -29,8 +29,9 @@ pub enum RoomType { /// The rule used for users wishing to join this room. /// -/// This type can hold an arbitrary string. To check for values that are not available as a -/// documented variant here, use its string representation, obtained through `.as_str()`. +/// This type can hold an arbitrary join rule. To check for values that are not available as a +/// documented variant here, get its kind with [`.kind()`](Self::kind) or its string representation +/// with [`.as_str()`](Self::as_str), and its associated data with [`.data()`](Self::data). #[derive(Clone, Debug, PartialEq, Eq, Serialize)] #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] #[serde(tag = "join_rule", rename_all = "snake_case")] @@ -59,8 +60,7 @@ pub enum JoinRule { Public, #[doc(hidden)] - #[serde(skip_serializing)] - _Custom(PrivOwnedStr), + _Custom(CustomJoinRule), } impl JoinRule { @@ -73,7 +73,9 @@ impl JoinRule { Self::Restricted(_) => JoinRuleKind::Restricted, Self::KnockRestricted(_) => JoinRuleKind::KnockRestricted, Self::Public => JoinRuleKind::Public, - Self::_Custom(rule) => JoinRuleKind::_Custom(rule.clone()), + Self::_Custom(CustomJoinRule { join_rule, .. }) => { + JoinRuleKind::_Custom(PrivOwnedStr(join_rule.as_str().into())) + } } } @@ -86,8 +88,55 @@ impl JoinRule { JoinRule::Restricted(_) => "restricted", JoinRule::KnockRestricted(_) => "knock_restricted", JoinRule::Public => "public", - JoinRule::_Custom(rule) => &rule.0, + JoinRule::_Custom(CustomJoinRule { join_rule, .. }) => join_rule, + } + } + + /// Returns the associated data of this `JoinRule`. + /// + /// The returned JSON object won't contain the `join_rule` field, use + /// [`.kind()`](Self::kind) or [`.as_str()`](Self::as_str) to access those. + /// + /// Prefer to use the public variants of `JoinRule` where possible; this method is meant to + /// be used for custom join rules only. + pub fn data(&self) -> Cow<'_, JsonObject> { + fn serialize(obj: &T) -> JsonObject { + match serde_json::to_value(obj).expect("join rule serialization should succeed") { + JsonValue::Object(mut obj) => { + obj.remove("body"); + obj + } + _ => panic!("all message types should serialize to objects"), + } } + + match self { + JoinRule::Invite | JoinRule::Knock | JoinRule::Private | JoinRule::Public => { + Cow::Owned(JsonObject::new()) + } + JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted) => { + Cow::Owned(serialize(restricted)) + } + Self::_Custom(c) => Cow::Borrowed(&c.data), + } + } + + /// Iterates room_id's for all AllowRules for the JoinRule variants Restricted and + /// KnockRestricted. For other variants no items are produced. + #[inline] + pub fn allowed_room_ids(&self) -> impl Iterator { + self.allow_rules() + .filter_map(|allow| as_variant!(allow, AllowRule::RoomMembership)) + .map(|room_membership| room_membership.room_id.as_ref()) + } + + /// Iterates AllowRules for JoinRule variants Restricted and KnockRestricted. For other variants + /// no items are produced. + #[inline] + pub fn allow_rules(&self) -> impl Iterator { + as_variant!(self, Self::Restricted | Self::KnockRestricted) + .into_iter() + .flat_map(|restricted| restricted.allow.iter()) } } @@ -116,11 +165,23 @@ impl<'de> Deserialize<'de> for JoinRule { "restricted" => from_raw_json_value(&json).map(Self::Restricted), "knock_restricted" => from_raw_json_value(&json).map(Self::KnockRestricted), "public" => Ok(Self::Public), - _ => Ok(Self::_Custom(PrivOwnedStr(join_rule.into()))), + _ => from_raw_json_value(&json).map(Self::_Custom), } } } +/// The payload for an unsupported join rule. +#[doc(hidden)] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct CustomJoinRule { + /// The kind of join rule. + join_rule: String, + + /// The remaining data. + #[serde(flatten)] + data: JsonObject, +} + /// Configuration of the `Restricted` join rule. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] @@ -405,8 +466,16 @@ impl<'de> Deserialize<'de> for RoomSummary { /// The rule used for users wishing to join a room. /// -/// In contrast to the regular `JoinRule` in `ruma_events`, this enum holds only simplified -/// conditions for joining restricted rooms. +/// In contrast to the regular [`JoinRule`], this enum holds only simplified conditions for joining +/// restricted rooms. +/// +/// This type can hold an arbitrary join rule. To check for values that are not available as a +/// documented variant here, get its kind with `.kind()` or use its string representation, obtained +/// through `.as_str()`. +/// +/// This type will fail to serialize if it doesn't match one of the documented variants. It is only +/// possible to construct an undocumented variant by deserializing it, so do not re-serialize this +/// type. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] #[serde(tag = "join_rule", rename_all = "snake_case")] @@ -466,6 +535,16 @@ impl JoinRuleSummary { Self::_Custom(rule) => &rule.0, } } + + /// Iterates allowed room_id's for Restricted and KnockRestricted rooms. All other variants + /// produce no items. + #[inline] + pub fn allowed_room_ids(&self) -> impl Iterator { + as_variant!(self, Self::Restricted | Self::KnockRestricted) + .into_iter() + .flat_map(|summary| summary.allowed_room_ids.iter()) + .map(AsRef::as_ref) + } } impl From for JoinRuleSummary { @@ -477,7 +556,9 @@ impl From for JoinRuleSummary { JoinRule::Restricted(restricted) => Self::Restricted(restricted.into()), JoinRule::KnockRestricted(restricted) => Self::KnockRestricted(restricted.into()), JoinRule::Public => Self::Public, - JoinRule::_Custom(rule) => Self::_Custom(rule), + JoinRule::_Custom(CustomJoinRule { join_rule, .. }) => { + Self::_Custom(PrivOwnedStr(join_rule.into())) + } } } } diff --git a/crates/ruma-common/src/room_version_rules.rs b/crates/ruma-common/src/room_version_rules.rs index 7d2739c28d..ede1166441 100644 --- a/crates/ruma-common/src/room_version_rules.rs +++ b/crates/ruma-common/src/room_version_rules.rs @@ -156,6 +156,12 @@ impl RoomVersionRules { redaction: RedactionRules::MSC2870, ..Self::V11 }; + + /// Rules for room version `org.matrix.msc4361` ([MSC4361]). + /// + /// [MSC4361]: https://github.com/matrix-org/matrix-spec-proposals/pull/4361 + #[cfg(feature = "unstable-msc4361")] + pub const MSC4361: Self = Self { authorization: AuthorizationRules::MSC4361, ..Self::V12 }; } /// The stability of a room version. @@ -361,6 +367,14 @@ pub struct AuthorizationRules { /// Whether to use the event ID of the `m.room.create` event of the room as the room ID, /// introduced in room version 12. pub room_create_event_id_as_room_id: bool, + + /// Whether to reject `m.room.member` events if the state key domain doesn't match the + /// `m.room.create` event sender's domain, if the room is not federated, introduced in + /// [MSC4361]. + /// + /// [MSC4361]: https://github.com/matrix-org/matrix-spec-proposals/pull/4361 + #[cfg(feature = "unstable-msc4361")] + pub reject_remote_members_in_nonfederated_rooms: bool, } impl AuthorizationRules { @@ -380,6 +394,8 @@ impl AuthorizationRules { explicitly_privilege_room_creators: false, additional_room_creators: false, room_create_event_id_as_room_id: false, + #[cfg(feature = "unstable-msc4361")] + reject_remote_members_in_nonfederated_rooms: false, }; /// Authorization rules with tweaks introduced in room version 3 ([spec]). @@ -425,6 +441,11 @@ impl AuthorizationRules { room_create_event_id_as_room_id: true, ..Self::V11 }; + + /// Authorization rules with tweaks introduced in MSC4361 + #[cfg(feature = "unstable-msc4361")] + pub const MSC4361: Self = + Self { reject_remote_members_in_nonfederated_rooms: true, ..Self::V12 }; } /// The tweaks in the [redaction] algorithm for a room version. diff --git a/crates/ruma-common/src/serde.rs b/crates/ruma-common/src/serde.rs index b4b026446a..f38c86e2b3 100644 --- a/crates/ruma-common/src/serde.rs +++ b/crates/ruma-common/src/serde.rs @@ -33,8 +33,9 @@ pub use self::{ raw::{JsonCastable, Raw}, strings::{ btreemap_deserialize_v1_powerlevel_values, deserialize_as_number_or_string, - deserialize_as_optional_number_or_string, deserialize_v1_powerlevel, empty_string_as_none, - none_as_empty_string, + deserialize_as_optional_number_or_string, deserialize_map_as_vec, + deserialize_v1_powerlevel, empty_string_as_none, none_as_empty_string, + vec_deserialize_int_powerlevel_values, vec_deserialize_v1_powerlevel_values, }, }; @@ -42,11 +43,13 @@ pub use self::{ pub type JsonObject = serde_json::Map; /// Check whether a value is equal to its default value. +#[inline] pub fn is_default(val: &T) -> bool { *val == T::default() } /// Deserialize a `T` via `Option`, falling back to `T::default()`. +#[inline] pub fn none_as_default<'de, D, T>(deserializer: D) -> Result where D: Deserializer<'de>, @@ -58,6 +61,7 @@ where /// Simply returns `true`. /// /// Useful for `#[serde(default = ...)]`. +#[inline] pub fn default_true() -> bool { true } @@ -66,11 +70,35 @@ pub fn default_true() -> bool { /// /// Useful for `#[serde(skip_serializing_if = ...)]`. #[allow(clippy::trivially_copy_pass_by_ref)] +#[inline] pub fn is_true(b: &bool) -> bool { *b } +/// Returns None if the serialization fails +pub fn or_empty<'de, D: Deserializer<'de>, T: for<'a> Deserialize<'a>>( + deserializer: D, +) -> Result, D::Error> { + let json = Box::::deserialize(deserializer)?; + + let res = serde_json::from_str::>(json.get()).map_err(de::Error::custom); + + match res { + Ok(a) => Ok(a), + Err(e) => { + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct Empty {} + if let Ok(Empty {}) = serde_json::from_str(json.get()) { + Ok(None) + } else { + Err(e) + } + } + } +} /// Helper function for `serde_json::value::RawValue` deserialization. +#[inline(never)] pub fn from_raw_json_value<'a, T, E>(val: &'a RawJsonValue) -> Result where T: Deserialize<'a>, diff --git a/crates/ruma-common/src/serde/base64.rs b/crates/ruma-common/src/serde/base64.rs index 78b3b39655..c097c6bdc8 100644 --- a/crates/ruma-common/src/serde/base64.rs +++ b/crates/ruma-common/src/serde/base64.rs @@ -75,16 +75,19 @@ impl Base64 { impl> Base64 { /// Create a `Base64` instance from raw bytes, to be base64-encoded in serialization. + #[inline] pub fn new(bytes: B) -> Self { Self { bytes, _phantom_conf: PhantomData } } /// Get a reference to the raw bytes held by this `Base64` instance. + #[inline] pub fn as_bytes(&self) -> &[u8] { self.bytes.as_ref() } /// Encode the bytes contained in this `Base64` instance to unpadded base64. + #[inline] pub fn encode(&self) -> String { Self::ENGINE.encode(self.as_bytes()) } @@ -92,6 +95,7 @@ impl> Base64 { impl Base64 { /// Get the raw bytes held by this `Base64` instance. + #[inline] pub fn into_inner(self) -> B { self.bytes } @@ -99,11 +103,13 @@ impl Base64 { impl Base64 { /// Create a `Base64` instance containing an empty `Vec`. + #[inline] pub fn empty() -> Self { Self::new(Vec::new()) } /// Parse some base64-encoded data to create a `Base64` instance. + #[inline] pub fn parse(encoded: impl AsRef<[u8]>) -> Result { Self::ENGINE.decode(encoded).map(Self::new).map_err(Base64DecodeError) } diff --git a/crates/ruma-common/src/serde/raw.rs b/crates/ruma-common/src/serde/raw.rs index 93628360d0..1380d9ea5b 100644 --- a/crates/ruma-common/src/serde/raw.rs +++ b/crates/ruma-common/src/serde/raw.rs @@ -112,10 +112,12 @@ impl Raw { impl Visitor<'_> for FieldVisitor<'_> { type Value = bool; + #[inline] fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!(formatter, "`{}`", self.0) } + #[inline] fn visit_str(self, value: &str) -> Result where E: de::Error, @@ -129,6 +131,7 @@ impl Raw { impl<'de> DeserializeSeed<'de> for Field<'_> { type Value = bool; + #[inline] fn deserialize(self, deserializer: D) -> Result where D: Deserializer<'de>, @@ -143,6 +146,7 @@ impl Raw { } impl<'b, T> SingleFieldVisitor<'b, T> { + #[inline] fn new(field_name: &'b str) -> Self { Self { field_name, _phantom: PhantomData } } @@ -154,10 +158,12 @@ impl Raw { { type Value = Option; + #[inline] fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("a string") } + #[inline(never)] fn visit_map(self, mut map: A) -> Result where A: MapAccess<'de>, @@ -180,6 +186,7 @@ impl Raw { } /// Try to deserialize the JSON as the expected type. + #[inline] pub fn deserialize<'a>(&'a self) -> serde_json::Result where T: Deserialize<'a>, @@ -188,6 +195,7 @@ impl Raw { } /// Try to deserialize the JSON as a custom type. + #[inline] pub fn deserialize_as<'a, U>(&'a self) -> serde_json::Result where T: JsonCastable, @@ -207,6 +215,7 @@ impl Raw { /// Turns `Raw` into `Raw` without changing the underlying JSON. /// /// This is useful for turning raw specific event types into raw event enum types. + #[inline] pub fn cast(self) -> Raw where T: JsonCastable, @@ -217,6 +226,7 @@ impl Raw { /// Turns `&Raw` into `&Raw` without changing the underlying JSON. /// /// This is useful for turning raw specific event types into raw event enum types. + #[inline] pub fn cast_ref(&self) -> &Raw where T: JsonCastable, @@ -225,11 +235,13 @@ impl Raw { } /// Same as [`cast`][Self::cast], but without the trait restriction. + #[inline] pub fn cast_unchecked(self) -> Raw { Raw::from_json(self.into_json()) } /// Same as [`cast_ref`][Self::cast_ref], but without the trait restriction. + #[inline] pub fn cast_ref_unchecked(&self) -> &Raw { unsafe { mem::transmute(self) } } @@ -249,6 +261,7 @@ impl Debug for Raw { } impl<'de, T> Deserialize<'de> for Raw { + #[inline] fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/crates/ruma-common/src/serde/strings.rs b/crates/ruma-common/src/serde/strings.rs index 7a8d769ee1..2cffef465f 100644 --- a/crates/ruma-common/src/serde/strings.rs +++ b/crates/ruma-common/src/serde/strings.rs @@ -252,6 +252,171 @@ where de.deserialize_map(IntMapVisitor::new()) } +/// Take a Map with values of either an integer number or a string and deserialize +/// those to integer numbers in a Vec of sorted pairs. +/// +/// To be used like this: +/// `#[serde(deserialize_with = "vec_deserialize_v1_powerlevel_values")]` +pub fn vec_deserialize_v1_powerlevel_values<'de, D, T>(de: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: Deserialize<'de> + Ord, +{ + #[repr(transparent)] + struct IntWrap(Int); + + impl<'de> Deserialize<'de> for IntWrap { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserialize_v1_powerlevel(deserializer).map(IntWrap) + } + } + + struct IntMapVisitor { + _phantom: PhantomData, + } + + impl IntMapVisitor { + fn new() -> Self { + Self { _phantom: PhantomData } + } + } + + impl<'de, T> Visitor<'de> for IntMapVisitor + where + T: Deserialize<'de> + Ord, + { + type Value = Vec<(T, Int)>; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a map with integers or strings as values") + } + + fn visit_map>(self, mut map: A) -> Result { + let mut res = Vec::new(); + if let Some(hint) = map.size_hint() { + res.reserve(hint); + } + + while let Some((k, IntWrap(v))) = map.next_entry()? { + res.push((k, v)); + } + + res.sort(); + res.reverse(); + res.dedup_by(|a, b| a.0 == b.0); + + Ok(res) + } + } + + de.deserialize_map(IntMapVisitor::new()) +} + +/// Take a Map with integer values and deserialize those to a Vec of sorted pairs +/// +/// To be used like this: +/// `#[serde(deserialize_with = "vec_deserialize_int_powerlevel_values")]` +pub fn vec_deserialize_int_powerlevel_values<'de, D, T>(de: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: Deserialize<'de> + Ord, +{ + struct IntMapVisitor { + _phantom: PhantomData, + } + + impl IntMapVisitor { + fn new() -> Self { + Self { _phantom: PhantomData } + } + } + + impl<'de, T> Visitor<'de> for IntMapVisitor + where + T: Deserialize<'de> + Ord, + { + type Value = Vec<(T, Int)>; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a map with integers as values") + } + + fn visit_map>(self, mut map: A) -> Result { + let mut res = Vec::new(); + if let Some(hint) = map.size_hint() { + res.reserve(hint); + } + + while let Some(item) = map.next_entry()? { + res.push(item); + } + + res.sort(); + res.reverse(); + res.dedup_by(|a, b| a.0 == b.0); + + Ok(res) + } + } + + de.deserialize_map(IntMapVisitor::new()) +} + +/// Deserialize a Map (i.e. JSON Object) to a Vec of sorted pairs. +/// +/// To be used like this: +/// `#[serde(deserialize_with = "deserialize_map_as_vec")]` +pub fn deserialize_map_as_vec<'de, D, K, V>(de: D) -> Result, D::Error> +where + D: Deserializer<'de>, + K: Deserialize<'de> + Ord, + V: Deserialize<'de>, +{ + struct IntMapVisitor { + _phantom: PhantomData, + } + + impl IntMapVisitor { + fn new() -> Self { + Self { _phantom: PhantomData } + } + } + + impl<'de, K, V> Visitor<'de> for IntMapVisitor<(K, V)> + where + K: Deserialize<'de> + Ord, + V: Deserialize<'de>, + { + type Value = Vec<(K, V)>; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a map of keys to values") + } + + fn visit_map>(self, mut map: A) -> Result { + let mut res = Self::Value::new(); + if let Some(hint) = map.size_hint() { + res.reserve(hint); + } + + while let Some(item) = map.next_entry()? { + res.push(item); + } + + res.sort_by(|a, b| a.0.cmp(&b.0)); + res.reverse(); + res.dedup_by(|a, b| a.0 == b.0); + + Ok(res) + } + } + + de.deserialize_map(IntMapVisitor::new()) +} + #[cfg(test)] mod tests { use js_int::{int, Int}; diff --git a/crates/ruma-common/src/third_party_invite.rs b/crates/ruma-common/src/third_party_invite.rs index 3024c6da6d..6b4e1e7dd3 100644 --- a/crates/ruma-common/src/third_party_invite.rs +++ b/crates/ruma-common/src/third_party_invite.rs @@ -86,6 +86,12 @@ impl PartialEq for IdentityServerBase64PublicKey { } } +impl PartialEq for IdentityServerBase64PublicKey { + fn eq(&self, other: &Base64) -> bool { + self.0.as_bytes().eq(other.as_bytes()) + } +} + #[cfg(test)] mod tests { use super::IdentityServerBase64PublicKey; diff --git a/crates/ruma-common/src/time.rs b/crates/ruma-common/src/time.rs index 9f5b17a2b2..d9ba9b7147 100644 --- a/crates/ruma-common/src/time.rs +++ b/crates/ruma-common/src/time.rs @@ -6,7 +6,7 @@ use time::OffsetDateTime; use web_time::{Duration, SystemTime, UNIX_EPOCH}; /// A timestamp represented as the number of milliseconds since the unix epoch. -#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] #[allow(clippy::exhaustive_structs)] #[serde(transparent)] pub struct MilliSecondsSinceUnixEpoch(pub UInt); @@ -14,6 +14,7 @@ pub struct MilliSecondsSinceUnixEpoch(pub UInt); impl MilliSecondsSinceUnixEpoch { /// Creates a new `MilliSecondsSinceUnixEpoch` from the given `SystemTime`, if it is not before /// the unix epoch, or too large to be represented. + #[inline] pub fn from_system_time(time: SystemTime) -> Option { let duration = time.duration_since(UNIX_EPOCH).ok()?; let millis = duration.as_millis().try_into().ok()?; @@ -21,6 +22,7 @@ impl MilliSecondsSinceUnixEpoch { } /// The current system time in milliseconds since the unix epoch. + #[inline] pub fn now() -> Self { #[cfg(not(all(target_family = "wasm", target_os = "unknown", feature = "js")))] return Self::from_system_time(SystemTime::now()).expect("date out of range"); @@ -30,16 +32,25 @@ impl MilliSecondsSinceUnixEpoch { } /// Creates a new `SystemTime` from `self`, if it can be represented. + #[inline] pub fn to_system_time(self) -> Option { UNIX_EPOCH.checked_add(Duration::from_millis(self.0.into())) } + /// Creates a new `Duration` from `self`, if it can be represented. + #[inline] + pub fn to_duration(self) -> Option { + self.to_system_time().as_ref().map(SystemTime::elapsed).transpose().ok().flatten() + } + /// Get the time since the unix epoch in milliseconds. + #[inline] pub fn get(&self) -> UInt { self.0 } /// Get time since the unix epoch in seconds. + #[inline] pub fn as_secs(&self) -> UInt { self.0 / uint!(1000) } @@ -73,7 +84,7 @@ impl fmt::Debug for MilliSecondsSinceUnixEpoch { } /// A timestamp represented as the number of seconds since the unix epoch. -#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] #[allow(clippy::exhaustive_structs)] #[serde(transparent)] pub struct SecondsSinceUnixEpoch(pub UInt); @@ -81,6 +92,7 @@ pub struct SecondsSinceUnixEpoch(pub UInt); impl SecondsSinceUnixEpoch { /// Creates a new `MilliSecondsSinceUnixEpoch` from the given `SystemTime`, if it is not before /// the unix epoch, or too large to be represented. + #[inline] pub fn from_system_time(time: SystemTime) -> Option { let duration = time.duration_since(UNIX_EPOCH).ok()?; let millis = duration.as_secs().try_into().ok()?; @@ -88,6 +100,7 @@ impl SecondsSinceUnixEpoch { } /// The current system-time as seconds since the unix epoch. + #[inline] pub fn now() -> Self { #[cfg(not(all(target_family = "wasm", target_os = "unknown", feature = "js")))] return Self::from_system_time(SystemTime::now()).expect("date out of range"); @@ -97,11 +110,19 @@ impl SecondsSinceUnixEpoch { } /// Creates a new `SystemTime` from `self`, if it can be represented. + #[inline] pub fn to_system_time(self) -> Option { UNIX_EPOCH.checked_add(Duration::from_secs(self.0.into())) } + /// Creates a new `Duration` from `self`, if it can be represented. + #[inline] + pub fn to_duration(self) -> Option { + self.to_system_time().as_ref().map(SystemTime::elapsed).transpose().ok().flatten() + } + /// Get time since the unix epoch in seconds. + #[inline] pub fn get(&self) -> UInt { self.0 } diff --git a/crates/ruma-common/tests/it/api/required_headers.rs b/crates/ruma-common/tests/it/api/required_headers.rs index 1a1ea80878..0f9445832e 100644 --- a/crates/ruma-common/tests/it/api/required_headers.rs +++ b/crates/ruma-common/tests/it/api/required_headers.rs @@ -28,7 +28,7 @@ pub struct Request { #[ruma_api(header = LOCATION)] pub location: String, #[ruma_api(header = CONTENT_DISPOSITION)] - pub content_disposition: ContentDisposition, + pub content_disposition: Option, } /// Response type for the `required_headers` endpoint. @@ -37,7 +37,7 @@ pub struct Response { #[ruma_api(header = LOCATION)] pub stuff: String, #[ruma_api(header = CONTENT_DISPOSITION)] - pub content_disposition: ContentDisposition, + pub content_disposition: Option, } #[test] @@ -45,8 +45,10 @@ fn request_serde() { let location = "https://other.tld/page/"; let content_disposition = ContentDisposition::new(ContentDispositionType::Attachment) .with_filename(Some("my_file".to_owned())); - let req = - Request { location: location.to_owned(), content_disposition: content_disposition.clone() }; + let req = Request { + location: location.to_owned(), + content_disposition: Some(content_disposition.clone()), + }; let supported = SupportedVersions { versions: [MatrixVersion::V1_1].into(), features: Default::default() }; @@ -63,7 +65,7 @@ fn request_serde() { let req2 = Request::try_from_http_request::<_, &str>(http_req.clone(), &[]).unwrap(); assert_eq!(req2.location, location); - assert_eq!(req2.content_disposition, content_disposition); + assert_eq!(req2.content_disposition, Some(content_disposition)); // Try removing the headers. http_req.headers_mut().remove(LOCATION).unwrap(); @@ -95,8 +97,10 @@ fn response_serde() { let location = "https://other.tld/page/"; let content_disposition = ContentDisposition::new(ContentDispositionType::Attachment) .with_filename(Some("my_file".to_owned())); - let res = - Response { stuff: location.to_owned(), content_disposition: content_disposition.clone() }; + let res = Response { + stuff: location.to_owned(), + content_disposition: Some(content_disposition.clone()), + }; let mut http_res = res.clone().try_into_http_response::>().unwrap(); assert_matches!(http_res.headers().get(LOCATION), Some(_)); @@ -104,7 +108,7 @@ fn response_serde() { let res2 = Response::try_from_http_response(http_res.clone()).unwrap(); assert_eq!(res2.stuff, location); - assert_eq!(res2.content_disposition, content_disposition); + assert_eq!(res2.content_disposition, Some(content_disposition)); // Try removing the headers. http_res.headers_mut().remove(LOCATION).unwrap(); diff --git a/crates/ruma-common/tests/it/api/ui/serde-flatten-response-body.stderr b/crates/ruma-common/tests/it/api/ui/serde-flatten-response-body.stderr index 9c70491ed8..25f2fd3428 100644 --- a/crates/ruma-common/tests/it/api/ui/serde-flatten-response-body.stderr +++ b/crates/ruma-common/tests/it/api/ui/serde-flatten-response-body.stderr @@ -6,28 +6,38 @@ error: Use `#[ruma_api(body)]` to represent the JSON body as a single field | |_______________________________^ error[E0277]: the trait bound `Response: IncomingResponse` is not satisfied - --> tests/it/api/ui/serde-flatten-response-body.rs:23:1 - | -23 | #[request] - | ^^^^^^^^^^ the trait `IncomingResponse` is not implemented for `Response` - | + --> tests/it/api/ui/serde-flatten-response-body.rs:23:1 + | + 23 | #[request] + | ^^^^^^^^^^ unsatisfied trait bound + | +help: the trait `IncomingResponse` is not implemented for `Response` + --> tests/it/api/ui/serde-flatten-response-body.rs:27:1 + | + 27 | pub struct Response { + | ^^^^^^^^^^^^^^^^^^^ note: required by a bound in `ruma_common::api::OutgoingRequest::IncomingResponse` - --> src/api.rs - | - | type IncomingResponse: IncomingResponse; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `OutgoingRequest::IncomingResponse` - = note: this error originates in the derive macro `::ruma_common::exports::ruma_macros::Request` (in Nightly builds, run with -Z macro-backtrace for more info) + --> src/api.rs + | + | type IncomingResponse: IncomingResponse; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `OutgoingRequest::IncomingResponse` + = note: this error originates in the derive macro `::ruma_common::exports::ruma_macros::Request` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `Response: OutgoingResponse` is not satisfied - --> tests/it/api/ui/serde-flatten-response-body.rs:23:1 - | -23 | #[request] - | ^^^^^^^^^^ the trait `OutgoingResponse` is not implemented for `Response` - | - = help: the trait `OutgoingResponse` is implemented for `MatrixError` + --> tests/it/api/ui/serde-flatten-response-body.rs:23:1 + | + 23 | #[request] + | ^^^^^^^^^^ unsatisfied trait bound + | +help: the trait `OutgoingResponse` is not implemented for `Response` + --> tests/it/api/ui/serde-flatten-response-body.rs:27:1 + | + 27 | pub struct Response { + | ^^^^^^^^^^^^^^^^^^^ + = help: the trait `OutgoingResponse` is implemented for `MatrixError` note: required by a bound in `ruma_common::api::IncomingRequest::OutgoingResponse` - --> src/api.rs - | - | type OutgoingResponse: OutgoingResponse; - | ^^^^^^^^^^^^^^^^ required by this bound in `IncomingRequest::OutgoingResponse` - = note: this error originates in the derive macro `::ruma_common::exports::ruma_macros::Request` (in Nightly builds, run with -Z macro-backtrace for more info) + --> src/api.rs + | + | type OutgoingResponse: OutgoingResponse; + | ^^^^^^^^^^^^^^^^ required by this bound in `IncomingRequest::OutgoingResponse` + = note: this error originates in the derive macro `::ruma_common::exports::ruma_macros::Request` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/ruma-events/CHANGELOG.md b/crates/ruma-events/CHANGELOG.md index f816f76cb9..438f2c7f4b 100644 --- a/crates/ruma-events/CHANGELOG.md +++ b/crates/ruma-events/CHANGELOG.md @@ -16,6 +16,9 @@ Breaking changes: Improvements: +- Add unstable support for the `m.invite_permission_config` account data event which blocks + invites to a user, wholesale: ([MSC4380](https://github.com/matrix-org/matrix-spec-proposals/pull/4380)). + - Add support for the room account data `m.space_order` event which powers top level space ordering as per [MSC3230](https://github.com/matrix-org/matrix-spec-proposals/pull/3230). - Add `m.rtc.notification` event support and deprecate the (non MSC conformant) diff --git a/crates/ruma-events/Cargo.toml b/crates/ruma-events/Cargo.toml index 6d9954f380..9e17ba7627 100644 --- a/crates/ruma-events/Cargo.toml +++ b/crates/ruma-events/Cargo.toml @@ -53,6 +53,7 @@ unstable-msc4319 = [] unstable-msc4310 = [] unstable-msc4334 = ["dep:language-tags"] unstable-msc4359 = [] +unstable-msc4380 = ["ruma-common/unstable-msc4380"] unstable-msc3230 = [] # Allow some mandatory fields to be missing, defaulting them to an empty string @@ -93,6 +94,8 @@ ruma-identifiers-validation = { workspace = true } ruma-macros = { workspace = true } serde = { workspace = true } serde_json = { workspace = true, features = ["raw_value"] } +smallstr = { workspace = true } +smallvec = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true, features = ["attributes"] } url = { workspace = true } @@ -111,7 +114,7 @@ maplit = { workspace = true } trybuild = "1.0.71" [[bench]] -name = "event_deserialize" +name = "event_serde" harness = false required-features = ["criterion"] diff --git a/crates/ruma-events/benches/event_deserialize.rs b/crates/ruma-events/benches/event_serde.rs similarity index 59% rename from crates/ruma-events/benches/event_deserialize.rs rename to crates/ruma-events/benches/event_serde.rs index 06d6f1c447..74ba38065b 100644 --- a/crates/ruma-events/benches/event_deserialize.rs +++ b/crates/ruma-events/benches/event_serde.rs @@ -47,30 +47,51 @@ fn power_levels() -> serde_json::Value { }) } -fn deserialize_any_room_event(c: &mut Criterion) { +fn interpret_any_room_event(c: &mut Criterion) { let json_data = power_levels(); - c.bench_function("deserialize to `AnyTimelineEvent`", |b| { + c.bench_function("interpret `AnyTimelineEvent`", |b| { b.iter(|| { let _ = serde_json::from_value::(json_data.clone()).unwrap(); }); }); } -fn deserialize_any_state_event(c: &mut Criterion) { +fn deserialize_any_room_event(c: &mut Criterion) { + let json_value = power_levels(); + let json_data = serde_json::to_vec(&json_value).unwrap(); + + c.bench_function("deserialize `AnyTimelineEvent`", |b| { + b.iter(|| { + let _ = serde_json::from_slice::(&json_data).unwrap(); + }); + }); +} + +fn serialize_any_room_event(c: &mut Criterion) { + let json_data = power_levels(); + + c.bench_function("serialize `AnyTimelineEvent`", |b| { + b.iter(|| { + let _ = serde_json::to_vec(&json_data).unwrap(); + }); + }); +} + +fn interpret_any_state_event(c: &mut Criterion) { let json_data = power_levels(); - c.bench_function("deserialize to `AnyStateEvent`", |b| { + c.bench_function("interpret `AnyStateEvent`", |b| { b.iter(|| { let _ = serde_json::from_value::(json_data.clone()).unwrap(); }); }); } -fn deserialize_specific_event(c: &mut Criterion) { +fn interpret_specific_event(c: &mut Criterion) { let json_data = power_levels(); - c.bench_function("deserialize to `OriginalStateEvent`", |b| { + c.bench_function("interpret `OriginalStateEvent`", |b| { b.iter(|| { let _ = serde_json::from_value::>( json_data.clone(), @@ -80,11 +101,27 @@ fn deserialize_specific_event(c: &mut Criterion) { }); } +fn deserialize_raw_specific_event(c: &mut Criterion) { + let json_data = power_levels(); + let json_data: Raw> = + serde_json::from_value(json_data).unwrap(); + + c.bench_function("deserialize Raw<_> to `OriginalStateEvent`", |b| { + b.iter(|| { + let _: OriginalStateEvent = + json_data.clone().deserialize().unwrap(); + }); + }); +} + criterion_group!( benches, + interpret_any_room_event, + serialize_any_room_event, deserialize_any_room_event, - deserialize_any_state_event, - deserialize_specific_event + interpret_any_state_event, + interpret_specific_event, + deserialize_raw_specific_event, ); criterion_main!(benches); diff --git a/crates/ruma-events/src/_custom.rs b/crates/ruma-events/src/_custom.rs index abbc76f757..53d9fd43e2 100644 --- a/crates/ruma-events/src/_custom.rs +++ b/crates/ruma-events/src/_custom.rs @@ -80,7 +80,7 @@ impl RedactedMessageLikeEventContent for CustomMessageLikeEventContent { custom_room_event_content!(CustomStateEventContent, StateEventType); impl StateEventContent for CustomStateEventContent { - type StateKey = String; + type StateKey = super::StateKey; fn event_type(&self) -> StateEventType { self.event_type[..].into() @@ -94,14 +94,14 @@ impl StaticStateEventContent for CustomStateEventContent { type PossiblyRedacted = Self; } impl PossiblyRedactedStateEventContent for CustomStateEventContent { - type StateKey = String; + type StateKey = super::StateKey; fn event_type(&self) -> StateEventType { self.event_type[..].into() } } impl RedactedStateEventContent for CustomStateEventContent { - type StateKey = String; + type StateKey = super::StateKey; fn event_type(&self) -> StateEventType { self.event_type[..].into() diff --git a/crates/ruma-events/src/call/member/member_state_key.rs b/crates/ruma-events/src/call/member/member_state_key.rs index cdcd1062b1..7cc864a439 100644 --- a/crates/ruma-events/src/call/member/member_state_key.rs +++ b/crates/ruma-events/src/call/member/member_state_key.rs @@ -143,7 +143,7 @@ impl FromStr for CallMemberStateKeyEnum { if has_underscore { Err(KeyParseError::LeadingUnderscoreNoMemberId) } else { - Ok(CallMemberStateKeyEnum::new(user_id, None, has_underscore)) + Ok(CallMemberStateKeyEnum::new(user_id.into(), None, has_underscore)) } } Err(err) => Err(KeyParseError::InvalidUser { @@ -162,7 +162,11 @@ impl FromStr for CallMemberStateKeyEnum { if member_id.is_empty() { return Err(KeyParseError::EmptyMemberId); } - Ok(CallMemberStateKeyEnum::new(user_id, Some(member_id.to_owned()), has_underscore)) + Ok(CallMemberStateKeyEnum::new( + user_id.into(), + Some(member_id.to_owned()), + has_underscore, + )) } Err(err) => Err(KeyParseError::InvalidUser { user_id: user_id.to_owned(), error: err }), } diff --git a/crates/ruma-events/src/enums.rs b/crates/ruma-events/src/enums.rs index bce0d349ea..904548f290 100644 --- a/crates/ruma-events/src/enums.rs +++ b/crates/ruma-events/src/enums.rs @@ -39,6 +39,9 @@ event_enum! { "m.push_rules" => super::push_rules, "m.secret_storage.default_key" => super::secret_storage::default_key, "m.secret_storage.key.*" => super::secret_storage::key, + #[cfg(feature = "unstable-msc4380")] + #[ruma_enum(ident = InvitePermissionConfig, alias = "m.invite_permission_config")] + "org.matrix.msc4380.invite_permission_config" => super::invite_permission_config, #[cfg(feature = "unstable-msc4278")] "m.media_preview_config" => super::media_preview_config, #[cfg(feature = "unstable-msc4278")] @@ -195,6 +198,8 @@ event_enum! { #[cfg(feature = "unstable-msc4171")] #[ruma_enum(alias = "m.member_hints")] "io.element.functional_members" => super::member_hints, + #[ruma_enum(alias = "m.room.preview_urls")] + "org.matrix.room.preview_urls" => super::room::preview_url, } /// Any to-device event. diff --git a/crates/ruma-events/src/invite_permission_config.rs b/crates/ruma-events/src/invite_permission_config.rs new file mode 100644 index 0000000000..ef56871661 --- /dev/null +++ b/crates/ruma-events/src/invite_permission_config.rs @@ -0,0 +1,67 @@ +//! Types for the [`m.invite_permission_config`] account data event. +//! +//! [`m.invite_permission_config`]: https://github.com/matrix-org/matrix-spec-proposals/pull/4380 + +use ruma_macros::EventContent; +use serde::{Deserialize, Serialize}; + +/// The content of an `m.invite_permission_config` event. +/// +/// A single property: `block_all`. +#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)] +#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] +#[ruma_event( + kind = GlobalAccountData, + type = "org.matrix.msc4380.invite_permission_config", + alias = "m.invite_permission_config", +)] +pub struct InvitePermissionConfigEventContent { + /// When set to true, indicates that the user does not wish to receive *any* room invites, and + /// they should be blocked. + #[serde(default)] + #[serde(deserialize_with = "ruma_common::serde::default_on_error")] + pub block_all: bool, +} + +impl InvitePermissionConfigEventContent { + /// Creates a new `InvitePermissionConfigEventContent` from the desired boolean state. + pub fn new(block_all: bool) -> Self { + Self { block_all } + } +} + +#[cfg(test)] +mod tests { + use assert_matches2::assert_matches; + use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; + + use super::InvitePermissionConfigEventContent; + use crate::AnyGlobalAccountDataEvent; + + #[test] + fn serialization() { + let invite_permission_config = InvitePermissionConfigEventContent::new(true); + + let json = json!({ + "block_all": true + }); + + assert_eq!(to_json_value(invite_permission_config).unwrap(), json); + } + + #[test] + fn deserialization() { + let json = json!({ + "content": { + "block_all": true + }, + "type": "m.invite_permission_config" + }); + + assert_matches!( + from_json_value::(json), + Ok(AnyGlobalAccountDataEvent::InvitePermissionConfig(ev)) + ); + assert!(ev.content.block_all); + } +} diff --git a/crates/ruma-events/src/kinds.rs b/crates/ruma-events/src/kinds.rs index f2c9557d4f..63e62b3057 100644 --- a/crates/ruma-events/src/kinds.rs +++ b/crates/ruma-events/src/kinds.rs @@ -17,6 +17,27 @@ use super::{ RedactionDeHelper, RoomAccountDataEventContent, StateEventType, StaticStateEventContent, ToDeviceEventContent, }; +use crate::{AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent}; + +/// Enum allowing to use the same structures for global and room account data +#[derive(Debug)] +#[allow(clippy::exhaustive_enums)] +pub enum AnyAccountDataEvent { + /// An event for a specific room + Room(AnyRoomAccountDataEvent), + /// An event for the whole account + Global(AnyGlobalAccountDataEvent), +} + +/// Enum allowing to use the same structures for global and room account data +#[derive(Debug)] +#[allow(clippy::exhaustive_enums)] +pub enum AnyRawAccountDataEvent { + /// An event for a specific room + Room(Raw), + /// An event for the whole account + Global(Raw), +} /// A global account data event. #[derive(Clone, Debug, Event)] @@ -530,6 +551,7 @@ impl InitialStateEvent { /// /// For cases where the state key is empty, /// [`with_empty_state_key()`](Self::with_empty_state_key) can be used instead. + #[inline] pub fn new(state_key: C::StateKey, content: C) -> Self { Self { content, state_key } } @@ -537,6 +559,7 @@ impl InitialStateEvent { /// Create a new `InitialStateEvent` for an event type with an empty state key. /// /// For cases where the state key is not empty, use [`new()`](Self::new). + #[inline] pub fn with_empty_state_key(content: C) -> Self where C: StaticStateEventContent, @@ -551,6 +574,7 @@ impl InitialStateEvent { /// with a `Serialize` implementation that can error (for example because it contains an /// `enum` with one or more variants that use the `#[serde(skip)]` attribute), this method /// can panic. + #[inline] pub fn to_raw(&self) -> Raw { Raw::new(self).unwrap() } @@ -562,6 +586,7 @@ impl InitialStateEvent { /// with a `Serialize` implementation that can error (for example because it contains an /// `enum` with one or more variants that use the `#[serde(skip)]` attribute), this method /// can panic. + #[inline] pub fn to_raw_any(&self) -> Raw { self.to_raw().cast() } @@ -857,6 +882,7 @@ where C::Redacted: RedactedStateEventContent, { /// Get the event’s type, like `m.room.create`. + #[inline] pub fn event_type(&self) -> StateEventType { match self { Self::Original { content, .. } => content.event_type(), @@ -870,6 +896,7 @@ where /// /// A small number of events have room-version specific redaction behavior, so a /// [`RedactionRules`] has to be specified. + #[inline] pub fn redact(self, rules: &RedactionRules) -> C::Redacted { match self { FullStateEventContent::Original { content, .. } => content.redact(rules), @@ -891,6 +918,7 @@ macro_rules! impl_possibly_redacted_event { $( C::Redacted: $trait, )? { /// Returns the `type` of this event. + #[inline] pub fn event_type(&self) -> $event_type { match self { Self::Original(ev) => ev.content.event_type(), @@ -899,6 +927,7 @@ macro_rules! impl_possibly_redacted_event { } /// Returns this event's `event_id` field. + #[inline] pub fn event_id(&self) -> &EventId { match self { Self::Original(ev) => &ev.event_id, @@ -907,6 +936,7 @@ macro_rules! impl_possibly_redacted_event { } /// Returns this event's `sender` field. + #[inline] pub fn sender(&self) -> &UserId { match self { Self::Original(ev) => &ev.sender, @@ -915,6 +945,7 @@ macro_rules! impl_possibly_redacted_event { } /// Returns this event's `origin_server_ts` field. + #[inline] pub fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch { match self { Self::Original(ev) => ev.origin_server_ts, @@ -954,6 +985,7 @@ impl_possibly_redacted_event!( MessageLikeEventContent, RedactedMessageLikeEventContent, MessageLikeEventType ) { /// Returns this event's `room_id` field. + #[inline] pub fn room_id(&self) -> &RoomId { match self { Self::Original(ev) => &ev.room_id, @@ -962,6 +994,7 @@ impl_possibly_redacted_event!( } /// Get the inner `OriginalMessageLikeEvent` if this is an unredacted event. + #[inline] pub fn as_original(&self) -> Option<&OriginalMessageLikeEvent> { as_variant!(self, Self::Original) } @@ -973,11 +1006,13 @@ impl_possibly_redacted_event!( MessageLikeEventContent, RedactedMessageLikeEventContent, MessageLikeEventType ) { /// Get the inner `OriginalSyncMessageLikeEvent` if this is an unredacted event. + #[inline] pub fn as_original(&self) -> Option<&OriginalSyncMessageLikeEvent> { as_variant!(self, Self::Original) } /// Convert this sync event into a full event (one with a `room_id` field). + #[inline] pub fn into_full_event(self, room_id: OwnedRoomId) -> MessageLikeEvent { match self { Self::Original(ev) => MessageLikeEvent::Original(ev.into_full_event(room_id)), @@ -993,6 +1028,7 @@ impl_possibly_redacted_event!( C::Redacted: RedactedStateEventContent, { /// Returns this event's `room_id` field. + #[inline] pub fn room_id(&self) -> &RoomId { match self { Self::Original(ev) => &ev.room_id, @@ -1001,6 +1037,7 @@ impl_possibly_redacted_event!( } /// Returns this event's `state_key` field. + #[inline] pub fn state_key(&self) -> &C::StateKey { match self { Self::Original(ev) => &ev.state_key, @@ -1009,6 +1046,7 @@ impl_possibly_redacted_event!( } /// Get the inner `OriginalStateEvent` if this is an unredacted event. + #[inline] pub fn as_original(&self) -> Option<&OriginalStateEvent> { as_variant!(self, Self::Original) } @@ -1021,6 +1059,7 @@ impl_possibly_redacted_event!( C::Redacted: RedactedStateEventContent, { /// Returns this event's `state_key` field. + #[inline] pub fn state_key(&self) -> &C::StateKey { match self { Self::Original(ev) => &ev.state_key, @@ -1029,11 +1068,13 @@ impl_possibly_redacted_event!( } /// Get the inner `OriginalSyncStateEvent` if this is an unredacted event. + #[inline] pub fn as_original(&self) -> Option<&OriginalSyncStateEvent> { as_variant!(self, Self::Original) } /// Convert this sync event into a full event (one with a `room_id` field). + #[inline] pub fn into_full_event(self, room_id: OwnedRoomId) -> StateEvent { match self { Self::Original(ev) => StateEvent::Original(ev.into_full_event(room_id)), @@ -1050,6 +1091,7 @@ macro_rules! impl_sync_from_full { C: $content_trait + RedactContent, C::Redacted: $redacted_content_trait, { + #[inline] fn from(full: $full) -> Self { match full { $full::Original(ev) => Self::Original(ev.into()), diff --git a/crates/ruma-events/src/lib.rs b/crates/ruma-events/src/lib.rs index 5fa316c249..d18f294879 100644 --- a/crates/ruma-events/src/lib.rs +++ b/crates/ruma-events/src/lib.rs @@ -165,6 +165,8 @@ pub mod image_pack; pub mod key; #[cfg(feature = "unstable-msc3488")] pub mod location; +#[cfg(feature = "unstable-msc4380")] +pub mod invite_permission_config; pub mod marked_unread; #[cfg(feature = "unstable-msc4278")] pub mod media_preview_config; @@ -204,7 +206,7 @@ pub use self::{ enums::*, kinds::*, relation::{BundledMessageLikeRelations, BundledStateRelations}, - state_key::EmptyStateKey, + state_key::{EmptyStateKey, StateKey}, unsigned::{MessageLikeUnsigned, RedactedUnsigned, StateUnsigned, UnsignedRoomRedactionEvent}, }; diff --git a/crates/ruma-events/src/media_preview_config.rs b/crates/ruma-events/src/media_preview_config.rs index a123408d0c..b509d3135c 100644 --- a/crates/ruma-events/src/media_preview_config.rs +++ b/crates/ruma-events/src/media_preview_config.rs @@ -37,7 +37,7 @@ impl JsonCastable for UnstableMediaPreviewConfig /// The configuration that handles if media previews should be shown in the timeline. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, StringEnum, Default)] #[ruma_enum(rename_all = "lowercase")] -#[non_exhaustive] +#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub enum MediaPreviews { /// Media previews should be hidden. Off, @@ -56,7 +56,7 @@ pub enum MediaPreviews { /// The configuration to handle if avatars should be shown in invites. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, StringEnum, Default)] #[ruma_enum(rename_all = "lowercase")] -#[non_exhaustive] +#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub enum InviteAvatars { /// Avatars in invites should be hidden. Off, diff --git a/crates/ruma-events/src/presence.rs b/crates/ruma-events/src/presence.rs index 1ea0527974..4fa60c0a32 100644 --- a/crates/ruma-events/src/presence.rs +++ b/crates/ruma-events/src/presence.rs @@ -21,8 +21,8 @@ pub struct PresenceEvent { /// Informs the room of members presence. /// /// This is the only type a `PresenceEvent` can contain as its `content` field. -#[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct PresenceEventContent { /// The current avatar URL for this user. /// diff --git a/crates/ruma-events/src/receipt.rs b/crates/ruma-events/src/receipt.rs index 2ea0b66e1f..68b80936e7 100644 --- a/crates/ruma-events/src/receipt.rs +++ b/crates/ruma-events/src/receipt.rs @@ -200,7 +200,7 @@ where None => Self::Unthreaded, Some(s) => match s.as_ref() { "main" => Self::Main, - s_ref if s_ref.starts_with('$') => Self::Thread(EventId::parse(s_ref)?), + s_ref if s_ref.starts_with('$') => Self::Thread(EventId::parse(s_ref)?.into()), _ => Self::_Custom(PrivOwnedStr(s.into())), }, }; diff --git a/crates/ruma-events/src/relation.rs b/crates/ruma-events/src/relation.rs index 785da72713..5640ef3699 100644 --- a/crates/ruma-events/src/relation.rs +++ b/crates/ruma-events/src/relation.rs @@ -294,7 +294,7 @@ impl BundledStateRelations { #[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] +#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub enum RelationType { /// `m.annotation`, an annotation, principally used by reactions. Annotation, diff --git a/crates/ruma-events/src/room.rs b/crates/ruma-events/src/room.rs index 2bc629b403..72de333a76 100644 --- a/crates/ruma-events/src/room.rs +++ b/crates/ruma-events/src/room.rs @@ -28,6 +28,7 @@ pub mod message; pub mod name; pub mod pinned_events; pub mod power_levels; +pub mod preview_url; pub mod redaction; pub mod server_acl; pub mod third_party_invite; diff --git a/crates/ruma-events/src/room/canonical_alias.rs b/crates/ruma-events/src/room/canonical_alias.rs index cb4597c48b..37500251ff 100644 --- a/crates/ruma-events/src/room/canonical_alias.rs +++ b/crates/ruma-events/src/room/canonical_alias.rs @@ -36,6 +36,11 @@ impl RoomCanonicalAliasEventContent { pub fn new() -> Self { Self { alias: None, alt_aliases: Vec::new() } } + + /// Returns an iterator over the canonical alias and alt aliases + pub fn aliases(&self) -> impl Iterator { + self.alias.iter().chain(self.alt_aliases.iter()) + } } #[cfg(test)] diff --git a/crates/ruma-events/src/room/create.rs b/crates/ruma-events/src/room/create.rs index 90260ef8c8..b059f05cd5 100644 --- a/crates/ruma-events/src/room/create.rs +++ b/crates/ruma-events/src/room/create.rs @@ -124,18 +124,16 @@ pub struct PreviousRoom { pub room_id: OwnedRoomId, /// The event ID of the last known event in the old room. - #[deprecated = "\ - This field should be sent by servers when possible for backwards compatibility \ - but clients should not rely on it."] + /// conduwuit makes this optional because Synapse allows an empty event_id + /// and there are no issues with this. #[serde(skip_serializing_if = "Option::is_none")] pub event_id: Option, } impl PreviousRoom { - /// Creates a new `PreviousRoom` from the given room ID. - pub fn new(room_id: OwnedRoomId) -> Self { - #[allow(deprecated)] - Self { room_id, event_id: None } + /// Creates a new `PreviousRoom` from the given room and event IDs. + pub fn new(room_id: OwnedRoomId, event_id: Option) -> Self { + Self { room_id, event_id } } } diff --git a/crates/ruma-events/src/room/join_rules.rs b/crates/ruma-events/src/room/join_rules.rs index 1a0ddd1f42..f4f8384e00 100644 --- a/crates/ruma-events/src/room/join_rules.rs +++ b/crates/ruma-events/src/room/join_rules.rs @@ -21,10 +21,10 @@ use crate::{ #[derive(Clone, Debug, Serialize, EventContent)] #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] #[ruma_event(type = "m.room.join_rules", kind = State, state_key_type = EmptyStateKey, custom_redacted)] +#[serde(transparent)] pub struct RoomJoinRulesEventContent { - /// The type of rules used for users wishing to join this room. + /// The rule used for users wishing to join this room. #[ruma_event(skip_redaction)] - #[serde(flatten)] pub join_rule: JoinRule, } @@ -125,6 +125,7 @@ impl SyncRoomJoinRulesEvent { mod tests { use assert_matches2::assert_matches; use ruma_common::owned_room_id; + use serde_json::json; use super::{ AllowRule, JoinRule, OriginalSyncRoomJoinRulesEvent, RedactedRoomJoinRulesEventContent, @@ -242,4 +243,16 @@ mod tests { RoomJoinRulesEventContent { join_rule: JoinRule::Restricted(_) } ); } + + #[test] + fn reserialize_unsupported_join_rule() { + let json = json!({"join_rule": "local.matrix.custom", "foo": "bar"}); + + let content = serde_json::from_value::(json.clone()).unwrap(); + assert_eq!(content.join_rule.as_str(), "local.matrix.custom"); + let data = content.join_rule.data(); + assert_eq!(data.get("foo").unwrap().as_str(), Some("bar")); + + assert_eq!(serde_json::to_value(&content).unwrap(), json); + } } diff --git a/crates/ruma-events/src/room/member.rs b/crates/ruma-events/src/room/member.rs index 4a810af4cf..09e8a17cc2 100644 --- a/crates/ruma-events/src/room/member.rs +++ b/crates/ruma-events/src/room/member.rs @@ -280,7 +280,7 @@ impl SyncRoomMemberEvent { #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, PartialEq, Eq, StringEnum)] #[ruma_enum(rename_all = "lowercase")] -#[non_exhaustive] +#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub enum MembershipState { /// The user is banned. Ban, diff --git a/crates/ruma-events/src/room/power_levels.rs b/crates/ruma-events/src/room/power_levels.rs index 86bc4a6c03..f483e499f0 100644 --- a/crates/ruma-events/src/room/power_levels.rs +++ b/crates/ruma-events/src/room/power_levels.rs @@ -25,14 +25,13 @@ use crate::{ /// The content of an `m.room.power_levels` event. /// /// Defines the power levels (privileges) of users in the room. -#[derive(Clone, Debug, Deserialize, Serialize, EventContent)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)] #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] #[ruma_event(type = "m.room.power_levels", kind = State, state_key_type = EmptyStateKey, custom_redacted)] pub struct RoomPowerLevelsEventContent { /// The level required to ban a user. #[serde( default = "default_power_level", - skip_serializing_if = "is_default_power_level", deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel" )] pub ban: Int, @@ -42,31 +41,21 @@ pub struct RoomPowerLevelsEventContent { /// This is a mapping from event type to power level required. #[serde( default, - skip_serializing_if = "BTreeMap::is_empty", deserialize_with = "ruma_common::serde::btreemap_deserialize_v1_powerlevel_values" )] pub events: BTreeMap, /// The default level required to send message events. - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel" - )] + #[serde(default, deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel")] pub events_default: Int, /// The level required to invite a user. - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel" - )] + #[serde(default, deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel")] pub invite: Int, /// The level required to kick a user. #[serde( default = "default_power_level", - skip_serializing_if = "is_default_power_level", deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel" )] pub kick: Int, @@ -74,7 +63,6 @@ pub struct RoomPowerLevelsEventContent { /// The level required to redact an event. #[serde( default = "default_power_level", - skip_serializing_if = "is_default_power_level", deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel" )] pub redact: Int, @@ -82,7 +70,6 @@ pub struct RoomPowerLevelsEventContent { /// The default level required to send state events. #[serde( default = "default_power_level", - skip_serializing_if = "is_default_power_level", deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel" )] pub state_default: Int, @@ -92,23 +79,18 @@ pub struct RoomPowerLevelsEventContent { /// This is a mapping from `user_id` to power level for that user. #[serde( default, - skip_serializing_if = "BTreeMap::is_empty", deserialize_with = "ruma_common::serde::btreemap_deserialize_v1_powerlevel_values" )] pub users: BTreeMap, /// The default power level for every user in the room. - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel" - )] + #[serde(default, deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel")] pub users_default: Int, /// The power level requirements for specific notification types. /// /// This is a mapping from `key` to power level for that notifications key. - #[serde(default, skip_serializing_if = "NotificationPowerLevels::is_default")] + #[serde(default)] pub notifications: NotificationPowerLevels, } @@ -174,12 +156,6 @@ impl RedactContent for RoomPowerLevelsEventContent { } } -/// Used with `#[serde(skip_serializing_if)]` to omit default power levels. -#[allow(clippy::trivially_copy_pass_by_ref)] -fn is_default_power_level(l: &Int) -> bool { - *l == int!(50) -} - impl RoomPowerLevelsEvent { /// Obtain the effective power levels, regardless of whether this event is redacted. pub fn power_levels( @@ -220,13 +196,12 @@ impl StrippedRoomPowerLevelsEvent { } /// Redacted form of [`RoomPowerLevelsEventContent`]. -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub struct RedactedRoomPowerLevelsEventContent { /// The level required to ban a user. #[serde( default = "default_power_level", - skip_serializing_if = "is_default_power_level", deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel" )] pub ban: Int, @@ -236,34 +211,24 @@ pub struct RedactedRoomPowerLevelsEventContent { /// This is a mapping from event type to power level required. #[serde( default, - skip_serializing_if = "BTreeMap::is_empty", deserialize_with = "ruma_common::serde::btreemap_deserialize_v1_powerlevel_values" )] pub events: BTreeMap, /// The default level required to send message events. - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel" - )] + #[serde(default, deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel")] pub events_default: Int, /// The level required to invite a user. /// /// This field was redacted in room versions 1 through 10. Starting from room version 11 it is /// preserved. - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel" - )] + #[serde(default, deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel")] pub invite: Int, /// The level required to kick a user. #[serde( default = "default_power_level", - skip_serializing_if = "is_default_power_level", deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel" )] pub kick: Int, @@ -271,7 +236,6 @@ pub struct RedactedRoomPowerLevelsEventContent { /// The level required to redact an event. #[serde( default = "default_power_level", - skip_serializing_if = "is_default_power_level", deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel" )] pub redact: Int, @@ -279,7 +243,6 @@ pub struct RedactedRoomPowerLevelsEventContent { /// The default level required to send state events. #[serde( default = "default_power_level", - skip_serializing_if = "is_default_power_level", deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel" )] pub state_default: Int, @@ -289,17 +252,12 @@ pub struct RedactedRoomPowerLevelsEventContent { /// This is a mapping from `user_id` to power level for that user. #[serde( default, - skip_serializing_if = "BTreeMap::is_empty", deserialize_with = "ruma_common::serde::btreemap_deserialize_v1_powerlevel_values" )] pub users: BTreeMap, /// The default power level for every user in the room. - #[serde( - default, - skip_serializing_if = "ruma_common::serde::is_default", - deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel" - )] + #[serde(default, deserialize_with = "ruma_common::serde::deserialize_v1_powerlevel")] pub users_default: Int, } diff --git a/crates/ruma-events/src/room/preview_url.rs b/crates/ruma-events/src/room/preview_url.rs new file mode 100644 index 0000000000..02c79f0098 --- /dev/null +++ b/crates/ruma-events/src/room/preview_url.rs @@ -0,0 +1,27 @@ +//! Types for the undocumented [`org.matrix.room.preview_urls`] event. +//! +//! [`org.matrix.room.preview_url`]: https://github.com/matrix-org/matrix-spec/issues/394 + +use ruma_macros::EventContent; +use serde::{Deserialize, Serialize}; + +use crate::EmptyStateKey; + +/// The content of an `org.matrix.room.preview_urls` event. +/// +/// An event to indicate whether URL previews are disabled by default for the room or not. +#[derive(Clone, Debug, Deserialize, Serialize, EventContent)] +#[ruma_event(type = "org.matrix.room.preview_urls", kind = State, state_key_type = EmptyStateKey)] +#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] +pub struct RoomPreviewUrlsEventContent { + /// Whether URL previews are disabled for the entire room + #[serde(default)] + pub disabled: bool, +} + +impl RoomPreviewUrlsEventContent { + /// Creates a new preview_url event with URL previews enabled by default (disabled: false) + pub fn new(disabled: bool) -> Self { + Self { disabled } + } +} diff --git a/crates/ruma-events/src/room/server_acl.rs b/crates/ruma-events/src/room/server_acl.rs index 596cf70d96..ffd58ffa68 100644 --- a/crates/ruma-events/src/room/server_acl.rs +++ b/crates/ruma-events/src/room/server_acl.rs @@ -49,20 +49,64 @@ pub struct RoomServerAclEventContent { impl RoomServerAclEventContent { /// Creates a new `RoomServerAclEventContent` with the given IP literal allowance flag, allowed /// and denied servers. + #[inline] pub fn new(allow_ip_literals: bool, allow: Vec, deny: Vec) -> Self { Self { allow_ip_literals, allow, deny } } /// Returns true if and only if the server is allowed by the ACL rules. + #[inline] pub fn is_allowed(&self, server_name: &ServerName) -> bool { if !self.allow_ip_literals && server_name.is_ip_literal() { return false; } let host = server_name.host(); + !self.deny_matches(host) && self.allow_matches(host) + } + + /// Returns true if the input matches a pattern in the allow list specifically + #[inline] + pub fn allow_matches(&self, host: &str) -> bool { + Self::matches(&self.allow, host) + } + + /// Returns true if the input matches a pattern in the deny list specifically + #[inline] + pub fn deny_matches(&self, host: &str) -> bool { + Self::matches(&self.deny, host) + } + + /// Returns true if the input is equal to a string in the allow list specifically + #[inline] + pub fn allow_contains(&self, host: &str) -> bool { + Self::contains(&self.allow, host) + } + + /// Returns true if the input is equal to a string in the deny list specifically + #[inline] + pub fn deny_contains(&self, host: &str) -> bool { + Self::contains(&self.deny, host) + } + + /// Returns true if the allow list is empty + #[inline] + pub fn allow_is_empty(&self) -> bool { + self.allow.is_empty() + } - self.deny.iter().all(|d| !WildMatch::new(d).matches(host)) - && self.allow.iter().any(|a| WildMatch::new(a).matches(host)) + /// Returns true if the deny list is empty + #[inline] + pub fn deny_is_empty(&self) -> bool { + self.deny.is_empty() + } + + fn matches(a: &[String], s: &str) -> bool { + a.iter().map(String::as_str).any(|a| WildMatch::new_case_insensitive(a).matches(s)) + } + + fn contains(a: &[String], s: &str) -> bool { + a.iter().map(String::as_str).any(|a| a.eq_ignore_ascii_case(s)) } } @@ -178,4 +222,19 @@ mod tests { assert!(!acl_event.is_allowed(server_name!("[2001:db8:1234::2]"))); assert!(acl_event.is_allowed(server_name!("[2001:db8:1234::1]"))); } + + #[test] + fn acl_case_insensitive() { + let acl_event = RoomServerAclEventContent { + allow_ip_literals: false, + allow: vec!["good.ServEr".to_owned()], + deny: vec!["bad.ServeR".to_owned()], + }; + assert!(!acl_event.is_allowed(server_name!("Bad.ServeR"))); + assert!(!acl_event.is_allowed(server_name!("bAD.sERvER"))); + assert!(!acl_event.is_allowed(server_name!("bAd.server"))); + assert!(acl_event.is_allowed(server_name!("good.ServEr"))); + assert!(acl_event.is_allowed(server_name!("good.server"))); + assert!(acl_event.is_allowed(server_name!("GOOD.SERVER"))); + } } diff --git a/crates/ruma-events/src/space/child.rs b/crates/ruma-events/src/space/child.rs index 5d25151485..70fde32b6f 100644 --- a/crates/ruma-events/src/space/child.rs +++ b/crates/ruma-events/src/space/child.rs @@ -304,7 +304,7 @@ mod tests { fn space_child_serialization() { let content = SpaceChildEventContent { via: vec![server_name!("example.com").to_owned()], - order: Some(SpaceChildOrder::parse("uwu").unwrap()), + order: Some(SpaceChildOrder::parse("uwu").unwrap().to_owned()), suggested: false, }; @@ -411,7 +411,8 @@ mod tests { origin_server_ts: UInt, ) -> HierarchySpaceChildEvent { let mut content = SpaceChildEventContent::new(vec![owned_server_name!("example.org")]); - content.order = order.and_then(|order| SpaceChildOrder::parse(order).ok()); + content.order = + order.and_then(|order| SpaceChildOrder::parse(order).ok()).map(ToOwned::to_owned); HierarchySpaceChildEvent { content, diff --git a/crates/ruma-events/src/space_order.rs b/crates/ruma-events/src/space_order.rs index 73f1e07f43..f1aa7b2262 100644 --- a/crates/ruma-events/src/space_order.rs +++ b/crates/ruma-events/src/space_order.rs @@ -66,7 +66,7 @@ mod tests { #[test] fn serialize() { - let space_order = SpaceOrderEventContent::new(SpaceChildOrder::parse("a").unwrap()); + let space_order = SpaceOrderEventContent::new(SpaceChildOrder::parse("a").unwrap().into()); let space_order_account_data = RoomAccountDataEvent { content: space_order }; assert_eq!( to_json_value(space_order_account_data).unwrap(), diff --git a/crates/ruma-events/src/state_key.rs b/crates/ruma-events/src/state_key.rs index 8f7046f676..519ef91a52 100644 --- a/crates/ruma-events/src/state_key.rs +++ b/crates/ruma-events/src/state_key.rs @@ -4,6 +4,12 @@ use serde::{ }, Serialize, Serializer, }; +use smallstr::SmallString; + +/// Opinionated value-oriented optimized String type for a state_key. +pub type StateKey = SmallString<[u8; STATE_KEY_INLINE_BYTES]>; + +const STATE_KEY_INLINE_BYTES: usize = 48; /// A type that can be used as the `state_key` for event types where that field is always empty. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] diff --git a/crates/ruma-federation-api/Cargo.toml b/crates/ruma-federation-api/Cargo.toml index 512b50d450..d8d45ea5c6 100644 --- a/crates/ruma-federation-api/Cargo.toml +++ b/crates/ruma-federation-api/Cargo.toml @@ -46,6 +46,7 @@ ruma-common = { workspace = true, features = ["api"] } ruma-events = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +smallstr = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } diff --git a/crates/ruma-federation-api/src/authenticated_media.rs b/crates/ruma-federation-api/src/authenticated_media.rs index 509ad052aa..e4bf18079e 100644 --- a/crates/ruma-federation-api/src/authenticated_media.rs +++ b/crates/ruma-federation-api/src/authenticated_media.rs @@ -2,6 +2,8 @@ //! //! [MSC3916]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916 +use std::borrow::Cow; + use ruma_common::http_headers::ContentDisposition; use serde::{Deserialize, Serialize}; @@ -46,7 +48,7 @@ pub struct Content { pub file: Vec, /// The content type of the file that was previously uploaded. - pub content_type: Option, + pub content_type: Option>, /// The value of the `Content-Disposition` HTTP header, possibly containing the name of the /// file that was previously uploaded. @@ -57,7 +59,7 @@ impl Content { /// Creates a new `Content` with the given bytes. pub fn new( file: Vec, - content_type: String, + content_type: Cow<'static, str>, content_disposition: ContentDisposition, ) -> Self { Self { @@ -250,7 +252,11 @@ fn try_from_multipart_mixed_response>( let content = if let Some(location) = location { FileOrLocation::Location(location) } else { - FileOrLocation::File(Content { file: file.to_owned(), content_type, content_disposition }) + FileOrLocation::File(Content { + file: file.to_owned(), + content_type: content_type.map(Into::into), + content_disposition, + }) }; Ok((metadata, content)) @@ -315,7 +321,7 @@ mod tests { let outgoing_metadata = ContentMetadata::new(); let outgoing_content = FileOrLocation::File(Content { file: file.to_vec(), - content_type: Some(content_type.to_owned()), + content_type: Some(content_type.into()), content_disposition: Some(content_disposition.clone()), }); @@ -342,7 +348,7 @@ mod tests { let outgoing_metadata = ContentMetadata::new(); let outgoing_content = FileOrLocation::File(Content { file: file.to_vec(), - content_type: Some(content_type.to_owned()), + content_type: Some(content_type.into()), content_disposition: Some(content_disposition.clone()), }); diff --git a/crates/ruma-federation-api/src/device/get_devices.rs b/crates/ruma-federation-api/src/device/get_devices.rs index f12d144edb..0580ea7261 100644 --- a/crates/ruma-federation-api/src/device/get_devices.rs +++ b/crates/ruma-federation-api/src/device/get_devices.rs @@ -16,6 +16,7 @@ pub mod v1 { OwnedDeviceId, OwnedUserId, }; use serde::{Deserialize, Serialize}; + use smallstr::SmallString; const METADATA: Metadata = metadata! { method: GET, @@ -95,9 +96,11 @@ pub mod v1 { /// Optional display name for the device #[serde(skip_serializing_if = "Option::is_none")] - pub device_display_name: Option, + pub device_display_name: Option, } + type DisplayName = SmallString<[u8; 40]>; + impl UserDevice { /// Creates a new `UserDevice` with the given device id and keys. pub fn new(device_id: OwnedDeviceId, keys: Raw) -> Self { diff --git a/crates/ruma-federation-api/src/discovery/get_server_version.rs b/crates/ruma-federation-api/src/discovery/get_server_version.rs index eadeb2b27d..e565ef9d2f 100644 --- a/crates/ruma-federation-api/src/discovery/get_server_version.rs +++ b/crates/ruma-federation-api/src/discovery/get_server_version.rs @@ -63,6 +63,22 @@ pub mod v1 { /// The version format depends on the implementation. #[serde(skip_serializing_if = "Option::is_none")] pub version: Option, + + /// Sourcecode version. + #[serde(skip_serializing_if = "Option::is_none")] + pub commit: Option, + + /// Compiler version. + #[serde(skip_serializing_if = "Option::is_none")] + pub compiler: Option, + + /// System version. + #[serde(skip_serializing_if = "Option::is_none")] + pub kernel: Option, + + /// Hardware architecture. + #[serde(skip_serializing_if = "Option::is_none")] + pub arch: Option, } impl Server { diff --git a/crates/ruma-federation-api/src/membership/create_join_event/v2.rs b/crates/ruma-federation-api/src/membership/create_join_event/v2.rs index 08faca9277..bf64d6310a 100644 --- a/crates/ruma-federation-api/src/membership/create_join_event/v2.rs +++ b/crates/ruma-federation-api/src/membership/create_join_event/v2.rs @@ -4,7 +4,7 @@ use ruma_common::{ api::{request, response, Metadata}, - metadata, OwnedEventId, OwnedRoomId, + metadata, OwnedEventId, OwnedRoomId, OwnedServerName, }; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue as RawJsonValue; @@ -111,7 +111,7 @@ pub struct RoomState { /// /// Required if `members_omitted` is set to `true`. #[serde(skip_serializing_if = "Option::is_none")] - pub servers_in_room: Option>, + pub servers_in_room: Option>, } impl RoomState { diff --git a/crates/ruma-federation-api/src/query/get_profile_information.rs b/crates/ruma-federation-api/src/query/get_profile_information.rs index eb13512427..b137a9785b 100644 --- a/crates/ruma-federation-api/src/query/get_profile_information.rs +++ b/crates/ruma-federation-api/src/query/get_profile_information.rs @@ -7,12 +7,15 @@ pub mod v1 { //! //! [spec]: https://spec.matrix.org/latest/server-server-api/#get_matrixfederationv1queryprofile + use std::collections::BTreeMap; + use ruma_common::{ api::{request, response, Metadata}, metadata, serde::StringEnum, OwnedMxcUri, OwnedUserId, }; + use serde_json::Value as JsonValue; use crate::PrivOwnedStr; @@ -64,6 +67,19 @@ pub mod v1 { #[cfg(feature = "unstable-msc2448")] #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")] pub blurhash: Option, + + /// [MSC4175][msc]: `m.tz` field for specifying a timezone the user is in + /// + /// [msc]: https://github.com/matrix-org/matrix-spec-proposals/blob/clokep/profile-tz/proposals/4175-profile-field-time-zone.md + /// + /// TODO: strong type this to be a valid IANA timezone? + #[serde(rename = "us.cloke.msc4175.tz", skip_serializing_if = "Option::is_none")] + pub tz: Option, + + /// Custom arbitrary profile fields as part of MSC4133 that are not reserved such as + /// MSC4175 + #[serde(flatten, skip_serializing_if = "BTreeMap::is_empty")] + pub custom_profile_fields: BTreeMap, } impl Request { diff --git a/crates/ruma-identity-service-api/Cargo.toml b/crates/ruma-identity-service-api/Cargo.toml index 0ccc20b375..499f23c5ca 100644 --- a/crates/ruma-identity-service-api/Cargo.toml +++ b/crates/ruma-identity-service-api/Cargo.toml @@ -22,6 +22,7 @@ js_int = { workspace = true, features = ["serde"] } ruma-common = { workspace = true, features = ["api"] } ruma-events = { workspace = true } serde = { workspace = true } +smallstr = { workspace = true } [dev-dependencies] serde_json = { workspace = true } diff --git a/crates/ruma-identity-service-api/src/discovery/get_supported_versions.rs b/crates/ruma-identity-service-api/src/discovery/get_supported_versions.rs index 7e4c5dcf00..437cf71c9d 100644 --- a/crates/ruma-identity-service-api/src/discovery/get_supported_versions.rs +++ b/crates/ruma-identity-service-api/src/discovery/get_supported_versions.rs @@ -16,6 +16,7 @@ use ruma_common::{ api::{request, response, Metadata, SupportedVersions}, metadata, }; +use smallstr::SmallString; const METADATA: Metadata = metadata! { method: GET, @@ -35,9 +36,15 @@ pub struct Request {} #[response] pub struct Response { /// A list of Matrix client API protocol versions supported by the endpoint. - pub versions: Vec, + pub versions: Vec, } +/// Opinionated optimized Version String type. +pub type Version = SmallString<[u8; 16]>; + +/// Opinionated optimized Feature String type. +pub type Feature = SmallString<[u8; 48]>; + impl Request { /// Creates an empty `Request`. pub fn new() -> Self { @@ -47,7 +54,7 @@ impl Request { impl Response { /// Creates a new `Response` with the given `versions`. - pub fn new(versions: Vec) -> Self { + pub fn new(versions: Vec) -> Self { Self { versions } } @@ -57,6 +64,9 @@ impl Response { /// Matrix versions that can't be parsed to a `MatrixVersion`, and features with the boolean /// value set to `false` are discarded. pub fn as_supported_versions(&self) -> SupportedVersions { - SupportedVersions::from_parts(&self.versions, &BTreeMap::new()) + SupportedVersions::from_parts( + self.versions.iter().map(Version::as_str), + BTreeMap::::new().iter().map(|(k, v)| (k.as_str(), v)), + ) } } diff --git a/crates/ruma-macros/src/api/request/outgoing.rs b/crates/ruma-macros/src/api/request/outgoing.rs index 4dd276a6f9..bcaf7d79bf 100644 --- a/crates/ruma-macros/src/api/request/outgoing.rs +++ b/crates/ruma-macros/src/api/request/outgoing.rs @@ -40,11 +40,14 @@ impl Request { quote! { "" } }; + let mut additional_headers: usize = 0; + // If there are no body fields, the request body will be empty (not `{}`), so the // `application/json` content-type would be wrong. It may also cause problems with CORS // policies that don't allow the `Content-Type` header (for things such as `.well-known` // that are commonly handled by something else than a homeserver). let mut header_kvs = if self.raw_body_field().is_some() || self.has_body_fields() { + additional_headers += 1; quote! { req_headers.insert( #http::header::CONTENT_TYPE, @@ -80,6 +83,7 @@ impl Request { } })); + additional_headers += 1; header_kvs.extend(quote! { req_headers.extend(METADATA.authorization_header(access_token)?); }); @@ -99,6 +103,8 @@ impl Request { let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); + let reserve_headers = self.header_fields().count() + additional_headers; + quote! { #[automatically_derived] #[cfg(feature = "client")] @@ -124,7 +130,10 @@ impl Request { )?); if let Some(mut req_headers) = req_builder.headers_mut() { + req_headers.reserve(#reserve_headers); #header_kvs + + debug_assert!(#reserve_headers >= req_headers.len(), "not enough headers reserved"); } let http_request = req_builder.body(#request_body)?; diff --git a/crates/ruma-macros/src/api/response.rs b/crates/ruma-macros/src/api/response.rs index 2bbed1d228..194e474a52 100644 --- a/crates/ruma-macros/src/api/response.rs +++ b/crates/ruma-macros/src/api/response.rs @@ -303,13 +303,6 @@ impl TryFrom for ResponseField { type Error = syn::Error; fn try_from(mut field: Field) -> syn::Result { - if has_lifetime(&field.ty) { - return Err(syn::Error::new_spanned( - field.ident, - "Lifetimes on Response fields cannot be supported until GAT are stable", - )); - } - let (mut api_attrs, attrs) = field.attrs.into_iter().partition::, _>(|attr| attr.path().is_ident("ruma_api")); field.attrs = attrs; @@ -341,6 +334,7 @@ impl ToTokens for ResponseField { } } +#[allow(dead_code)] fn has_lifetime(ty: &Type) -> bool { struct Visitor { found_lifetime: bool, diff --git a/crates/ruma-macros/src/api/response/incoming.rs b/crates/ruma-macros/src/api/response/incoming.rs index 8be167937d..75699ef8b9 100644 --- a/crates/ruma-macros/src/api/response/incoming.rs +++ b/crates/ruma-macros/src/api/response/incoming.rs @@ -64,11 +64,25 @@ impl Response { let syn::GenericArgument::Type(field_type) = option_args.first().unwrap() else { panic!("Option brackets should contain type"); }; - quote! { - #( #cfg_attrs )* - #field_name: { - headers.remove(#header_name) - .and_then(|h| { h.to_str().ok()?.parse::<#field_type>().ok() }) + let Type::Path(syn::TypePath { path: syn::Path { segments, .. }, .. }) = field_type else { + panic!("Option type should have a path") + }; + let ident = &segments.last().expect("Option type should have path segments").ident; + if ident == "Cow" { + quote! { + #( #cfg_attrs )* + #field_name: { + headers.remove(#header_name) + .map(|h| h.to_str().map(str::to_owned)).transpose()?.map(Into::into) + } + } + } else { + quote! { + #( #cfg_attrs )* + #field_name: { + headers.remove(#header_name) + .and_then(|h| { h.to_str().ok()?.parse::<#field_type>().ok() }) + } } } } @@ -84,6 +98,7 @@ impl Response { .to_str()? .parse::<#field_type>() .map_err(|e| #ruma_common::api::error::HeaderDeserializationError::InvalidHeader(e.into()))? + .into() } } } diff --git a/crates/ruma-macros/src/api/response/outgoing.rs b/crates/ruma-macros/src/api/response/outgoing.rs index 114d95d894..4d036ac483 100644 --- a/crates/ruma-macros/src/api/response/outgoing.rs +++ b/crates/ruma-macros/src/api/response/outgoing.rs @@ -5,10 +5,15 @@ use syn::Ident; use super::{Response, ResponseField}; impl Response { + #[allow(clippy::wildcard_in_or_patterns)] pub fn expand_outgoing(&self, status_ident: &Ident, ruma_common: &TokenStream) -> TokenStream { let bytes = quote! { #ruma_common::exports::bytes }; let http = quote! { #ruma_common::exports::http }; + let reserve_headers = self.fields.iter().fold(0_usize, |acc, response_field| { + acc + (response_field.as_header_field().is_some() as usize) + }); + let serialize_response_headers = self.fields.iter().filter_map(|response_field| { response_field.as_header_field().map(|(field, header_name)| { let field_name = @@ -18,19 +23,56 @@ impl Response { syn::Type::Path(syn::TypePath { path: syn::Path { segments, .. }, .. }) if segments.last().unwrap().ident == "Option" => { - quote! { - if let Some(header) = self.#field_name { - headers.insert( - #header_name, - header.to_string().parse()?, - ); + let syn::PathArguments::AngleBracketed( + syn::AngleBracketedGenericArguments { args: option_args, .. }, + ) = &segments.last().unwrap().arguments + else { + panic!("Option should use angle brackets"); + }; + let syn::GenericArgument::Type(field_type) = option_args.first().unwrap() + else { + panic!("Option brackets should contain type"); + }; + let syn::Type::Path(syn::TypePath { + path: syn::Path { segments, .. }, .. + }) = field_type + else { + panic!("Option type should have a path") + }; + let ident = + &segments.last().expect("Option type should have path segments").ident; + match ident.to_string().as_str() { + "Cow" => { + quote! { + if let Some(ref header) = self.#field_name { + headers.insert( + #header_name, + match header { + ::std::borrow::Cow::Borrowed(ref header) => + #http::header::HeaderValue::from_static(header), + ::std::borrow::Cow::Owned(ref header) => + #http::header::HeaderValue::from_str(&header)?, + }, + ); + } + } + } + "ContentDisposition" | _ => { + quote! { + if let Some(ref header) = self.#field_name { + headers.insert( + #header_name, + header.try_into()?, + ); + } + } } } } _ => quote! { headers.insert( #header_name, - self.#field_name.to_string().parse()?, + self.#field_name.try_into()?, ); }, } @@ -68,11 +110,15 @@ impl Response { fn try_into_http_response( self, ) -> ::std::result::Result<#http::Response, #ruma_common::api::error::IntoHttpError> { + static APPLICATION_JSON: #http::header::HeaderValue = + #http::header::HeaderValue::from_static("application/json"); + let mut resp_builder = #http::Response::builder() - .status(#http::StatusCode::#status_ident) - .header(#http::header::CONTENT_TYPE, "application/json"); + .status(#http::StatusCode::#status_ident); if let Some(mut headers) = resp_builder.headers_mut() { + headers.reserve(1 + #reserve_headers); + headers.insert(#http::header::CONTENT_TYPE, APPLICATION_JSON.clone()); #(#serialize_response_headers)* } diff --git a/crates/ruma-macros/src/events/event.rs b/crates/ruma-macros/src/events/event.rs index c86a5929ff..2bb58bc538 100644 --- a/crates/ruma-macros/src/events/event.rs +++ b/crates/ruma-macros/src/events/event.rs @@ -87,6 +87,8 @@ fn expand_deserialize_event( }) .collect(); + let state_key_ty = quote! { ruma_events::StateKey }; + let deserialize_var_types: Vec<_> = fields .iter() .map(|field| { @@ -98,7 +100,7 @@ fn expand_deserialize_event( quote! { #content_type } } } else if name == "state_key" && var == EventVariation::Initial { - quote! { ::std::string::String } + quote! { #state_key_ty } } else { let ty = &field.ty; quote! { #ty } @@ -127,9 +129,9 @@ fn expand_deserialize_event( } else if name == "state_key" && var == EventVariation::Initial { let ty = &field.ty; quote! { - let state_key: ::std::string::String = state_key.unwrap_or_default(); + let state_key: #state_key_ty = state_key.unwrap_or(<#state_key_ty>::new()); let state_key: #ty = <#ty as #serde::de::Deserialize>::deserialize( - #serde::de::IntoDeserializer::::into_deserializer(state_key), + #serde::de::IntoDeserializer::::into_deserializer(state_key.as_bytes()), )?; } } else { diff --git a/crates/ruma-macros/src/events/event_enum/event_type.rs b/crates/ruma-macros/src/events/event_enum/event_type.rs index 96fd16d984..0e5ba2a725 100644 --- a/crates/ruma-macros/src/events/event_enum/event_type.rs +++ b/crates/ruma-macros/src/events/event_enum/event_type.rs @@ -163,7 +163,8 @@ fn generate_enum( #[allow(deprecated)] impl #ident { - fn to_cow_str(&self) -> ::std::borrow::Cow<'_, ::std::primitive::str> { + /// Access the string for the type + pub fn to_cow_str(&self) -> ::std::borrow::Cow<'_, ::std::primitive::str> { match self { #(#to_cow_str_match_arms,)* Self::_Custom(crate::PrivOwnedStr(s)) => ::std::borrow::Cow::Borrowed(s), @@ -202,6 +203,13 @@ fn generate_enum( } } + #[allow(deprecated)] + impl<'a> ::std::convert::From> for #ident { + fn from(s: std::borrow::Cow<'a, ::std::primitive::str>) -> Self { + Self::from(s.as_ref()) + } + } + #[allow(deprecated)] impl<'de> #serde::Deserialize<'de> for #ident { fn deserialize(deserializer: D) -> ::std::result::Result diff --git a/crates/ruma-macros/src/identifiers.rs b/crates/ruma-macros/src/identifiers.rs index f3bdf555fa..1eb0f6efe5 100644 --- a/crates/ruma-macros/src/identifiers.rs +++ b/crates/ruma-macros/src/identifiers.rs @@ -24,11 +24,6 @@ impl Parse for IdentifierInput { } pub fn expand_id_dst(input: ItemStruct) -> syn::Result { - let id = &input.ident; - let owned = format_ident!("Owned{id}"); - - let owned_decl = expand_owned_id(&input); - let meta = input.attrs.iter().filter(|attr| attr.path().is_ident("ruma_id")).try_fold( IdDstMeta::default(), |meta, attr| { @@ -53,8 +48,20 @@ pub fn expand_id_dst(input: ItemStruct) -> syn::Result { // So we don't have to insert #where_clause everywhere when it is always None in practice assert_eq!(where_clause, None, "where clauses on identifier types are not currently supported"); + let id = &input.ident; + let id_ty = quote! { #id #ty_generics }; + + const INLINE_BYTES: usize = 32; + let inline_bytes = meta.inline_bytes.unwrap_or(INLINE_BYTES); + let inline_array = quote! { [u8; #inline_bytes] }; + let sv_decl = quote! { smallvec::SmallVec<#inline_array> }; + let sv = quote! { smallvec::SmallVec::<#inline_array> }; + let as_str_docs = format!("Creates a string slice from this `{id}`."); let as_bytes_docs = format!("Creates a byte slice from this `{id}`."); + let max_bytes_docs = format!("Maximum byte length for any `{id}`."); + let len_docs = format!("Get the string length of {id}."); + let is_empty_docs = format!("Returns true if {id} has zero length."); let as_str_impl = match &input.fields { Fields::Named(_) | Fields::Unit => { @@ -67,42 +74,65 @@ pub fn expand_id_dst(input: ItemStruct) -> syn::Result { } }; - let id_ty = quote! { #id #ty_generics }; - let owned_ty = quote! { #owned #ty_generics }; - let as_str_impls = expand_as_str_impls(id_ty.clone(), &impl_generics); + let partial_eq_string = expand_partial_eq_string(id_ty.clone(), &impl_generics); + // FIXME: Remove? let box_partial_eq_string = expand_partial_eq_string(quote! { Box<#id_ty> }, &impl_generics); + let owned_decl = expand_owned_id(&input, inline_bytes); + Ok(quote! { #owned_decl #[automatically_derived] impl #impl_generics #id_ty { + #[doc = #max_bytes_docs] + pub const ID_MAX_BYTES: usize = ruma_identifiers_validation::ID_MAX_BYTES; + + #[inline] pub(super) const fn from_borrowed(s: &str) -> &Self { unsafe { std::mem::transmute(s) } } + #[inline] pub(super) fn from_box(s: Box) -> Box { unsafe { Box::from_raw(Box::into_raw(s) as _) } } + #[inline] pub(super) fn from_rc(s: std::rc::Rc) -> std::rc::Rc { unsafe { std::rc::Rc::from_raw(std::rc::Rc::into_raw(s) as _) } } + #[inline] pub(super) fn from_arc(s: std::sync::Arc) -> std::sync::Arc { unsafe { std::sync::Arc::from_raw(std::sync::Arc::into_raw(s) as _) } } - pub(super) fn into_owned(self: Box) -> Box { - unsafe { Box::from_raw(Box::into_raw(self) as _) } + #[inline] + pub(super) fn into_owned(self: Box) -> #sv_decl { + let len = self.as_bytes().len(); + let p: *mut u8 = Box::into_raw(self).cast(); + let v = unsafe { Vec::::from_raw_parts(p, len, len) }; + #sv::from_vec(v) + } + + #[inline] + pub(super) fn into_box(s: Box) -> Box { + unsafe { Box::from_raw(Box::into_raw(s) as _) } } - #[doc = #as_str_docs] #[inline] - pub fn as_str(&self) -> &str { - #as_str_impl + #[doc = #is_empty_docs] + pub fn is_empty(&self) -> bool { + self.as_str().is_empty() + } + + #[inline] + #[doc = #len_docs] + pub fn len(&self) -> usize { + self.as_str().len() } #[doc = #as_bytes_docs] @@ -110,6 +140,11 @@ pub fn expand_id_dst(input: ItemStruct) -> syn::Result { pub fn as_bytes(&self) -> &[u8] { self.as_str().as_bytes() } + #[doc = #as_str_docs] + #[inline] + pub fn as_str(&self) -> &str { + #as_str_impl + } } #[automatically_derived] @@ -119,17 +154,9 @@ pub fn expand_id_dst(input: ItemStruct) -> syn::Result { } } - #[automatically_derived] - impl #impl_generics ToOwned for #id_ty { - type Owned = #owned_ty; - - fn to_owned(&self) -> Self::Owned { - #owned::from_ref(self) - } - } - #[automatically_derived] impl #impl_generics AsRef<#id_ty> for #id_ty { + #[inline] fn as_ref(&self) -> &#id_ty { self } @@ -137,6 +164,7 @@ pub fn expand_id_dst(input: ItemStruct) -> syn::Result { #[automatically_derived] impl #impl_generics AsRef for #id_ty { + #[inline] fn as_ref(&self) -> &str { self.as_str() } @@ -144,6 +172,7 @@ pub fn expand_id_dst(input: ItemStruct) -> syn::Result { #[automatically_derived] impl #impl_generics AsRef for Box<#id_ty> { + #[inline] fn as_ref(&self) -> &str { self.as_str() } @@ -151,6 +180,7 @@ pub fn expand_id_dst(input: ItemStruct) -> syn::Result { #[automatically_derived] impl #impl_generics AsRef<[u8]> for #id_ty { + #[inline] fn as_ref(&self) -> &[u8] { self.as_bytes() } @@ -158,11 +188,14 @@ pub fn expand_id_dst(input: ItemStruct) -> syn::Result { #[automatically_derived] impl #impl_generics AsRef<[u8]> for Box<#id_ty> { + #[inline] fn as_ref(&self) -> &[u8] { self.as_bytes() } } + #as_str_impls + #[automatically_derived] impl #impl_generics From<&#id_ty> for String { fn from(id: &#id_ty) -> Self { @@ -172,8 +205,9 @@ pub fn expand_id_dst(input: ItemStruct) -> syn::Result { #[automatically_derived] impl #impl_generics From> for String { + #[inline] fn from(id: Box<#id_ty>) -> Self { - id.into_owned().into() + String::from(<#id_ty>::into_box(id)) } } @@ -202,6 +236,7 @@ pub fn expand_id_dst(input: ItemStruct) -> syn::Result { #[automatically_derived] impl #impl_generics PartialEq<#id_ty> for Box<#id_ty> { + #[inline] fn eq(&self, other: &#id_ty) -> bool { self.as_str() == other.as_str() } @@ -209,6 +244,7 @@ pub fn expand_id_dst(input: ItemStruct) -> syn::Result { #[automatically_derived] impl #impl_generics PartialEq<&'_ #id_ty> for Box<#id_ty> { + #[inline] fn eq(&self, other: &&#id_ty) -> bool { self.as_str() == other.as_str() } @@ -216,6 +252,7 @@ pub fn expand_id_dst(input: ItemStruct) -> syn::Result { #[automatically_derived] impl #impl_generics PartialEq> for #id_ty { + #[inline] fn eq(&self, other: &Box<#id_ty>) -> bool { self.as_str() == other.as_str() } @@ -223,91 +260,124 @@ pub fn expand_id_dst(input: ItemStruct) -> syn::Result { #[automatically_derived] impl #impl_generics PartialEq> for &'_ #id_ty { + #[inline] fn eq(&self, other: &Box<#id_ty>) -> bool { self.as_str() == other.as_str() } } - #as_str_impls + #partial_eq_string #box_partial_eq_string + #extra_impls }) } -fn expand_owned_id(input: &ItemStruct) -> TokenStream { - let id = &input.ident; - let owned = format_ident!("Owned{id}"); - - let doc_header = format!("Owned variant of {id}"); +fn expand_owned_id(input: &ItemStruct, inline_bytes: usize) -> TokenStream { let (impl_generics, ty_generics, _where_clause) = input.generics.split_for_impl(); + let listed_generics: Punctuated<_, Token![,]> = + input.generics.type_params().map(|param| ¶m.ident).collect(); + let id = &input.ident; + let owned = format_ident!("Owned{id}"); let id_ty = quote! { #id #ty_generics }; let owned_ty = quote! { #owned #ty_generics }; + let inline_array = quote! { [u8; #inline_bytes] }; + let sv_decl = quote! { smallvec::SmallVec<#inline_array> }; + + let has_generics = !listed_generics.is_empty(); + let phantom_decl = if has_generics { + quote! { _p: std::marker::PhantomData<(#listed_generics)>, } + } else { + quote! {} + }; + + let phantom_impl = if has_generics { + quote! { _p: std::marker::PhantomData::<(#listed_generics)>, } + } else { + quote! {} + }; + let as_str_impls = expand_as_str_impls(owned_ty.clone(), &impl_generics); + let partial_eq_string = expand_partial_eq_string(owned_ty.clone(), &impl_generics); + + let doc_header = format!("Owned variant of {id}"); + let capacity_doc = format!("Get the size of the buffer backing {id}"); quote! { #[doc = #doc_header] - /// - /// The wrapper type for this type is variable, by default it'll use [`Box`], - /// but you can change that by setting "`--cfg=ruma_identifiers_storage=...`" using - /// `RUSTFLAGS` or `.cargo/config.toml` (under `[build]` -> `rustflags = ["..."]`) - /// to the following; - /// - `ruma_identifiers_storage="Arc"` to use [`Arc`](std::sync::Arc) as a wrapper type. pub struct #owned #impl_generics { - #[cfg(not(any(ruma_identifiers_storage = "Arc")))] - inner: Box<#id_ty>, - #[cfg(ruma_identifiers_storage = "Arc")] - inner: std::sync::Arc<#id_ty>, + inner: #sv_decl, + #phantom_decl } #[automatically_derived] impl #impl_generics #owned_ty { - fn from_ref(v: &#id_ty) -> Self { + #[inline] + fn new(inner: #sv_decl) -> Self { Self { - #[cfg(not(any(ruma_identifiers_storage = "Arc")))] - inner: #id::from_box(v.as_str().into()), - #[cfg(ruma_identifiers_storage = "Arc")] - inner: #id::from_arc(v.as_str().into()), + inner, + #phantom_impl } } + + #[inline] + fn from_iov(iov: &[&str]) -> Result { + use std::io::Write; + let len = iov.iter().map(|s| s.len()).fold(0_usize, usize::saturating_add); + let mut s = Self::new(<#sv_decl>::with_capacity(len)); + iov.iter().try_for_each(|part| s.inner.write_all(part.as_bytes()))?; + Ok(s) + } + + #[inline] + #[doc = #capacity_doc] + pub fn capacity(&self) -> usize { + self.inner.capacity() + } } #[automatically_derived] impl #impl_generics AsRef<#id_ty> for #owned_ty { + #[inline] fn as_ref(&self) -> &#id_ty { - &*self.inner + let s: &str = self.as_ref(); + <#id_ty>::from_borrowed(s) } } #[automatically_derived] impl #impl_generics AsRef for #owned_ty { + #[inline] + #[cfg(debug_assertions)] + fn as_ref(&self) -> &str { + let s: &[u8] = self.as_ref(); + std::str::from_utf8(s).expect("identifier buffer contained invalid utf8") + } + + #[inline] + #[cfg(not(debug_assertions))] fn as_ref(&self) -> &str { - self.inner.as_str() + let s: &[u8] = self.as_ref(); + unsafe { std::str::from_utf8_unchecked(s) } } } #[automatically_derived] impl #impl_generics AsRef<[u8]> for #owned_ty { + #[inline] fn as_ref(&self) -> &[u8] { - self.inner.as_bytes() + self.inner.as_slice() } } - #[automatically_derived] - impl #impl_generics From<#owned_ty> for String { - fn from(id: #owned_ty) -> String { - #[cfg(not(any(ruma_identifiers_storage = "Arc")))] - { id.inner.into() } - #[cfg(ruma_identifiers_storage = "Arc")] - { id.inner.as_ref().into() } - } - } + #as_str_impls #[automatically_derived] impl #impl_generics std::clone::Clone for #owned_ty { fn clone(&self) -> Self { - (&*self.inner).into() + Self::new(self.inner.clone()) } } @@ -315,76 +385,108 @@ fn expand_owned_id(input: &ItemStruct) -> TokenStream { impl #impl_generics std::ops::Deref for #owned_ty { type Target = #id_ty; + #[inline] fn deref(&self) -> &Self::Target { - &self.inner + self.as_ref() + } + } + + #[automatically_derived] + impl #impl_generics std::borrow::ToOwned for #id_ty { + type Owned = #owned_ty; + + fn to_owned(&self) -> Self::Owned { + Self::Owned::new(self.as_bytes().into()) } } #[automatically_derived] impl #impl_generics std::borrow::Borrow<#id_ty> for #owned_ty { + #[inline] fn borrow(&self) -> &#id_ty { self.as_ref() } } + #[automatically_derived] + impl #impl_generics std::hash::Hash for #owned_ty { + fn hash(&self, state: &mut H) { + self.as_str().hash(state) + } + } + #[automatically_derived] impl #impl_generics From<&'_ #id_ty> for #owned_ty { fn from(id: &#id_ty) -> #owned_ty { - #owned { inner: id.into() } + Self::new(id.as_bytes().into()) + } + } + + #[automatically_derived] + impl #impl_generics From<&'_ #owned_ty> for #owned_ty { + fn from(id: &#owned_ty) -> #owned_ty { + id.clone() } } #[automatically_derived] impl #impl_generics From> for #owned_ty { fn from(b: Box<#id_ty>) -> #owned_ty { - Self { inner: b.into() } + Self::new(<#id_ty>::into_owned(b)) } } #[automatically_derived] impl #impl_generics From> for #owned_ty { - fn from(a: std::sync::Arc<#id_ty>) -> #owned_ty { - Self { - #[cfg(not(any(ruma_identifiers_storage = "Arc")))] - inner: a.as_ref().into(), - #[cfg(ruma_identifiers_storage = "Arc")] - inner: a, - } + fn from(id: std::sync::Arc<#id_ty>) -> #owned_ty { + (*id).to_owned() + } + } + + #[automatically_derived] + impl #impl_generics From<#owned_ty> for String { + #[cfg(debug_assertions)] + fn from(id: #owned_ty) -> String { + String::from_utf8(id.inner.into_vec()).expect("identifier buffer contained invalid utf8") + } + + #[cfg(not(debug_assertions))] + fn from(id: #owned_ty) -> String { + unsafe { String::from_utf8_unchecked(id.inner.into_vec()) } } } #[automatically_derived] impl #impl_generics From<#owned_ty> for Box<#id_ty> { - fn from(a: #owned_ty) -> Box<#id_ty> { - #[cfg(not(any(ruma_identifiers_storage = "Arc")))] - { a.inner } - #[cfg(ruma_identifiers_storage = "Arc")] - { a.inner.as_ref().into() } + fn from(id: #owned_ty) -> Box<#id_ty> { + let id: String = id.into(); + let id: Box = id.into_boxed_str(); + <#id_ty>::from_box(id) } } #[automatically_derived] impl #impl_generics From<#owned_ty> for std::sync::Arc<#id_ty> { - fn from(a: #owned_ty) -> std::sync::Arc<#id_ty> { - #[cfg(not(any(ruma_identifiers_storage = "Arc")))] - { a.inner.into() } - #[cfg(ruma_identifiers_storage = "Arc")] - { a.inner } + fn from(id: #owned_ty) -> std::sync::Arc<#id_ty> { + let id: Box<#id_ty> = id.into(); + std::sync::Arc::from(id) } } + #[automatically_derived] + impl #impl_generics std::cmp::Eq for #owned_ty {} + #[automatically_derived] impl #impl_generics std::cmp::PartialEq for #owned_ty { + #[inline] fn eq(&self, other: &Self) -> bool { self.as_str() == other.as_str() } } - #[automatically_derived] - impl #impl_generics std::cmp::Eq for #owned_ty {} - #[automatically_derived] impl #impl_generics std::cmp::PartialOrd for #owned_ty { + #[inline] fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } @@ -392,25 +494,15 @@ fn expand_owned_id(input: &ItemStruct) -> TokenStream { #[automatically_derived] impl #impl_generics std::cmp::Ord for #owned_ty { + #[inline] fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.as_str().cmp(other.as_str()) } } - #[automatically_derived] - impl #impl_generics std::hash::Hash for #owned_ty { - fn hash(&self, state: &mut H) - where - H: std::hash::Hasher, - { - self.as_str().hash(state) - } - } - - #as_str_impls - #[automatically_derived] impl #impl_generics PartialEq<#id_ty> for #owned_ty { + #[inline] fn eq(&self, other: &#id_ty) -> bool { AsRef::<#id_ty>::as_ref(self) == other } @@ -418,6 +510,7 @@ fn expand_owned_id(input: &ItemStruct) -> TokenStream { #[automatically_derived] impl #impl_generics PartialEq<#owned_ty> for #id_ty { + #[inline] fn eq(&self, other: &#owned_ty) -> bool { self == AsRef::<#id_ty>::as_ref(other) } @@ -425,6 +518,7 @@ fn expand_owned_id(input: &ItemStruct) -> TokenStream { #[automatically_derived] impl #impl_generics PartialEq<&#id_ty> for #owned_ty { + #[inline] fn eq(&self, other: &&#id_ty) -> bool { AsRef::<#id_ty>::as_ref(self) == *other } @@ -432,6 +526,7 @@ fn expand_owned_id(input: &ItemStruct) -> TokenStream { #[automatically_derived] impl #impl_generics PartialEq<#owned_ty> for &#id_ty { + #[inline] fn eq(&self, other: &#owned_ty) -> bool { *self == AsRef::<#id_ty>::as_ref(other) } @@ -439,6 +534,7 @@ fn expand_owned_id(input: &ItemStruct) -> TokenStream { #[automatically_derived] impl #impl_generics PartialEq> for #owned_ty { + #[inline] fn eq(&self, other: &Box<#id_ty>) -> bool { AsRef::<#id_ty>::as_ref(self) == AsRef::<#id_ty>::as_ref(other) } @@ -446,6 +542,7 @@ fn expand_owned_id(input: &ItemStruct) -> TokenStream { #[automatically_derived] impl #impl_generics PartialEq<#owned_ty> for Box<#id_ty> { + #[inline] fn eq(&self, other: &#owned_ty) -> bool { AsRef::<#id_ty>::as_ref(self) == AsRef::<#id_ty>::as_ref(other) } @@ -453,6 +550,7 @@ fn expand_owned_id(input: &ItemStruct) -> TokenStream { #[automatically_derived] impl #impl_generics PartialEq> for #owned_ty { + #[inline] fn eq(&self, other: &std::sync::Arc<#id_ty>) -> bool { AsRef::<#id_ty>::as_ref(self) == AsRef::<#id_ty>::as_ref(other) } @@ -460,84 +558,142 @@ fn expand_owned_id(input: &ItemStruct) -> TokenStream { #[automatically_derived] impl #impl_generics PartialEq<#owned_ty> for std::sync::Arc<#id_ty> { + #[inline] fn eq(&self, other: &#owned_ty) -> bool { AsRef::<#id_ty>::as_ref(self) == AsRef::<#id_ty>::as_ref(other) } } + + #partial_eq_string } } fn expand_checked_impls(input: &ItemStruct, validate: Path) -> TokenStream { - let id = &input.ident; - let owned = format_ident!("Owned{id}"); - let (impl_generics, ty_generics, _where_clause) = input.generics.split_for_impl(); let generic_params = &input.generics.params; + let id = &input.ident; + let owned = format_ident!("Owned{id}"); + let id_ty = quote! { #id #ty_generics }; + let owned_ty = quote! { #owned #ty_generics }; + let parse_doc_header = format!("Try parsing a `&str` into an `Owned{id}`."); let parse_box_doc_header = format!("Try parsing a `&str` into a `Box<{id}>`."); let parse_rc_docs = format!("Try parsing a `&str` into an `Rc<{id}>`."); let parse_arc_docs = format!("Try parsing a `&str` into an `Arc<{id}>`."); - - let id_ty = quote! { #id #ty_generics }; - let owned_ty = quote! { #owned #ty_generics }; + let from_parts_docs = format!("Try parsing parts of an {id} into an `Owned{id}`."); quote! { #[automatically_derived] - impl #impl_generics #id_ty { + impl #impl_generics #owned_ty { + #[inline] #[doc = #parse_doc_header] - /// - /// The same can also be done using `FromStr`, `TryFrom` or `TryInto`. - /// This function is simply more constrained and thus useful in generic contexts. - pub fn parse( - s: impl AsRef, + pub fn parse(s: S) -> Result<#owned_ty, crate::IdParseError> + where + S: Into + Sized, + { + <#id_ty>::parse_owned(s.into()) + } + + #[inline] + #[doc = #from_parts_docs] + pub fn from_parts( + sigil: char, + local: &str, + domain: Option<&str>, ) -> Result<#owned_ty, crate::IdParseError> { - let s = s.as_ref(); + let mut buf: [u8; 4] = Default::default(); + let iov = [ + sigil.encode_utf8(&mut buf), + local, + domain.clone().map(|_| ":").unwrap_or(""), + domain.unwrap_or(""), + ]; + + let s = Self::from_iov(&iov) + .expect("failed to gather into identifier buffer"); + + #validate(s.as_ref())?; + Ok(s) + } + } + + #[automatically_derived] + impl #impl_generics #id_ty { + #[inline] + #[doc = #parse_doc_header] + pub fn parse(s: &S) -> Result<&'_ #id_ty, crate::IdParseError> + where + S: AsRef + ?Sized, + { + Self::parse_ref(s.as_ref()) + } + + #[doc = #parse_doc_header] + fn parse_into_owned(s: String) -> Result<#owned_ty, crate::IdParseError> { + #validate(s.as_str())?; + + let s: Vec = s.into(); + Ok(<#owned_ty>::new(s.into())) + } + + #[inline] + #[doc = #parse_doc_header] + fn parse_owned(s: impl AsRef) -> Result<#owned_ty, crate::IdParseError> { + #validate(s.as_ref())?; + Ok(<#id_ty>::from_borrowed(s.as_ref()).to_owned()) + } + + #[inline] + #[doc = #parse_doc_header] + fn parse_ref<'a>( + s: &'a str, + ) -> Result<&'a #id_ty, crate::IdParseError> { #validate(s)?; - Ok(#id::from_borrowed(s).to_owned()) + Ok(<#id_ty>::from_borrowed(s)) } + #[inline] #[doc = #parse_box_doc_header] - /// - /// The same can also be done using `FromStr`, `TryFrom` or `TryInto`. - /// This function is simply more constrained and thus useful in generic contexts. - pub fn parse_box( + fn parse_box( s: impl AsRef + Into>, ) -> Result, crate::IdParseError> { #validate(s.as_ref())?; - Ok(#id::from_box(s.into())) + Ok(<#id_ty>::from_box(s.into())) } + #[inline] #[doc = #parse_rc_docs] - pub fn parse_rc( + fn parse_rc( s: impl AsRef + Into>, ) -> Result, crate::IdParseError> { #validate(s.as_ref())?; - Ok(#id::from_rc(s.into())) + Ok(<#id_ty>::from_rc(s.into())) } + #[inline] #[doc = #parse_arc_docs] - pub fn parse_arc( + fn parse_arc( s: impl AsRef + Into>, ) -> Result, crate::IdParseError> { #validate(s.as_ref())?; - Ok(#id::from_arc(s.into())) + Ok(<#id_ty>::from_arc(s.into())) } } #[automatically_derived] - impl<'de, #generic_params> serde::Deserialize<'de> for Box<#id_ty> { + impl<'de, #generic_params> serde::Deserialize<'de> for &'de #id_ty { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { use serde::de::Error; - let s = String::deserialize(deserializer)?; + let s = <&'de str>::deserialize(deserializer)?; - match #id::parse_box(s) { - Ok(o) => Ok(o), + match <#id_ty>::parse_ref(s) { Err(e) => Err(D::Error::custom(e)), + Ok(o) => Ok(o), } } } @@ -552,9 +708,26 @@ fn expand_checked_impls(input: &ItemStruct, validate: Path) -> TokenStream { let s = String::deserialize(deserializer)?; - match #id::parse(s) { + match <#id_ty>::parse_into_owned(s) { + Err(e) => Err(D::Error::custom(e)), Ok(o) => Ok(o), + } + } + } + + #[automatically_derived] + impl<'de, #generic_params> serde::Deserialize<'de> for Box<#id_ty> { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + + let s = Box::::deserialize(deserializer)?; + + match <#id_ty>::parse_box(s) { Err(e) => Err(D::Error::custom(e)), + Ok(o) => Ok(o), } } } @@ -563,16 +736,58 @@ fn expand_checked_impls(input: &ItemStruct, validate: Path) -> TokenStream { impl<'a, #generic_params> std::convert::TryFrom<&'a str> for &'a #id_ty { type Error = crate::IdParseError; + #[inline] fn try_from(s: &'a str) -> Result { #validate(s)?; Ok(<#id_ty>::from_borrowed(s)) } } + #[automatically_derived] + impl<'a, #generic_params> std::convert::TryFrom<&'a serde_json::Value> for &'a #id_ty { + type Error = crate::IdParseError; + + #[inline] + fn try_from(v: &'a serde_json::Value) -> Result { + <#id_ty>::parse_ref(v.as_str().unwrap_or_default()) + } + } + + #[automatically_derived] + impl<'a, #generic_params> std::convert::TryFrom<&'a crate::CanonicalJsonValue> for &'a #id_ty { + type Error = crate::IdParseError; + + #[inline] + fn try_from(v: &'a crate::CanonicalJsonValue) -> Result { + <#id_ty>::parse_ref(v.as_str().unwrap_or_default()) + } + } + + #[automatically_derived] + impl<'a, #generic_params> std::convert::TryFrom> for &'a #id_ty { + type Error = crate::IdParseError; + + #[inline] + fn try_from(v: Option<&'a serde_json::Value>) -> Result { + <#id_ty>::parse_ref(v.map(|v| v.as_str()).flatten().unwrap_or_default()) + } + } + + #[automatically_derived] + impl<'a, #generic_params> std::convert::TryFrom> for &'a #id_ty { + type Error = crate::IdParseError; + + #[inline] + fn try_from(v: Option<&'a crate::CanonicalJsonValue>) -> Result { + <#id_ty>::parse_ref(v.map(|v| v.as_str()).flatten().unwrap_or_default()) + } + } + #[automatically_derived] impl #impl_generics std::str::FromStr for Box<#id_ty> { type Err = crate::IdParseError; + #[inline] fn from_str(s: &str) -> Result { <#id_ty>::parse_box(s) } @@ -582,6 +797,7 @@ fn expand_checked_impls(input: &ItemStruct, validate: Path) -> TokenStream { impl #impl_generics std::convert::TryFrom<&str> for Box<#id_ty> { type Error = crate::IdParseError; + #[inline] fn try_from(s: &str) -> Result { <#id_ty>::parse_box(s) } @@ -591,8 +807,9 @@ fn expand_checked_impls(input: &ItemStruct, validate: Path) -> TokenStream { impl #impl_generics std::convert::TryFrom for Box<#id_ty> { type Error = crate::IdParseError; + #[inline] fn try_from(s: String) -> Result { - <#id_ty>::parse_box(s) + <#id_ty>::parse_box(s.into_boxed_str()) } } @@ -600,8 +817,9 @@ fn expand_checked_impls(input: &ItemStruct, validate: Path) -> TokenStream { impl #impl_generics std::str::FromStr for #owned_ty { type Err = crate::IdParseError; - fn from_str(s: &str) -> Result { - <#id_ty>::parse(s) + #[inline] + fn from_str(s: &str) -> Result { + <#id_ty>::parse_owned(s) } } @@ -609,8 +827,9 @@ fn expand_checked_impls(input: &ItemStruct, validate: Path) -> TokenStream { impl #impl_generics std::convert::TryFrom<&str> for #owned_ty { type Error = crate::IdParseError; + #[inline] fn try_from(s: &str) -> Result { - <#id_ty>::parse(s) + <#id_ty>::parse_owned(s) } } @@ -618,103 +837,121 @@ fn expand_checked_impls(input: &ItemStruct, validate: Path) -> TokenStream { impl #impl_generics std::convert::TryFrom for #owned_ty { type Error = crate::IdParseError; + #[inline] fn try_from(s: String) -> Result { - <#id_ty>::parse(s) + <#id_ty>::parse_into_owned(s) } } } } fn expand_unchecked_impls(input: &ItemStruct) -> TokenStream { + let (impl_generics, ty_generics, _where_clause) = input.generics.split_for_impl(); + let generic_params = &input.generics.params; + let id = &input.ident; let owned = format_ident!("Owned{id}"); + let id_ty = quote! { #id #ty_generics }; + let owned_ty = quote! { #owned #ty_generics }; quote! { #[automatically_derived] - impl<'a> From<&'a str> for &'a #id { + impl<'a, #generic_params> From<&'a str> for &'a #id_ty { + #[inline] fn from(s: &'a str) -> Self { - #id::from_borrowed(s) + <#id_ty>::from_borrowed(s) } } #[automatically_derived] - impl From<&str> for #owned { + impl #impl_generics From<&str> for #owned_ty { + #[inline] fn from(s: &str) -> Self { - <&#id>::from(s).into() + <&#id_ty>::from(s).into() } } #[automatically_derived] - impl From> for #owned { + impl #impl_generics From> for #owned_ty { fn from(s: Box) -> Self { - <&#id>::from(&*s).into() + let s: String = s.into(); + Self::from(s) } } #[automatically_derived] - impl From for #owned { + impl #impl_generics From for #owned_ty { fn from(s: String) -> Self { - <&#id>::from(s.as_str()).into() + Self::new(s.into_bytes().into()) } } #[automatically_derived] - impl From<&str> for Box<#id> { + impl #impl_generics From<&str> for Box<#id_ty> { fn from(s: &str) -> Self { - #id::from_box(s.into()) + Self::from(s.to_owned().into_boxed_str()) } } #[automatically_derived] - impl From> for Box<#id> { + impl #impl_generics From> for Box<#id_ty> { fn from(s: Box) -> Self { - #id::from_box(s) + <#id_ty>::from_box(s) } } #[automatically_derived] - impl From for Box<#id> { + impl #impl_generics From for Box<#id_ty> { fn from(s: String) -> Self { - #id::from_box(s.into()) + let s = s.into_boxed_str(); + Self::from(s) + } + } + + #[automatically_derived] + impl #impl_generics From> for Box { + fn from(id: Box<#id_ty>) -> Self { + <#id_ty>::into_box(id) } } #[automatically_derived] - impl From> for Box { - fn from(id: Box<#id>) -> Self { - id.into_owned() + impl<'de, #generic_params> serde::Deserialize<'de> for &'de #id_ty { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + <&'de str>::deserialize(deserializer).map(<#id_ty>::from_borrowed).map(Into::into) } } #[automatically_derived] - impl<'de> serde::Deserialize<'de> for Box<#id> { + impl<'de, #generic_params> serde::Deserialize<'de> for #owned_ty { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { - Box::::deserialize(deserializer).map(#id::from_box) + Box::::deserialize(deserializer).map(<#id_ty>::from_box).map(Into::into) } } #[automatically_derived] - impl<'de> serde::Deserialize<'de> for #owned { + impl<'de, #generic_params> serde::Deserialize<'de> for Box<#id_ty> { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { - // FIXME: Deserialize inner, convert that - Box::::deserialize(deserializer).map(#id::from_box).map(Into::into) + Box::::deserialize(deserializer).map(<#id_ty>::from_box) } } } } fn expand_as_str_impls(ty: TokenStream, impl_generics: &ImplGenerics<'_>) -> TokenStream { - let partial_eq_string = expand_partial_eq_string(ty.clone(), impl_generics); - quote! { #[automatically_derived] impl #impl_generics std::fmt::Display for #ty { + #[inline] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_str()) } @@ -722,6 +959,7 @@ fn expand_as_str_impls(ty: TokenStream, impl_generics: &ImplGenerics<'_>) -> Tok #[automatically_derived] impl #impl_generics std::fmt::Debug for #ty { + #[inline] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { ::fmt(self.as_str(), f) } @@ -736,8 +974,6 @@ fn expand_as_str_impls(ty: TokenStream, impl_generics: &ImplGenerics<'_>) -> Tok serializer.serialize_str(self.as_str()) } } - - #partial_eq_string } } @@ -754,9 +990,9 @@ fn expand_partial_eq_string(ty: TokenStream, impl_generics: &ImplGenerics<'_>) - quote! { #[automatically_derived] impl #impl_generics PartialEq<#rhs> for #lhs { + #[inline] fn eq(&self, other: &#rhs) -> bool { - AsRef::::as_ref(self) - == AsRef::::as_ref(other) + AsRef::::as_ref(self) == AsRef::::as_ref(other) } } } @@ -766,11 +1002,13 @@ fn expand_partial_eq_string(ty: TokenStream, impl_generics: &ImplGenerics<'_>) - mod kw { syn::custom_keyword!(validate); + syn::custom_keyword!(inline_bytes); } #[derive(Default)] struct IdDstMeta { validate: Option, + inline_bytes: Option, } impl IdDstMeta { @@ -785,7 +1023,17 @@ impl IdDstMeta { } }; - Ok(Self { validate }) + let inline_bytes = match (self.inline_bytes, other.inline_bytes) { + (None, None) => None, + (Some(val), None) | (None, Some(val)) => Some(val), + (Some(a), Some(b)) => { + let mut error = syn::Error::new_spanned(b, "duplicate attribute argument"); + error.combine(syn::Error::new_spanned(a, "note: first one here")); + return Err(error); + } + }; + + Ok(Self { validate, inline_bytes }) } } @@ -794,6 +1042,15 @@ impl Parse for IdDstMeta { let _: kw::validate = input.parse()?; let _: Token![=] = input.parse()?; let validate = Some(input.parse()?); - Ok(Self { validate }) + + let _: Option = input.parse()?; + + let _: Option = input.parse()?; + let _: Option = input.parse()?; + let inline_bytes: Option = input.parse()?; + let inline_bytes = + inline_bytes.map(|ib| ib.base10_digits().parse().expect("inline_bytes is an integer")); + + Ok(Self { validate, inline_bytes }) } } diff --git a/crates/ruma-macros/src/util.rs b/crates/ruma-macros/src/util.rs index e7ea406dba..01b01720d8 100644 --- a/crates/ruma-macros/src/util.rs +++ b/crates/ruma-macros/src/util.rs @@ -123,7 +123,7 @@ pub fn cfg_expand_struct(item: &mut syn::ItemStruct) { struct CfgAttrExpand; impl VisitMut for CfgAttrExpand { - fn visit_attribute_mut(&mut self, attr: &mut syn::Attribute) { + fn visit_attribute_mut(&mut self, attr: &mut Attribute) { if attr.meta.path().is_ident("cfg_attr") { // Ignore invalid cfg attributes let Meta::List(list) = &attr.meta else { return }; diff --git a/crates/ruma-push-gateway-api/src/send_event_notification.rs b/crates/ruma-push-gateway-api/src/send_event_notification.rs index a8183cff36..0647b8c01d 100644 --- a/crates/ruma-push-gateway-api/src/send_event_notification.rs +++ b/crates/ruma-push-gateway-api/src/send_event_notification.rs @@ -48,6 +48,7 @@ pub mod v1 { /// pushkeys and remove the associated pushers. It may not necessarily be the notification /// in the request that failed: it could be that a previous notification to the same /// pushkey failed. May be empty. + #[serde(default)] pub rejected: Vec, } diff --git a/crates/ruma-signatures/src/functions.rs b/crates/ruma-signatures/src/functions.rs index 59361feedd..bb89eddb47 100644 --- a/crates/ruma-signatures/src/functions.rs +++ b/crates/ruma-signatures/src/functions.rs @@ -1,18 +1,17 @@ //! Functions for signing and verifying JSON and events. use std::{ - borrow::Cow, collections::{BTreeMap, BTreeSet}, mem, }; use base64::{alphabet, Engine}; use ruma_common::{ - canonical_json::{redact, JsonType}, + canonical_json::{redact, CanonicalJsonName, JsonType}, room_version_rules::{EventIdFormatVersion, RedactionRules, RoomVersionRules, SignaturesRules}, serde::{base64::Standard, Base64}, AnyKeyName, CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedServerName, - SigningKeyAlgorithm, SigningKeyId, UserId, + OwnedServerSigningKeyId, SigningKeyAlgorithm, SigningKeyId, UserId, }; use serde_json::to_string as to_json_string; use sha2::{digest::Digest, Sha256}; @@ -104,11 +103,12 @@ pub fn sign_json( where K: KeyPair, { - let (signatures_key, mut signature_map) = match object.remove_entry("signatures") { - Some((key, CanonicalJsonValue::Object(signatures))) => (Cow::Owned(key), signatures), - Some(_) => return Err(JsonError::not_of_type("signatures", JsonType::Object)), - None => (Cow::Borrowed("signatures"), BTreeMap::new()), - }; + let (signatures_key, mut signature_map): (CanonicalJsonName, _) = + match object.remove_entry("signatures") { + Some((key, CanonicalJsonValue::Object(signatures))) => (key, signatures), + Some(_) => return Err(JsonError::not_of_type("signatures", JsonType::Object)), + None => ("signatures".into(), BTreeMap::new()), + }; let maybe_unsigned_entry = object.remove_entry("unsigned"); @@ -120,17 +120,17 @@ where // Insert the new signature in the map we pulled out (or created) previously. let signature_set = signature_map - .entry(entity_id.to_owned()) + .entry(entity_id.into()) .or_insert_with(|| CanonicalJsonValue::Object(BTreeMap::new())); let CanonicalJsonValue::Object(signature_set) = signature_set else { return Err(JsonError::not_multiples_of_type("signatures", JsonType::Object)); }; - signature_set.insert(signature.id(), CanonicalJsonValue::String(signature.base64())); + signature_set.insert(signature.id().into(), CanonicalJsonValue::String(signature.base64())); // Put `signatures` and `unsigned` back in. - object.insert(signatures_key.into(), CanonicalJsonValue::Object(signature_map)); + object.insert(signatures_key, CanonicalJsonValue::Object(signature_map)); if let Some((k, v)) = maybe_unsigned_entry { object.insert(k, v); @@ -156,7 +156,7 @@ where /// }"#; /// /// let object = serde_json::from_str(input).unwrap(); -/// let canonical = ruma_signatures::canonical_json(&object).unwrap(); +/// let canonical = ruma_signatures::canonical_json(object).unwrap(); /// /// assert_eq!(canonical, r#"{"日":1,"本":2}"#); /// ``` @@ -212,7 +212,7 @@ pub fn canonical_json(object: &CanonicalJsonObject) -> Result { /// public_key_map.insert("domain".into(), public_key_set); /// /// // Verify at least one signature for each entity in `public_key_map`. -/// assert!(ruma_signatures::verify_json(&public_key_map, &object).is_ok()); +/// assert!(ruma_signatures::verify_json(&public_key_map, object).is_ok()); /// ``` pub fn verify_json( public_key_map: &PublicKeyMap, @@ -288,7 +288,7 @@ fn verify_canonical_json_for_entity( let Some(public_key) = public_keys.get(key_id) else { return Err(VerificationError::PublicKeyNotFound { entity: entity_id.to_owned(), - key_id: key_id.clone(), + key_id: key_id.clone().into_string(), } .into()); }; @@ -555,7 +555,7 @@ where let hash = content_hash(object)?; let hashes_value = object - .entry("hashes".to_owned()) + .entry("hashes".into()) .or_insert_with(|| CanonicalJsonValue::Object(BTreeMap::new())); match hashes_value { @@ -711,6 +711,34 @@ fn canonical_json_with_fields_to_remove( to_json_string(&owned_object).map_err(|e| Error::Json(e.into())) } +/// Extracts the server names and key ids to check signatures for given event. +pub fn required_keys( + object: &CanonicalJsonObject, + rules: &SignaturesRules, +) -> Result>, Error> { + use CanonicalJsonValue::Object; + + let mut map = BTreeMap::>::new(); + let Some(Object(signatures)) = object.get("signatures") else { + return Ok(map); + }; + + for server in servers_to_check_signatures(object, rules)? { + let Some(Object(set)) = signatures.get(server.as_str()) else { + continue; + }; + + let entry = map.entry(server.clone()).or_default(); + set.keys() + .map(CanonicalJsonName::to_string) + .map(TryInto::try_into) + .filter_map(Result::ok) + .for_each(|key_id| entry.push(key_id)); + } + + Ok(map) +} + /// Extracts the server names to check signatures for given event. /// /// Respects the rules for [validating signatures on received events] for populating the result: @@ -722,7 +750,7 @@ fn canonical_json_with_fields_to_remove( /// `join_authorised_via_users_server`, add the server of that user. /// /// [validating signatures on received events]: https://spec.matrix.org/latest/server-server-api/#validating-hashes-and-signatures-on-received-events -fn servers_to_check_signatures( +pub fn servers_to_check_signatures( object: &CanonicalJsonObject, rules: &SignaturesRules, ) -> Result, Error> { diff --git a/crates/ruma-signatures/src/functions/tests.rs b/crates/ruma-signatures/src/functions/tests.rs index 351684ffab..fac6ca5a17 100644 --- a/crates/ruma-signatures/src/functions/tests.rs +++ b/crates/ruma-signatures/src/functions/tests.rs @@ -23,25 +23,25 @@ fn generate_key_pair(name: &str) -> Ed25519KeyPair { } fn add_key_to_map(public_key_map: &mut PublicKeyMap, name: &str, pair: &Ed25519KeyPair) { - let sender_key_map = public_key_map.entry(name.to_owned()).or_default(); + let sender_key_map = public_key_map.entry(name.into()).or_default(); let encoded_public_key = Base64::new(pair.public_key().to_vec()); let version = ServerSigningKeyId::from_parts( SigningKeyAlgorithm::Ed25519, pair.version().try_into().unwrap(), ); - sender_key_map.insert(version.to_string(), encoded_public_key); + sender_key_map.insert(version.as_str().into(), encoded_public_key); } fn add_invalid_key_to_map(public_key_map: &mut PublicKeyMap, name: &str, pair: &Ed25519KeyPair) { - let sender_key_map = public_key_map.entry(name.to_owned()).or_default(); + let sender_key_map = public_key_map.entry(name.into()).or_default(); let encoded_public_key = Base64::new(pair.public_key().to_vec()); let version = ServerSigningKeyId::from_parts( SigningKeyAlgorithm::from("an-unknown-algorithm"), pair.version().try_into().unwrap(), ); - sender_key_map.insert(version.to_string(), encoded_public_key); + sender_key_map.insert(version.as_str().into(), encoded_public_key); } #[test] @@ -308,8 +308,8 @@ fn verify_event_fails_if_public_key_is_invalid() { SigningKeyAlgorithm::Ed25519, key_pair_sender.version().try_into().unwrap(), ); - sender_key_map.insert(version.to_string(), encoded_public_key); - public_key_map.insert("domain-sender".to_owned(), sender_key_map); + sender_key_map.insert(version.as_str().into(), encoded_public_key); + public_key_map.insert("domain-sender".into(), sender_key_map); let verification_result = verify_event(&public_key_map, &signed_event, &RoomVersionRules::V6); diff --git a/crates/ruma-signatures/src/keys.rs b/crates/ruma-signatures/src/keys.rs index 723fa25cbd..d65b0633cc 100644 --- a/crates/ruma-signatures/src/keys.rs +++ b/crates/ruma-signatures/src/keys.rs @@ -9,7 +9,9 @@ use ed25519_dalek::{pkcs8::ALGORITHM_OID, SecretKey, Signer, SigningKey, PUBLIC_ use pkcs8::{ der::zeroize::Zeroizing, DecodePrivateKey, EncodePrivateKey, ObjectIdentifier, PrivateKeyInfo, }; -use ruma_common::{serde::Base64, SigningKeyAlgorithm, SigningKeyId}; +use ruma_common::{ + canonical_json::CanonicalJsonName, serde::Base64, SigningKeyAlgorithm, SigningKeyId, +}; use crate::{signatures::Signature, Error, ParseError}; @@ -178,12 +180,12 @@ impl Debug for Ed25519KeyPair { /// A map from entity names to sets of public keys for that entity. /// /// An entity is generally a homeserver, e.g. `example.com`. -pub type PublicKeyMap = BTreeMap; +pub type PublicKeyMap = BTreeMap; /// A set of public keys for a single homeserver. /// /// This is represented as a map from key ID to base64-encoded signature. -pub type PublicKeySet = BTreeMap; +pub type PublicKeySet = BTreeMap; #[cfg(test)] mod tests { diff --git a/crates/ruma-signatures/src/lib.rs b/crates/ruma-signatures/src/lib.rs index 4b6ca7e443..d1b75626dc 100644 --- a/crates/ruma-signatures/src/lib.rs +++ b/crates/ruma-signatures/src/lib.rs @@ -53,8 +53,9 @@ pub use ruma_common::{IdParseError, SigningKeyAlgorithm}; pub use self::{ error::{Error, JsonError, ParseError, VerificationError}, functions::{ - canonical_json, content_hash, hash_and_sign_event, reference_hash, sign_json, - verify_canonical_json_bytes, verify_event, verify_json, + canonical_json, content_hash, hash_and_sign_event, reference_hash, required_keys, + servers_to_check_signatures, sign_json, verify_canonical_json_bytes, verify_event, + verify_json, }, keys::{Ed25519KeyPair, KeyPair, PublicKeyMap, PublicKeySet}, signatures::Signature, diff --git a/crates/ruma-signatures/src/signatures.rs b/crates/ruma-signatures/src/signatures.rs index 0facd547ee..250d130382 100644 --- a/crates/ruma-signatures/src/signatures.rs +++ b/crates/ruma-signatures/src/signatures.rs @@ -33,7 +33,7 @@ impl Signature { pub fn new(id: &str, bytes: &[u8]) -> Result { let key_id = SigningKeyId::::parse(id)?; - Ok(Self { key_id, signature: bytes.to_vec() }) + Ok(Self { key_id: key_id.into(), signature: bytes.to_vec() }) } /// The algorithm used to generate the signature. @@ -55,8 +55,8 @@ impl Signature { /// The key identifier, a string containing the signature algorithm and the key "version" /// separated by a colon, e.g. `ed25519:1`. - pub fn id(&self) -> String { - self.key_id.to_string() + pub fn id(&self) -> &str { + self.key_id.as_str() } /// The "version" of the key used for this signature. diff --git a/crates/ruma-signatures/tests/tests.rs b/crates/ruma-signatures/tests/tests.rs index c15aaa39dd..27045bbf0d 100644 --- a/crates/ruma-signatures/tests/tests.rs +++ b/crates/ruma-signatures/tests/tests.rs @@ -8,14 +8,14 @@ use ruma_signatures::{sign_json, verify_event, Ed25519KeyPair, PublicKeyMap, Ver static PKCS8_ED25519_DER: &[u8] = include_bytes!("./keys/ed25519.der"); fn add_key_to_map(public_key_map: &mut PublicKeyMap, name: &str, pair: &Ed25519KeyPair) { - let sender_key_map = public_key_map.entry(name.to_owned()).or_default(); + let sender_key_map = public_key_map.entry(name.into()).or_default(); let encoded_public_key = Base64::new(pair.public_key().to_vec()); let version = ServerSigningKeyId::from_parts( SigningKeyAlgorithm::Ed25519, pair.version().try_into().unwrap(), ); - sender_key_map.insert(version.to_string(), encoded_public_key); + sender_key_map.insert(version.as_str().into(), encoded_public_key); } #[test] diff --git a/crates/ruma-state-res/src/event_format.rs b/crates/ruma-state-res/src/event_format.rs index b8991737c7..a5f96f90a4 100644 --- a/crates/ruma-state-res/src/event_format.rs +++ b/crates/ruma-state-res/src/event_format.rs @@ -353,7 +353,7 @@ mod tests { let mut pdu = pdu_v3(); let content = pdu.get_mut("content").unwrap().as_object_mut().unwrap(); let long_string = repeat_n('a', 66_000).collect::(); - content.insert("big_data".to_owned(), long_string.into()); + content.insert("big_data".into(), long_string.into()); check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err(); } @@ -375,7 +375,7 @@ mod tests { for field in &["event_id", "sender", "room_id", "type", "state_key"] { let mut pdu = pdu_v1(); let value = repeat_n('a', 300).collect::(); - pdu.insert((*field).to_owned(), value.into()); + pdu.insert((*field).into(), value.into()); check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err(); } } @@ -384,7 +384,7 @@ mod tests { fn check_pdu_format_strings_wrong_format() { for field in &["event_id", "sender", "room_id", "type", "state_key"] { let mut pdu = pdu_v1(); - pdu.insert((*field).to_owned(), true.into()); + pdu.insert((*field).into(), true.into()); check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err(); } } @@ -393,9 +393,8 @@ mod tests { fn check_pdu_format_arrays_too_big() { for field in &["prev_events", "auth_events"] { let mut pdu = pdu_v3(); - let value = - repeat_n(CanonicalJsonValue::from("$eventid".to_owned()), 30).collect::>(); - pdu.insert((*field).to_owned(), value.into()); + let value = repeat_n(CanonicalJsonValue::from("$eventid"), 30).collect::>(); + pdu.insert((*field).into(), value.into()); check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err(); } } @@ -404,7 +403,7 @@ mod tests { fn check_pdu_format_arrays_wrong_format() { for field in &["prev_events", "auth_events"] { let mut pdu = pdu_v3(); - pdu.insert((*field).to_owned(), true.into()); + pdu.insert((*field).into(), true.into()); check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err(); } } @@ -412,14 +411,14 @@ mod tests { #[test] fn check_pdu_format_negative_depth() { let mut pdu = pdu_v3(); - pdu.insert("depth".to_owned(), int!(-1).into()).unwrap(); + pdu.insert("depth".into(), int!(-1).into()).unwrap(); check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err(); } #[test] fn check_pdu_format_depth_wrong_format() { let mut pdu = pdu_v3(); - pdu.insert("depth".to_owned(), true.into()); + pdu.insert("depth".into(), true.into()); check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err(); } diff --git a/crates/ruma-state-res/src/utils.rs b/crates/ruma-state-res/src/utils.rs index 14553385e9..73ba708e76 100644 --- a/crates/ruma-state-res/src/utils.rs +++ b/crates/ruma-state-res/src/utils.rs @@ -1,4 +1,4 @@ -use ruma_common::{EventId, IdParseError, OwnedEventId, RoomId}; +use ruma_common::{IdParseError, OwnedEventId, RoomId}; /// Convenience extension trait for [`RoomId`]. pub(crate) trait RoomIdExt { @@ -12,6 +12,6 @@ where T: AsRef, { fn room_create_event_id(&self) -> Result { - EventId::parse(format!("${}", self.as_ref().strip_sigil())) + OwnedEventId::from_parts('$', self.as_ref().strip_sigil(), None) } } diff --git a/crates/ruma/Cargo.toml b/crates/ruma/Cargo.toml index f8fa00b7e6..6afb0c4a54 100644 --- a/crates/ruma/Cargo.toml +++ b/crates/ruma/Cargo.toml @@ -25,12 +25,32 @@ appservice-api-c = ["api", "events", "dep:ruma-appservice-api", "ruma-appservice appservice-api-s = ["api", "events", "dep:ruma-appservice-api", "ruma-appservice-api?/server"] appservice-api = ["appservice-api-c", "appservice-api-s"] -client-api-c = ["api", "events", "dep:ruma-client-api", "ruma-client-api?/client"] -client-api-s = ["api", "events", "dep:ruma-client-api", "ruma-client-api?/server"] +client-api-c = [ + "api", + "events", + "dep:ruma-client-api", + "ruma-client-api?/client", +] +client-api-s = [ + "api", + "events", + "dep:ruma-client-api", + "ruma-client-api?/server", +] client-api = ["client-api-c", "client-api-s"] -federation-api-c = ["api", "signatures", "dep:ruma-federation-api", "ruma-federation-api?/client"] -federation-api-s = ["api", "signatures", "dep:ruma-federation-api", "ruma-federation-api?/server"] +federation-api-c = [ + "api", + "signatures", + "dep:ruma-federation-api", + "ruma-federation-api?/client", +] +federation-api-s = [ + "api", + "signatures", + "dep:ruma-federation-api", + "ruma-federation-api?/server", +] federation-api = ["federation-api-c", "federation-api-s"] identity-service-api-c = [ @@ -45,8 +65,18 @@ identity-service-api-s = [ ] identity-service-api = ["identity-service-api-c", "identity-service-api-s"] -push-gateway-api-c = ["api", "dep:ruma-push-gateway-api", "ruma-push-gateway-api?/client"] -push-gateway-api-s = ["api", "dep:ruma-push-gateway-api", "ruma-push-gateway-api?/server"] +identifiers-validation = ["dep:ruma-identifiers-validation"] + +push-gateway-api-c = [ + "api", + "dep:ruma-push-gateway-api", + "ruma-push-gateway-api?/client", +] +push-gateway-api-s = [ + "api", + "dep:ruma-push-gateway-api", + "ruma-push-gateway-api?/server", +] push-gateway-api = ["push-gateway-api-c", "push-gateway-api-s"] # Required for randomness, current system time in browser environments @@ -151,9 +181,11 @@ unstable-msc2545 = ["ruma-events?/unstable-msc2545"] unstable-msc2654 = ["ruma-client-api?/unstable-msc2654"] unstable-msc2666 = ["ruma-common/unstable-msc2666", "ruma-client-api?/unstable-msc2666"] unstable-msc2747 = ["ruma-events?/unstable-msc2747"] +unstable-msc2815 = ["ruma-client-api?/unstable-msc2815"] unstable-msc2867 = ["ruma-events?/unstable-msc2867"] unstable-msc2870 = ["ruma-common/unstable-msc2870"] unstable-msc2967 = ["ruma-client-api?/unstable-msc2967"] +unstable-msc3026 = ["ruma-common/unstable-msc3026"] unstable-msc3061 = ["ruma-events?/unstable-msc3061"] unstable-msc3202 = ["ruma-appservice-api?/unstable-msc3202"] unstable-msc3245 = ["ruma-events?/unstable-msc3245"] @@ -196,6 +228,7 @@ unstable-msc4143 = ["ruma-client-api?/unstable-msc4143"] unstable-msc4171 = ["ruma-events?/unstable-msc4171"] unstable-msc4186 = ["ruma-common/unstable-msc4186", "ruma-client-api?/unstable-msc4186"] unstable-msc4191 = ["ruma-client-api?/unstable-msc4191"] +unstable-msc4195 = ["unstable-msc4143", "ruma-client-api?/unstable-msc4195"] unstable-msc4203 = ["ruma-appservice-api?/unstable-msc4203"] unstable-msc4222 = ["ruma-client-api?/unstable-msc4222"] unstable-msc4230 = ["ruma-events?/unstable-msc4230"] @@ -211,6 +244,12 @@ unstable-msc4319 = ["ruma-events?/unstable-msc4319"] unstable-msc4310 = ["ruma-events?/unstable-msc4310"] unstable-msc4334 = ["ruma-events?/unstable-msc4334", "dep:language-tags"] unstable-msc4359 = ["ruma-events?/unstable-msc4359"] +unstable-msc4361 = ["ruma-common/unstable-msc4361"] +unstable-msc4380 = [ + "ruma-client-api?/unstable-msc4380", + "ruma-common/unstable-msc4380", + "ruma-events?/unstable-msc4380", +] unstable-msc3230 = ["ruma-events?/unstable-msc3230"] # Private features, only used in test / benchmarking code @@ -277,6 +316,7 @@ __unstable-mscs = [ "unstable-msc4319", "unstable-msc4334", "unstable-msc4359", + "unstable-msc4380", "unstable-msc3230", ] __ci = ["full", "compat-upload-signatures", "__unstable-mscs"] @@ -314,6 +354,7 @@ ruma-appservice-api = { workspace = true, optional = true } ruma-client-api = { workspace = true, optional = true } ruma-federation-api = { workspace = true, optional = true } ruma-identity-service-api = { workspace = true, optional = true } +ruma-identifiers-validation = { workspace = true, optional = true } ruma-push-gateway-api = { workspace = true, optional = true } [dev-dependencies] diff --git a/crates/ruma/src/lib.rs b/crates/ruma/src/lib.rs index 50c6c6dbf4..62a2ac9225 100644 --- a/crates/ruma/src/lib.rs +++ b/crates/ruma/src/lib.rs @@ -96,6 +96,9 @@ pub use ruma_events as events; #[cfg(feature = "html")] #[doc(inline)] pub use ruma_html as html; +#[cfg(feature = "identifiers-validation")] +#[doc(inline)] +pub use ruma_identifiers_validation as identifiers_validation; #[cfg(feature = "signatures")] #[doc(inline)] pub use ruma_signatures as signatures; @@ -135,15 +138,13 @@ pub mod api { #[doc(no_inline)] pub use assign::assign; #[doc(no_inline)] -pub use js_int::{int, uint, Int, UInt}; +pub use js_int::{ + int, uint, Int, ParseIntError as JsParseIntError, TryFromIntError as JsTryFromIntError, UInt, +}; #[doc(no_inline)] pub use js_option::JsOption; #[cfg(all(feature = "events", feature = "unstable-msc4334"))] #[doc(no_inline)] pub use language_tags::LanguageTag; pub use ruma_common::*; -#[cfg(feature = "canonical-json")] -pub use ruma_common::{ - canonical_json, CanonicalJsonError, CanonicalJsonObject, CanonicalJsonValue, -}; pub use web_time as time; diff --git a/xtask/src/release.rs b/xtask/src/release.rs index 2dda5376e2..17dd439ac9 100644 --- a/xtask/src/release.rs +++ b/xtask/src/release.rs @@ -72,8 +72,7 @@ impl ReleaseTask { pub(crate) fn run(&mut self) -> Result<()> { let title = &self.title(); let prerelease = !self.version.pre.is_empty(); - let publish_only = - ["ruma-identifiers-validation", "ruma-macros"].contains(&self.package.name.as_str()); + let publish_only = ["ruma-macros"].contains(&self.package.name.as_str()); println!( "Starting {}{} for {title}…",