Merge branch 'develop' into madlittlemods/msc3871-gappy-timeline

This commit is contained in:
Eric Eastwood
2026-03-26 15:35:36 -05:00
28 changed files with 1460 additions and 632 deletions
+1
View File
@@ -0,0 +1 @@
Remove `redacted_because` from internal unsigned.
+1
View File
@@ -0,0 +1 @@
Port `RoomVersion` to Rust.
+6
View File
@@ -261,6 +261,11 @@ pub struct EventInternalMetadata {
#[pyo3(get, set)]
instance_name: Option<String>,
/// The event ID of the redaction event, if this event has been redacted.
/// This is set dynamically at load time and is not persisted to the database.
#[pyo3(get, set)]
redacted_by: Option<String>,
/// whether this event is an outlier (ie, whether we have the state at that
/// point in the DAG)
#[pyo3(get, set)]
@@ -289,6 +294,7 @@ impl EventInternalMetadata {
data,
stream_ordering: None,
instance_name: None,
redacted_by: None,
outlier: false,
})
}
+2
View File
@@ -15,6 +15,7 @@ pub mod matrix_const;
pub mod msc4388_rendezvous;
pub mod push;
pub mod rendezvous;
pub mod room_versions;
pub mod segmenter;
lazy_static! {
@@ -58,6 +59,7 @@ fn synapse_rust(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
rendezvous::register_module(py, m)?;
msc4388_rendezvous::register_module(py, m)?;
segmenter::register_module(py, m)?;
room_versions::register_module(py, m)?;
Ok(())
}
+825
View File
@@ -0,0 +1,825 @@
/*
* This file is licensed under the Affero General Public License (AGPL) version 3.
*
* Copyright (C) 2026 Element Creations Ltd.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* See the GNU Affero General Public License for more details:
* <https://www.gnu.org/licenses/agpl-3.0.html>.
*/
//! Rust implementation of room version definitions.
use std::sync::{Arc, LazyLock, RwLock};
use pyo3::{
exceptions::{PyKeyError, PyRuntimeError},
prelude::*,
types::{PyFrozenSet, PyIterator, PyModule, PyModuleMethods},
Bound, IntoPyObjectExt, PyResult, Python,
};
/// Internal enum for tracking the version of the event format,
/// independently of the room version.
///
/// To reduce confusion, the event format versions are named after the room
/// versions that they were used or introduced in.
/// The concept of an 'event format version' is specific to Synapse (the
/// specification does not mention this term.)
#[pyclass(frozen)]
pub struct EventFormatVersions {}
#[pymethods]
impl EventFormatVersions {
/// $id:server event id format: used for room v1 and v2
#[classattr]
const ROOM_V1_V2: i32 = 1;
/// MSC1659-style $hash event id format: used for room v3
#[classattr]
const ROOM_V3: i32 = 2;
/// MSC1884-style $hash format: introduced for room v4
#[classattr]
const ROOM_V4_PLUS: i32 = 3;
/// MSC4291 room IDs as hashes: introduced for room HydraV11
#[classattr]
const ROOM_V11_HYDRA_PLUS: i32 = 4;
}
/// Enum to identify the state resolution algorithms.
#[pyclass(frozen)]
pub struct StateResolutionVersions {}
#[pymethods]
impl StateResolutionVersions {
/// Room v1 state res
#[classattr]
const V1: i32 = 1;
/// MSC1442 state res: room v2 and later
#[classattr]
const V2: i32 = 2;
/// MSC4297 state res
#[classattr]
const V2_1: i32 = 3;
}
/// Room disposition constants.
#[pyclass(frozen)]
pub struct RoomDisposition {}
#[pymethods]
impl RoomDisposition {
#[classattr]
const STABLE: &'static str = "stable";
#[classattr]
const UNSTABLE: &'static str = "unstable";
}
/// Enum for listing possible MSC3931 room version feature flags, for push rules.
#[pyclass(frozen)]
pub struct PushRuleRoomFlag {}
#[pymethods]
impl PushRuleRoomFlag {
/// MSC3932: Room version supports MSC1767 Extensible Events.
#[classattr]
const EXTENSIBLE_EVENTS: &'static str = "org.matrix.msc3932.extensible_events";
}
/// An object which describes the unique attributes of a room version.
#[pyclass(frozen, eq, hash, get_all)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct RoomVersion {
/// The identifier for this version.
pub identifier: &'static str,
/// One of the RoomDisposition constants.
pub disposition: &'static str,
/// One of the EventFormatVersions constants.
pub event_format: i32,
/// One of the StateResolutionVersions constants.
pub state_res: i32,
pub enforce_key_validity: bool,
/// Before MSC2432, m.room.aliases had special auth rules and redaction rules.
pub special_case_aliases_auth: bool,
/// Strictly enforce canonicaljson, do not allow:
/// * Integers outside the range of [-2^53 + 1, 2^53 - 1]
/// * Floats
/// * NaN, Infinity, -Infinity
pub strict_canonicaljson: bool,
/// MSC2209: Check 'notifications' key while verifying
/// m.room.power_levels auth rules.
pub limit_notifications_power_levels: bool,
/// MSC3820: No longer include the creator in m.room.create events (room version 11).
pub implicit_room_creator: bool,
/// MSC3820: Apply updated redaction rules algorithm from room version 11.
pub updated_redaction_rules: bool,
/// Support the 'restricted' join rule.
pub restricted_join_rule: bool,
/// Support for the proper redaction rules for the restricted join rule.
/// This requires restricted_join_rule to be enabled.
pub restricted_join_rule_fix: bool,
/// Support the 'knock' join rule.
pub knock_join_rule: bool,
/// MSC3389: Protect relation information from redaction.
pub msc3389_relation_redactions: bool,
/// Support the 'knock_restricted' join rule.
pub knock_restricted_join_rule: bool,
/// Enforce integer power levels.
pub enforce_int_power_levels: bool,
/// MSC3931: Adds a push rule condition for "room version feature flags", making
/// some push rules room version dependent. Note that adding a flag to this list
/// is not enough to mark it "supported": the push rule evaluator also needs to
/// support the flag. Unknown flags are ignored by the evaluator, making conditions
/// fail if used. Values from PushRuleRoomFlag.
pub msc3931_push_features: &'static [&'static str],
/// MSC3757: Restricting who can overwrite a state event.
pub msc3757_enabled: bool,
/// MSC4289: Creator power enabled.
pub msc4289_creator_power_enabled: bool,
/// MSC4291: Room IDs as hashes of the create event.
pub msc4291_room_ids_as_hashes: bool,
/// Set of room versions where Synapse strictly enforces event key size limits
/// in bytes, rather than in codepoints.
///
/// In these room versions, we are stricter with event size validation.
pub strict_event_byte_limits_room_versions: bool,
}
const ROOM_VERSION_V1: RoomVersion = RoomVersion {
identifier: "1",
disposition: RoomDisposition::STABLE,
event_format: EventFormatVersions::ROOM_V1_V2,
state_res: StateResolutionVersions::V1,
enforce_key_validity: false,
special_case_aliases_auth: true,
strict_canonicaljson: false,
limit_notifications_power_levels: false,
implicit_room_creator: false,
updated_redaction_rules: false,
restricted_join_rule: false,
restricted_join_rule_fix: false,
knock_join_rule: false,
msc3389_relation_redactions: false,
knock_restricted_join_rule: false,
enforce_int_power_levels: false,
msc3931_push_features: &[],
msc3757_enabled: false,
msc4289_creator_power_enabled: false,
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
};
const ROOM_VERSION_V2: RoomVersion = RoomVersion {
identifier: "2",
disposition: RoomDisposition::STABLE,
event_format: EventFormatVersions::ROOM_V1_V2,
state_res: StateResolutionVersions::V2,
enforce_key_validity: false,
special_case_aliases_auth: true,
strict_canonicaljson: false,
limit_notifications_power_levels: false,
implicit_room_creator: false,
updated_redaction_rules: false,
restricted_join_rule: false,
restricted_join_rule_fix: false,
knock_join_rule: false,
msc3389_relation_redactions: false,
knock_restricted_join_rule: false,
enforce_int_power_levels: false,
msc3931_push_features: &[],
msc3757_enabled: false,
msc4289_creator_power_enabled: false,
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
};
const ROOM_VERSION_V3: RoomVersion = RoomVersion {
identifier: "3",
disposition: RoomDisposition::STABLE,
event_format: EventFormatVersions::ROOM_V3,
state_res: StateResolutionVersions::V2,
enforce_key_validity: false,
special_case_aliases_auth: true,
strict_canonicaljson: false,
limit_notifications_power_levels: false,
implicit_room_creator: false,
updated_redaction_rules: false,
restricted_join_rule: false,
restricted_join_rule_fix: false,
knock_join_rule: false,
msc3389_relation_redactions: false,
knock_restricted_join_rule: false,
enforce_int_power_levels: false,
msc3931_push_features: &[],
msc3757_enabled: false,
msc4289_creator_power_enabled: false,
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
};
const ROOM_VERSION_V4: RoomVersion = RoomVersion {
identifier: "4",
disposition: RoomDisposition::STABLE,
event_format: EventFormatVersions::ROOM_V4_PLUS,
state_res: StateResolutionVersions::V2,
enforce_key_validity: false,
special_case_aliases_auth: true,
strict_canonicaljson: false,
limit_notifications_power_levels: false,
implicit_room_creator: false,
updated_redaction_rules: false,
restricted_join_rule: false,
restricted_join_rule_fix: false,
knock_join_rule: false,
msc3389_relation_redactions: false,
knock_restricted_join_rule: false,
enforce_int_power_levels: false,
msc3931_push_features: &[],
msc3757_enabled: false,
msc4289_creator_power_enabled: false,
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
};
const ROOM_VERSION_V5: RoomVersion = RoomVersion {
identifier: "5",
disposition: RoomDisposition::STABLE,
event_format: EventFormatVersions::ROOM_V4_PLUS,
state_res: StateResolutionVersions::V2,
enforce_key_validity: true,
special_case_aliases_auth: true,
strict_canonicaljson: false,
limit_notifications_power_levels: false,
implicit_room_creator: false,
updated_redaction_rules: false,
restricted_join_rule: false,
restricted_join_rule_fix: false,
knock_join_rule: false,
msc3389_relation_redactions: false,
knock_restricted_join_rule: false,
enforce_int_power_levels: false,
msc3931_push_features: &[],
msc3757_enabled: false,
msc4289_creator_power_enabled: false,
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
};
const ROOM_VERSION_V6: RoomVersion = RoomVersion {
identifier: "6",
disposition: RoomDisposition::STABLE,
event_format: EventFormatVersions::ROOM_V4_PLUS,
state_res: StateResolutionVersions::V2,
enforce_key_validity: true,
special_case_aliases_auth: false,
strict_canonicaljson: true,
limit_notifications_power_levels: true,
implicit_room_creator: false,
updated_redaction_rules: false,
restricted_join_rule: false,
restricted_join_rule_fix: false,
knock_join_rule: false,
msc3389_relation_redactions: false,
knock_restricted_join_rule: false,
enforce_int_power_levels: false,
msc3931_push_features: &[],
msc3757_enabled: false,
msc4289_creator_power_enabled: false,
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
};
const ROOM_VERSION_V7: RoomVersion = RoomVersion {
identifier: "7",
disposition: RoomDisposition::STABLE,
event_format: EventFormatVersions::ROOM_V4_PLUS,
state_res: StateResolutionVersions::V2,
enforce_key_validity: true,
special_case_aliases_auth: false,
strict_canonicaljson: true,
limit_notifications_power_levels: true,
implicit_room_creator: false,
updated_redaction_rules: false,
restricted_join_rule: false,
restricted_join_rule_fix: false,
knock_join_rule: true,
msc3389_relation_redactions: false,
knock_restricted_join_rule: false,
enforce_int_power_levels: false,
msc3931_push_features: &[],
msc3757_enabled: false,
msc4289_creator_power_enabled: false,
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
};
const ROOM_VERSION_V8: RoomVersion = RoomVersion {
identifier: "8",
disposition: RoomDisposition::STABLE,
event_format: EventFormatVersions::ROOM_V4_PLUS,
state_res: StateResolutionVersions::V2,
enforce_key_validity: true,
special_case_aliases_auth: false,
strict_canonicaljson: true,
limit_notifications_power_levels: true,
implicit_room_creator: false,
updated_redaction_rules: false,
restricted_join_rule: true,
restricted_join_rule_fix: false,
knock_join_rule: true,
msc3389_relation_redactions: false,
knock_restricted_join_rule: false,
enforce_int_power_levels: false,
msc3931_push_features: &[],
msc3757_enabled: false,
msc4289_creator_power_enabled: false,
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
};
const ROOM_VERSION_V9: RoomVersion = RoomVersion {
identifier: "9",
disposition: RoomDisposition::STABLE,
event_format: EventFormatVersions::ROOM_V4_PLUS,
state_res: StateResolutionVersions::V2,
enforce_key_validity: true,
special_case_aliases_auth: false,
strict_canonicaljson: true,
limit_notifications_power_levels: true,
implicit_room_creator: false,
updated_redaction_rules: false,
restricted_join_rule: true,
restricted_join_rule_fix: true,
knock_join_rule: true,
msc3389_relation_redactions: false,
knock_restricted_join_rule: false,
enforce_int_power_levels: false,
msc3931_push_features: &[],
msc3757_enabled: false,
msc4289_creator_power_enabled: false,
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
};
const ROOM_VERSION_V10: RoomVersion = RoomVersion {
identifier: "10",
disposition: RoomDisposition::STABLE,
event_format: EventFormatVersions::ROOM_V4_PLUS,
state_res: StateResolutionVersions::V2,
enforce_key_validity: true,
special_case_aliases_auth: false,
strict_canonicaljson: true,
limit_notifications_power_levels: true,
implicit_room_creator: false,
updated_redaction_rules: false,
restricted_join_rule: true,
restricted_join_rule_fix: true,
knock_join_rule: true,
msc3389_relation_redactions: false,
knock_restricted_join_rule: true,
enforce_int_power_levels: true,
msc3931_push_features: &[],
msc3757_enabled: false,
msc4289_creator_power_enabled: false,
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
};
/// MSC3389 (Redaction changes for events with a relation) based on room version "10".
const ROOM_VERSION_MSC3389V10: RoomVersion = RoomVersion {
identifier: "org.matrix.msc3389.10",
disposition: RoomDisposition::UNSTABLE,
event_format: EventFormatVersions::ROOM_V4_PLUS,
state_res: StateResolutionVersions::V2,
enforce_key_validity: true,
special_case_aliases_auth: false,
strict_canonicaljson: true,
limit_notifications_power_levels: true,
implicit_room_creator: false,
updated_redaction_rules: false,
restricted_join_rule: true,
restricted_join_rule_fix: true,
knock_join_rule: true,
msc3389_relation_redactions: true, // Changed from v10
knock_restricted_join_rule: true,
enforce_int_power_levels: true,
msc3931_push_features: &[],
msc3757_enabled: false,
msc4289_creator_power_enabled: false,
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: true,
};
/// MSC1767 (Extensible Events) based on room version "10".
const ROOM_VERSION_MSC1767V10: RoomVersion = RoomVersion {
identifier: "org.matrix.msc1767.10",
disposition: RoomDisposition::UNSTABLE,
event_format: EventFormatVersions::ROOM_V4_PLUS,
state_res: StateResolutionVersions::V2,
enforce_key_validity: true,
special_case_aliases_auth: false,
strict_canonicaljson: true,
limit_notifications_power_levels: true,
implicit_room_creator: false,
updated_redaction_rules: false,
restricted_join_rule: true,
restricted_join_rule_fix: true,
knock_join_rule: true,
msc3389_relation_redactions: false,
knock_restricted_join_rule: true,
enforce_int_power_levels: true,
msc3931_push_features: &[PushRuleRoomFlag::EXTENSIBLE_EVENTS],
msc3757_enabled: false,
msc4289_creator_power_enabled: false,
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
};
/// MSC3757 (Restricting who can overwrite a state event) based on room version "10".
const ROOM_VERSION_MSC3757V10: RoomVersion = RoomVersion {
identifier: "org.matrix.msc3757.10",
disposition: RoomDisposition::UNSTABLE,
event_format: EventFormatVersions::ROOM_V4_PLUS,
state_res: StateResolutionVersions::V2,
enforce_key_validity: true,
special_case_aliases_auth: false,
strict_canonicaljson: true,
limit_notifications_power_levels: true,
implicit_room_creator: false,
updated_redaction_rules: false,
restricted_join_rule: true,
restricted_join_rule_fix: true,
knock_join_rule: true,
msc3389_relation_redactions: false,
knock_restricted_join_rule: true,
enforce_int_power_levels: true,
msc3931_push_features: &[],
msc3757_enabled: true,
msc4289_creator_power_enabled: false,
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
};
const ROOM_VERSION_V11: RoomVersion = RoomVersion {
identifier: "11",
disposition: RoomDisposition::STABLE,
event_format: EventFormatVersions::ROOM_V4_PLUS,
state_res: StateResolutionVersions::V2,
enforce_key_validity: true,
special_case_aliases_auth: false,
strict_canonicaljson: true,
limit_notifications_power_levels: true,
implicit_room_creator: true, // Used by MSC3820
updated_redaction_rules: true, // Used by MSC3820
restricted_join_rule: true,
restricted_join_rule_fix: true,
knock_join_rule: true,
msc3389_relation_redactions: false,
knock_restricted_join_rule: true,
enforce_int_power_levels: true,
msc3931_push_features: &[],
msc3757_enabled: false,
msc4289_creator_power_enabled: false,
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: true, // Changed from v10
};
/// MSC3757 (Restricting who can overwrite a state event) based on room version "11".
const ROOM_VERSION_MSC3757V11: RoomVersion = RoomVersion {
identifier: "org.matrix.msc3757.11",
disposition: RoomDisposition::UNSTABLE,
event_format: EventFormatVersions::ROOM_V4_PLUS,
state_res: StateResolutionVersions::V2,
enforce_key_validity: true,
special_case_aliases_auth: false,
strict_canonicaljson: true,
limit_notifications_power_levels: true,
implicit_room_creator: true, // Used by MSC3820
updated_redaction_rules: true, // Used by MSC3820
restricted_join_rule: true,
restricted_join_rule_fix: true,
knock_join_rule: true,
msc3389_relation_redactions: false,
knock_restricted_join_rule: true,
enforce_int_power_levels: true,
msc3931_push_features: &[],
msc3757_enabled: true,
msc4289_creator_power_enabled: false,
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: true,
};
const ROOM_VERSION_HYDRA_V11: RoomVersion = RoomVersion {
identifier: "org.matrix.hydra.11",
disposition: RoomDisposition::UNSTABLE,
event_format: EventFormatVersions::ROOM_V11_HYDRA_PLUS,
state_res: StateResolutionVersions::V2_1, // Changed from v11
enforce_key_validity: true,
special_case_aliases_auth: false,
strict_canonicaljson: true,
limit_notifications_power_levels: true,
implicit_room_creator: true, // Used by MSC3820
updated_redaction_rules: true, // Used by MSC3820
restricted_join_rule: true,
restricted_join_rule_fix: true,
knock_join_rule: true,
msc3389_relation_redactions: false,
knock_restricted_join_rule: true,
enforce_int_power_levels: true,
msc3931_push_features: &[],
msc3757_enabled: false,
msc4289_creator_power_enabled: true, // Changed from v11
msc4291_room_ids_as_hashes: true, // Changed from v11
strict_event_byte_limits_room_versions: true,
};
const ROOM_VERSION_V12: RoomVersion = RoomVersion {
identifier: "12",
disposition: RoomDisposition::STABLE,
event_format: EventFormatVersions::ROOM_V11_HYDRA_PLUS,
state_res: StateResolutionVersions::V2_1, // Changed from v11
enforce_key_validity: true,
special_case_aliases_auth: false,
strict_canonicaljson: true,
limit_notifications_power_levels: true,
implicit_room_creator: true, // Used by MSC3820
updated_redaction_rules: true, // Used by MSC3820
restricted_join_rule: true,
restricted_join_rule_fix: true,
knock_join_rule: true,
msc3389_relation_redactions: false,
knock_restricted_join_rule: true,
enforce_int_power_levels: true,
msc3931_push_features: &[],
msc3757_enabled: false,
msc4289_creator_power_enabled: true, // Changed from v11
msc4291_room_ids_as_hashes: true, // Changed from v11
strict_event_byte_limits_room_versions: true,
};
/// Helper class for managing the known room versions, and providing dict-like
/// access to them for Python.
///
/// Note: this is not necessarily all room versions, as we may not want to
/// support all experimental room versions.
///
/// Note: room versions can be added to this mapping at startup (allowing
/// support for experimental room versions to be behind experimental feature
/// flags).
#[pyclass(frozen, mapping)]
#[derive(Clone)]
pub struct KnownRoomVersionsMapping {
// Note we use a Vec here to ensure that the order of keys is
// deterministic. We rely on this when generating parameterized tests in
// Python, as Python will generate the tests names including the room
// version and index (and we need test names to be stable to e.g. support
// running tests in parallel).
pub versions: Arc<RwLock<Vec<RoomVersion>>>,
}
#[pymethods]
impl KnownRoomVersionsMapping {
/// Add a new room version to the mapping, indicating that this instance
/// supports it.
fn add_room_version(&self, version: RoomVersion) -> PyResult<()> {
let mut versions = self
.versions
.write()
.map_err(|_| PyRuntimeError::new_err("KnownRoomVersionsMapping lock poisoned"))?;
if versions.iter().any(|v| v.identifier == version.identifier) {
// We already have this room version, so we don't add it again (as
// otherwise we'd end up with duplicates).
return Ok(());
}
versions.push(version);
Ok(())
}
fn __getitem__(&self, key: &str) -> PyResult<RoomVersion> {
let versions = self.versions.read().unwrap();
versions
.iter()
.find(|v| v.identifier == key)
.copied()
.ok_or_else(|| PyKeyError::new_err(key.to_string()))
}
fn __contains__(&self, key: &str) -> PyResult<bool> {
let versions = self.versions.read().unwrap();
Ok(versions.iter().any(|v| v.identifier == key))
}
fn keys(&self) -> PyResult<Vec<&'static str>> {
// Note that technically we should return a view here (that also acts
// like a Set *and* has a stable ordering). We don't depend on this, so
// for simplicity we just return a list of the keys.
let versions = self.versions.read().unwrap();
Ok(versions.iter().map(|v| v.identifier).collect())
}
fn values(&self) -> PyResult<Vec<RoomVersion>> {
let versions = self.versions.read().unwrap();
Ok(versions.clone())
}
fn items(&self) -> PyResult<Vec<(&'static str, RoomVersion)>> {
// Note that technically we should return a view here (that also acts
// like a Set *and* has a stable ordering). We don't depend on this, so
// for simplicity we just return a list of the items.
let versions = self.versions.read().unwrap();
Ok(versions.iter().map(|v| (v.identifier, *v)).collect())
}
#[pyo3(signature = (key, default=None))]
fn get<'py>(
&self,
py: Python<'py>,
key: Bound<'py, PyAny>,
default: Option<Bound<'py, PyAny>>,
) -> PyResult<Option<Bound<'py, PyAny>>> {
// We need to accept anything as the key, but we know that only strings
// are valid keys, so if it's not a string we just return the default.
let Ok(key) = key.extract::<&str>() else {
return Ok(default);
};
let versions = self.versions.read().unwrap();
if let Some(version) = versions.iter().find(|v| v.identifier == key).copied() {
return Ok(Some(version.into_bound_py_any(py)?));
}
Ok(default)
}
fn __len__(&self) -> PyResult<usize> {
let versions = self
.versions
.read()
.map_err(|_| PyRuntimeError::new_err("KnownRoomVersionsMapping lock poisoned"))?;
Ok(versions.len())
}
fn __iter__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyIterator>> {
let key_list = self.keys()?;
let bound_key_list = key_list.into_bound_py_any(py)?;
PyIterator::from_object(&bound_key_list)
}
}
impl<'py> IntoPyObject<'py> for &KnownRoomVersionsMapping {
type Target = KnownRoomVersionsMapping;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
self.clone().into_pyobject(py)
}
}
/// All room versions this instance knows about, used to build the
/// `KNOWN_ROOM_VERSIONS` dict.
///
/// Note: this is not necessarily all room versions, as we may not want to
/// support all experimental room versions.
static KNOWN_ROOM_VERSIONS: LazyLock<KnownRoomVersionsMapping> = LazyLock::new(|| {
let vec = vec![
ROOM_VERSION_V1,
ROOM_VERSION_V2,
ROOM_VERSION_V3,
ROOM_VERSION_V4,
ROOM_VERSION_V5,
ROOM_VERSION_V6,
ROOM_VERSION_V7,
ROOM_VERSION_V8,
ROOM_VERSION_V9,
ROOM_VERSION_V10,
ROOM_VERSION_V11,
ROOM_VERSION_V12,
ROOM_VERSION_MSC3757V10,
ROOM_VERSION_MSC3757V11,
ROOM_VERSION_HYDRA_V11,
];
KnownRoomVersionsMapping {
versions: Arc::new(RwLock::new(vec)),
}
});
/// Container class for room version constants.
///
/// This should contain all room versions that we know about.
#[pyclass(frozen)]
pub struct RoomVersions {}
#[pymethods]
#[allow(non_snake_case)]
impl RoomVersions {
#[classattr]
fn V1(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_V1.into_py_any(py)
}
#[classattr]
fn V2(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_V2.into_py_any(py)
}
#[classattr]
fn V3(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_V3.into_py_any(py)
}
#[classattr]
fn V4(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_V4.into_py_any(py)
}
#[classattr]
fn V5(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_V5.into_py_any(py)
}
#[classattr]
fn V6(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_V6.into_py_any(py)
}
#[classattr]
fn V7(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_V7.into_py_any(py)
}
#[classattr]
fn V8(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_V8.into_py_any(py)
}
#[classattr]
fn V9(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_V9.into_py_any(py)
}
#[classattr]
fn V10(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_V10.into_py_any(py)
}
#[classattr]
fn MSC1767v10(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_MSC1767V10.into_py_any(py)
}
#[classattr]
fn MSC3389v10(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_MSC3389V10.into_py_any(py)
}
#[classattr]
fn MSC3757v10(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_MSC3757V10.into_py_any(py)
}
#[classattr]
fn V11(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_V11.into_py_any(py)
}
#[classattr]
fn MSC3757v11(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_MSC3757V11.into_py_any(py)
}
#[classattr]
fn HydraV11(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_HYDRA_V11.into_py_any(py)
}
#[classattr]
fn V12(py: Python<'_>) -> PyResult<Py<PyAny>> {
ROOM_VERSION_V12.into_py_any(py)
}
}
/// Called when registering modules with python.
pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
let child_module = PyModule::new(py, "room_versions")?;
child_module.add_class::<EventFormatVersions>()?;
child_module.add_class::<StateResolutionVersions>()?;
child_module.add_class::<PushRuleRoomFlag>()?;
child_module.add_class::<RoomVersion>()?;
child_module.add_class::<RoomVersions>()?;
child_module.add_class::<KnownRoomVersionsMapping>()?;
child_module.add_class::<RoomDisposition>()?;
// Build KNOWN_EVENT_FORMAT_VERSIONS as a frozenset
let known_ef: [i32; 4] = [
EventFormatVersions::ROOM_V1_V2,
EventFormatVersions::ROOM_V3,
EventFormatVersions::ROOM_V4_PLUS,
EventFormatVersions::ROOM_V11_HYDRA_PLUS,
];
let known_event_format_versions = PyFrozenSet::new(py, known_ef)?;
child_module.add("KNOWN_EVENT_FORMAT_VERSIONS", known_event_format_versions)?;
child_module.add("KNOWN_ROOM_VERSIONS", &*KNOWN_ROOM_VERSIONS)?;
m.add_submodule(&child_module)?;
// Register in sys.modules
py.import("sys")?
.getattr("modules")?
.set_item("synapse.synapse_rust.room_versions", child_module)?;
Ok(())
}
+1 -1
View File
@@ -122,7 +122,7 @@ BOOLEAN_COLUMNS = {
"presence_stream": ["currently_active"],
"public_room_list_stream": ["visibility"],
"pushers": ["enabled"],
"redactions": ["have_censored"],
"redactions": ["have_censored", "recheck"],
"remote_media_cache": ["authenticated"],
"room_memberships": ["participant"],
"room_stats_state": ["is_federatable"],
+18 -476
View File
@@ -18,480 +18,22 @@
#
#
from synapse.synapse_rust.room_versions import (
KNOWN_EVENT_FORMAT_VERSIONS,
KNOWN_ROOM_VERSIONS,
EventFormatVersions,
PushRuleRoomFlag,
RoomVersion,
RoomVersions,
StateResolutionVersions,
)
import attr
class EventFormatVersions:
"""This is an internal enum for tracking the version of the event format,
independently of the room version.
To reduce confusion, the event format versions are named after the room
versions that they were used or introduced in.
The concept of an 'event format version' is specific to Synapse (the
specification does not mention this term.)
"""
ROOM_V1_V2 = 1 # $id:server event id format: used for room v1 and v2
ROOM_V3 = 2 # MSC1659-style $hash event id format: used for room v3
ROOM_V4_PLUS = 3 # MSC1884-style $hash format: introduced for room v4
ROOM_V11_HYDRA_PLUS = 4 # MSC4291 room IDs as hashes: introduced for room HydraV11
KNOWN_EVENT_FORMAT_VERSIONS = {
EventFormatVersions.ROOM_V1_V2,
EventFormatVersions.ROOM_V3,
EventFormatVersions.ROOM_V4_PLUS,
EventFormatVersions.ROOM_V11_HYDRA_PLUS,
}
class StateResolutionVersions:
"""Enum to identify the state resolution algorithms"""
V1 = 1 # room v1 state res
V2 = 2 # MSC1442 state res: room v2 and later
V2_1 = 3 # MSC4297 state res
class RoomDisposition:
STABLE = "stable"
UNSTABLE = "unstable"
class PushRuleRoomFlag:
"""Enum for listing possible MSC3931 room version feature flags, for push rules"""
# MSC3932: Room version supports MSC1767 Extensible Events.
EXTENSIBLE_EVENTS = "org.matrix.msc3932.extensible_events"
@attr.s(slots=True, frozen=True, auto_attribs=True)
class RoomVersion:
"""An object which describes the unique attributes of a room version."""
identifier: str # the identifier for this version
disposition: str # one of the RoomDispositions
event_format: int # one of the EventFormatVersions
state_res: int # one of the StateResolutionVersions
enforce_key_validity: bool
# Before MSC2432, m.room.aliases had special auth rules and redaction rules
special_case_aliases_auth: bool
# Strictly enforce canonicaljson, do not allow:
# * Integers outside the range of [-2 ^ 53 + 1, 2 ^ 53 - 1]
# * Floats
# * NaN, Infinity, -Infinity
strict_canonicaljson: bool
# MSC2209: Check 'notifications' key while verifying
# m.room.power_levels auth rules.
limit_notifications_power_levels: bool
# MSC3820: No longer include the creator in m.room.create events (room version 11)
implicit_room_creator: bool
# MSC3820: Apply updated redaction rules algorithm from room version 11
updated_redaction_rules: bool
# Support the 'restricted' join rule.
restricted_join_rule: bool
# Support for the proper redaction rules for the restricted join rule. This requires
# restricted_join_rule to be enabled.
restricted_join_rule_fix: bool
# Support the 'knock' join rule.
knock_join_rule: bool
# MSC3389: Protect relation information from redaction.
msc3389_relation_redactions: bool
# Support the 'knock_restricted' join rule.
knock_restricted_join_rule: bool
# Enforce integer power levels
enforce_int_power_levels: bool
# MSC3931: Adds a push rule condition for "room version feature flags", making
# some push rules room version dependent. Note that adding a flag to this list
# is not enough to mark it "supported": the push rule evaluator also needs to
# support the flag. Unknown flags are ignored by the evaluator, making conditions
# fail if used.
msc3931_push_features: tuple[str, ...] # values from PushRuleRoomFlag
# MSC3757: Restricting who can overwrite a state event
msc3757_enabled: bool
# MSC4289: Creator power enabled
msc4289_creator_power_enabled: bool
# MSC4291: Room IDs as hashes of the create event
msc4291_room_ids_as_hashes: bool
class RoomVersions:
V1 = RoomVersion(
"1",
RoomDisposition.STABLE,
EventFormatVersions.ROOM_V1_V2,
StateResolutionVersions.V1,
enforce_key_validity=False,
special_case_aliases_auth=True,
strict_canonicaljson=False,
limit_notifications_power_levels=False,
implicit_room_creator=False,
updated_redaction_rules=False,
restricted_join_rule=False,
restricted_join_rule_fix=False,
knock_join_rule=False,
msc3389_relation_redactions=False,
knock_restricted_join_rule=False,
enforce_int_power_levels=False,
msc3931_push_features=(),
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
)
V2 = RoomVersion(
"2",
RoomDisposition.STABLE,
EventFormatVersions.ROOM_V1_V2,
StateResolutionVersions.V2,
enforce_key_validity=False,
special_case_aliases_auth=True,
strict_canonicaljson=False,
limit_notifications_power_levels=False,
implicit_room_creator=False,
updated_redaction_rules=False,
restricted_join_rule=False,
restricted_join_rule_fix=False,
knock_join_rule=False,
msc3389_relation_redactions=False,
knock_restricted_join_rule=False,
enforce_int_power_levels=False,
msc3931_push_features=(),
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
)
V3 = RoomVersion(
"3",
RoomDisposition.STABLE,
EventFormatVersions.ROOM_V3,
StateResolutionVersions.V2,
enforce_key_validity=False,
special_case_aliases_auth=True,
strict_canonicaljson=False,
limit_notifications_power_levels=False,
implicit_room_creator=False,
updated_redaction_rules=False,
restricted_join_rule=False,
restricted_join_rule_fix=False,
knock_join_rule=False,
msc3389_relation_redactions=False,
knock_restricted_join_rule=False,
enforce_int_power_levels=False,
msc3931_push_features=(),
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
)
V4 = RoomVersion(
"4",
RoomDisposition.STABLE,
EventFormatVersions.ROOM_V4_PLUS,
StateResolutionVersions.V2,
enforce_key_validity=False,
special_case_aliases_auth=True,
strict_canonicaljson=False,
limit_notifications_power_levels=False,
implicit_room_creator=False,
updated_redaction_rules=False,
restricted_join_rule=False,
restricted_join_rule_fix=False,
knock_join_rule=False,
msc3389_relation_redactions=False,
knock_restricted_join_rule=False,
enforce_int_power_levels=False,
msc3931_push_features=(),
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
)
V5 = RoomVersion(
"5",
RoomDisposition.STABLE,
EventFormatVersions.ROOM_V4_PLUS,
StateResolutionVersions.V2,
enforce_key_validity=True,
special_case_aliases_auth=True,
strict_canonicaljson=False,
limit_notifications_power_levels=False,
implicit_room_creator=False,
updated_redaction_rules=False,
restricted_join_rule=False,
restricted_join_rule_fix=False,
knock_join_rule=False,
msc3389_relation_redactions=False,
knock_restricted_join_rule=False,
enforce_int_power_levels=False,
msc3931_push_features=(),
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
)
V6 = RoomVersion(
"6",
RoomDisposition.STABLE,
EventFormatVersions.ROOM_V4_PLUS,
StateResolutionVersions.V2,
enforce_key_validity=True,
special_case_aliases_auth=False,
strict_canonicaljson=True,
limit_notifications_power_levels=True,
implicit_room_creator=False,
updated_redaction_rules=False,
restricted_join_rule=False,
restricted_join_rule_fix=False,
knock_join_rule=False,
msc3389_relation_redactions=False,
knock_restricted_join_rule=False,
enforce_int_power_levels=False,
msc3931_push_features=(),
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
)
V7 = RoomVersion(
"7",
RoomDisposition.STABLE,
EventFormatVersions.ROOM_V4_PLUS,
StateResolutionVersions.V2,
enforce_key_validity=True,
special_case_aliases_auth=False,
strict_canonicaljson=True,
limit_notifications_power_levels=True,
implicit_room_creator=False,
updated_redaction_rules=False,
restricted_join_rule=False,
restricted_join_rule_fix=False,
knock_join_rule=True,
msc3389_relation_redactions=False,
knock_restricted_join_rule=False,
enforce_int_power_levels=False,
msc3931_push_features=(),
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
)
V8 = RoomVersion(
"8",
RoomDisposition.STABLE,
EventFormatVersions.ROOM_V4_PLUS,
StateResolutionVersions.V2,
enforce_key_validity=True,
special_case_aliases_auth=False,
strict_canonicaljson=True,
limit_notifications_power_levels=True,
implicit_room_creator=False,
updated_redaction_rules=False,
restricted_join_rule=True,
restricted_join_rule_fix=False,
knock_join_rule=True,
msc3389_relation_redactions=False,
knock_restricted_join_rule=False,
enforce_int_power_levels=False,
msc3931_push_features=(),
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
)
V9 = RoomVersion(
"9",
RoomDisposition.STABLE,
EventFormatVersions.ROOM_V4_PLUS,
StateResolutionVersions.V2,
enforce_key_validity=True,
special_case_aliases_auth=False,
strict_canonicaljson=True,
limit_notifications_power_levels=True,
implicit_room_creator=False,
updated_redaction_rules=False,
restricted_join_rule=True,
restricted_join_rule_fix=True,
knock_join_rule=True,
msc3389_relation_redactions=False,
knock_restricted_join_rule=False,
enforce_int_power_levels=False,
msc3931_push_features=(),
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
)
V10 = RoomVersion(
"10",
RoomDisposition.STABLE,
EventFormatVersions.ROOM_V4_PLUS,
StateResolutionVersions.V2,
enforce_key_validity=True,
special_case_aliases_auth=False,
strict_canonicaljson=True,
limit_notifications_power_levels=True,
implicit_room_creator=False,
updated_redaction_rules=False,
restricted_join_rule=True,
restricted_join_rule_fix=True,
knock_join_rule=True,
msc3389_relation_redactions=False,
knock_restricted_join_rule=True,
enforce_int_power_levels=True,
msc3931_push_features=(),
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
)
MSC1767v10 = RoomVersion(
# MSC1767 (Extensible Events) based on room version "10"
"org.matrix.msc1767.10",
RoomDisposition.UNSTABLE,
EventFormatVersions.ROOM_V4_PLUS,
StateResolutionVersions.V2,
enforce_key_validity=True,
special_case_aliases_auth=False,
strict_canonicaljson=True,
limit_notifications_power_levels=True,
implicit_room_creator=False,
updated_redaction_rules=False,
restricted_join_rule=True,
restricted_join_rule_fix=True,
knock_join_rule=True,
msc3389_relation_redactions=False,
knock_restricted_join_rule=True,
enforce_int_power_levels=True,
msc3931_push_features=(PushRuleRoomFlag.EXTENSIBLE_EVENTS,),
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
)
MSC3757v10 = RoomVersion(
# MSC3757 (Restricting who can overwrite a state event) based on room version "10"
"org.matrix.msc3757.10",
RoomDisposition.UNSTABLE,
EventFormatVersions.ROOM_V4_PLUS,
StateResolutionVersions.V2,
enforce_key_validity=True,
special_case_aliases_auth=False,
strict_canonicaljson=True,
limit_notifications_power_levels=True,
implicit_room_creator=False,
updated_redaction_rules=False,
restricted_join_rule=True,
restricted_join_rule_fix=True,
knock_join_rule=True,
msc3389_relation_redactions=False,
knock_restricted_join_rule=True,
enforce_int_power_levels=True,
msc3931_push_features=(),
msc3757_enabled=True,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
)
V11 = RoomVersion(
"11",
RoomDisposition.STABLE,
EventFormatVersions.ROOM_V4_PLUS,
StateResolutionVersions.V2,
enforce_key_validity=True,
special_case_aliases_auth=False,
strict_canonicaljson=True,
limit_notifications_power_levels=True,
implicit_room_creator=True, # Used by MSC3820
updated_redaction_rules=True, # Used by MSC3820
restricted_join_rule=True,
restricted_join_rule_fix=True,
knock_join_rule=True,
msc3389_relation_redactions=False,
knock_restricted_join_rule=True,
enforce_int_power_levels=True,
msc3931_push_features=(),
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
)
MSC3757v11 = RoomVersion(
# MSC3757 (Restricting who can overwrite a state event) based on room version "11"
"org.matrix.msc3757.11",
RoomDisposition.UNSTABLE,
EventFormatVersions.ROOM_V4_PLUS,
StateResolutionVersions.V2,
enforce_key_validity=True,
special_case_aliases_auth=False,
strict_canonicaljson=True,
limit_notifications_power_levels=True,
implicit_room_creator=True, # Used by MSC3820
updated_redaction_rules=True, # Used by MSC3820
restricted_join_rule=True,
restricted_join_rule_fix=True,
knock_join_rule=True,
msc3389_relation_redactions=False,
knock_restricted_join_rule=True,
enforce_int_power_levels=True,
msc3931_push_features=(),
msc3757_enabled=True,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
)
HydraV11 = RoomVersion(
"org.matrix.hydra.11",
RoomDisposition.UNSTABLE,
EventFormatVersions.ROOM_V11_HYDRA_PLUS,
StateResolutionVersions.V2_1, # Changed from v11
enforce_key_validity=True,
special_case_aliases_auth=False,
strict_canonicaljson=True,
limit_notifications_power_levels=True,
implicit_room_creator=True, # Used by MSC3820
updated_redaction_rules=True, # Used by MSC3820
restricted_join_rule=True,
restricted_join_rule_fix=True,
knock_join_rule=True,
msc3389_relation_redactions=False,
knock_restricted_join_rule=True,
enforce_int_power_levels=True,
msc3931_push_features=(),
msc3757_enabled=False,
msc4289_creator_power_enabled=True, # Changed from v11
msc4291_room_ids_as_hashes=True, # Changed from v11
)
V12 = RoomVersion(
"12",
RoomDisposition.STABLE,
EventFormatVersions.ROOM_V11_HYDRA_PLUS,
StateResolutionVersions.V2_1, # Changed from v11
enforce_key_validity=True,
special_case_aliases_auth=False,
strict_canonicaljson=True,
limit_notifications_power_levels=True,
implicit_room_creator=True, # Used by MSC3820
updated_redaction_rules=True, # Used by MSC3820
restricted_join_rule=True,
restricted_join_rule_fix=True,
knock_join_rule=True,
msc3389_relation_redactions=False,
knock_restricted_join_rule=True,
enforce_int_power_levels=True,
msc3931_push_features=(),
msc3757_enabled=False,
msc4289_creator_power_enabled=True, # Changed from v11
msc4291_room_ids_as_hashes=True, # Changed from v11
)
KNOWN_ROOM_VERSIONS: dict[str, RoomVersion] = {
v.identifier: v
for v in (
RoomVersions.V1,
RoomVersions.V2,
RoomVersions.V3,
RoomVersions.V4,
RoomVersions.V5,
RoomVersions.V6,
RoomVersions.V7,
RoomVersions.V8,
RoomVersions.V9,
RoomVersions.V10,
RoomVersions.V11,
RoomVersions.V12,
RoomVersions.MSC3757v10,
RoomVersions.MSC3757v11,
RoomVersions.HydraV11,
)
}
__all__ = [
"EventFormatVersions",
"KNOWN_EVENT_FORMAT_VERSIONS",
"KNOWN_ROOM_VERSIONS",
"PushRuleRoomFlag",
"RoomVersion",
"RoomVersions",
"StateResolutionVersions",
]
+21 -27
View File
@@ -32,7 +32,7 @@ from typing import (
from prometheus_client import Counter
from typing_extensions import ParamSpec, TypeGuard
from synapse.api.constants import EventTypes, Membership, ThirdPartyEntityKind
from synapse.api.constants import ThirdPartyEntityKind
from synapse.api.errors import CodeMessageException, HttpResponseException
from synapse.appservice import (
ApplicationService,
@@ -40,7 +40,7 @@ from synapse.appservice import (
TransactionUnusedFallbackKeys,
)
from synapse.events import EventBase
from synapse.events.utils import SerializeEventConfig, serialize_event
from synapse.events.utils import SerializeEventConfig
from synapse.http.client import SimpleHttpClient, is_unknown_endpoint
from synapse.logging import opentracing
from synapse.metrics import SERVER_NAME_LABEL
@@ -128,6 +128,7 @@ class ApplicationServiceApi(SimpleHttpClient):
self.server_name = hs.hostname
self.clock = hs.get_clock()
self.config = hs.config.appservice
self._event_serializer = hs.get_event_client_serializer()
self.protocol_meta_cache: ResponseCache[tuple[str, str]] = ResponseCache(
clock=hs.get_clock(),
@@ -343,7 +344,7 @@ class ApplicationServiceApi(SimpleHttpClient):
# This is required by the configuration.
assert service.hs_token is not None
serialized_events = self._serialize(service, events)
serialized_events = await self._serialize(service, events)
if txn_id is None:
logger.warning(
@@ -539,30 +540,23 @@ class ApplicationServiceApi(SimpleHttpClient):
return response
def _serialize(
async def _serialize(
self, service: "ApplicationService", events: Iterable[EventBase]
) -> list[JsonDict]:
time_now = self.clock.time_msec()
return [
serialize_event(
e,
time_now,
config=SerializeEventConfig(
as_client_event=True,
# If this is an invite or a knock membership event, and we're interested
# in this user, then include any stripped state alongside the event.
include_stripped_room_state=(
e.type == EventTypes.Member
and (
e.membership == Membership.INVITE
or e.membership == Membership.KNOCK
)
and service.is_interested_in_user(e.state_key)
),
# Appservices are considered 'trusted' by the admin and should have
# applicable metadata on their events.
include_admin_metadata=True,
),
)
for e in events
]
return await self._event_serializer.serialize_events(
list(events),
time_now,
config=SerializeEventConfig(
as_client_event=True,
# If this is an invite or a knock membership event, then include
# any stripped state alongside the event. We could narrow this
# down to only users the appservice is "interested in", however
# it's not worth the complexity of doing so, and it's simpler to
# just include it for all users.
include_stripped_room_state=True,
# Appservices are considered 'trusted' by the admin and should have
# applicable metadata on their events.
include_admin_metadata=True,
),
)
+1 -2
View File
@@ -477,8 +477,7 @@ class ExperimentalConfig(Config):
self.msc1767_enabled: bool = experimental.get("msc1767_enabled", False)
if self.msc1767_enabled:
# Enable room version (and thus applicable push rules from MSC3931/3932)
version_id = RoomVersions.MSC1767v10.identifier
KNOWN_ROOM_VERSIONS[version_id] = RoomVersions.MSC1767v10
KNOWN_ROOM_VERSIONS.add_room_version(RoomVersions.MSC1767v10)
# MSC3391: Removing account data.
self.msc3391_enabled = experimental.get("msc3391_enabled", False)
+1 -23
View File
@@ -60,7 +60,6 @@ from synapse.api.room_versions import (
KNOWN_ROOM_VERSIONS,
EventFormatVersions,
RoomVersion,
RoomVersions,
)
from synapse.events import is_creator
from synapse.state import CREATE_KEY
@@ -383,25 +382,6 @@ def check_state_dependent_auth_rules(
logger.debug("Allowing! %s", event)
# Set of room versions where Synapse did not apply event key size limits
# in bytes, but rather in codepoints.
# In these room versions, we are more lenient with event size validation.
LENIENT_EVENT_BYTE_LIMITS_ROOM_VERSIONS = {
RoomVersions.V1,
RoomVersions.V2,
RoomVersions.V3,
RoomVersions.V4,
RoomVersions.V5,
RoomVersions.V6,
RoomVersions.V7,
RoomVersions.V8,
RoomVersions.V9,
RoomVersions.V10,
RoomVersions.MSC1767v10,
RoomVersions.MSC3757v10,
}
def _check_size_limits(event: "EventBase") -> None:
"""
Checks the size limits in a PDU.
@@ -440,9 +420,7 @@ def _check_size_limits(event: "EventBase") -> None:
if len(event.event_id) > 255:
raise EventSizeError("'event_id' too large", unpersistable=True)
strict_byte_limits = (
event.room_version not in LENIENT_EVENT_BYTE_LIMITS_ROOM_VERSIONS
)
strict_byte_limits = event.room_version.strict_event_byte_limits_room_versions
# Byte size check: if these fail, then be lenient to avoid breaking rooms.
if len(event.user_id.encode("utf-8")) > 255:
+63 -18
View File
@@ -88,6 +88,7 @@ def prune_event(event: EventBase) -> EventBase:
)
pruned_event.internal_metadata.instance_name = event.internal_metadata.instance_name
pruned_event.internal_metadata.outlier = event.internal_metadata.outlier
pruned_event.internal_metadata.redacted_by = event.internal_metadata.redacted_by
# Mark the event as redacted
pruned_event.internal_metadata.redacted = True
@@ -123,6 +124,7 @@ def clone_event(event: EventBase) -> EventBase:
)
new_event.internal_metadata.instance_name = event.internal_metadata.instance_name
new_event.internal_metadata.outlier = event.internal_metadata.outlier
new_event.internal_metadata.redacted_by = event.internal_metadata.redacted_by
return new_event
@@ -423,7 +425,7 @@ class SerializeEventConfig:
# the transaction_id and delay_id in the unsigned section of the event.
requester: Requester | None = None
# List of event fields to include. If empty, all fields will be returned.
only_event_fields: list[str] | None = None
only_event_fields: list[str] | None = attr.ib(default=None)
# Some events can have stripped room state stored in the `unsigned` field.
# This is required for invite and knock functionality. If this option is
# False, that state will be removed from the event before it is returned.
@@ -434,6 +436,16 @@ class SerializeEventConfig:
# whether an event was soft failed by the server.
include_admin_metadata: bool = False
@only_event_fields.validator
def _validate_only_event_fields(
self, attribute: attr.Attribute, value: Any
) -> None:
if value is None:
return
if not isinstance(value, list) or not all(isinstance(f, str) for f in value):
raise TypeError("only_event_fields must be a list of strings")
_DEFAULT_SERIALIZE_EVENT_CONFIG = SerializeEventConfig()
@@ -444,7 +456,7 @@ def make_config_for_admin(existing: SerializeEventConfig) -> SerializeEventConfi
return attr.evolve(existing, include_admin_metadata=True)
def serialize_event(
def _serialize_event(
e: JsonDict | EventBase,
time_now_ms: int,
*,
@@ -476,13 +488,6 @@ def serialize_event(
d["unsigned"]["age"] = time_now_ms - d["unsigned"]["age_ts"]
del d["unsigned"]["age_ts"]
if "redacted_because" in e.unsigned:
d["unsigned"]["redacted_because"] = serialize_event(
e.unsigned["redacted_because"],
time_now_ms,
config=config,
)
# If we have applicable fields saved in the internal_metadata, include them in the
# unsigned section of the event if the event was sent by the same session (or when
# appropriate, just the same sender) as the one requesting the event.
@@ -559,14 +564,6 @@ def serialize_event(
if e.internal_metadata.policy_server_spammy:
d["unsigned"]["io.element.synapse.policy_server_spammy"] = True
only_event_fields = config.only_event_fields
if only_event_fields:
if not isinstance(only_event_fields, list) or not all(
isinstance(f, str) for f in only_event_fields
):
raise TypeError("only_event_fields must be a list of strings")
d = only_fields(d, only_event_fields)
return d
@@ -591,6 +588,7 @@ class EventClientSerializer:
*,
config: SerializeEventConfig = _DEFAULT_SERIALIZE_EVENT_CONFIG,
bundle_aggregations: dict[str, "BundledAggregations"] | None = None,
redaction_map: Mapping[str, "EventBase"] | None = None,
) -> JsonDict:
"""Serializes a single event.
@@ -600,6 +598,8 @@ class EventClientSerializer:
config: Event serialization config
bundle_aggregations: A map from event_id to the aggregations to be bundled
into the event.
redaction_map: Optional pre-fetched map from redaction event_id to event,
used to avoid per-event DB lookups when serializing many events.
Returns:
The serialized event
@@ -617,7 +617,34 @@ class EventClientSerializer:
):
config = make_config_for_admin(config)
serialized_event = serialize_event(event, time_now, config=config)
serialized_event = _serialize_event(event, time_now, config=config)
# If the event was redacted, fetch the redaction event from the database
# and include it in the serialized event's unsigned section.
redacted_by: str | None = event.internal_metadata.redacted_by
if redacted_by is not None:
serialized_event.setdefault("unsigned", {})["redacted_by"] = redacted_by
if redaction_map is not None:
redaction_event: EventBase | None = redaction_map.get(redacted_by)
else:
redaction_event = await self._store.get_event(
redacted_by,
allow_none=True,
)
if redaction_event is not None:
serialized_redaction = _serialize_event(
redaction_event, time_now, config=config
)
serialized_event.setdefault("unsigned", {})["redacted_because"] = (
serialized_redaction
)
# format_event_for_client_v1 copies redacted_because to the
# top level, but since we add it after that runs, do it here.
if (
config.as_client_event
and config.event_format is format_event_for_client_v1
):
serialized_event["redacted_because"] = serialized_redaction
new_unsigned = {}
for callback in self._add_extra_fields_to_unsigned_client_event_callbacks:
@@ -630,6 +657,13 @@ class EventClientSerializer:
new_unsigned.update(serialized_event["unsigned"])
serialized_event["unsigned"] = new_unsigned
# Only include fields that the client has requested.
#
# Note: we always return bundled aggregations, though it is unclear why.
only_event_fields = config.only_event_fields
if only_event_fields:
serialized_event = only_fields(serialized_event, only_event_fields)
# Check if there are any bundled aggregations to include with the event.
if bundle_aggregations:
if event.event_id in bundle_aggregations:
@@ -745,12 +779,23 @@ class EventClientSerializer:
str(len(events)),
)
# Batch-fetch all redaction events in one go rather than one per event.
redaction_ids = {
e.internal_metadata.redacted_by
for e in events
if isinstance(e, EventBase) and e.internal_metadata.redacted_by is not None
}
redaction_map = (
await self._store.get_events(redaction_ids) if redaction_ids else {}
)
return [
await self.serialize_event(
event,
time_now,
config=config,
bundle_aggregations=bundle_aggregations,
redaction_map=redaction_map,
)
for event in events
]
+6 -2
View File
@@ -5,7 +5,6 @@ from synapse.api.errors import NotFoundError
from synapse.events.utils import (
SerializeEventConfig,
format_event_raw,
serialize_event,
)
from synapse.http.servlet import RestServlet
from synapse.http.site import SynapseRequest
@@ -40,6 +39,7 @@ class EventRestServlet(RestServlet):
self._auth = hs.get_auth()
self._store = hs.get_datastores().main
self._clock = hs.get_clock()
self._event_serializer = hs.get_event_client_serializer()
async def on_GET(
self, request: SynapseRequest, event_id: str
@@ -64,6 +64,10 @@ class EventRestServlet(RestServlet):
include_stripped_room_state=True,
include_admin_metadata=True,
)
res = {"event": serialize_event(event, self._clock.time_msec(), config=config)}
res = {
"event": await self._event_serializer.serialize_event(
event, self._clock.time_msec(), config=config
)
}
return HTTPStatus.OK, res
+2 -2
View File
@@ -55,7 +55,6 @@ from synapse.events.utils import (
EventClientSerializer,
SerializeEventConfig,
format_event_for_client_v2,
serialize_event,
)
from synapse.handlers.pagination import GetMessagesResult
from synapse.http.server import HttpServer
@@ -221,6 +220,7 @@ class RoomStateEventRestServlet(RestServlet):
self.delayed_events_handler = hs.get_delayed_events_handler()
self.auth = hs.get_auth()
self.clock = hs.get_clock()
self._event_serializer = hs.get_event_client_serializer()
self._max_event_delay_ms = hs.config.server.max_event_delay_ms
self._spam_checker_module_callbacks = hs.get_module_api_callbacks().spam_checker
self._msc4354_enabled = hs.config.experimental.msc4354_enabled
@@ -292,7 +292,7 @@ class RoomStateEventRestServlet(RestServlet):
raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND)
if format == "event":
event = serialize_event(
event = await self._event_serializer.serialize_event(
data,
self.clock.time_msec(),
config=SerializeEventConfig(
+1
View File
@@ -2944,6 +2944,7 @@ class PersistEventsStore:
values={
"redacts": event.redacts,
"received_ts": self._clock.time_msec(),
"recheck": event.internal_metadata.need_to_check_redaction(),
},
)
@@ -159,6 +159,11 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
"redactions_received_ts", self._redactions_received_ts
)
self.db_pool.updates.register_background_update_handler(
_BackgroundUpdates.REDACTIONS_RECHECK_BG_UPDATE,
self._redactions_recheck_bg_update,
)
# This index gets deleted in `event_fix_redactions_bytes` update
self.db_pool.updates.register_background_index_update(
"event_fix_redactions_bytes_create_index",
@@ -747,6 +752,66 @@ class EventsBackgroundUpdatesStore(StreamWorkerStore, StateDeltasStore, SQLBaseS
return count
async def _redactions_recheck_bg_update(
self, progress: JsonDict, batch_size: int
) -> int:
"""Fills in the `recheck` column of the `redactions` table based on
the `recheck_redaction` field in each event's internal metadata."""
last_event_id = progress.get("last_event_id", "")
def _txn(txn: LoggingTransaction) -> int:
sql = """
SELECT r.event_id, ej.internal_metadata
FROM redactions AS r
LEFT JOIN event_json AS ej USING (event_id)
WHERE r.event_id > ?
ORDER BY r.event_id ASC
LIMIT ?
"""
txn.execute(sql, (last_event_id, batch_size))
rows = txn.fetchall()
if not rows:
return 0
updates = []
for event_id, internal_metadata_json in rows:
if internal_metadata_json is not None:
internal_metadata = db_to_json(internal_metadata_json)
recheck = bool(internal_metadata.get("recheck_redaction", False))
else:
recheck = False
if not recheck:
# Column defaults to true, so we only need to update rows
# where recheck should be false.
updates.append((event_id, recheck))
self.db_pool.simple_update_many_txn(
txn,
table="redactions",
key_names=("event_id",),
key_values=[(event_id,) for event_id, _ in updates],
value_names=("recheck",),
value_values=[(recheck,) for _, recheck in updates],
)
upper_event_id = rows[-1][0]
self.db_pool.updates._background_update_progress_txn(
txn,
_BackgroundUpdates.REDACTIONS_RECHECK_BG_UPDATE,
{"last_event_id": upper_event_id},
)
return len(rows)
count = await self.db_pool.runInteraction("_redactions_recheck_bg_update", _txn)
if not count:
await self.db_pool.updates._end_background_update(
_BackgroundUpdates.REDACTIONS_RECHECK_BG_UPDATE
)
return count
async def _event_fix_redactions_bytes(
self, progress: JsonDict, batch_size: int
) -> int:
+46 -21
View File
@@ -182,7 +182,11 @@ class _EventRow:
rejected_reason: if the event was rejected, the reason why.
redactions: a list of event-ids which (claim to) redact this event.
unconfirmed_redactions: a list of event-ids which (claim to) redact this event
and need to be rechecked.
confirmed_redactions: a list of event-ids which redact this event and have been
confirmed as valid redactions.
outlier: True if this event is an outlier.
"""
@@ -195,7 +199,8 @@ class _EventRow:
format_version: int | None
room_version_id: str | None
rejected_reason: str | None
redactions: list[str]
unconfirmed_redactions: list[str]
confirmed_redactions: list[str]
outlier: bool
@@ -1390,14 +1395,20 @@ class EventsWorkerStore(SQLBaseStore):
)
row_map = await self._enqueue_events(event_ids_to_fetch)
# we need to recursively fetch any redactions of those events
# we need to recursively fetch redaction events that require
# rechecking, so we can validate them
redaction_ids: set[str] = set()
for event_id in event_ids_to_fetch:
row = row_map.get(event_id)
fetched_event_ids.add(event_id)
if row:
fetched_events[event_id] = row
redaction_ids.update(row.redactions)
# If this event only has unconfirmed redactions we fetch
# them from the DB so that we check them to see if any are
# valid.
if not row.confirmed_redactions:
redaction_ids.update(row.unconfirmed_redactions)
event_ids_to_fetch = redaction_ids.difference(fetched_event_ids)
return event_ids_to_fetch
@@ -1541,9 +1552,12 @@ class EventsWorkerStore(SQLBaseStore):
# the cache entries.
result_map: dict[str, EventCacheEntry] = {}
for event_id, original_ev in event_map.items():
redactions = fetched_events[event_id].redactions
row = fetched_events[event_id]
redacted_event = self._maybe_redact_event_row(
original_ev, redactions, event_map
original_ev,
row.unconfirmed_redactions,
row.confirmed_redactions,
event_map,
)
cache_entry = EventCacheEntry(
@@ -1637,21 +1651,25 @@ class EventsWorkerStore(SQLBaseStore):
format_version=row[5],
room_version_id=row[6],
rejected_reason=row[7],
redactions=[],
unconfirmed_redactions=[],
confirmed_redactions=[],
outlier=bool(row[8]), # This is an int in SQLite3
)
# check for redactions
redactions_sql = "SELECT event_id, redacts FROM redactions WHERE "
redactions_sql = "SELECT event_id, redacts, recheck FROM redactions WHERE "
clause, args = make_in_list_sql_clause(txn.database_engine, "redacts", evs)
txn.execute(redactions_sql + clause, args)
for redacter, redacted in txn:
for redacter, redacted, recheck in txn:
d = event_dict.get(redacted)
if d:
d.redactions.append(redacter)
if recheck:
d.unconfirmed_redactions.append(redacter)
else:
d.confirmed_redactions.append(redacter)
# check for MSC4293 redactions
to_check = []
@@ -1700,24 +1718,28 @@ class EventsWorkerStore(SQLBaseStore):
# backfilled events, as they have a negative stream ordering
if e_row.stream_ordering >= redact_end_ordering:
continue
e_row.redactions.append(redacting_event_id)
e_row.unconfirmed_redactions.append(redacting_event_id)
return event_dict
def _maybe_redact_event_row(
self,
original_ev: EventBase,
redactions: Iterable[str],
unconfirmed_redactions: Iterable[str],
confirmed_redactions: Iterable[str],
event_map: dict[str, EventBase],
) -> EventBase | None:
"""Given an event object and a list of possible redacting event ids,
"""Given an event object and lists of possible redacting event ids,
determine whether to honour any of those redactions and if so return a redacted
event.
Args:
original_ev: The original event.
redactions: list of event ids of potential redaction events
unconfirmed_redactions: list of event ids of redaction events that need
domain rechecking (room v3+).
confirmed_redactions: list of event ids of redaction events that have
already been validated and do not need rechecking.
event_map: other events which have been fetched, in which we can
look up the redaaction events. Map from event id to event.
look up the redaction events. Map from event id to event.
Returns:
If the event should be redacted, a pruned event object. Otherwise, None.
@@ -1726,7 +1748,12 @@ class EventsWorkerStore(SQLBaseStore):
# we choose to ignore redactions of m.room.create events.
return None
for redaction_id in redactions:
for redaction_id in confirmed_redactions:
redacted_event = prune_event(original_ev)
redacted_event.internal_metadata.redacted_by = redaction_id
return redacted_event
for redaction_id in unconfirmed_redactions:
redaction_event = event_map.get(redaction_id)
if not redaction_event or redaction_event.rejected_reason:
# we don't have the redaction event, or the redaction event was not
@@ -1767,12 +1794,10 @@ class EventsWorkerStore(SQLBaseStore):
# we found a good redaction event. Redact!
redacted_event = prune_event(original_ev)
redacted_event.unsigned["redacted_by"] = redaction_id
# It's fine to add the event directly, since get_pdu_json
# will serialise this field correctly
redacted_event.unsigned["redacted_because"] = redaction_event
redacted_event.internal_metadata.redacted_by = redaction_id
# Note: The `redacted_because` field will later be populated by
# `EventClientSerializer.serialize_event`.
return redacted_event
# no valid redaction found for this event
+4 -1
View File
@@ -19,7 +19,7 @@
#
#
SCHEMA_VERSION = 93 # remember to update the list below when updating
SCHEMA_VERSION = 94 # remember to update the list below when updating
"""Represents the expectations made by the codebase about the database schema
This should be incremented whenever the codebase changes its requirements on the
@@ -171,6 +171,9 @@ Changes in SCHEMA_VERSION = 92
Changes in SCHEMA_VERSION = 93
- MSC4140: Set delayed events to be uniquely identifiable by their delay ID.
Changes in SCHEMA_VERSION = 94
- Add `recheck` column (boolean, default true) to the `redactions` table.
"""
@@ -0,0 +1,15 @@
--
-- This file is licensed under the Affero General Public License (AGPL) version 3.
--
-- Copyright (C) 2026 Element Creations, Ltd
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as
-- published by the Free Software Foundation, either version 3 of the
-- License, or (at your option) any later version.
--
-- See the GNU Affero General Public License for more details:
-- <https://www.gnu.org/licenses/agpl-3.0.html>.
ALTER TABLE redactions ADD COLUMN recheck boolean NOT NULL DEFAULT true;
@@ -0,0 +1,15 @@
--
-- This file is licensed under the Affero General Public License (AGPL) version 3.
--
-- Copyright (C) 2026 Element Creations, Ltd
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as
-- published by the Free Software Foundation, either version 3 of the
-- License, or (at your option) any later version.
--
-- See the GNU Affero General Public License for more details:
-- <https://www.gnu.org/licenses/agpl-3.0.html>.
INSERT INTO background_updates (ordering, update_name, progress_json) VALUES
(9402, 'redactions_recheck', '{}');
+2
View File
@@ -21,6 +21,8 @@ class EventInternalMetadata:
"""the stream ordering of this event. None, until it has been persisted."""
instance_name: str | None
"""the instance name of the server that persisted this event. None, until it has been persisted."""
redacted_by: str | None
"""the event ID of the redaction event, if this event has been redacted. Set dynamically at load time, not persisted."""
outlier: bool
"""whether this event is an outlier (ie, whether we have the state at that
+1 -1
View File
@@ -65,7 +65,7 @@ class PushRuleEvaluator:
notification_power_levels: Mapping[str, int],
related_events_flattened: Mapping[str, Mapping[str, JsonValue]],
related_event_match_enabled: bool,
room_version_feature_flags: tuple[str, ...],
room_version_feature_flags: list[str],
msc3931_enabled: bool,
msc4210_enabled: bool,
msc4306_enabled: bool,
+142
View File
@@ -0,0 +1,142 @@
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2026 Element Creations Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.
from collections.abc import Mapping
from typing import Iterator
class EventFormatVersions:
"""Internal enum for tracking the version of the event format,
independently of the room version.
To reduce confusion, the event format versions are named after the room
versions that they were used or introduced in.
The concept of an 'event format version' is specific to Synapse (the
specification does not mention this term.)
"""
ROOM_V1_V2: int
""":server event id format: used for room v1 and v2"""
ROOM_V3: int
"""MSC1659-style event id format: used for room v3"""
ROOM_V4_PLUS: int
"""MSC1884-style format: introduced for room v4"""
ROOM_V11_HYDRA_PLUS: int
"""MSC4291 room IDs as hashes: introduced for room HydraV11"""
KNOWN_EVENT_FORMAT_VERSIONS: frozenset[int]
class StateResolutionVersions:
"""Enum to identify the state resolution algorithms."""
V1: int
"""Room v1 state res"""
V2: int
"""MSC1442 state res: room v2 and later"""
V2_1: int
"""MSC4297 state res"""
class RoomDisposition:
"""Room disposition constants."""
STABLE: str
UNSTABLE: str
class PushRuleRoomFlag:
"""Enum for listing possible MSC3931 room version feature flags, for push rules."""
EXTENSIBLE_EVENTS: str
"""MSC3932: Room version supports MSC1767 Extensible Events."""
class RoomVersion:
"""An object which describes the unique attributes of a room version."""
identifier: str
"""The identifier for this version."""
disposition: str
"""One of the RoomDisposition constants."""
event_format: int
"""One of the EventFormatVersions constants."""
state_res: int
"""One of the StateResolutionVersions constants."""
enforce_key_validity: bool
special_case_aliases_auth: bool
"""Before MSC2432, m.room.aliases had special auth rules and redaction rules."""
strict_canonicaljson: bool
"""Strictly enforce canonicaljson, do not allow:
* Integers outside the range of [-2^53 + 1, 2^53 - 1]
* Floats
* NaN, Infinity, -Infinity
"""
limit_notifications_power_levels: bool
"""MSC2209: Check 'notifications' key while verifying
m.room.power_levels auth rules."""
implicit_room_creator: bool
"""MSC3820: No longer include the creator in m.room.create events (room version 11)."""
updated_redaction_rules: bool
"""MSC3820: Apply updated redaction rules algorithm from room version 11."""
restricted_join_rule: bool
"""Support the 'restricted' join rule."""
restricted_join_rule_fix: bool
"""Support for the proper redaction rules for the restricted join rule.
This requires restricted_join_rule to be enabled."""
knock_join_rule: bool
"""Support the 'knock' join rule."""
msc3389_relation_redactions: bool
"""MSC3389: Protect relation information from redaction."""
knock_restricted_join_rule: bool
"""Support the 'knock_restricted' join rule."""
enforce_int_power_levels: bool
"""Enforce integer power levels."""
msc3931_push_features: list[str]
"""MSC3931: Adds a push rule condition for "room version feature flags", making
some push rules room version dependent. Note that adding a flag to this list
is not enough to mark it "supported": the push rule evaluator also needs to
support the flag. Unknown flags are ignored by the evaluator, making conditions
fail if used. Values from PushRuleRoomFlag."""
msc3757_enabled: bool
"""MSC3757: Restricting who can overwrite a state event."""
msc4289_creator_power_enabled: bool
"""MSC4289: Creator power enabled."""
msc4291_room_ids_as_hashes: bool
"""MSC4291: Room IDs as hashes of the create event."""
strict_event_byte_limits_room_versions: bool
"""Whether this room version strictly enforces event key size limits in bytes,
rather than in codepoints.
If true, this room version uses stricter event size validation."""
class RoomVersions:
V1: RoomVersion
V2: RoomVersion
V3: RoomVersion
V4: RoomVersion
V5: RoomVersion
V6: RoomVersion
V7: RoomVersion
V8: RoomVersion
V9: RoomVersion
V10: RoomVersion
MSC1767v10: RoomVersion
MSC3389v10: RoomVersion
MSC3757v10: RoomVersion
V11: RoomVersion
MSC3757v11: RoomVersion
HydraV11: RoomVersion
V12: RoomVersion
class KnownRoomVersionsMapping(Mapping[str, RoomVersion]):
def add_room_version(self, room_version: RoomVersion) -> None: ...
def __getitem__(self, key: str) -> RoomVersion: ...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[str]: ...
KNOWN_ROOM_VERSIONS: KnownRoomVersionsMapping
+2
View File
@@ -64,3 +64,5 @@ class _BackgroundUpdates:
)
FIXUP_MAX_DEPTH_CAP = "fixup_max_depth_cap"
REDACTIONS_RECHECK_BG_UPDATE = "redactions_recheck"
+72 -21
View File
@@ -20,9 +20,8 @@
#
import unittest as stdlib_unittest
from typing import Any, Mapping
from typing import TYPE_CHECKING, Any, Mapping
import attr
from parameterized import parameterized
from synapse.api.constants import EventContentFields
@@ -38,11 +37,15 @@ from synapse.events.utils import (
make_config_for_admin,
maybe_upsert_event_field,
prune_event,
serialize_event,
)
from synapse.types import JsonDict, create_requester
from synapse.util.frozenutils import freeze
from tests.unittest import HomeserverTestCase
if TYPE_CHECKING:
from synapse.server import HomeServer
def MockEvent(**kwargs: Any) -> EventBase:
if "event_id" not in kwargs:
@@ -553,11 +556,6 @@ class PruneEventTestCase(stdlib_unittest.TestCase):
room_version=RoomVersions.V10,
)
# Create a new room version.
msc3389_room_ver = attr.evolve(
RoomVersions.V10, msc3389_relation_redactions=True
)
self.run_test(
{
"type": "m.room.message",
@@ -581,7 +579,7 @@ class PruneEventTestCase(stdlib_unittest.TestCase):
"signatures": {},
"unsigned": {},
},
room_version=msc3389_room_ver,
room_version=RoomVersions.MSC3389v10,
)
# If the field is not an object, redact it.
@@ -599,7 +597,7 @@ class PruneEventTestCase(stdlib_unittest.TestCase):
"signatures": {},
"unsigned": {},
},
room_version=msc3389_room_ver,
room_version=RoomVersions.MSC3389v10,
)
# If the m.relates_to property would be empty, redact it.
@@ -614,7 +612,7 @@ class PruneEventTestCase(stdlib_unittest.TestCase):
"signatures": {},
"unsigned": {},
},
room_version=msc3389_room_ver,
room_version=RoomVersions.MSC3389v10,
)
@@ -644,19 +642,27 @@ class CloneEventTestCase(stdlib_unittest.TestCase):
self.assertEqual(cloned.internal_metadata.txn_id, "txn")
class SerializeEventTestCase(stdlib_unittest.TestCase):
class SerializeEventTestCase(HomeserverTestCase):
def prepare(self, reactor: Any, clock: Any, hs: "HomeServer") -> None:
self._event_serializer = hs.get_event_client_serializer()
def serialize(
self,
ev: EventBase,
fields: list[str] | None,
include_admin_metadata: bool = False,
redaction_map: Mapping[str, EventBase] | None = None,
) -> JsonDict:
return serialize_event(
ev,
1479807801915,
config=SerializeEventConfig(
only_event_fields=fields, include_admin_metadata=include_admin_metadata
),
return self.get_success(
self._event_serializer.serialize_event(
ev,
1479807801915,
config=SerializeEventConfig(
only_event_fields=fields,
include_admin_metadata=include_admin_metadata,
),
redaction_map=redaction_map,
)
)
def test_event_fields_works_with_keys(self) -> None:
@@ -770,9 +776,8 @@ class SerializeEventTestCase(stdlib_unittest.TestCase):
def test_event_fields_fail_if_fields_not_str(self) -> None:
with self.assertRaises(TypeError):
self.serialize(
MockEvent(room_id="!foo:bar", content={"foo": "bar"}),
["room_id", 4], # type: ignore[list-item]
SerializeEventConfig(
only_event_fields=["room_id", 4], # type: ignore[list-item]
)
def test_default_serialize_config_excludes_admin_metadata(self) -> None:
@@ -873,6 +878,52 @@ class SerializeEventTestCase(stdlib_unittest.TestCase):
)
self.assertTrue(admin_config.include_admin_metadata)
def test_redacted_because_is_filtered_out(self) -> None:
"""If an event's unsigned dict has a `redacted_by` field, then the
`redacted_because` should be filtered out if not specified in
`only_event_fields`."""
redaction_id = "$redaction_event_id"
event = MockEvent(
type="foo",
event_id="test",
room_id="!foo:bar",
content={"foo": "bar"},
)
event.internal_metadata.redacted_by = redaction_id
redaction_event = MockEvent(
type="m.room.redaction",
event_id=redaction_id,
content={"redacts": "test"},
)
self.assertEqual(
self.serialize(
event,
["content.foo"],
redaction_map={redaction_id: redaction_event},
),
{
"content": {"foo": "bar"},
},
)
self.assertEqual(
self.serialize(
event,
["content.foo", "unsigned.redacted_because"],
redaction_map={redaction_id: redaction_event},
),
{
"content": {"foo": "bar"},
"unsigned": {
"redacted_because": self.serialize(redaction_event, fields=None),
},
},
)
class CopyPowerLevelsContentTestCase(stdlib_unittest.TestCase):
def setUp(self) -> None:
+2 -4
View File
@@ -101,11 +101,10 @@ class EventsWorkerStoreTestCase(BaseWorkerStoreTestCase):
msg_dict = msg.get_dict()
msg_dict["content"] = {}
msg_dict["unsigned"]["redacted_by"] = redaction.event_id
msg_dict["unsigned"]["redacted_because"] = redaction
redacted = make_event_from_dict(
msg_dict, internal_metadata_dict=msg.internal_metadata.get_dict()
)
redacted.internal_metadata.redacted_by = redaction.event_id
self.check(
"get_event", [msg.event_id], redacted, asserter=self.assertEventsEqual
)
@@ -125,11 +124,10 @@ class EventsWorkerStoreTestCase(BaseWorkerStoreTestCase):
msg_dict = msg.get_dict()
msg_dict["content"] = {}
msg_dict["unsigned"]["redacted_by"] = redaction.event_id
msg_dict["unsigned"]["redacted_because"] = redaction
redacted = make_event_from_dict(
msg_dict, internal_metadata_dict=msg.internal_metadata.get_dict()
)
redacted.internal_metadata.redacted_by = redaction.event_id
self.check(
"get_event", [msg.event_id], redacted, asserter=self.assertEventsEqual
)
+5 -5
View File
@@ -4950,10 +4950,10 @@ class MSC4293RedactOnBanKickTestCase(unittest.FederatingHomeserverTestCase):
original = self.get_success(self.store.get_event(message.event_id))
if not original:
self.fail("Expected to find remote message in DB")
redacted_because = original.unsigned.get("redacted_because")
if not redacted_because:
self.fail("Did not find redacted_because field")
self.assertEqual(redacted_because.event_id, ban_event_id)
redacted_by = original.internal_metadata.redacted_by
if not redacted_by:
self.fail("Did not find redacted_by field")
self.assertEqual(redacted_by, ban_event_id)
def test_unbanning_remote_user_stops_redaction_action(self) -> None:
bad_user = "@remote_bad_user:" + self.OTHER_SERVER_NAME
@@ -5315,7 +5315,7 @@ class MSC4293RedactOnBanKickTestCase(unittest.FederatingHomeserverTestCase):
original = self.get_success(self.store.get_event(message.event_id))
if not original:
self.fail("Expected to find remote message in DB")
self.assertEqual(original.unsigned["redacted_by"], ban_event_id)
self.assertEqual(original.internal_metadata.redacted_by, ban_event_id)
def test_rejoining_kicked_remote_user_stops_redaction_action(self) -> None:
bad_user = "@remote_bad_user:" + self.OTHER_SERVER_NAME
+133
View File
@@ -14,11 +14,14 @@
#
from canonicaljson import encode_canonical_json
from twisted.internet.testing import MemoryReactor
from synapse.api.constants import MAX_DEPTH
from synapse.api.room_versions import RoomVersion, RoomVersions
from synapse.server import HomeServer
from synapse.types.storage import _BackgroundUpdates
from synapse.util.clock import Clock
from tests.unittest import HomeserverTestCase
@@ -154,3 +157,133 @@ class TestFixupMaxDepthCapBgUpdate(HomeserverTestCase):
# Assert that the topological_ordering of events has not been changed
# from their depth.
self.assertDictEqual(event_id_to_depth, dict(rows))
class TestRedactionsRecheckBgUpdate(HomeserverTestCase):
"""Test the background update that backfills the `recheck` column in redactions."""
def prepare(
self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
) -> None:
self.store = self.hs.get_datastores().main
self.db_pool = self.store.db_pool
# Re-insert the background update, since it already ran during setup.
self.get_success(
self.db_pool.simple_insert(
table="background_updates",
values={
"update_name": _BackgroundUpdates.REDACTIONS_RECHECK_BG_UPDATE,
"progress_json": "{}",
},
)
)
self.db_pool.updates._all_done = False
def _insert_redaction(
self,
event_id: str,
redacts: str,
recheck_redaction: bool | None = None,
insert_event_json: bool = True,
) -> None:
"""Insert a row into `redactions` and optionally a matching `event_json` row.
Args:
event_id: The event ID of the redaction event.
redacts: The event ID being redacted.
recheck_redaction: The value of `recheck_redaction` in internal metadata.
If None, the key is omitted from internal metadata.
insert_event_json: Whether to insert a corresponding row in `event_json`.
"""
self.get_success(
self.db_pool.simple_insert(
table="redactions",
values={
"event_id": event_id,
"redacts": redacts,
"have_censored": False,
"received_ts": 0,
},
)
)
if insert_event_json:
internal_metadata: dict = {}
if recheck_redaction is not None:
internal_metadata["recheck_redaction"] = recheck_redaction
self.get_success(
self.db_pool.simple_insert(
table="event_json",
values={
"event_id": event_id,
"room_id": "!room:test",
"internal_metadata": encode_canonical_json(
internal_metadata
).decode("utf-8"),
"json": "{}",
"format_version": 3,
},
)
)
def _get_recheck(self, event_id: str) -> bool:
row = self.get_success(
self.db_pool.simple_select_one(
table="redactions",
keyvalues={"event_id": event_id},
retcols=["recheck"],
)
)
return bool(row[0])
def test_recheck_true(self) -> None:
"""A redaction with recheck_redaction=True in internal metadata gets recheck=True."""
self._insert_redaction("$redact1:test", "$target1:test", recheck_redaction=True)
self.wait_for_background_updates()
self.assertTrue(self._get_recheck("$redact1:test"))
def test_recheck_false(self) -> None:
"""A redaction with recheck_redaction=False in internal metadata gets recheck=False."""
self._insert_redaction(
"$redact2:test", "$target2:test", recheck_redaction=False
)
self.wait_for_background_updates()
self.assertFalse(self._get_recheck("$redact2:test"))
def test_recheck_absent_from_metadata(self) -> None:
"""A redaction with no recheck_redaction key in internal metadata gets recheck=False."""
self._insert_redaction("$redact3:test", "$target3:test", recheck_redaction=None)
self.wait_for_background_updates()
self.assertFalse(self._get_recheck("$redact3:test"))
def test_recheck_no_event_json(self) -> None:
"""A redaction with no event_json row gets recheck=False."""
self._insert_redaction(
"$redact4:test", "$target4:test", insert_event_json=False
)
self.wait_for_background_updates()
self.assertFalse(self._get_recheck("$redact4:test"))
def test_batching(self) -> None:
"""The update processes rows in batches, completing when all are done."""
self._insert_redaction("$redact5:test", "$target5:test", recheck_redaction=True)
self._insert_redaction(
"$redact6:test", "$target6:test", recheck_redaction=False
)
self._insert_redaction("$redact7:test", "$target7:test", recheck_redaction=True)
self.wait_for_background_updates()
self.assertTrue(self._get_recheck("$redact5:test"))
self.assertFalse(self._get_recheck("$redact6:test"))
self.assertTrue(self._get_recheck("$redact7:test"))
+7 -28
View File
@@ -158,7 +158,7 @@ class RedactionTestCase(unittest.HomeserverTestCase):
event,
)
self.assertFalse("redacted_because" in event.unsigned)
self.assertIsNone(event.internal_metadata.redacted_by)
# Redact event
reason = "Because I said so"
@@ -168,7 +168,7 @@ class RedactionTestCase(unittest.HomeserverTestCase):
self.assertEqual(msg_event.event_id, event.event_id)
self.assertTrue("redacted_because" in event.unsigned)
self.assertIsNotNone(event.internal_metadata.redacted_by)
self.assertObjectHasAttributes(
{
@@ -179,15 +179,6 @@ class RedactionTestCase(unittest.HomeserverTestCase):
event,
)
self.assertObjectHasAttributes(
{
"type": EventTypes.Redaction,
"user_id": self.u_alice.to_string(),
"content": {"reason": reason},
},
event.unsigned["redacted_because"],
)
def test_redact_join(self) -> None:
self.inject_room_member(self.room1, self.u_alice, Membership.JOIN)
@@ -206,7 +197,7 @@ class RedactionTestCase(unittest.HomeserverTestCase):
event,
)
self.assertFalse(hasattr(event, "redacted_because"))
self.assertIsNone(event.internal_metadata.redacted_by)
# Redact event
reason = "Because I said so"
@@ -216,7 +207,7 @@ class RedactionTestCase(unittest.HomeserverTestCase):
event = self.get_success(self.store.get_event(msg_event.event_id))
self.assertTrue("redacted_because" in event.unsigned)
self.assertIsNotNone(event.internal_metadata.redacted_by)
self.assertObjectHasAttributes(
{
@@ -227,15 +218,6 @@ class RedactionTestCase(unittest.HomeserverTestCase):
event,
)
self.assertObjectHasAttributes(
{
"type": EventTypes.Redaction,
"user_id": self.u_alice.to_string(),
"content": {"reason": reason},
},
event.unsigned["redacted_because"],
)
def test_circular_redaction(self) -> None:
redaction_event_id1 = "$redaction1_id:test"
redaction_event_id2 = "$redaction2_id:test"
@@ -331,10 +313,7 @@ class RedactionTestCase(unittest.HomeserverTestCase):
fetched = self.get_success(self.store.get_event(redaction_event_id1))
# it should have been redacted
self.assertEqual(fetched.unsigned["redacted_by"], redaction_event_id2)
self.assertEqual(
fetched.unsigned["redacted_because"].event_id, redaction_event_id2
)
self.assertEqual(fetched.internal_metadata.redacted_by, redaction_event_id2)
def test_redact_censor(self) -> None:
"""Test that a redacted event gets censored in the DB after a month"""
@@ -355,7 +334,7 @@ class RedactionTestCase(unittest.HomeserverTestCase):
event,
)
self.assertFalse("redacted_because" in event.unsigned)
self.assertIsNone(event.internal_metadata.redacted_by)
# Redact event
reason = "Because I said so"
@@ -363,7 +342,7 @@ class RedactionTestCase(unittest.HomeserverTestCase):
event = self.get_success(self.store.get_event(msg_event.event_id))
self.assertTrue("redacted_because" in event.unsigned)
self.assertIsNotNone(event.internal_metadata.redacted_by)
self.assertObjectHasAttributes(
{