refactor: Ruma upstraming, bake a little more

This commit is contained in:
Jade Ellis
2026-04-07 14:40:10 +01:00
committed by Ginger
parent 204bc1367e
commit a4e64383b7
115 changed files with 1907 additions and 1504 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ pub fn gen_event_id(
room_version_id: &RoomVersionId,
) -> Result<OwnedEventId> {
let Some(rules) = room_version_id.rules() else {
return Err!("Cannot generate event ID for unknown room version {room_version_id}")
return Err!("Cannot generate event ID for unknown room version {room_version_id}");
};
let reference_hash = ruma::signatures::reference_hash(value, &rules)?;
let event_id: OwnedEventId = format!("${reference_hash}").try_into()?;
+1 -1
View File
@@ -9,4 +9,4 @@ pub mod versions;
pub use event::{Event, TypeExt as EventTypeExt};
pub use pdu::{Pdu, PduBuilder, PduCount, PduEvent, PduId, RawPduId, ShortId};
pub use state_key::StateKey;
pub use state_res::{StateMap, TypeStateKey};
pub use state_res::{StateMap, TypeStateKey};
+2 -3
View File
@@ -6,7 +6,7 @@ use crate::{Err, Error, Result, err, implement};
#[implement(super::Pdu)]
pub fn redact(&mut self, room_version_id: &RoomVersionId, reason: JsonValue) -> Result {
let Some(rules) = room_version_id.rules() else {
return Err!("Cannot redact event for unknown room version {room_version_id}")
return Err!("Cannot redact event for unknown room version {room_version_id}");
};
self.unsigned = None;
@@ -14,8 +14,7 @@ pub fn redact(&mut self, room_version_id: &RoomVersionId, reason: JsonValue) ->
let mut content = serde_json::from_str(self.content.get())
.map_err(|e| err!(Request(BadJson("Failed to deserialize content into type: {e}"))))?;
redact_content_in_place(&mut content, &rules.redaction, self.kind.to_string())
.map_err(|e| Error::Redaction(self.sender.server_name().to_owned(), e))?;
redact_content_in_place(&mut content, &rules.redaction, self.kind.to_string());
let reason = serde_json::to_value(reason).expect("Failed to preserialize reason");
+41 -21
View File
@@ -5,12 +5,16 @@ use futures::{
future::{OptionFuture, join, join3},
};
use ruma::{
Int, OwnedUserId, RoomVersionId, UserId, events::room::{
Int, OwnedUserId, RoomVersionId, UserId,
events::room::{
create::RoomCreateEventContent,
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, ThirdPartyInvite},
power_levels::RoomPowerLevelsEventContent,
}, int, room_version_rules::{RoomIdFormatVersion, RoomVersionRules}, serde::Raw,
},
int,
room_version_rules::{RoomIdFormatVersion, RoomVersionRules},
serde::Raw,
};
use serde::{
Deserialize,
@@ -117,11 +121,11 @@ pub fn auth_types_for_event(
// TODO: restore this once 3pid support isn't broken
// if membership == MembershipState::Invite {
// if let Some(Ok(t_id)) = content.third_party_invite.map(|t| t.deserialize()) {
// let key =
// (StateEventType::RoomThirdPartyInvite, t_id.signed.token.into());
// if !auth_types.contains(&key) {
// auth_types.push(key);
// if let Some(Ok(t_id)) = content.third_party_invite.map(|t|
// t.deserialize()) { let key =
// (StateEventType::RoomThirdPartyInvite,
// t_id.signed.token.into()); if !auth_types.contains(&
// key) { auth_types.push(key);
// }
// }
// }
@@ -214,13 +218,17 @@ where
return Ok(false);
}
if room_version.room_id_format == RoomIdFormatVersion::V2 && incoming_event.room_id().is_some() {
if room_version.room_id_format == RoomIdFormatVersion::V2
&& incoming_event.room_id().is_some()
{
warn!("room create event incorrectly claims to have a room ID when it should not");
return Ok(false);
}
if !room_version.authorization.use_room_create_sender
&& !room_version.authorization.explicitly_privilege_room_creators
&& !room_version
.authorization
.explicitly_privilege_room_creators
{
// If content has no creator field, reject
if content.creator.is_none() {
@@ -343,7 +351,7 @@ where
// Only in some room versions 6 and below
if room_version.authorization.special_case_room_aliases {
// 4. If type is m.room.aliases
if *incoming_event.event_type() == TimelineEventType::RoomAliases {
if *incoming_event.event_type() == TimelineEventType::from("m.room.aliases") {
debug!("starting m.room.aliases check");
// If sender's domain doesn't matches state_key, reject
@@ -493,7 +501,10 @@ where
if is_creator { int!(100) } else { int!(0) }
},
};
if room_version.authorization.explicitly_privilege_room_creators {
if room_version
.authorization
.explicitly_privilege_room_creators
{
// If the user sent the create event, or is listed in additional_creators, just
// give them Int::MAX
if sender == room_create_event.sender()
@@ -555,7 +566,10 @@ where
if *incoming_event.event_type() == TimelineEventType::RoomPowerLevels {
debug!("starting m.room.power_levels check");
let mut creators = BTreeSet::new();
if room_version.authorization.explicitly_privilege_room_creators {
if room_version
.authorization
.explicitly_privilege_room_creators
{
creators.insert(create_event.sender().to_owned());
for creator in room_create_content.additional_creators.iter().flatten() {
creators.insert(creator.deserialize()?);
@@ -710,7 +724,10 @@ where
let mut creators = BTreeSet::new();
creators.insert(create_room.sender().to_owned());
if room_version.authorization.explicitly_privilege_room_creators {
if room_version
.authorization
.explicitly_privilege_room_creators
{
// Explicitly privilege room creators
// If the sender sent the create event, or in additional_creators, give them
// Int::MAX. Same case for target.
@@ -878,7 +895,8 @@ where
trace!(sender=%sender, "sender is invited or already joined to room, allowing join");
true
},
| JoinRule::KnockRestricted(_) if !room_version.authorization.knock_restricted_join_rule =>
| JoinRule::KnockRestricted(_)
if !room_version.authorization.knock_restricted_join_rule =>
{
warn!(
"Join rule is knock_restricted but room version does not support it"
@@ -1508,15 +1526,17 @@ fn verify_third_party_invite(
#[cfg(test)]
mod tests {
use ruma::{events::{
StateEventType, TimelineEventType,
room::{
join_rules::{
AllowRule, JoinRule, Restricted, RoomJoinRulesEventContent,
use ruma::{
events::{
StateEventType, TimelineEventType,
room::{
join_rules::{AllowRule, JoinRule, Restricted, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
},
member::{MembershipState, RoomMemberEventContent},
},
}, room::RoomMembership, room_version_rules::RoomVersionRules};
room::RoomMembership,
room_version_rules::RoomVersionRules,
};
use serde_json::value::to_raw_value as to_raw_json_value;
use crate::{
+48 -31
View File
@@ -25,13 +25,14 @@ use ruma::{
StateEventType, TimelineEventType,
room::member::{MembershipState, RoomMemberEventContent},
},
int, room_version_rules::{RoomIdFormatVersion, RoomVersionRules, StateResolutionVersion},
int,
room_version_rules::{RoomIdFormatVersion, RoomVersionRules, StateResolutionVersion},
};
use serde_json::from_str as from_json_str;
pub(crate) use self::error::Error;
use self::power_levels::PowerLevelsContentFields;
pub use self::event_auth::{auth_check, auth_types_for_event};
use self::power_levels::PowerLevelsContentFields;
use crate::{
debug, debug_error, err,
matrix::{Event, StateKey},
@@ -106,19 +107,21 @@ where
debug!(count = conflicting.len(), "conflicting events");
trace!(map = ?conflicting, "conflicting events");
let (conflicted_state_subgraph, initial_state) =
if let StateResolutionVersion::V2(v2_rules) = stateres_version && v2_rules.consider_conflicted_state_subgraph {
let csg = calculate_conflicted_subgraph(&conflicting, event_fetch)
.await
.ok_or_else(|| {
Error::InvalidPdu("Failed to calculate conflicted subgraph".to_owned())
})?;
debug!(count = csg.len(), "conflicted subgraph");
trace!(set = ?csg, "conflicted subgraph");
(csg, HashMap::new())
} else {
(HashSet::new(), unconflicted.clone())
};
let (conflicted_state_subgraph, initial_state) = if let StateResolutionVersion::V2(v2_rules) =
stateres_version
&& v2_rules.consider_conflicted_state_subgraph
{
let csg = calculate_conflicted_subgraph(&conflicting, event_fetch)
.await
.ok_or_else(|| {
Error::InvalidPdu("Failed to calculate conflicted subgraph".to_owned())
})?;
debug!(count = csg.len(), "conflicted subgraph");
trace!(set = ?csg, "conflicted subgraph");
(csg, HashMap::new())
} else {
(HashSet::new(), unconflicted.clone())
};
// `all_conflicted` contains unique items
// synapse says `full_set = {eid for eid in full_conflicted_set if eid in
@@ -974,10 +977,14 @@ mod tests {
use maplit::{hashmap, hashset};
use rand::seq::SliceRandom;
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedEventId, RoomVersionId, events::{
MilliSecondsSinceUnixEpoch, OwnedEventId, RoomVersionId,
events::{
StateEventType, TimelineEventType,
room::join_rules::{JoinRule, RoomJoinRulesEventContent},
}, int, room_version_rules::RoomVersionRules, uint
},
int,
room_version_rules::RoomVersionRules,
uint,
};
use serde_json::{json, value::to_raw_value as to_raw_json_value};
@@ -1423,13 +1430,18 @@ mod tests {
})
.collect();
let resolved =
match super::resolve(&RoomVersionRules::V2, &state_sets, &auth_chain, &fetcher, &exists)
.await
{
| Ok(state) => state,
| Err(e) => panic!("{e}"),
};
let resolved = match super::resolve(
&RoomVersionRules::V2,
&state_sets,
&auth_chain,
&fetcher,
&exists,
)
.await
{
| Ok(state) => state,
| Err(e) => panic!("{e}"),
};
assert_eq!(expected, resolved);
}
@@ -1536,13 +1548,18 @@ mod tests {
let fetcher = |id: OwnedEventId| ready(ev_map.get(&id).cloned());
let exists = |id: OwnedEventId| ready(ev_map.get(&id).is_some());
let resolved =
match super::resolve(&RoomVersionRules::V6, &state_sets, &auth_chain, &fetcher, &exists)
.await
{
| Ok(state) => state,
| Err(e) => panic!("{e}"),
};
let resolved = match super::resolve(
&RoomVersionRules::V6,
&state_sets,
&auth_chain,
&fetcher,
&exists,
)
.await
{
| Ok(state) => state,
| Err(e) => panic!("{e}"),
};
debug!(
resolved = ?resolved
+14 -5
View File
@@ -1,13 +1,16 @@
use std::collections::BTreeMap;
use ruma::{
Int, OwnedUserId, UserId, events::{TimelineEventType, room::power_levels::RoomPowerLevelsEventContent}, power_levels::{NotificationPowerLevels, default_power_level}, room_version_rules::{AuthorizationRules, RoomVersionRules}, serde::deserialize_v1_powerlevel
Int, OwnedUserId, UserId,
events::{TimelineEventType, room::power_levels::RoomPowerLevelsEventContent},
power_levels::{NotificationPowerLevels, default_power_level},
room_version_rules::{AuthorizationRules, RoomVersionRules},
serde::deserialize_v1_powerlevel,
};
use super::serde_backports::*;
use serde::Deserialize;
use serde_json::{Error, from_str as from_json_str};
use super::Result;
use super::{Result, serde_backports::*};
use crate::error;
#[derive(Deserialize)]
@@ -44,7 +47,10 @@ struct IntRoomPowerLevelsEventContent {
}
impl IntRoomPowerLevelsEventContent {
fn to_room_power_levels_content(self, auth_rules: &AuthorizationRules) -> RoomPowerLevelsEventContent {
fn to_room_power_levels_content(
self,
auth_rules: &AuthorizationRules,
) -> RoomPowerLevelsEventContent {
let IntRoomPowerLevelsEventContent {
ban,
events,
@@ -105,7 +111,10 @@ pub(crate) fn deserialize_power_levels(
}
}
fn deserialize_integer_power_levels(content: &str, auth_rules: &AuthorizationRules) -> Option<RoomPowerLevelsEventContent> {
fn deserialize_integer_power_levels(
content: &str,
auth_rules: &AuthorizationRules,
) -> Option<RoomPowerLevelsEventContent> {
match from_json_str::<IntRoomPowerLevelsEventContent>(content) {
| Ok(content) => Some(content.to_room_power_levels_content(auth_rules)),
| Err(_) => {
+83 -83
View File
@@ -1,120 +1,120 @@
//! These functions are copied from an old version of Ruma. power_levels.rs uses them to lazily deserialize power level events.
//! Upstream Ruma uses a much more elegant approach in its state resolution code, which we may want
//! These functions are copied from an old version of Ruma. power_levels.rs uses
//! them to lazily deserialize power level events. Upstream Ruma uses a much
//! more elegant approach in its state resolution code, which we may want
//! to look into at some point.
use std::marker::PhantomData;
use std::fmt;
use std::{fmt, marker::PhantomData};
use serde::{Deserialize, Deserializer, de::{MapAccess, Visitor}};
use ruma::{Int, serde::deserialize_v1_powerlevel};
use serde::{
Deserialize, Deserializer,
de::{MapAccess, Visitor},
};
/// 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.
/// 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<Vec<(T, Int)>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de> + Ord,
D: Deserializer<'de>,
T: Deserialize<'de> + Ord,
{
#[repr(transparent)]
struct IntWrap(Int);
#[repr(transparent)]
struct IntWrap(Int);
impl<'de> Deserialize<'de> for IntWrap {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserialize_v1_powerlevel(deserializer).map(IntWrap)
}
}
impl<'de> Deserialize<'de> for IntWrap {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserialize_v1_powerlevel(deserializer).map(IntWrap)
}
}
struct IntMapVisitor<T> {
_phantom: PhantomData<T>,
}
struct IntMapVisitor<T> {
_phantom: PhantomData<T>,
}
impl<T> IntMapVisitor<T> {
fn new() -> Self {
Self { _phantom: PhantomData }
}
}
impl<T> IntMapVisitor<T> {
fn new() -> Self { Self { _phantom: PhantomData } }
}
impl<'de, T> Visitor<'de> for IntMapVisitor<T>
where
T: Deserialize<'de> + Ord,
{
type Value = Vec<(T, Int)>;
impl<'de, T> Visitor<'de> for IntMapVisitor<T>
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 expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a map with integers or strings as values")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut res = Vec::new();
if let Some(hint) = map.size_hint() {
res.reserve(hint);
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
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));
}
while let Some((k, IntWrap(v))) = map.next_entry()? {
res.push((k, v));
}
res.sort_unstable();
res.dedup_by(|a, b| a.0 == b.0);
res.sort_unstable();
res.dedup_by(|a, b| a.0 == b.0);
Ok(res)
}
}
Ok(res)
}
}
de.deserialize_map(IntMapVisitor::new())
de.deserialize_map(IntMapVisitor::new())
}
/// Take a Map with integer values and deserialize those to a Vec of sorted pairs
/// 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<Vec<(T, Int)>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de> + Ord,
D: Deserializer<'de>,
T: Deserialize<'de> + Ord,
{
struct IntMapVisitor<T> {
_phantom: PhantomData<T>,
}
struct IntMapVisitor<T> {
_phantom: PhantomData<T>,
}
impl<T> IntMapVisitor<T> {
fn new() -> Self {
Self { _phantom: PhantomData }
}
}
impl<T> IntMapVisitor<T> {
fn new() -> Self { Self { _phantom: PhantomData } }
}
impl<'de, T> Visitor<'de> for IntMapVisitor<T>
where
T: Deserialize<'de> + Ord,
{
type Value = Vec<(T, Int)>;
impl<'de, T> Visitor<'de> for IntMapVisitor<T>
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 expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a map with integers as values")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut res = Vec::new();
if let Some(hint) = map.size_hint() {
res.reserve(hint);
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
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);
}
while let Some(item) = map.next_entry()? {
res.push(item);
}
res.sort_unstable();
res.dedup_by(|a, b| a.0 == b.0);
res.sort_unstable();
res.dedup_by(|a, b| a.0 == b.0);
Ok(res)
}
}
Ok(res)
}
}
de.deserialize_map(IntMapVisitor::new())
}
de.deserialize_map(IntMapVisitor::new())
}
+15 -5
View File
@@ -6,13 +6,18 @@ use std::{
use futures::future::ready;
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, ServerSignatures, UserId, event_id, events::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, ServerSignatures,
UserId, event_id,
events::{
TimelineEventType,
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
},
}, int, room_id, room_version_rules::RoomVersionRules, uint, user_id
},
int, room_id,
room_version_rules::RoomVersionRules,
uint, user_id,
};
use serde_json::{
json,
@@ -130,9 +135,14 @@ pub(crate) async fn do_check(
let event_map = &event_map;
let fetch = |id: OwnedEventId| ready(event_map.get(&id).cloned());
let exists = |id: OwnedEventId| ready(event_map.get(&id).is_some());
let resolved =
super::resolve(&RoomVersionRules::V6, state_sets, &auth_chain_sets, &fetch, &exists)
.await;
let resolved = super::resolve(
&RoomVersionRules::V6,
state_sets,
&auth_chain_sets,
&fetch,
&exists,
)
.await;
match resolved {
| Ok(state) => state,
+38 -38
View File
@@ -1,44 +1,44 @@
use std::collections::BTreeMap;
pub fn versions() -> Vec<String> {
vec![
"r0.0.1".to_owned(),
"r0.1.0".to_owned(),
"r0.2.0".to_owned(),
"r0.3.0".to_owned(),
"r0.4.0".to_owned(),
"r0.5.0".to_owned(),
"r0.6.0".to_owned(),
"r0.6.1".to_owned(),
"v1.1".to_owned(),
"v1.2".to_owned(),
"v1.3".to_owned(),
"v1.4".to_owned(),
"v1.5".to_owned(),
"v1.8".to_owned(),
"v1.11".to_owned(),
"v1.12".to_owned(),
"v1.13".to_owned(),
"v1.14".to_owned(),
]
vec![
"r0.0.1".to_owned(),
"r0.1.0".to_owned(),
"r0.2.0".to_owned(),
"r0.3.0".to_owned(),
"r0.4.0".to_owned(),
"r0.5.0".to_owned(),
"r0.6.0".to_owned(),
"r0.6.1".to_owned(),
"v1.1".to_owned(),
"v1.2".to_owned(),
"v1.3".to_owned(),
"v1.4".to_owned(),
"v1.5".to_owned(),
"v1.8".to_owned(),
"v1.11".to_owned(),
"v1.12".to_owned(),
"v1.13".to_owned(),
"v1.14".to_owned(),
]
}
pub fn unstable_features() -> BTreeMap<String, bool> {
BTreeMap::from_iter([
("org.matrix.e2e_cross_signing".to_owned(), true),
("org.matrix.msc2285.stable".to_owned(), true), /* private read receipts (https://github.com/matrix-org/matrix-spec-proposals/pull/2285) */
("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true), /* query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666) */
("org.matrix.msc2836".to_owned(), true), /* threading/threads (https://github.com/matrix-org/matrix-spec-proposals/pull/2836) */
("org.matrix.msc2946".to_owned(), true), /* spaces/hierarchy summaries (https://github.com/matrix-org/matrix-spec-proposals/pull/2946) */
("org.matrix.msc3026.busy_presence".to_owned(), true), /* busy presence status (https://github.com/matrix-org/matrix-spec-proposals/pull/3026) */
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
("org.matrix.msc3916.stable".to_owned(), true), /* authenticated media (https://github.com/matrix-org/matrix-spec-proposals/pull/3916) */
("org.matrix.msc4180".to_owned(), true), /* stable flag for 3916 (https://github.com/matrix-org/matrix-spec-proposals/pull/4180) */
("uk.tcpip.msc4133".to_owned(), true), /* Extending User Profile API with Key:Value Pairs (https://github.com/matrix-org/matrix-spec-proposals/pull/4133) */
("us.cloke.msc4175".to_owned(), true), /* Profile field for user time zone (https://github.com/matrix-org/matrix-spec-proposals/pull/4175) */
("org.matrix.simplified_msc3575".to_owned(), true), /* Simplified Sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/4186) */
("uk.timedout.msc4323".to_owned(), true), /* agnostic suspend (https://github.com/matrix-org/matrix-spec-proposals/pull/4323) */
("org.matrix.msc4155".to_owned(), true), /* invite filtering (https://github.com/matrix-org/matrix-spec-proposals/pull/4155) */
])
}
BTreeMap::from_iter([
("org.matrix.e2e_cross_signing".to_owned(), true),
("org.matrix.msc2285.stable".to_owned(), true), /* private read receipts (https://github.com/matrix-org/matrix-spec-proposals/pull/2285) */
("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true), /* query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666) */
("org.matrix.msc2836".to_owned(), true), /* threading/threads (https://github.com/matrix-org/matrix-spec-proposals/pull/2836) */
("org.matrix.msc2946".to_owned(), true), /* spaces/hierarchy summaries (https://github.com/matrix-org/matrix-spec-proposals/pull/2946) */
("org.matrix.msc3026.busy_presence".to_owned(), true), /* busy presence status (https://github.com/matrix-org/matrix-spec-proposals/pull/3026) */
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
("org.matrix.msc3916.stable".to_owned(), true), /* authenticated media (https://github.com/matrix-org/matrix-spec-proposals/pull/3916) */
("org.matrix.msc4180".to_owned(), true), /* stable flag for 3916 (https://github.com/matrix-org/matrix-spec-proposals/pull/4180) */
("uk.tcpip.msc4133".to_owned(), true), /* Extending User Profile API with Key:Value Pairs (https://github.com/matrix-org/matrix-spec-proposals/pull/4133) */
("us.cloke.msc4175".to_owned(), true), /* Profile field for user time zone (https://github.com/matrix-org/matrix-spec-proposals/pull/4175) */
("org.matrix.simplified_msc3575".to_owned(), true), /* Simplified Sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/4186) */
("uk.timedout.msc4323".to_owned(), true), /* agnostic suspend (https://github.com/matrix-org/matrix-spec-proposals/pull/4323) */
("org.matrix.msc4155".to_owned(), true), /* invite filtering (https://github.com/matrix-org/matrix-spec-proposals/pull/4155) */
])
}