mirror of
https://github.com/element-hq/synapse.git
synced 2026-07-11 22:49:31 +00:00
Re-expose the standalone format_event_* transforms for backwards compat (#19922)
The port of event serialization to Rust (#19837) removed `format_event_raw`, `format_event_for_client_v1`, `format_event_for_client_v2` and `format_event_for_client_v2_without_room_id` from `synapse.events.utils`, but there are modules in the wild that import them from there. Reimplement them as standalone pyfunctions in Rust, operating directly on the Python dict so the original semantics are preserved exactly (in-place mutation, returning the same dict, arbitrary non-JSON values passing through, KeyError on a missing `unsigned` in the v1 format), and re-export them from `synapse.events.utils`. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
Port the synchronous core of client event serialization to Rust.
|
||||
@@ -118,6 +118,13 @@ pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()>
|
||||
child_module.add_function(wrap_pyfunction!(redact_event_py, m)?)?;
|
||||
child_module.add_function(wrap_pyfunction!(redact_event_dict, m)?)?;
|
||||
child_module.add_function(wrap_pyfunction!(serialize::serialize_events, m)?)?;
|
||||
child_module.add_function(wrap_pyfunction!(serialize::format_event_raw, m)?)?;
|
||||
child_module.add_function(wrap_pyfunction!(serialize::format_event_for_client_v1, m)?)?;
|
||||
child_module.add_function(wrap_pyfunction!(serialize::format_event_for_client_v2, m)?)?;
|
||||
child_module.add_function(wrap_pyfunction!(
|
||||
serialize::format_event_for_client_v2_without_room_id,
|
||||
m
|
||||
)?)?;
|
||||
|
||||
m.add_submodule(&child_module)?;
|
||||
|
||||
|
||||
@@ -32,7 +32,9 @@ use std::collections::HashMap;
|
||||
|
||||
use pyo3::{
|
||||
exceptions::{PyTypeError, PyValueError},
|
||||
pyclass, pyfunction, pymethods, Bound, PyAny, PyResult, Python,
|
||||
pyclass, pyfunction, pymethods,
|
||||
types::{PyAnyMethods, PyDict, PyDictMethods},
|
||||
Bound, PyAny, PyResult, Python,
|
||||
};
|
||||
use pythonize::pythonize;
|
||||
use serde_json::{Map, Number, Value};
|
||||
@@ -582,6 +584,69 @@ fn format_for_client_v2(d: &mut Map<String, Value>) {
|
||||
}
|
||||
}
|
||||
|
||||
// Standalone versions of the client format transforms, re-exported from
|
||||
// `synapse.events.utils` purely as a backwards compatibility hack: they have
|
||||
// never been part of the module API and modules shouldn't be pulling them in,
|
||||
// but some in the wild import them from there anyway. They may be removed in
|
||||
// the future; nothing in Synapse itself should use them.
|
||||
//
|
||||
// Unlike [`apply_event_format`], these mutate a Python dict in place and
|
||||
// return it, matching the original Python implementations that used to live
|
||||
// in `synapse/events/utils.py`.
|
||||
|
||||
/// Return the event dict unchanged (federation format).
|
||||
#[pyfunction]
|
||||
pub fn format_event_raw(d: Bound<'_, PyDict>) -> Bound<'_, PyDict> {
|
||||
d
|
||||
}
|
||||
|
||||
/// Apply the legacy `/events`-style v1 client format to `d` in place.
|
||||
#[pyfunction]
|
||||
pub fn format_event_for_client_v1(d: Bound<'_, PyDict>) -> PyResult<Bound<'_, PyDict>> {
|
||||
let d = format_event_for_client_v2(d)?;
|
||||
|
||||
if let Some(sender) = d.get_item(SENDER)? {
|
||||
if !sender.is_none() {
|
||||
d.set_item(USER_ID, sender)?;
|
||||
}
|
||||
}
|
||||
|
||||
// As in the original Python (`d["unsigned"]`), a missing `unsigned` key
|
||||
// raises `KeyError`.
|
||||
let unsigned = d.as_any().get_item(UNSIGNED)?;
|
||||
for key in V1_COPY_KEYS {
|
||||
if unsigned.contains(key)? {
|
||||
d.set_item(key, unsigned.get_item(key)?)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// Apply the `/sync`-style v2 client format to `d` in place.
|
||||
#[pyfunction]
|
||||
pub fn format_event_for_client_v2(d: Bound<'_, PyDict>) -> PyResult<Bound<'_, PyDict>> {
|
||||
for key in V2_DROP_KEYS {
|
||||
// Equivalent to `d.pop(key, None)`.
|
||||
if d.contains(key)? {
|
||||
d.del_item(key)?;
|
||||
}
|
||||
}
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// Apply the v2 client format to `d` in place, additionally stripping `room_id`.
|
||||
#[pyfunction]
|
||||
pub fn format_event_for_client_v2_without_room_id(
|
||||
d: Bound<'_, PyDict>,
|
||||
) -> PyResult<Bound<'_, PyDict>> {
|
||||
let d = format_event_for_client_v2(d)?;
|
||||
if d.contains(ROOM_ID)? {
|
||||
d.del_item(ROOM_ID)?;
|
||||
}
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// Return a mutable reference to `map["unsigned"]`, creating it as an empty
|
||||
/// object if it is missing or not an object.
|
||||
fn unsigned_mut(map: &mut Map<String, Value>) -> PyResult<&mut Map<String, Value>> {
|
||||
|
||||
+17
-1
@@ -45,6 +45,10 @@ from synapse.synapse_rust.events import (
|
||||
EventFormat,
|
||||
SerializeEventConfig,
|
||||
Unsigned,
|
||||
format_event_for_client_v1,
|
||||
format_event_for_client_v2,
|
||||
format_event_for_client_v2_without_room_id,
|
||||
format_event_raw,
|
||||
redact_event,
|
||||
serialize_events,
|
||||
)
|
||||
@@ -56,7 +60,19 @@ from . import EventBase, StrippedStateEvent
|
||||
# These are imported only to re-export them (callers import them from this
|
||||
# module); listing them in __all__ stops the unused-import lint flagging them
|
||||
# and re-exports them for `import *`.
|
||||
__all__ = ["EventFormat", "SerializeEventConfig"]
|
||||
#
|
||||
# The `format_event_*` functions are a backwards compatibility hack: they have
|
||||
# never been part of the module API and modules shouldn't be pulling them in,
|
||||
# but some in the wild import them from here anyway. They may be removed in
|
||||
# the future; nothing in Synapse itself should use them.
|
||||
__all__ = [
|
||||
"EventFormat",
|
||||
"SerializeEventConfig",
|
||||
"format_event_for_client_v1",
|
||||
"format_event_for_client_v2",
|
||||
"format_event_for_client_v2_without_room_id",
|
||||
"format_event_raw",
|
||||
]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.handlers.relations import BundledAggregations
|
||||
|
||||
@@ -442,6 +442,40 @@ def serialize_events(
|
||||
The serialized events, in the same order as `events`.
|
||||
"""
|
||||
|
||||
# The standalone `format_event_*` transforms below are a backwards
|
||||
# compatibility hack: they have never been part of the module API and modules
|
||||
# shouldn't be pulling them in, but some in the wild import them (via
|
||||
# `synapse.events.utils`) anyway. They may be removed in the future; nothing
|
||||
# in Synapse itself should use them.
|
||||
|
||||
def format_event_raw(d: JsonDict) -> JsonDict:
|
||||
"""Return the event dict unchanged (federation format).
|
||||
|
||||
Deprecated backwards compatibility hack for modules importing it from
|
||||
`synapse.events.utils`; don't use this in new code.
|
||||
"""
|
||||
|
||||
def format_event_for_client_v1(d: JsonDict) -> JsonDict:
|
||||
"""Apply the legacy `/events`-style v1 client format to `d` in place.
|
||||
|
||||
Deprecated backwards compatibility hack for modules importing it from
|
||||
`synapse.events.utils`; don't use this in new code.
|
||||
"""
|
||||
|
||||
def format_event_for_client_v2(d: JsonDict) -> JsonDict:
|
||||
"""Apply the `/sync`-style v2 client format to `d` in place.
|
||||
|
||||
Deprecated backwards compatibility hack for modules importing it from
|
||||
`synapse.events.utils`; don't use this in new code.
|
||||
"""
|
||||
|
||||
def format_event_for_client_v2_without_room_id(d: JsonDict) -> JsonDict:
|
||||
"""Apply the v2 client format to `d` in place, additionally stripping `room_id`.
|
||||
|
||||
Deprecated backwards compatibility hack for modules importing it from
|
||||
`synapse.events.utils`; don't use this in new code.
|
||||
"""
|
||||
|
||||
def redact_event(event: Event) -> Event:
|
||||
"""Returns a pruned version of the given event, which removes all keys we
|
||||
don't know about or think could potentially be dodgy.
|
||||
|
||||
@@ -30,6 +30,10 @@ from synapse.events.utils import (
|
||||
PowerLevelsContent,
|
||||
clone_event,
|
||||
copy_and_fixup_power_levels_contents,
|
||||
format_event_for_client_v1,
|
||||
format_event_for_client_v2,
|
||||
format_event_for_client_v2_without_room_id,
|
||||
format_event_raw,
|
||||
maybe_upsert_event_field,
|
||||
prune_event,
|
||||
)
|
||||
@@ -997,3 +1001,99 @@ class CopyPowerLevelsContentTestCase(stdlib_unittest.TestCase):
|
||||
def test_invalid_nesting_raises_type_error(self) -> None:
|
||||
with self.assertRaises(TypeError):
|
||||
copy_and_fixup_power_levels_contents({"a": {"b": {"c": 1}}}) # type: ignore[dict-item]
|
||||
|
||||
|
||||
class FormatEventForClientTestCase(stdlib_unittest.TestCase):
|
||||
"""Tests for the standalone `format_event_*` transforms.
|
||||
|
||||
These are Rust reimplementations kept purely as a backwards compatibility
|
||||
hack for modules in the wild that import them from `synapse.events.utils`
|
||||
(they were never part of the module API, and nothing in Synapse itself uses
|
||||
them); like the original Python implementations they must mutate the dict
|
||||
in place and return it.
|
||||
"""
|
||||
|
||||
def make_event(self) -> JsonDict:
|
||||
return {
|
||||
"event_id": "$event_id",
|
||||
"room_id": "!room:test",
|
||||
"sender": "@sender:test",
|
||||
"type": "m.room.message",
|
||||
"content": {"body": "hello"},
|
||||
"auth_events": [],
|
||||
"prev_events": [],
|
||||
"hashes": {},
|
||||
"signatures": {},
|
||||
"depth": 5,
|
||||
"origin": "test",
|
||||
"prev_state": [],
|
||||
"unsigned": {"age": 100, "replaces_state": "$old", "other": 1},
|
||||
}
|
||||
|
||||
def test_raw(self) -> None:
|
||||
event_dict = self.make_event()
|
||||
result = format_event_raw(event_dict)
|
||||
self.assertIs(result, event_dict)
|
||||
self.assertEqual(result, self.make_event())
|
||||
|
||||
def test_v2_drops_federation_keys(self) -> None:
|
||||
event_dict = self.make_event()
|
||||
result = format_event_for_client_v2(event_dict)
|
||||
self.assertIs(result, event_dict)
|
||||
self.assertEqual(
|
||||
result,
|
||||
{
|
||||
"event_id": "$event_id",
|
||||
"room_id": "!room:test",
|
||||
"sender": "@sender:test",
|
||||
"type": "m.room.message",
|
||||
"content": {"body": "hello"},
|
||||
"unsigned": {"age": 100, "replaces_state": "$old", "other": 1},
|
||||
},
|
||||
)
|
||||
|
||||
def test_v2_without_room_id(self) -> None:
|
||||
event_dict = self.make_event()
|
||||
result = format_event_for_client_v2_without_room_id(event_dict)
|
||||
self.assertIs(result, event_dict)
|
||||
self.assertNotIn("room_id", result)
|
||||
|
||||
def test_v1_copies_unsigned_keys(self) -> None:
|
||||
event_dict = self.make_event()
|
||||
result = format_event_for_client_v1(event_dict)
|
||||
self.assertIs(result, event_dict)
|
||||
self.assertEqual(
|
||||
result,
|
||||
{
|
||||
"event_id": "$event_id",
|
||||
"room_id": "!room:test",
|
||||
"sender": "@sender:test",
|
||||
"user_id": "@sender:test",
|
||||
"type": "m.room.message",
|
||||
"content": {"body": "hello"},
|
||||
"age": 100,
|
||||
"replaces_state": "$old",
|
||||
"unsigned": {"age": 100, "replaces_state": "$old", "other": 1},
|
||||
},
|
||||
)
|
||||
|
||||
def test_v1_no_sender(self) -> None:
|
||||
event_dict = self.make_event()
|
||||
del event_dict["sender"]
|
||||
result = format_event_for_client_v1(event_dict)
|
||||
self.assertNotIn("user_id", result)
|
||||
|
||||
def test_non_json_values_pass_through(self) -> None:
|
||||
# The transforms only move keys around; values that aren't
|
||||
# JSON-serializable must survive untouched.
|
||||
marker = object()
|
||||
event_dict = {
|
||||
"sender": "@sender:test",
|
||||
"auth_events": marker,
|
||||
"content": marker,
|
||||
"unsigned": {"age": marker},
|
||||
}
|
||||
result = format_event_for_client_v1(event_dict)
|
||||
self.assertIs(result["content"], marker)
|
||||
self.assertIs(result["age"], marker)
|
||||
self.assertNotIn("auth_events", result)
|
||||
|
||||
Reference in New Issue
Block a user