refactor: Configuration-gate experiments

This commit is contained in:
timedout authored and Ellis Git committed 2026-11-14 13:03:16 +00:00
1 parent b34dfe343a
commit 39035d735e
15 files changed
+238 -90

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
# Unstable features policy
Like several other homeservers in the Matrix ecosystem, Continuwuity adopts a "disable by default"
approach to experimental features. This ensures every deployment gets the same baseline stability,
allowing users to opt in to potentially unstable features.
Historically, Continuwuity used maintainer discretion to decide whether an unstable feature should
be enabled by default. However, as the project has grown, and the pace of spec development has
increased, keeping up with unstable proposals becomes less and less feasible by the day, and this
causes the implementation of unstable proposals to gradually drift. This can lead to implementations
becoming so stale they're incompatible, with no bandwidth to get them fixed up.
Continuwuity is already selective about which unstable features it implements: the maintainer team
typically only accept implementations of proposals that seem like they are a net positive for a
large portion of the Matrix ecosystem, rather than aiming to "implement everything". As such, the
unstable feature list should typically be small and manageable.
## Adding an unstable feature
First, navigate to `src/core/config/experiments.rs`. There, you have two options for adding an
experiment to the `Experiments` struct:
1. Use `SimpleExperiment` if the experiment only requires enabling with no further configuration.
2. Define a new struct that implements the `ToggledExperiment` trait.
All experiments must be optional and default to off (i.e. `is_enabled()` returns `false`).
`SimpleExperiment` is actually a wrapper around `Option<bool>`, and the omission of the
configuration OR setting it to `false` disables the experiment. `ToggledExperiment` impls should
copy similar behaviour.
Then, add a public field to the `Experiments` struct that is named the MSC's ID in lowercase.
Add a docstring to it, starting with the proposal title (including ID), then a brief paragraph or
so explaining what the proposal does in simple terms, and then finally end the docstring with a link
to the proposal.
### Advertising support for unstable features
If the proposal requires advertising an unstable feature flag, that flag must be added to
`unstable_features` in `src/core/matrix/versions.rs`. For example:
```diff
pub fn unstable_features(experiments: &Experiments) -> BTreeMap<String, bool> {
BTreeMap::from_iter([
// Always enabled feature (https://github.com/matrix-org/matrix-spec-proposals/pull/0000)
// Expected for spec v1.20
("org.matrix.msc0000".to_owned(), true),
+ // MSC9999: My Cool Feature (https://github.com/matrix-org/matrix-spec-proposals/pull/9999)
+ ("example.matrix.msc9999".to_owned(), experiments.msc9999.is_enabled())
])
}
```
This will allow the `/_matrix/client/versions` endpoint to dynamically advertise whether the unstable
feature is enabled on the current server.
## Removing unstable features
If a proposal is abandoned, becomes obsolete, or is accepted into the specification, the unstable
configuration should be removed (and possibly migrated) as soon as possible.
After removing a field from `experiments`, the key should be added to the `DEPRECATED_KEYS` slice in
`src/core/config/mod.rs` (really far down). This will ensure that a warning is printed on startup
when a configuration is using a now-removed config key. For example:
```diff
pub struct Experiments {
- pub msc9999: SimpleExperiment,
}
```
```diff
const DEPRECATED_KEYS: &[&str] = [
"foo",
+ "experiments.msc9999",
];
```
+7 -2
View File
@@ -1,5 +1,7 @@
use axum::extract::{Query, State};
use conduwuit::{Err, Error, Event, PduEvent, Result, debug_warn, err};
use conduwuit::{
Err, Error, Event, PduEvent, Result, config::experiments::ToggledExperiment, debug_warn, err,
};
use futures::TryFutureExt;
use http::StatusCode;
use ruma::{
@@ -68,7 +70,7 @@ pub(crate) async fn get_room_event_route(
return Err!(Request(Forbidden("You don't have permission to view this event.")));
}
if services.config.allow_moderators_to_view_redacted_content
if services.config.experiments.msc2815.is_enabled()
&& params.include_unredacted_content.unwrap_or(false)
&& event.is_redacted()
{
@@ -126,6 +128,9 @@ async fn unredact(services: &crate::State, sender_user: &UserId, event: &mut Pdu
// CustomErrorKind has private fields; only serde can build one.
let keep_ms = services
.config
.experiments
.msc2815
.expect("msc2815 config is disabled but unredact() attempted to access it anyway")
.unredacted_content_max_age_secs
.saturating_mul(1000);
+1 -6
View File
@@ -95,12 +95,7 @@ pub(crate) async fn send_message_event_route(
PartialPdu {
event_type: body.event_type.clone().into(),
content,
sticky: services
.config
.allow_sticky_events
.then_some(body.sticky_duration_ms)
.flatten()
.map(sticky::object),
sticky: body.sticky_duration_ms.map(sticky::object),
unsigned: Some(unsigned),
timestamp: if body.identity.is_appservice()
|| services.admin.user_is_admin(sender_user).await
-4
View File
@@ -273,10 +273,6 @@ async fn build_sticky_events(
timeline: &TimelinePdus,
joined_since_last_sync: bool,
) -> Sticky {
if !services.config.allow_sticky_events {
return Sticky::new();
}
let SyncContext {
syncing_user,
last_sync_end_count,
+1 -1
View File
@@ -1123,7 +1123,7 @@ async fn collect_sticky_events(
let mut delivered = StickyDelivered::new();
let sticky_events = &body.extensions.sticky_events;
if !services.config.allow_sticky_events || !sticky_events.enabled.unwrap_or(false) {
if !sticky_events.enabled.unwrap_or(false) {
return (response, delivered);
}
+1 -11
View File
@@ -27,17 +27,7 @@ pub(crate) async fn get_supported_versions_route(
State(services): State<crate::State>,
_body: Ruma<get_supported_versions::Request>,
) -> Result<get_supported_versions::Response> {
let mut unstable_features = unstable_features();
if services.config.allow_sticky_events {
// sticky events (https://github.com/matrix-org/matrix-spec-proposals/pull/4354)
unstable_features.insert("org.matrix.msc4354".to_owned(), true);
}
if services.config.allow_moderators_to_view_redacted_content {
// moderators can view redacted content (https://github.com/matrix-org/matrix-spec-proposals/pull/2815)
unstable_features.insert("fi.mau.msc2815".to_owned(), true);
}
let unstable_features = unstable_features(&services.config.experiments);
Ok(assign!(get_supported_versions::Response::new(versions()), {
unstable_features
+115
View File
@@ -0,0 +1,115 @@
use serde::Deserialize;
/// Configuration for experimental features.
#[derive(Debug, Deserialize, Default, Clone)]
pub struct Experiments {
/// MSC2815: Proposal to allow room moderators to view redacted event
/// content
///
/// Allows the server to retain unredacted copies of events that have been
/// redacted for a limited period of time, which allows room moderators and
/// server administrators to reveal redacted contents. Useful for moderation
/// and investigating reports.
///
/// Requires a supporting client to use.
///
/// https://github.com/matrix-org/matrix-spec-proposals/pull/2815
#[serde(default)]
pub msc2815: Option<MSC2815>,
/// MSC4155: Invite filtering
///
/// Provides advanced invite filtering rules for users, allowing complex
/// configurations that can allow, ignore, and reject invites from users and
/// servers broadly.
///
/// Requires a supporting client to set up on an account.
///
/// https://github.com/matrix-org/matrix-spec-proposals/pull/4155
#[serde(default)]
pub msc4155: SimpleExperiment,
/// MSC4174: Web push
///
/// Adds server-side support for web push, allowing web clients to receive
/// push notifications directly without the need for an intermediary push
/// gateway like Sygnal.
///
/// https://github.com/matrix-org/matrix-spec-proposals/pull/4174
#[serde(default)]
pub msc4174: SimpleExperiment,
/// MSC4484: Server Administration OAuth Scope
///
/// Adds a new OAuth2 scope that explicitly grants a token temporary
/// homeserver administrator privileges, granting temporary token access
/// to administrator endpoints like locking & suspension.
///
/// https://github.com/matrix-org/matrix-spec-proposals/pull/4484
#[serde(default)]
pub msc4484: SimpleExperiment,
/// MSC4494: Membership-based invite blocking
///
/// Adds a new invite blocking action that will reject all invites coming
/// from users who do not share a room with the invitee that is not `public`
/// (the join rule).
///
/// This is not related to MSC4155, this proposal instead builds on the
/// "invite blocking" proposal that landed in the specification in v1.17,
/// which simply allows enabling or disabling invites altogether.
///
/// MSC4494 is a good middle-ground between the allowlisting possible with
/// MSC4155, and the general desire to block invites from users you may not
/// know.
///
/// Requires a supporting client to enable on an account.
///
/// https://github.com/matrix-org/matrix-spec-proposals/pull/4494
#[serde(default)]
pub msc4494: SimpleExperiment,
/// MSC4560: Stripped state requests over federation
///
/// Implements a more efficient way for servers to acquire and serve
/// "stripped state" to clients, which is typically only used for displaying
/// rooms. The display of space hierarchies and room summaries is
/// significantly faster when this proposal is enabled and used.
///
/// https://github.com/matrix-org/matrix-spec-proposals/pull/4560
#[serde(default)]
pub msc4560: SimpleExperiment,
}
/// Represents a "simple" experiment, which is either disabled or enabled.
///
/// More complex experiments that require configuration should define their own
/// type and implement `ToggledExperiment` instead.
#[derive(Debug, Deserialize, Default, Clone, Copy)]
pub struct SimpleExperiment(Option<bool>);
pub trait ToggledExperiment {
/// Returns true if this experiment is explicitly enabled.
#[must_use]
fn is_enabled(&self) -> bool;
}
impl ToggledExperiment for SimpleExperiment {
#[inline]
fn is_enabled(&self) -> bool { self.0.is_some_and(crate::is_true!()) }
}
/// Configuration for [MSC2815]: Proposal to allow room moderators to view
/// redacted event content
///
/// MSC2815: https://github.com/matrix-org/matrix-spec-proposals/pull/2815
#[derive(Debug, Deserialize, Default, Clone, Copy)]
pub struct MSC2815 {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_unredacted_content_max_age_secs")]
pub unredacted_content_max_age_secs: u64,
}
#[inline]
fn default_unredacted_content_max_age_secs() -> u64 { 7_776_000 }
impl ToggledExperiment for Option<MSC2815> {
#[inline]
fn is_enabled(&self) -> bool { self.is_some_and(|config| config.enabled) }
}
+11 -31
View File
@@ -1,5 +1,6 @@
#![allow(clippy::doc_link_with_quotes)]
pub mod check;
pub mod experiments;
pub mod manager;
pub mod proxy;
@@ -18,6 +19,7 @@ use either::{
Either,
Either::{Left, Right},
};
pub use experiments::Experiments;
use figment::providers::{Env, Format, Toml};
pub use figment::{Figment, value::Value as FigmentValue};
use governor::{DefaultKeyedRateLimiter, Quota, RateLimiter};
@@ -1768,35 +1770,6 @@ pub struct Config {
#[serde(default = "true_fn")]
pub allow_incoming_typing: bool,
/// Allow the use of unstable persistent "sticky events" ([MSC4354])
///
/// MSC4354: https://c10y.cc/MSC4354
///
/// default: false
#[serde(default)]
pub allow_sticky_events: bool,
/// Allow room moderators to view redacted event content ([MSC2815]).
///
/// Redacted content is readable by users at or above the room's `redact`
/// power level, and by server admins, until it expires.
///
/// MSC2815: https://github.com/matrix-org/matrix-spec-proposals/pull/2815
///
/// default: false
#[serde(default)]
pub allow_moderators_to_view_redacted_content: bool,
/// How long to retain the content of redacted events, in seconds. Expired
/// content is deleted by a periodic purge and is no longer served.
///
/// Only has an effect when allow_moderators_to_view_redacted_content is
/// enabled.
///
/// default: 7776000 (90 days)
#[serde(default = "default_unredacted_content_max_age_secs")]
pub unredacted_content_max_age_secs: u64,
/// Maximum time federation user can indicate typing.
///
/// default: 30
@@ -2474,6 +2447,12 @@ pub struct Config {
#[serde(default)]
pub matrix_rtc: MatrixRtcConfig,
/// Configuration for unstable features.
///
/// display: none
#[serde(default)]
pub experiments: Experiments,
#[serde(flatten)]
#[allow(clippy::zero_sized_map_values)]
// this is a catchall, the map shouldn't be zero at runtime
@@ -3007,6 +2986,9 @@ const DEPRECATED_KEYS: &[&str] = &[
"well_known_support_email",
"well_known_support_mxid",
"well_known.rtc_focus_server_urls",
"allow_sticky_events",
"allow_moderators_to_view_redacted_content",
"unredacted_content_max_age_secs",
];
impl Config {
@@ -3239,8 +3221,6 @@ fn default_remote_join_pdu_queue_capacity() -> usize { 50 }
fn default_transaction_id_cache_max_age_secs() -> u64 { 60 * 60 * 2 }
fn default_unredacted_content_max_age_secs() -> u64 { 60 * 60 * 24 * 90 }
fn default_transaction_id_cache_max_entries() -> usize { 8192 }
fn default_tracing_flame_filter() -> String {
+12 -16
View File
@@ -1,5 +1,7 @@
use std::collections::BTreeMap;
use crate::config::{Experiments, experiments::ToggledExperiment};
#[must_use]
pub fn versions() -> Vec<String> {
vec![
@@ -25,32 +27,26 @@ pub fn versions() -> Vec<String> {
"v1.16".to_owned(),
"v1.17".to_owned(),
"v1.18".to_owned(),
"v1.19".to_owned(),
]
}
#[must_use]
pub fn unstable_features() -> BTreeMap<String, bool> {
pub fn unstable_features(experiments: &Experiments) -> BTreeMap<String, bool> {
BTreeMap::from_iter([
// MatrixRTC (https://github.com/matrix-org/matrix-spec-proposals/pull/4143)
// Expected for spec v1.20
("org.matrix.msc4143".to_owned(), true),
// query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666)
// Expected for spec v1.19
("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true),
("uk.half-shot.msc2666.query_mutual_rooms.stable".to_owned(), true),
// Simplified Sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/4186)
// Expected for spec v1.19
// Expected for spec v1.20
("org.matrix.simplified_msc3575".to_owned(), true),
// invite filtering (https://github.com/matrix-org/matrix-spec-proposals/pull/4155)
("org.matrix.msc4155".to_owned(), true),
("org.matrix.msc4155".to_owned(), experiments.msc4155.is_enabled()),
// profile change propagation (https://github.com/matrix-org/matrix-spec-proposals/pull/4466)
("computer.gingershaped.msc4466".to_owned(), true),
// web push (https://github.com/matrix-org/matrix-spec-proposals/pull/4174)
("org.matrix.msc4174".to_owned(), true),
// Membership-based invite blocking (https://github.com/matrix-org/matrix-spec-proposals/pull/4494)
("uk.timedout.msc4494".to_owned(), true),
// server administration OAuth scope (https://github.com/matrix-org/matrix-spec-proposals/pull/4484)
("org.continuwuity.msc4484.unstable".to_owned(), true),
// stripped state over federation (https://github.com/matrix-org/matrix-spec-proposals/pull/4560)
("org.continuwuity.msc4560".to_owned(), true),
("org.matrix.msc4174".to_owned(), experiments.msc4174.is_enabled()),
("uk.timedout.msc4494".to_owned(), experiments.msc4494.is_enabled()),
("org.continuwuity.msc4484.unstable".to_owned(), experiments.msc4484.is_enabled()),
("org.continuwuity.msc4560".to_owned(), experiments.msc4560.is_enabled()),
("fi.mau.msc2815".to_owned(), experiments.msc2815.is_enabled()),
])
}
+4 -2
View File
@@ -42,6 +42,8 @@ pub mod transactions;
pub mod uiaa;
pub mod users;
use std::collections::BTreeMap;
pub(crate) use service::{Args, Dep, Service};
pub use crate::services::Services;
@@ -51,8 +53,8 @@ conduwuit::mod_dtor! {}
use std::sync::LazyLock;
use conduwuit::matrix::versions::{unstable_features, versions};
use conduwuit::matrix::versions::versions;
use ruma::api::SupportedVersions;
pub static SUPPORTED_VERSIONS: LazyLock<SupportedVersions> =
LazyLock::new(|| SupportedVersions::from_parts(&versions(), &unstable_features()));
LazyLock::new(|| SupportedVersions::from_parts(&versions(), &BTreeMap::new()));
+1 -3
View File
@@ -591,9 +591,7 @@ impl super::Service {
room_id: &RoomId,
) -> Option<OwnedServerName> {
let server = target_user_id.server_name();
if !self.services.config.allow_sticky_events
|| self.services.globals.server_is_ours(server)
{
if self.services.globals.server_is_ours(server) {
return None;
}
+1 -7
View File
@@ -48,13 +48,7 @@ impl super::Service {
PartialPdu {
event_type: event_type.to_string().into(),
content,
sticky: self
.services
.config
.allow_sticky_events
.then_some(sticky_duration_ms)
.flatten()
.map(sticky::object),
sticky: sticky_duration_ms.map(sticky::object),
state_key: Some(state_key.into()),
timestamp,
..Default::default()
+6 -1
View File
@@ -287,7 +287,12 @@ impl Service {
}
fn unredacted_content_cutoff(&self) -> u64 {
let max_age = self.services.config.unredacted_content_max_age_secs;
let max_age = self
.services
.config
.experiments
.msc2815
.map_or_default(|c| c.unredacted_content_max_age_secs);
utils::millis_since_unix_epoch().saturating_sub(max_age.saturating_mul(1000))
}
+2 -5
View File
@@ -1,3 +1,4 @@
use conduwuit::config::experiments::ToggledExperiment;
use conduwuit_core::{
Result, err,
matrix::event::Event,
@@ -52,11 +53,7 @@ impl super::Service {
.get_room_version(&pdu.room_id_or_hash())
.await?;
if self
.services
.config
.allow_moderators_to_view_redacted_content
{
if self.services.config.experiments.msc2815.is_enabled() {
let mut pdu_json = self.get_pdu_json_from_id(&pdu_id).await?;
pdu_json.remove("event_id");
if content_hash(&pdu_json)
+1 -1
View File
@@ -327,7 +327,7 @@ impl Service {
/// still-sticky events. Only netburst delivers this queue, so with it
/// disabled there is nothing to keep the event for.
async fn survives_netburst_trim(&self, event: &SendingEvent) -> bool {
if !self.server.config.allow_sticky_events || !self.server.config.startup_netburst {
if !self.server.config.startup_netburst {
return false;
}