Skip to content

Commit 09cf676

Browse files
erikjohnstonclaude
andcommitted
Port the synchronous event serialization core to Rust
Move the synchronous core of client event serialization out of `synapse/events/utils.py` and into `rust/src/events/serialize.rs`. The Python `EventClientSerializer` now performs all DB/IO (fetching redactions, running module callbacks, resolving admin/MSC4354 config) up front in `_prepare_serialization`, then hands the pre-fetched data to the Rust `serialize_event`, which recurses entirely in Rust. Bundled aggregations are read directly from the Rust `BundledAggregations` pyclass. `SerializeEventConfig` and the `event_format` callable become a Rust pyclass and the `EventFormat` enum respectively; call sites are updated to pass the enum. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8b29522 commit 09cf676

13 files changed

Lines changed: 1028 additions & 596 deletions

File tree

rust/src/duration.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ impl SynapseDuration {
4040
Self { milliseconds }
4141
}
4242

43+
/// Returns the duration as a number of milliseconds.
44+
pub const fn as_millis(&self) -> u64 {
45+
self.milliseconds
46+
}
47+
4348
/// Creates a `SynapseDuration` from a number of hours.
4449
pub const fn from_hours(hours: u32) -> Self {
4550
// We take a u32 here so that we know the multiplication won't overflow.

rust/src/events/constants.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,36 @@ pub mod unsigned_field {
7474
pub const AGE_TS: &str = "age_ts";
7575
/// Unsigned field: redacted_because
7676
pub const REDACTED_BECAUSE: &str = "redacted_because";
77+
/// Unsigned field: redacted_by
78+
pub const REDACTED_BY: &str = "redacted_by";
79+
/// Unsigned field: transaction_id
80+
pub const TRANSACTION_ID: &str = "transaction_id";
81+
/// Unsigned field: org.matrix.msc4140.delay_id
82+
pub const DELAY_ID: &str = "org.matrix.msc4140.delay_id";
83+
/// Unsigned field: membership (MSC4115)
84+
pub const MEMBERSHIP: &str = "membership";
85+
/// Unsigned field: msc4354_sticky_duration_ttl_ms (MSC4354)
86+
pub const STICKY_TTL: &str = "msc4354_sticky_duration_ttl_ms";
87+
/// Unsigned field: io.element.synapse.soft_failed (admin metadata)
88+
pub const SOFT_FAILED: &str = "io.element.synapse.soft_failed";
89+
/// Unsigned field: io.element.synapse.policy_server_spammy (admin metadata)
90+
pub const POLICY_SERVER_SPAMMY: &str = "io.element.synapse.policy_server_spammy";
91+
/// Unsigned field: invite_room_state
92+
pub const INVITE_ROOM_STATE: &str = "invite_room_state";
93+
/// Unsigned field: knock_room_state
94+
pub const KNOCK_ROOM_STATE: &str = "knock_room_state";
95+
/// Unsigned field: m.relations
96+
pub const M_RELATIONS: &str = "m.relations";
97+
}
98+
99+
/// Relation types (the `rel_type` of an `m.relates_to`).
100+
pub mod relation_type {
101+
/// Relation type: m.reference
102+
pub const REFERENCE: &str = "m.reference";
103+
/// Relation type: m.replace
104+
pub const REPLACE: &str = "m.replace";
105+
/// Relation type: m.thread
106+
pub const THREAD: &str = "m.thread";
77107
}
78108

79109
/// Membership Event Fields

rust/src/events/internal_metadata.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,41 @@ impl EventInternalMetadata {
498498
.write()
499499
.map_err(|_| PyRuntimeError::new_err("EventInternalMetadata lock poisoned"))
500500
}
501+
502+
/// The event ID of the redaction event, if this event has been redacted.
503+
pub fn redacted_by(&self) -> PyResult<Option<String>> {
504+
Ok(self.read_inner()?.redacted_by.clone())
505+
}
506+
507+
/// The transaction ID, if set when the event was created.
508+
pub fn txn_id(&self) -> PyResult<Option<String>> {
509+
Ok(self.read_inner()?.get_txn_id().map(|s| s.to_owned()))
510+
}
511+
512+
/// The device ID of the sender, if set.
513+
pub fn device_id(&self) -> PyResult<Option<String>> {
514+
Ok(self.read_inner()?.get_device_id().map(|s| s.to_owned()))
515+
}
516+
517+
/// The access token ID of the sender, if set.
518+
pub fn token_id(&self) -> PyResult<Option<i64>> {
519+
Ok(self.read_inner()?.get_token_id())
520+
}
521+
522+
/// The delay ID, set only if the event was a delayed event.
523+
pub fn delay_id(&self) -> PyResult<Option<String>> {
524+
Ok(self.read_inner()?.get_delay_id().map(|s| s.to_owned()))
525+
}
526+
527+
/// Whether the event has been soft failed.
528+
pub fn soft_failed(&self) -> PyResult<bool> {
529+
Ok(self.read_inner()?.is_soft_failed())
530+
}
531+
532+
/// Whether the policy server marked this event as spammy.
533+
pub fn policy_server_spammy(&self) -> PyResult<bool> {
534+
Ok(self.read_inner()?.get_policy_server_spammy())
535+
}
501536
}
502537

503538
/// Helper to convert `None` to an `AttributeError` for a property getter.

rust/src/events/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ pub mod formats;
8888
pub mod internal_metadata;
8989
pub mod json_object;
9090
pub mod relations;
91+
pub mod serialize;
9192
pub mod signatures;
9293
pub mod unsigned;
9394
pub mod utils;
@@ -110,9 +111,12 @@ pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()>
110111
child_module.add_class::<Event>()?;
111112
child_module.add_class::<relations::BundledAggregations>()?;
112113
child_module.add_class::<relations::ThreadAggregation>()?;
114+
child_module.add_class::<serialize::EventFormat>()?;
115+
child_module.add_class::<serialize::SerializeEventConfig>()?;
113116
child_module.add_function(wrap_pyfunction!(filter::event_visible_to_server_py, m)?)?;
114117
child_module.add_function(wrap_pyfunction!(redact_event_py, m)?)?;
115118
child_module.add_function(wrap_pyfunction!(redact_event_dict, m)?)?;
119+
child_module.add_function(wrap_pyfunction!(serialize::serialize_events, m)?)?;
116120

117121
m.add_submodule(&child_module)?;
118122

0 commit comments

Comments
 (0)