From 495c96a469d480b2b6111ff37cd4929bd67d4555 Mon Sep 17 00:00:00 2001 From: Kierre Date: Tue, 10 Feb 2026 11:49:46 -0500 Subject: [PATCH 1/8] events: fix ACLs being case sensitive --- crates/ruma-events/src/room/server_acl.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/ruma-events/src/room/server_acl.rs b/crates/ruma-events/src/room/server_acl.rs index a1c49f8cfd..dc59091d0c 100644 --- a/crates/ruma-events/src/room/server_acl.rs +++ b/crates/ruma-events/src/room/server_acl.rs @@ -102,11 +102,11 @@ impl RoomServerAclEventContent { } fn matches(a: &[String], s: &str) -> bool { - a.iter().map(String::as_str).any(|a| WildMatch::new(a).matches(s)) + 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 == s) + a.iter().map(String::as_str).any(|a| a.to_lowercase().to_str() == s.to_lowercase().to_str()) } } @@ -222,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"))); + } } From e8038f1c06a9554468e28a29a1e9d5458fdc58a9 Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Thu, 12 Feb 2026 08:31:02 +0000 Subject: [PATCH 2/8] Optimize case-insensitive comparisons. Signed-off-by: Jason Volk --- crates/ruma-common/src/push/condition.rs | 11 +++++++---- crates/ruma-events/src/room/server_acl.rs | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/ruma-common/src/push/condition.rs b/crates/ruma-common/src/push/condition.rs index 234daa5057..57e1a73def 100644 --- a/crates/ruma-common/src/push/condition.rs +++ b/crates/ruma-common/src/push/condition.rs @@ -503,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-events/src/room/server_acl.rs b/crates/ruma-events/src/room/server_acl.rs index dc59091d0c..ffd58ffa68 100644 --- a/crates/ruma-events/src/room/server_acl.rs +++ b/crates/ruma-events/src/room/server_acl.rs @@ -106,7 +106,7 @@ impl RoomServerAclEventContent { } fn contains(a: &[String], s: &str) -> bool { - a.iter().map(String::as_str).any(|a| a.to_lowercase().to_str() == s.to_lowercase().to_str()) + a.iter().map(String::as_str).any(|a| a.eq_ignore_ascii_case(s)) } } From 2f53677f4d621cb4560119c5938888c9ce983e56 Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Fri, 13 Feb 2026 23:22:29 +0000 Subject: [PATCH 3/8] Replace version discovery Strings with SmallStrings. Signed-off-by: Jason Volk --- Cargo.lock | 1 + .../src/discovery/get_supported_versions.rs | 18 ++++++++++++++---- crates/ruma-common/src/api/metadata.rs | 16 +++++++++++----- crates/ruma-identity-service-api/Cargo.toml | 1 + .../src/discovery/get_supported_versions.rs | 16 +++++++++++++--- 5 files changed, 40 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7cf90e10a1..943a320d80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2047,6 +2047,7 @@ dependencies = [ "ruma-events", "serde", "serde_json", + "smallstr", ] [[package]] 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-common/src/api/metadata.rs b/crates/ruma-common/src/api/metadata.rs index 1e125da0df..eb600cf35d 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, }; @@ -1144,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(), } } 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)), + ) } } From 3cb939f5c8a67197433cbb3dc7e256f0ddaee978 Mon Sep 17 00:00:00 2001 From: dasha_uwu Date: Fri, 20 Feb 2026 00:41:04 +0500 Subject: [PATCH 4/8] Add function to iterate over aliases in RoomCanonicalAliasEventContent --- crates/ruma-events/src/room/canonical_alias.rs | 5 +++++ 1 file changed, 5 insertions(+) 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)] From 30d063c4503c3b630cdd55eda71a0bc3504518a2 Mon Sep 17 00:00:00 2001 From: June Strawberry Date: Sun, 15 Feb 2026 15:43:00 -0500 Subject: [PATCH 5/8] remove skip_serializing_if on empty display_name and avatar_url for /joined_members Signed-off-by: June Strawberry --- crates/ruma-client-api/src/membership/joined_members.rs | 2 -- 1 file changed, 2 deletions(-) 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") From d4b779ab8abb416e2ddc2fc7f322ed86bd4b5648 Mon Sep 17 00:00:00 2001 From: dasha_uwu Date: Wed, 25 Mar 2026 09:24:33 +0500 Subject: [PATCH 6/8] Implement new MSC4143 endpoint --- crates/ruma-client-api/Cargo.toml | 1 + .../src/discovery/discover_homeserver.rs | 194 +----------------- crates/ruma-client-api/src/lib.rs | 2 + crates/ruma-client-api/src/rtc.rs | 169 +++++++++++++++ crates/ruma-client-api/src/rtc/transports.rs | 52 +++++ crates/ruma/Cargo.toml | 1 + 6 files changed, 228 insertions(+), 191 deletions(-) create mode 100644 crates/ruma-client-api/src/rtc.rs create mode 100644 crates/ruma-client-api/src/rtc/transports.rs diff --git a/crates/ruma-client-api/Cargo.toml b/crates/ruma-client-api/Cargo.toml index a203eaa027..c452937666 100644 --- a/crates/ruma-client-api/Cargo.toml +++ b/crates/ruma-client-api/Cargo.toml @@ -53,6 +53,7 @@ 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 = [] 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/lib.rs b/crates/ruma-client-api/src/lib.rs index 341eb1dac9..f01f9a4581 100644 --- a/crates/ruma-client-api/src/lib.rs +++ b/crates/ruma-client-api/src/lib.rs @@ -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/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/Cargo.toml b/crates/ruma/Cargo.toml index a96a0e8a8b..282e7c1cfb 100644 --- a/crates/ruma/Cargo.toml +++ b/crates/ruma/Cargo.toml @@ -228,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"] From 8564227896ed0f20f46eb4d717b7061f4ddd17ae Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Wed, 25 Mar 2026 11:15:41 +0000 Subject: [PATCH 7/8] Use OwnedServerName instead of String for list of servers. Signed-off-by: Jason Volk --- .../src/membership/create_join_event/v2.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 { From 46ab0cd1c836204848b30f8ad1c38cb9090a2f78 Mon Sep 17 00:00:00 2001 From: Donjuanplatinum Date: Sun, 15 Mar 2026 00:13:46 +0800 Subject: [PATCH 8/8] impl MSC4380 --- crates/ruma-client-api/CHANGELOG.md | 5 ++ crates/ruma-client-api/Cargo.toml | 1 + crates/ruma-client-api/src/error.rs | 19 ++++++ .../ruma-client-api/src/error/kind_serde.rs | 2 + crates/ruma-client-api/src/lib.rs | 2 +- .../src/membership/invite_user.rs | 1 - crates/ruma-common/CHANGELOG.md | 4 ++ crates/ruma-common/Cargo.toml | 1 + crates/ruma-common/src/api/metadata.rs | 9 +++ crates/ruma-events/CHANGELOG.md | 3 + crates/ruma-events/Cargo.toml | 1 + crates/ruma-events/src/enums.rs | 3 + .../src/invite_permission_config.rs | 67 +++++++++++++++++++ crates/ruma-events/src/lib.rs | 2 + crates/ruma/Cargo.toml | 6 ++ 15 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 crates/ruma-events/src/invite_permission_config.rs diff --git a/crates/ruma-client-api/CHANGELOG.md b/crates/ruma-client-api/CHANGELOG.md index 195e1f0bf0..b022f42856 100644 --- a/crates/ruma-client-api/CHANGELOG.md +++ b/crates/ruma-client-api/CHANGELOG.md @@ -54,6 +54,11 @@ Breaking changes: 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. diff --git a/crates/ruma-client-api/Cargo.toml b/crates/ruma-client-api/Cargo.toml index c452937666..9b00400382 100644 --- a/crates/ruma-client-api/Cargo.toml +++ b/crates/ruma-client-api/Cargo.toml @@ -58,6 +58,7 @@ unstable-msc4222 = [] # Thread subscription support. unstable-msc4306 = [] unstable-msc4308 = [] +unstable-msc4380 = ["ruma-common/unstable-msc4380", "ruma-events/unstable-msc4380"] [dependencies] as_variant = { workspace = true } diff --git a/crates/ruma-client-api/src/error.rs b/crates/ruma-client-api/src/error.rs index 6ba66b0563..68627929ab 100644 --- a/crates/ruma-client-api/src/error.rs +++ b/crates/ruma-client-api/src/error.rs @@ -169,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 @@ -453,6 +460,8 @@ impl ErrorKind { 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, @@ -633,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 diff --git a/crates/ruma-client-api/src/error/kind_serde.rs b/crates/ruma-client-api/src/error/kind_serde.rs index 5b31faeccd..c0b6052101 100644 --- a/crates/ruma-client-api/src/error/kind_serde.rs +++ b/crates/ruma-client-api/src/error/kind_serde.rs @@ -199,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::) diff --git a/crates/ruma-client-api/src/lib.rs b/crates/ruma-client-api/src/lib.rs index f01f9a4581..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; 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-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 f76f3679df..82b76c1dff 100644 --- a/crates/ruma-common/Cargo.toml +++ b/crates/ruma-common/Cargo.toml @@ -38,6 +38,7 @@ unstable-msc4186 = [] # Thread subscriptions. unstable-msc4306 = [] unstable-msc4361 = [] +unstable-msc4380 = [] # Allow IDs to exceed 255 bytes. compat-arbitrary-length-ids = [ diff --git a/crates/ruma-common/src/api/metadata.rs b/crates/ruma-common/src/api/metadata.rs index eb600cf35d..0870ad24fa 100644 --- a/crates/ruma-common/src/api/metadata.rs +++ b/crates/ruma-common/src/api/metadata.rs @@ -1271,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-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 2e19dede75..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 diff --git a/crates/ruma-events/src/enums.rs b/crates/ruma-events/src/enums.rs index 3f7885c755..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")] 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/lib.rs b/crates/ruma-events/src/lib.rs index 854899c99a..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; diff --git a/crates/ruma/Cargo.toml b/crates/ruma/Cargo.toml index 282e7c1cfb..6afb0c4a54 100644 --- a/crates/ruma/Cargo.toml +++ b/crates/ruma/Cargo.toml @@ -245,6 +245,11 @@ 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 @@ -311,6 +316,7 @@ __unstable-mscs = [ "unstable-msc4319", "unstable-msc4334", "unstable-msc4359", + "unstable-msc4380", "unstable-msc3230", ] __ci = ["full", "compat-upload-signatures", "__unstable-mscs"]