mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-11-14 20:56:49 +00:00
refactor: Configuration-gate experiments
This commit is contained in:
1 parent
b34dfe343a
commit
39035d735e
15 files changed
+238
-90
No files matched your search
@@ -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",
|
||||
];
|
||||
```
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()));
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user