mirror of
https://github.com/element-hq/synapse.git
synced 2026-06-07 02:22:20 +00:00
Port Event class to Rust (#19701)
Ports the event class to Rust. The main difference here are: 1. There is now a single event class 2. We now validate a lot more at event construction time than we previously did (we basically checked nothing before). This required some changes to the tests, including https://github.com/matrix-org/sytest/pull/1423 Reviewable commit-by-commit. ### Overview of Event Rust structure The format of the event struct in Rust is quite different than that in Python. The top-level looks like: ```rust pub struct Event { /// The parsed event JSON. fields: FormattedEvent, /// The event ID. For format v1 this is read directly from the JSON; /// for v2+ it is computed from the canonical-JSON hash at /// construction time and cached here. event_id: Arc<str>, /// Synapse-internal per-event state that lives outside the federated /// JSON (e.g. outlier flag, soft-failure, stream positions). #[pyo3(get)] internal_metadata: EventInternalMetadata, /// The room version this event was parsed for. #[pyo3(get)] room_version: &'static RoomVersion, /// `None` for accepted events; otherwise a short reason set by auth /// when the event was rejected. rejected_reason: Option<Box<str>>, } ``` which includes the actual parsed event in `FormattedEvent`, plus the rest of the event metadata. ```rust pub struct FormattedEvent<E = Arc<EventFormatEnum>> { #[serde(default)] pub signatures: Signatures, #[serde(default)] pub unsigned: Unsigned, #[serde(flatten)] pub specific_fields: E, #[serde(flatten)] pub common_fields: Arc<EventCommonFields>, } ``` The struct is further split into the common fields, format specific fields, plus the signatures and unsigned. We split out the signature and unsigned fields as they are mutable, so when we clone the event we can still share the common and specific fields and only copy signature and unsigned. The `specific_fields` are the fields that depend on the format version. They can either be a specific format (e.g. `E = EventFormatV1`) or a type-erased enum `EventFormatEnum` that is across all room versions: ```rust pub enum EventFormatEnum { V1(EventFormatV1), V2V3(EventFormatV2V3), V4(EventFormatV4), VMSC4242(EventFormatVMSC4242), } ``` For example: ```rust /// Shared flat-list encoding of `auth_events` and `prev_events`, reused /// by every format from v2/v3 onwards. #[derive(Serialize, Deserialize)] pub struct SimpleAuthPrevEvents { pub auth_events: Vec<String>, pub prev_events: Vec<String>, } /// Version-specific fields for room versions 3-10. #[derive(Serialize, Deserialize)] pub struct EventFormatV2V3 { pub room_id: Box<str>, #[serde(flatten)] pub auth_prev_events: SimpleAuthPrevEvents, } ``` ### Dev notes As discussed in [`#element-backend-internal:matrix.org`](https://matrix.to/#/!SGNQGPGUwtcPBUotTL:matrix.org/$3gTjDO440GbAz57cXcCawwiyFLiD0crrarvS1uhzKOY?via=jki.re&via=element.io&via=matrix.org) --------- Co-authored-by: Eric Eastwood <erice@element.io>
This commit is contained in:
+27
-5
@@ -29,19 +29,41 @@ fn duration_module(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {
|
||||
}
|
||||
|
||||
/// Mirrors the `synapse.util.duration.Duration` Python class.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct SynapseDuration {
|
||||
microseconds: u64,
|
||||
milliseconds: u64,
|
||||
}
|
||||
|
||||
impl SynapseDuration {
|
||||
/// For now we only need to create durations from milliseconds.
|
||||
pub fn from_milliseconds(milliseconds: u64) -> Self {
|
||||
/// Creates a `SynapseDuration` from a number of milliseconds.
|
||||
pub const fn from_milliseconds(milliseconds: u64) -> Self {
|
||||
Self { milliseconds }
|
||||
}
|
||||
|
||||
/// Creates a `SynapseDuration` from a number of hours.
|
||||
pub const fn from_hours(hours: u32) -> Self {
|
||||
// We take a u32 here so that we know the multiplication won't overflow.
|
||||
// We could instead panic, but that is unstable in a const context (for
|
||||
// the current MSRV 1.82).
|
||||
Self {
|
||||
microseconds: milliseconds * 1_000,
|
||||
milliseconds: (hours as u64) * 3_600_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for SynapseDuration {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let duration_module = duration_module(py)?;
|
||||
let kwargs = [("milliseconds", self.milliseconds)].into_py_dict(py)?;
|
||||
let duration_instance = duration_module.call_method("Duration", (), Some(&kwargs))?;
|
||||
Ok(duration_instance.into_bound())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &SynapseDuration {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
@@ -49,7 +71,7 @@ impl<'py> IntoPyObject<'py> for &SynapseDuration {
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let duration_module = duration_module(py)?;
|
||||
let kwargs = [("microseconds", self.microseconds)].into_py_dict(py)?;
|
||||
let kwargs = [("milliseconds", self.milliseconds)].into_py_dict(py)?;
|
||||
let duration_instance = duration_module.call_method("Duration", (), Some(&kwargs))?;
|
||||
Ok(duration_instance.into_bound())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
//! Matrix Events
|
||||
//!
|
||||
//! Contains types and utilities for working with Matrix events.
|
||||
|
||||
/// Maximum size of a PDU
|
||||
pub const MAX_PDU_SIZE_BYTES: usize = 65_536;
|
||||
|
||||
/// Event Types
|
||||
pub mod event_type {
|
||||
/// Event type: m.room.member
|
||||
pub const M_ROOM_MEMBER: &str = "m.room.member";
|
||||
/// Event type: m.room.create
|
||||
pub const M_ROOM_CREATE: &str = "m.room.create";
|
||||
/// Event type: m.room.join_rules
|
||||
pub const M_ROOM_JOIN_RULES: &str = "m.room.join_rules";
|
||||
/// Event type: m.room.power_levels
|
||||
pub const M_ROOM_POWER_LEVELS: &str = "m.room.power_levels";
|
||||
/// Event type: m.room.aliases
|
||||
pub const M_ROOM_ALIASES: &str = "m.room.aliases";
|
||||
/// Event type: m.room.history_visibility
|
||||
pub const M_ROOM_HISTORY_VISIBILITY: &str = "m.room.history_visibility";
|
||||
/// Event type: m.room.redaction
|
||||
pub const M_ROOM_REDACTION: &str = "m.room.redaction";
|
||||
}
|
||||
|
||||
/// Event Fields
|
||||
pub mod event_field {
|
||||
/// Event field: auth_events
|
||||
pub const AUTH_EVENTS: &str = "auth_events";
|
||||
/// Event field: content
|
||||
pub const CONTENT: &str = "content";
|
||||
/// Event field: depth
|
||||
pub const DEPTH: &str = "depth";
|
||||
/// Event field: hashes
|
||||
pub const HASHES: &str = "hashes";
|
||||
/// Event field: origin_server_ts
|
||||
pub const ORIGIN_SERVER_TS: &str = "origin_server_ts";
|
||||
/// Event field: prev_events
|
||||
pub const PREV_EVENTS: &str = "prev_events";
|
||||
/// Event field: room_id
|
||||
pub const ROOM_ID: &str = "room_id";
|
||||
/// Event field: sender
|
||||
pub const SENDER: &str = "sender";
|
||||
/// Event field: signatures
|
||||
pub const SIGNATURES: &str = "signatures";
|
||||
/// Event field: state_key
|
||||
pub const STATE_KEY: &str = "state_key";
|
||||
/// Event field: type
|
||||
pub const TYPE: &str = "type";
|
||||
/// Event field: unsigned
|
||||
pub const UNSIGNED: &str = "unsigned";
|
||||
/// Event field: event_id
|
||||
pub const EVENT_ID: &str = "event_id";
|
||||
/// Event field: origin
|
||||
pub const ORIGIN: &str = "origin";
|
||||
/// Event field: prev_state
|
||||
pub const PREV_STATE: &str = "prev_state";
|
||||
/// Event field: membership
|
||||
pub const MEMBERSHIP: &str = "membership";
|
||||
/// Event field: replaces_state
|
||||
pub const REPLACES_STATE: &str = "replaces_state";
|
||||
/// Event field: msc4354_sticky
|
||||
pub const MSC4354_STICKY: &str = "msc4354_sticky";
|
||||
// Event field: prev_state_events
|
||||
pub const PREV_STATE_EVENTS: &str = "prev_state_events";
|
||||
// Event field: m.relates_to
|
||||
pub const M_RELATES_TO: &str = "m.relates_to";
|
||||
}
|
||||
|
||||
pub mod unsigned_field {
|
||||
/// Unsigned field: age
|
||||
pub const AGE: &str = "age";
|
||||
/// Unsigned field: age_ts
|
||||
pub const AGE_TS: &str = "age_ts";
|
||||
/// Unsigned field: redacted_because
|
||||
pub const REDACTED_BECAUSE: &str = "redacted_because";
|
||||
}
|
||||
|
||||
/// Membership Event Fields
|
||||
pub mod membership_field {
|
||||
/// Membership event field: membership
|
||||
pub const MEMBERSHIP: &str = "membership";
|
||||
/// Membership event field: join_authorised_via_users_server
|
||||
pub const JOIN_AUTHORISED_VIA_USERS_SERVER: &str = "join_authorised_via_users_server";
|
||||
/// Membership event field: third_party_invite
|
||||
pub const THIRD_PARTY_INVITE: &str = "third_party_invite";
|
||||
/// Membership event field: signed
|
||||
pub const SIGNED: &str = "signed";
|
||||
}
|
||||
|
||||
/// Create Event Fields
|
||||
pub mod create_field {
|
||||
/// Create event field: creator
|
||||
pub const CREATOR: &str = "creator";
|
||||
}
|
||||
|
||||
/// Join Rules Event Fields
|
||||
pub mod join_rules_field {
|
||||
/// Join Rules event field: join_rule
|
||||
pub const JOIN_RULE: &str = "join_rule";
|
||||
/// Join Rules event field: allow
|
||||
pub const ALLOW: &str = "allow";
|
||||
}
|
||||
|
||||
/// Power Levels Event Fields
|
||||
pub mod power_levels_field {
|
||||
/// Power Levels event field: users
|
||||
pub const USERS: &str = "users";
|
||||
/// Power Levels event field: users_default
|
||||
pub const USERS_DEFAULT: &str = "users_default";
|
||||
/// Power Levels event field: events
|
||||
pub const EVENTS: &str = "events";
|
||||
/// Power Levels event field: events_default
|
||||
pub const EVENTS_DEFAULT: &str = "events_default";
|
||||
/// Power Levels event field: state_default
|
||||
pub const STATE_DEFAULT: &str = "state_default";
|
||||
/// Power Levels event field: ban
|
||||
pub const BAN: &str = "ban";
|
||||
/// Power Levels event field: kick
|
||||
pub const KICK: &str = "kick";
|
||||
/// Power Levels event field: redact
|
||||
pub const REDACT: &str = "redact";
|
||||
/// Power Levels event field: invite
|
||||
pub const INVITE: &str = "invite";
|
||||
}
|
||||
|
||||
/// Aliases Event Fields
|
||||
pub mod aliases_field {
|
||||
/// Aliases event field: aliases
|
||||
pub const ALIASES: &str = "aliases";
|
||||
}
|
||||
|
||||
/// History Visibility Event Fields
|
||||
pub mod history_visibility_field {
|
||||
/// History Visibility event field: history_visibility
|
||||
pub const HISTORY_VISIBILITY: &str = "history_visibility";
|
||||
}
|
||||
|
||||
/// Redaction Event Fields
|
||||
pub mod redaction_field {
|
||||
/// Redacts event field: redacts
|
||||
pub const REDACTS: &str = "redacts";
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* 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>.
|
||||
*
|
||||
*/
|
||||
|
||||
//! Over-the-wire representations of Matrix events, parameterised by event
|
||||
//! format version (i.e. the structure we parse the event JSON into).
|
||||
//!
|
||||
//! # Design
|
||||
//!
|
||||
//! The shape of an event's JSON varies with the room version, but there are
|
||||
//! many common fields across all of them — `type`, `sender`, `content`, etc.
|
||||
//!
|
||||
//! We model this with a single [`FormattedEvent`] container that is generic
|
||||
//! over the format-specific tail `E`. Serde `#[serde(flatten)]` merges the
|
||||
//! common and specific halves into a single JSON object over the wire, while
|
||||
//! keeping them as distinct structs in Rust. This lets version-agnostic code
|
||||
//! (field getters, the `unsigned` accessor, …) read [`EventCommonFields`]
|
||||
//! directly, and only the small amount of version-aware logic (auth-event
|
||||
//! derivation, room-ID lookup, validation) needs to match on the format.
|
||||
//!
|
||||
//! The default `E` parameter is the type-erased [`EventFormatEnum`], which can
|
||||
//! contain any known format. The [`FormattedEvent::into_general`] method allows
|
||||
//! converting from a specific format to the general enum.
|
||||
//!
|
||||
//! The `signatures` and `unsigned` fields are kept distinct from the
|
||||
//! common/specific as they allow mutation. When copying an event they need to
|
||||
//! be deep-copied, but the common/specific fields (which are immutable) can be
|
||||
//! shared.
|
||||
//!
|
||||
//! # Format variants
|
||||
//!
|
||||
//! Different room versions have different over-the-wire formats, which is
|
||||
//! tracked by [`crate::room_versions::RoomVersion::event_format`] field.
|
||||
//!
|
||||
//! Each format struct owns only its version-specific fields and any
|
||||
//! validation/derivation logic; the rest lives in [`EventCommonFields`]. The
|
||||
//! [`EventFormatEnum`] sum type erases the generic parameter when an `Event`
|
||||
//! needs to be stored alongside others of unknown room version.
|
||||
//!
|
||||
//! Note that any fields not recognised by the format-specific struct or by the
|
||||
//! common fields are captured into [`EventCommonFields::other_fields`] and
|
||||
//! round-tripped losslessly. This is useful for capturing optional fields that
|
||||
//! don't need to be parsed up front. Generally, optional fields should be
|
||||
//! handled via `other_fields`, as this saves space when they are not present.
|
||||
//!
|
||||
//! # Serialization and deserialization
|
||||
//!
|
||||
//! Deserializing a Matrix Event from JSON is done by specifying the expected
|
||||
//! format struct (e.g. [`FormattedEvent<EventFormatV4>`]), which enforces the
|
||||
//! invariants of that format at parse time. This can then be converted into the
|
||||
//! version-agnostic [`FormattedEvent`] with the
|
||||
//! [`FormattedEvent::into_general`] method, which erases the format-specific
|
||||
//! type but keeps the parsed fields intact.
|
||||
//!
|
||||
//! Serializing a [`FormattedEvent`] produces the correct Matrix Event JSON
|
||||
//! shape for the format variant it contains.
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use anyhow::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
events::{json_object::JsonObject, signatures::Signatures, unsigned::Unsigned},
|
||||
json::AllowMissing,
|
||||
};
|
||||
|
||||
mod v1;
|
||||
mod v2v3;
|
||||
mod v4;
|
||||
mod vmsc4242;
|
||||
|
||||
pub use v1::EventFormatV1;
|
||||
pub use v2v3::EventFormatV2V3;
|
||||
pub use v4::EventFormatV4;
|
||||
pub use vmsc4242::EventFormatVMSC4242;
|
||||
|
||||
/// A parsed Matrix event in its over-the-wire layout.
|
||||
///
|
||||
/// `E` is the format-specific tail. Code that deserialises a known
|
||||
/// room version picks a concrete `E` (e.g. `FormattedEvent<EventFormatV4>`);
|
||||
/// the default `Arc<EventFormatEnum>` is used once the event has been
|
||||
/// boxed into the version-agnostic [`Event`](crate::events::Event)
|
||||
/// pyclass.
|
||||
///
|
||||
/// The `signatures` and `unsigned` fields are kept separate from the other
|
||||
/// fields as they are mutable (and must be deep-copied if the event is cloned).
|
||||
/// `common_fields` and `specific_fields` are both `#[serde(flatten)]`ed so that
|
||||
/// the serialised JSON is a single flat object matching the Matrix spec.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FormattedEvent<E = Arc<EventFormatEnum>> {
|
||||
/// The event's signatures.
|
||||
///
|
||||
/// Kept separate from common/specific fields as this this is a mutable
|
||||
/// field.
|
||||
#[serde(default)]
|
||||
pub signatures: Signatures,
|
||||
|
||||
/// The event's unsigned data.
|
||||
///
|
||||
/// Kept separate from common/specific fields as this this is a mutable
|
||||
/// field.
|
||||
#[serde(default)]
|
||||
pub unsigned: Unsigned,
|
||||
|
||||
/// The format-specific fields of the event. This is an immutable field.
|
||||
#[serde(flatten)]
|
||||
pub specific_fields: E,
|
||||
|
||||
/// The fields common to all event formats. This is an immutable field.
|
||||
#[serde(flatten)]
|
||||
pub common_fields: Arc<EventCommonFields>,
|
||||
}
|
||||
|
||||
impl FormattedEvent {
|
||||
/// Creates a deep copy of this event, allowing the signatures and unsigned
|
||||
/// to be mutated without affecting the original.
|
||||
///
|
||||
/// The common and specific fields are shared between the copy and the
|
||||
/// original, as they are immutable.
|
||||
pub fn deep_copy(&self) -> FormattedEvent {
|
||||
FormattedEvent {
|
||||
signatures: self.signatures.deep_copy(),
|
||||
unsigned: self.unsigned.deep_copy(),
|
||||
// These fields can safely be shared among all of the copies as they
|
||||
// are immutable (they're behind an Arc and so you can't get a
|
||||
// mutable reference and they have no interior mutability) and these
|
||||
// write protections extend into Python land as well (i.e. you can't
|
||||
// accidentally do the wrong thing and mutate)
|
||||
specific_fields: Arc::clone(&self.specific_fields),
|
||||
common_fields: Arc::clone(&self.common_fields),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), Error> {
|
||||
match &*self.specific_fields {
|
||||
EventFormatEnum::V1(format) => format.validate(&self.common_fields),
|
||||
EventFormatEnum::V2V3(format) => format.validate(&self.common_fields),
|
||||
EventFormatEnum::V4(format) => format.validate(&self.common_fields),
|
||||
EventFormatEnum::VMSC4242(format) => format.validate(&self.common_fields),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> FormattedEvent<E>
|
||||
where
|
||||
E: Into<EventFormatEnum>,
|
||||
{
|
||||
/// Transforms a container of a specific event format into a container of
|
||||
/// the enum type.
|
||||
pub fn into_general(self) -> FormattedEvent {
|
||||
let format: Arc<EventFormatEnum> = Arc::new(self.specific_fields.into());
|
||||
FormattedEvent {
|
||||
signatures: self.signatures,
|
||||
unsigned: self.unsigned,
|
||||
specific_fields: format,
|
||||
common_fields: self.common_fields,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<FormattedEvent<E>> for FormattedEvent
|
||||
where
|
||||
E: Into<EventFormatEnum>,
|
||||
{
|
||||
fn from(container: FormattedEvent<E>) -> Self {
|
||||
container.into_general()
|
||||
}
|
||||
}
|
||||
|
||||
/// Fields that appear in every supported event format.
|
||||
///
|
||||
/// Anything not recognised by the format-specific tail or by the fields
|
||||
/// named here is captured into `other_fields` so events round-trip
|
||||
/// losslessly even when they carry experimental or future-version
|
||||
/// keys.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EventCommonFields {
|
||||
pub content: JsonObject,
|
||||
pub depth: i64,
|
||||
pub hashes: HashMap<Box<str>, Box<str>>,
|
||||
pub origin_server_ts: i64,
|
||||
pub sender: Box<str>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "crate::json::allow_missing",
|
||||
skip_serializing_if = "AllowMissing::is_absent"
|
||||
)]
|
||||
pub state_key: AllowMissing<Box<str>>,
|
||||
|
||||
/// The `type` field of the event (we use `type_` in Rust to avoid the
|
||||
/// reserved keyword).
|
||||
#[serde(rename = "type")]
|
||||
pub type_: Box<str>,
|
||||
|
||||
/// All other fields that are not required/parsed by the specific/common
|
||||
/// fields. This allows us to round-trip events that contain extra fields.
|
||||
///
|
||||
/// Generally, optional fields should be handled via `other_fields`, as this
|
||||
/// saves space when they are not present. However, that does mean we don't
|
||||
/// do any type-checking until they get used.
|
||||
#[serde(flatten)]
|
||||
pub other_fields: HashMap<Box<str>, serde_json::Value>,
|
||||
}
|
||||
|
||||
impl EventCommonFields {
|
||||
/// Helper method to check if the event is a state event and return the
|
||||
/// tuple of `(type, state_key)` if so.
|
||||
fn type_state_key_tuple(&self) -> Option<(&str, &str)> {
|
||||
if let AllowMissing::Some(state_key) = &self.state_key {
|
||||
Some((&self.type_, state_key))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Type-erased version-specific tail.
|
||||
///
|
||||
/// Used as the default `E` parameter on [`FormattedEvent`] so the
|
||||
/// pyclass [`Event`](crate::events::Event) can hold any room version
|
||||
/// behind a single type. The enum is `#[serde(untagged)]` because the
|
||||
/// discriminator (the room version) lives outside the JSON; in
|
||||
/// practice the only direction this is serialised in is `Event ->
|
||||
/// JSON`, where the chosen variant alone determines the shape.
|
||||
#[derive(Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum EventFormatEnum {
|
||||
V1(EventFormatV1),
|
||||
V2V3(EventFormatV2V3),
|
||||
V4(EventFormatV4),
|
||||
VMSC4242(EventFormatVMSC4242),
|
||||
}
|
||||
|
||||
impl From<EventFormatV1> for EventFormatEnum {
|
||||
fn from(format: EventFormatV1) -> Self {
|
||||
EventFormatEnum::V1(format)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventFormatV2V3> for EventFormatEnum {
|
||||
fn from(format: EventFormatV2V3) -> Self {
|
||||
EventFormatEnum::V2V3(format)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventFormatV4> for EventFormatEnum {
|
||||
fn from(format: EventFormatV4) -> Self {
|
||||
EventFormatEnum::V4(format)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventFormatVMSC4242> for EventFormatEnum {
|
||||
fn from(format: EventFormatVMSC4242) -> Self {
|
||||
EventFormatEnum::VMSC4242(format)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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>.
|
||||
*
|
||||
*/
|
||||
|
||||
//! Event format v1 (room versions 1 and 2).
|
||||
//!
|
||||
//! Distinguishing features compared to later formats:
|
||||
//!
|
||||
//! - `auth_events` and `prev_events` are `[event_id, hashes]` pairs
|
||||
//! rather than flat lists of IDs.
|
||||
//! - `event_id` is carried explicitly in the event JSON, rather than
|
||||
//! being derived from the canonical-JSON hash.
|
||||
//! - `room_id` is always present.
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use anyhow::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::events::formats::EventCommonFields;
|
||||
|
||||
/// Version-specific fields for room versions 1 and 2.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EventFormatV1 {
|
||||
pub auth_events: Vec<(String, HashMap<String, String>)>,
|
||||
pub prev_events: Vec<(String, HashMap<String, String>)>,
|
||||
pub room_id: Arc<str>,
|
||||
pub event_id: Arc<str>,
|
||||
}
|
||||
|
||||
impl EventFormatV1 {
|
||||
pub fn validate(&self, _common_fields: &EventCommonFields) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn auth_event_ids(&self) -> Vec<String> {
|
||||
self.auth_events.iter().map(|(id, _)| id.clone()).collect()
|
||||
}
|
||||
|
||||
pub fn prev_event_ids(&self) -> Vec<String> {
|
||||
self.prev_events.iter().map(|(id, _)| id.clone()).collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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>.
|
||||
*
|
||||
*/
|
||||
|
||||
//! Event format v2/v3 (room versions 3 through 10).
|
||||
//!
|
||||
//! Differences from v1:
|
||||
//!
|
||||
//! - `auth_events` and `prev_events` are flat `Vec<String>` lists of event IDs
|
||||
//! rather than `[id, hashes]` pairs.
|
||||
//! - `event_id` is no longer in the event JSON; it is derived from the
|
||||
//! canonical-JSON hash at parse time.
|
||||
//!
|
||||
//! Note that the difference between event format v2 and v3 is purely in the
|
||||
//! base64 encoding of the event ID, so the same struct can be used for both
|
||||
//! formats.
|
||||
//!
|
||||
//! [`SimpleAuthPrevEvents`] is shared with [`v4`](super::v4) since the
|
||||
//! flat-list encoding carries forward unchanged.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::events::formats::EventCommonFields;
|
||||
|
||||
/// Version-specific fields for room versions 3-10.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EventFormatV2V3 {
|
||||
pub room_id: Arc<str>,
|
||||
pub auth_events: Vec<String>,
|
||||
pub prev_events: Vec<String>,
|
||||
}
|
||||
|
||||
impl EventFormatV2V3 {
|
||||
pub fn validate(&self, common_fields: &EventCommonFields) -> Result<(), Error> {
|
||||
// Ensure that we don't have an event_id set.
|
||||
if common_fields.other_fields.contains_key("event_id") {
|
||||
bail!("v2/v3 events must not have an explicit event_id");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn auth_event_ids(&self) -> Vec<String> {
|
||||
self.auth_events.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* 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>.
|
||||
*
|
||||
*/
|
||||
|
||||
//! Event format v4 (room version 11).
|
||||
//!
|
||||
//! The main change from v2/v3 is that `room_id` becomes optional: an
|
||||
//! `m.room.create` event no longer carries an explicit room ID, and the
|
||||
//! room ID is instead *derived* from the create event's ID by replacing
|
||||
//! the leading `$` with `!`. Conversely, every non-create event still
|
||||
//! has an explicit `room_id`, and the create event is implicitly
|
||||
//! included in the auth chain of every non-create event (so it does not
|
||||
//! need to appear in `auth_events`).
|
||||
//!
|
||||
//! [`EventFormatV4::validate`] enforces these invariants at parse time;
|
||||
//! [`EventFormatV4::room_id`] and [`EventFormatV4::auth_event_ids`]
|
||||
//! expose the derived values to callers.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{bail, ensure, Error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
events::{constants::event_type::M_ROOM_CREATE, formats::EventCommonFields},
|
||||
json::AllowMissing,
|
||||
};
|
||||
|
||||
/// Version-specific fields for room version 11.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EventFormatV4 {
|
||||
#[serde(
|
||||
default,
|
||||
with = "crate::json::allow_missing",
|
||||
skip_serializing_if = "AllowMissing::is_absent"
|
||||
)]
|
||||
pub room_id: AllowMissing<Arc<str>>,
|
||||
pub auth_events: Vec<String>,
|
||||
pub prev_events: Vec<String>,
|
||||
}
|
||||
|
||||
impl EventFormatV4 {
|
||||
pub fn validate(&self, common_fields: &EventCommonFields) -> Result<(), Error> {
|
||||
// Ensure that we don't have an event_id set.
|
||||
if common_fields.other_fields.contains_key("event_id") {
|
||||
bail!("v4 events must not have an explicit event_id");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn room_id(
|
||||
&self,
|
||||
event_id: &str,
|
||||
common_fields: &EventCommonFields,
|
||||
) -> Result<Arc<str>, Error> {
|
||||
get_room_id_for_optional_room_id(self.room_id.as_ref_opt(), event_id, common_fields)
|
||||
}
|
||||
|
||||
pub fn auth_event_ids(&self, common_fields: &EventCommonFields) -> Result<Vec<String>, Error> {
|
||||
let is_create = common_fields.type_state_key_tuple() == Some((M_ROOM_CREATE, ""));
|
||||
|
||||
if is_create {
|
||||
// The create event itself has no implicit auth events.
|
||||
return Ok(self.auth_events.clone());
|
||||
}
|
||||
|
||||
// For non-create events, the create event is implicitly part of
|
||||
// the auth chain. Derive its event ID from the room ID by
|
||||
// replacing the leading '!' with '$'.
|
||||
let room_id = self
|
||||
.room_id
|
||||
.as_deref_opt()
|
||||
.ok_or_else(|| anyhow::anyhow!("non-create event has no room_id"))?;
|
||||
|
||||
let mut create_event_id = String::with_capacity(room_id.len());
|
||||
create_event_id.push('$');
|
||||
create_event_id.push_str(&room_id[1..]);
|
||||
|
||||
ensure!(
|
||||
!self.auth_events.contains(&create_event_id),
|
||||
"The create event ID is implicitly part of the auth chain and should not be explicitly be in the auth_events"
|
||||
);
|
||||
|
||||
let mut auth_events = self.auth_events.clone();
|
||||
auth_events.push(create_event_id);
|
||||
Ok(auth_events)
|
||||
}
|
||||
}
|
||||
|
||||
/// Validation helper for v4+ events that can have an optional room ID.
|
||||
///
|
||||
/// Returns the validated room ID (which will be `None` for create events).
|
||||
pub fn validate_optional_room_id(
|
||||
room_id: Option<&Arc<str>>,
|
||||
common_fields: &'_ EventCommonFields,
|
||||
) -> Result<Option<Arc<str>>, Error> {
|
||||
let is_create_event = common_fields.type_state_key_tuple() == Some((M_ROOM_CREATE, ""));
|
||||
|
||||
match (is_create_event, room_id) {
|
||||
// For non-create events, room_id must be present.
|
||||
(false, None) => bail!("non-create event must have a room ID"),
|
||||
(false, Some(room_id)) => {
|
||||
// We later derive the create event ID from the room ID by replacing
|
||||
// the leading '!' with '$', so we require the room ID to start with
|
||||
// '!'.
|
||||
ensure!(
|
||||
room_id.starts_with("!"),
|
||||
"room_id must start with '!': {}",
|
||||
room_id
|
||||
);
|
||||
|
||||
Ok(Some(Arc::clone(room_id)))
|
||||
}
|
||||
|
||||
// For create events, room_id must be absent.
|
||||
(true, Some(_)) => bail!("create event must not have a room ID"),
|
||||
(true, None) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Room ID derivation helper for v4+ events, which can have an optional room
|
||||
/// ID.
|
||||
pub fn get_room_id_for_optional_room_id(
|
||||
room_id: Option<&Arc<str>>,
|
||||
event_id: &str,
|
||||
common_fields: &EventCommonFields,
|
||||
) -> Result<Arc<str>, Error> {
|
||||
match validate_optional_room_id(room_id, common_fields)? {
|
||||
Some(room_id) => Ok(room_id),
|
||||
None => {
|
||||
// This is the create event, where the room ID is derived from the
|
||||
// event ID by replacing the leading '$' with '!'.
|
||||
if !event_id.starts_with('$') {
|
||||
bail!("Create event ID does not start with '$': {}", event_id);
|
||||
}
|
||||
|
||||
let mut room_id = String::with_capacity(event_id.len());
|
||||
room_id.push('!');
|
||||
room_id.push_str(&event_id[1..]);
|
||||
|
||||
Ok(room_id.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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>.
|
||||
*
|
||||
*/
|
||||
|
||||
//! Event format for [MSC4242] (prev-state events).
|
||||
//!
|
||||
//! Adds `prev_state_events` and removes `auth_events` from the v4 layout
|
||||
//! — auth chains are derived implicitly from the state DAG rather than
|
||||
//! carried on each event. `room_id`, `prev_events` and the create-event
|
||||
//! derivation rules carry over unchanged from v4 and are delegated to
|
||||
//! [`EventFormatV4::validate`] via a shim that supplies an empty
|
||||
//! explicit auth list.
|
||||
//!
|
||||
//! [MSC4242]: https://github.com/matrix-org/matrix-spec-proposals/pull/4242
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::bail;
|
||||
use anyhow::Error;
|
||||
use pyo3::exceptions::PyAssertionError;
|
||||
use pyo3::PyResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::events::constants::event_type::M_ROOM_CREATE;
|
||||
use crate::events::formats::v4::get_room_id_for_optional_room_id;
|
||||
use crate::events::formats::EventCommonFields;
|
||||
use crate::events::Event;
|
||||
use crate::json::AllowMissing;
|
||||
|
||||
/// Version-specific fields for the MSC4242 event format.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EventFormatVMSC4242 {
|
||||
pub prev_state_events: Vec<String>,
|
||||
pub prev_events: Vec<String>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "crate::json::allow_missing",
|
||||
skip_serializing_if = "AllowMissing::is_absent"
|
||||
)]
|
||||
pub room_id: AllowMissing<Arc<str>>,
|
||||
}
|
||||
|
||||
impl EventFormatVMSC4242 {
|
||||
pub fn validate(&self, common_fields: &EventCommonFields) -> Result<(), Error> {
|
||||
// Ensure that we don't have any `auth_events` or `event_id` fields
|
||||
// set.
|
||||
if common_fields.other_fields.contains_key("auth_events") {
|
||||
bail!("MSC4242 events must not have explicit auth_events");
|
||||
}
|
||||
if common_fields.other_fields.contains_key("event_id") {
|
||||
bail!("MSC4242 events must not have an explicit event_id");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn room_id(
|
||||
&self,
|
||||
event_id: &str,
|
||||
common_fields: &EventCommonFields,
|
||||
) -> Result<Arc<str>, Error> {
|
||||
get_room_id_for_optional_room_id(self.room_id.as_ref_opt(), event_id, common_fields)
|
||||
}
|
||||
|
||||
pub fn auth_event_ids(&self, event: &Event) -> PyResult<Vec<String>> {
|
||||
// In the MSC4242 format, the auth events are calculated and stored in
|
||||
// internal metadata.
|
||||
let auth_event_ids = event.internal_metadata.get_calculated_auth_event_ids()?;
|
||||
|
||||
// Catches cases where we accidentally call auth_event_ids() prior to calculating what they
|
||||
// actually are. The exception being the m.room.create event which has no auth events.
|
||||
if event.parsed_event.common_fields.type_state_key_tuple() != Some((M_ROOM_CREATE, ""))
|
||||
&& auth_event_ids.is_empty()
|
||||
{
|
||||
return Err(PyAssertionError::new_err(format!(
|
||||
"auth_event_ids has not been calculated for event_id='{}'. This is most likely a Synapse programming error.",
|
||||
event.event_id
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(auth_event_ids)
|
||||
}
|
||||
}
|
||||
@@ -510,7 +510,7 @@ fn attr_err<T>(val: Option<T>, name: &str) -> PyResult<T> {
|
||||
#[pymethods]
|
||||
impl EventInternalMetadata {
|
||||
#[new]
|
||||
fn new(dict: &Bound<'_, PyDict>) -> PyResult<Self> {
|
||||
pub fn new(dict: &Bound<'_, PyDict>) -> PyResult<Self> {
|
||||
let mut data = Vec::with_capacity(dict.len());
|
||||
|
||||
for (key, value) in dict.iter() {
|
||||
@@ -536,7 +536,10 @@ impl EventInternalMetadata {
|
||||
})
|
||||
}
|
||||
|
||||
fn copy(&self) -> PyResult<Self> {
|
||||
/// Create a deep copy of this `EventInternalMetadata` to allow modification
|
||||
/// without affecting other references to the same metadata. This is needed
|
||||
/// when we clone an event.
|
||||
pub fn deep_copy(&self) -> PyResult<Self> {
|
||||
let guard = self.read_inner()?;
|
||||
Ok(EventInternalMetadata {
|
||||
inner: Arc::new(RwLock::new(guard.clone())),
|
||||
@@ -723,7 +726,7 @@ impl EventInternalMetadata {
|
||||
attr_err(self.read_inner()?.get_redacted(), "redacted")
|
||||
}
|
||||
#[setter]
|
||||
fn set_redacted(&self, obj: bool) -> PyResult<()> {
|
||||
pub fn set_redacted(&self, obj: bool) -> PyResult<()> {
|
||||
self.write_inner()?.set_redacted(obj);
|
||||
Ok(())
|
||||
}
|
||||
@@ -742,7 +745,7 @@ impl EventInternalMetadata {
|
||||
|
||||
/// The calculated auth event IDs, if it was set when the event was created.
|
||||
#[getter]
|
||||
fn get_calculated_auth_event_ids(&self) -> PyResult<Vec<String>> {
|
||||
pub fn get_calculated_auth_event_ids(&self) -> PyResult<Vec<String>> {
|
||||
let guard = self.read_inner()?;
|
||||
attr_err(
|
||||
guard.get_calculated_auth_event_ids().cloned(),
|
||||
|
||||
@@ -193,6 +193,12 @@ impl JsonObject {
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonObject {
|
||||
pub fn get_field(&self, key: &str) -> Option<&serde_json::Value> {
|
||||
self.object.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper class returned by `JsonObject.keys()` to act as a view into the keys
|
||||
/// of the object.
|
||||
///
|
||||
|
||||
+880
-8
@@ -18,18 +18,77 @@
|
||||
*
|
||||
*/
|
||||
|
||||
//! Classes for representing Events.
|
||||
//! Classes for representing Matrix events.
|
||||
//!
|
||||
//! # Overview
|
||||
//!
|
||||
//! A Matrix event has a JSON shape that varies by *room version*. The
|
||||
//! per-room-version shape is captured in the [`formats`] module, where
|
||||
//! [`FormattedEvent`] is a generic container parametrised by the
|
||||
//! room-version-specific portion (`EventFormatV1`, `EventFormatV2V3`,
|
||||
//! `EventFormatV4`, `EventFormatVMSC4242`). See [`formats`] for the layout
|
||||
//! of the over-the-wire JSON and how the room-version-agnostic fields are
|
||||
//! split from the version-specific ones.
|
||||
//!
|
||||
//! [`Event`] is the `pyclass` exposed to Python. It bundles a fully parsed
|
||||
//! [`FormattedEvent`] (with the version-specific part type-erased as
|
||||
//! [`formats::EventFormatEnum`]) together with the pieces of state that
|
||||
//! live alongside the event JSON in Synapse:
|
||||
//!
|
||||
//! - `event_id` — either taken from the event JSON (format v1) or derived
|
||||
//! from the canonical-JSON hash (v2+); computed once at construction
|
||||
//! time and cached.
|
||||
//! - `room_version` — a `'static` reference into the global room-version
|
||||
//! table, used to drive format-dependent behaviour (e.g. where the
|
||||
//! `redacts` field lives, which redaction rules apply).
|
||||
//! - `internal_metadata` — Synapse-internal flags that are *not* part of
|
||||
//! the federated event (outlier status, soft-failure, stream positions,
|
||||
//! …). These come from a separate dict at construction time.
|
||||
//! - `rejected_reason` — `None` for accepted events; otherwise a short
|
||||
//! string describing why auth rejected the event.
|
||||
//!
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Error;
|
||||
use pyo3::{
|
||||
types::{PyAnyMethods, PyMapping, PyModule, PyModuleMethods},
|
||||
wrap_pyfunction, Bound, PyResult, Python,
|
||||
exceptions::{PyAttributeError, PyKeyError, PyValueError},
|
||||
pyclass, pyfunction, pymethods,
|
||||
types::{PyAnyMethods, PyDict, PyDictMethods, PyList, PyMapping, PyModule, PyModuleMethods},
|
||||
wrap_pyfunction, Bound, IntoPyObject, PyAny, PyResult, Python,
|
||||
};
|
||||
use pythonize::{depythonize, pythonize};
|
||||
|
||||
use crate::events::{
|
||||
formats::{
|
||||
EventFormatEnum, EventFormatV1, EventFormatV2V3, EventFormatV4, EventFormatVMSC4242,
|
||||
FormattedEvent,
|
||||
},
|
||||
signatures::Signatures,
|
||||
unsigned::Unsigned,
|
||||
utils::redact,
|
||||
};
|
||||
use crate::{
|
||||
duration::SynapseDuration,
|
||||
events::{
|
||||
constants::event_field::{HASHES, MSC4354_STICKY, SIGNATURES, UNSIGNED},
|
||||
constants::membership_field::MEMBERSHIP,
|
||||
constants::redaction_field::REDACTS,
|
||||
constants::unsigned_field::{AGE, AGE_TS, REDACTED_BECAUSE},
|
||||
internal_metadata::EventInternalMetadata,
|
||||
utils::calculate_event_id,
|
||||
},
|
||||
room_versions::{EventFormatVersions, RoomVersion},
|
||||
};
|
||||
|
||||
pub mod constants;
|
||||
pub mod filter;
|
||||
mod internal_metadata;
|
||||
mod json_object;
|
||||
pub mod formats;
|
||||
pub mod internal_metadata;
|
||||
pub mod json_object;
|
||||
pub mod signatures;
|
||||
pub mod unsigned;
|
||||
pub mod utils;
|
||||
|
||||
use json_object::JsonObject;
|
||||
|
||||
@@ -39,14 +98,17 @@ pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()>
|
||||
PyMapping::register::<JsonObject>(py)?;
|
||||
|
||||
let child_module = PyModule::new(py, "events")?;
|
||||
child_module.add_class::<internal_metadata::EventInternalMetadata>()?;
|
||||
child_module.add_class::<signatures::Signatures>()?;
|
||||
child_module.add_class::<unsigned::Unsigned>()?;
|
||||
child_module.add_class::<EventInternalMetadata>()?;
|
||||
child_module.add_class::<Signatures>()?;
|
||||
child_module.add_class::<Unsigned>()?;
|
||||
child_module.add_class::<JsonObject>()?;
|
||||
child_module.add_class::<json_object::JsonObjectKeysView>()?;
|
||||
child_module.add_class::<json_object::JsonObjectValuesView>()?;
|
||||
child_module.add_class::<json_object::JsonObjectItemsView>()?;
|
||||
child_module.add_class::<Event>()?;
|
||||
child_module.add_function(wrap_pyfunction!(filter::event_visible_to_server_py, m)?)?;
|
||||
child_module.add_function(wrap_pyfunction!(redact_event_py, m)?)?;
|
||||
child_module.add_function(wrap_pyfunction!(redact_event_dict, m)?)?;
|
||||
|
||||
m.add_submodule(&child_module)?;
|
||||
|
||||
@@ -58,3 +120,813 @@ pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()>
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The Rust-side representation of a Matrix event, exposed to Python.
|
||||
///
|
||||
/// Wraps a parsed [`FormattedEvent`] together with the per-event state
|
||||
/// that Synapse tracks outside the event JSON (event ID, internal
|
||||
/// metadata, rejection reason, and a reference to the room version that
|
||||
/// produced this event). See the module-level docs for the high-level
|
||||
/// design.
|
||||
#[pyclass(frozen, weakref)]
|
||||
pub struct Event {
|
||||
/// The parsed event JSON.
|
||||
parsed_event: FormattedEvent,
|
||||
|
||||
/// The event ID. For format v1 this is read directly from the JSON;
|
||||
/// for v2+ it is computed from the canonical-JSON hash at
|
||||
/// construction time and cached here.
|
||||
event_id: Arc<str>,
|
||||
|
||||
/// The calculated room ID.
|
||||
///
|
||||
/// For some room versions, this may be derived, e.g. for create events in
|
||||
/// v4.
|
||||
room_id: Arc<str>,
|
||||
|
||||
/// Synapse-internal per-event state that lives outside the federated
|
||||
/// JSON (e.g. outlier flag, soft-failure, stream positions).
|
||||
#[pyo3(get)]
|
||||
internal_metadata: EventInternalMetadata,
|
||||
|
||||
/// The room version this event was parsed for.
|
||||
#[pyo3(get)]
|
||||
room_version: &'static RoomVersion,
|
||||
|
||||
/// `None` for accepted events; otherwise a short reason set by auth
|
||||
/// when the event was rejected.
|
||||
rejected_reason: Option<Box<str>>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl Event {
|
||||
#[new]
|
||||
fn new_from_py<'a, 'py>(
|
||||
py: Python<'py>,
|
||||
event_dict: &'a Bound<'py, PyAny>,
|
||||
room_version: &'a Bound<'py, PyAny>,
|
||||
internal_metadata_dict: &'a Bound<'py, PyDict>,
|
||||
rejected_reason: Option<String>,
|
||||
) -> PyResult<Self> {
|
||||
let room_version: &RoomVersion = {
|
||||
let r = room_version.getattr("identifier")?;
|
||||
let room_version_str = r.extract::<&str>()?;
|
||||
room_version_str
|
||||
.parse()
|
||||
.map_err(|e| PyValueError::new_err(format!("Unsupported room version: {}", e)))?
|
||||
};
|
||||
|
||||
let rejected_reason = rejected_reason.map(String::into_boxed_str);
|
||||
|
||||
// Parse the event dict into a FormattedEvent, converting any failures to
|
||||
// a `ValueError`.
|
||||
let parsed_event = depythonize_event_dict(room_version, event_dict).map_err(|err| {
|
||||
let new_err = PyValueError::new_err(format!(
|
||||
"Failed to parse event for room version {}",
|
||||
room_version
|
||||
));
|
||||
new_err.set_cause(py, Some(err));
|
||||
new_err
|
||||
})?;
|
||||
|
||||
let internal_metadata = EventInternalMetadata::new(internal_metadata_dict)?;
|
||||
|
||||
let event_id = match &*parsed_event.specific_fields {
|
||||
EventFormatEnum::V1(format) => {
|
||||
// V1/V2 events have the event_id in the event dict.
|
||||
Arc::clone(&format.event_id)
|
||||
}
|
||||
_ => {
|
||||
// Calculate the event ID by hashing the event JSON. This can
|
||||
// fail if the event can't be serialized to canonical JSON (e.g.
|
||||
// having out-of-range integers), which we report as
|
||||
// `ValueError` as it indicates the event is invalid.
|
||||
let event_value = serde_json::to_value(&parsed_event).map_err(|err| {
|
||||
PyValueError::new_err(format!("Failed to serialize event: {}", err))
|
||||
})?;
|
||||
calculate_event_id(&event_value, room_version)
|
||||
.map_err(|err| {
|
||||
PyValueError::new_err(format!("Failed to calculate event_id: {}", err))
|
||||
})?
|
||||
.into()
|
||||
}
|
||||
};
|
||||
|
||||
let room_id = match &*parsed_event.specific_fields {
|
||||
EventFormatEnum::V1(format) => Arc::clone(&format.room_id),
|
||||
EventFormatEnum::V2V3(format) => Arc::clone(&format.room_id),
|
||||
EventFormatEnum::V4(format) => format
|
||||
.room_id(&event_id, &parsed_event.common_fields)
|
||||
.map_err(|err| {
|
||||
PyValueError::new_err(format!(
|
||||
"Failed to calculate room_id for event {}: {}",
|
||||
event_id, err
|
||||
))
|
||||
})?,
|
||||
EventFormatEnum::VMSC4242(format) => format
|
||||
.room_id(&event_id, &parsed_event.common_fields)
|
||||
.map_err(|err| {
|
||||
PyValueError::new_err(format!(
|
||||
"Failed to calculate room_id for event {}: {}",
|
||||
event_id, err
|
||||
))
|
||||
})?,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
parsed_event,
|
||||
|
||||
event_id,
|
||||
room_id,
|
||||
room_version,
|
||||
rejected_reason,
|
||||
internal_metadata,
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert the event to a dictionary suitable for serialisation.
|
||||
fn get_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
|
||||
Ok(pythonize(py, &self.parsed_event)?)
|
||||
}
|
||||
|
||||
/// Like `get_dict`, but serializes `unsigned` in a form suitable for
|
||||
/// persistence.
|
||||
fn get_dict_for_persistence<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
|
||||
let binding = self.get_dict(py)?;
|
||||
let dict = binding.cast::<PyDict>()?;
|
||||
|
||||
dict.set_item("unsigned", self.parsed_event.unsigned.for_persistence(py)?)?;
|
||||
|
||||
Ok(binding)
|
||||
}
|
||||
|
||||
/// Like [`Event::get_dict`], but serializes `unsigned` in a form suitable
|
||||
/// for sending over federation.
|
||||
#[pyo3(signature = (time_now = None))]
|
||||
fn get_pdu_json<'py>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
time_now: Option<i64>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
let obj = self.get_dict(py)?;
|
||||
let dict = obj.cast::<PyDict>()?;
|
||||
|
||||
// Get or create the unsigned dict
|
||||
if let Ok(Some(unsigned)) = dict.get_item(UNSIGNED) {
|
||||
let unsigned = unsigned.cast::<PyDict>()?;
|
||||
|
||||
if let Some(time_now) = time_now {
|
||||
if let Ok(Some(age_ts)) = unsigned.get_item(AGE_TS) {
|
||||
let age = time_now - age_ts.extract::<i64>()?;
|
||||
unsigned.set_item(AGE, age)?;
|
||||
unsigned.del_item(AGE_TS)?;
|
||||
}
|
||||
}
|
||||
|
||||
// This may be a frozen event
|
||||
unsigned.del_item(REDACTED_BECAUSE).ok();
|
||||
}
|
||||
|
||||
Ok(obj)
|
||||
}
|
||||
|
||||
/// Like [`Event::get_dict`], except strips fields like `signatures`,
|
||||
/// `hashes` and `unsigned` so that the result is suitable as a template for
|
||||
/// creating new events. Used in make_{join,leave,knock} flows.
|
||||
fn get_templated_pdu_json<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
|
||||
// Use get_dict but strip signatures, unsigned, and hashes — the
|
||||
// joining/leaving/knocking server will re-sign and recalculate hashes.
|
||||
let obj = self.get_dict(py)?;
|
||||
let dict = obj.cast::<PyDict>()?;
|
||||
dict.del_item(SIGNATURES).ok();
|
||||
dict.del_item(UNSIGNED).ok();
|
||||
dict.del_item(HASHES).ok();
|
||||
|
||||
Ok(obj)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn rejected_reason(&self) -> Option<&str> {
|
||||
self.rejected_reason.as_deref()
|
||||
}
|
||||
|
||||
/// Returns the list of prev event IDs. The order matches the order
|
||||
/// specified in the event, though there is no meaning to it.
|
||||
fn prev_event_ids(&self) -> Vec<String> {
|
||||
match &*self.parsed_event.specific_fields {
|
||||
EventFormatEnum::V1(format) => format.prev_event_ids(),
|
||||
EventFormatEnum::V2V3(format) => format.prev_events.clone(),
|
||||
EventFormatEnum::V4(format) => format.prev_events.clone(),
|
||||
EventFormatEnum::VMSC4242(format) => format.prev_events.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the list of auth event IDs. The order matches the order
|
||||
/// specified in the event, though there is no meaning to it.
|
||||
fn auth_event_ids(&self) -> PyResult<Vec<String>> {
|
||||
match &*self.parsed_event.specific_fields {
|
||||
EventFormatEnum::V1(format) => Ok(format.auth_event_ids()),
|
||||
EventFormatEnum::V2V3(format) => Ok(format.auth_event_ids()),
|
||||
EventFormatEnum::V4(format) => {
|
||||
Ok(format.auth_event_ids(&self.parsed_event.common_fields)?)
|
||||
}
|
||||
EventFormatEnum::VMSC4242(format) => Ok(format.auth_event_ids(self)?),
|
||||
}
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn membership<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
|
||||
let content = self.content();
|
||||
let value = content.get_field(MEMBERSHIP);
|
||||
match value {
|
||||
Some(value) => Ok(pythonize(py, value)?),
|
||||
None => Err(PyKeyError::new_err(MEMBERSHIP)),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_state(&self) -> bool {
|
||||
self.parsed_event.common_fields.state_key.is_some()
|
||||
}
|
||||
|
||||
/// Get the state key of this event, or None if it's not a state event.
|
||||
fn get_state_key(&self) -> Option<&str> {
|
||||
self.parsed_event.common_fields.state_key.as_deref_opt()
|
||||
}
|
||||
|
||||
/// The EventFormatVersion implemented by this event.
|
||||
#[getter]
|
||||
fn format_version(&self) -> i32 {
|
||||
self.room_version.event_format
|
||||
}
|
||||
|
||||
/// Returns a deep copy of this object, such that modifying the copy will
|
||||
/// not affect the original.
|
||||
fn deep_copy(&self) -> PyResult<Event> {
|
||||
let internal_metadata = self.internal_metadata.deep_copy()?;
|
||||
|
||||
let new_event = Event {
|
||||
parsed_event: self.parsed_event.deep_copy(),
|
||||
internal_metadata,
|
||||
room_version: self.room_version,
|
||||
rejected_reason: self.rejected_reason.clone(),
|
||||
event_id: self.event_id.clone(),
|
||||
room_id: self.room_id.clone(),
|
||||
};
|
||||
Ok(new_event)
|
||||
}
|
||||
|
||||
/// If this event has the `msc4354_sticky` top-level field, returns a
|
||||
/// `SynapseDuration` representing the sticky duration. Otherwise returns
|
||||
/// `None`.
|
||||
fn sticky_duration(&self) -> Option<SynapseDuration> {
|
||||
const MAX_DURATION: SynapseDuration = SynapseDuration::from_hours(1);
|
||||
|
||||
let sticky_obj = self
|
||||
.parsed_event
|
||||
.common_fields
|
||||
.other_fields
|
||||
.get(MSC4354_STICKY);
|
||||
|
||||
let sticky_obj = match sticky_obj {
|
||||
Some(serde_json::Value::Object(obj)) => obj,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// Check for a valid duration field. The MSC requires `duration_ms` to
|
||||
// be a non-negative integer. If it's missing or invalid, we treat the
|
||||
// event as non-sticky by returning `None`.
|
||||
let duration_ms = sticky_obj.get("duration_ms")?.as_u64()?;
|
||||
|
||||
let duration = SynapseDuration::from_milliseconds(duration_ms);
|
||||
|
||||
let duration = std::cmp::min(duration, MAX_DURATION);
|
||||
|
||||
Some(duration)
|
||||
}
|
||||
|
||||
// Below are the methods for interacting with the event as a mapping.
|
||||
//
|
||||
// These are rarely used, so we take the easy approach of re-serializing the
|
||||
// event to a Python dict and then delegating to the standard dict methods.
|
||||
// We can't remove these functions as third-party modules may rely on them.
|
||||
|
||||
fn __contains__<'py>(&self, py: Python<'py>, key: &str) -> PyResult<bool> {
|
||||
let dict = self.get_dict(py)?;
|
||||
dict.contains(key)
|
||||
}
|
||||
|
||||
/// This is deprecated in favor of `get`, but we still need to support it
|
||||
/// for backwards compatibility with modules. This is therefore not exposed
|
||||
/// in the type stubs.
|
||||
fn __getitem__<'py>(&self, py: Python<'py>, key: &str) -> PyResult<Bound<'py, PyAny>> {
|
||||
let dict = self.get_dict(py)?;
|
||||
if dict.contains(key)? {
|
||||
dict.get_item(key)
|
||||
} else {
|
||||
Err(PyKeyError::new_err(key.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
#[pyo3(signature = (key, default=None))]
|
||||
fn get<'py>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
key: &str,
|
||||
default: Option<Bound<'py, PyAny>>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
let dict = self.get_dict(py)?;
|
||||
if dict.contains(key)? {
|
||||
dict.get_item(key)
|
||||
} else {
|
||||
Ok(default.into_pyobject(py)?)
|
||||
}
|
||||
}
|
||||
|
||||
fn items<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
|
||||
let dict = self.get_dict(py)?;
|
||||
let dict = dict.cast::<PyDict>()?;
|
||||
Ok(dict.items())
|
||||
}
|
||||
|
||||
fn keys<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
|
||||
let dict = self.get_dict(py)?;
|
||||
let dict = dict.cast::<PyDict>()?;
|
||||
Ok(dict.keys())
|
||||
}
|
||||
|
||||
// Below are the getters for the top-level fields on Matrix events.
|
||||
|
||||
#[getter]
|
||||
fn event_id(&self) -> &str {
|
||||
&self.event_id
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn room_id(&self) -> &str {
|
||||
&self.room_id
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn signatures(&self) -> Signatures {
|
||||
self.parsed_event.signatures.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn content(&self) -> JsonObject {
|
||||
self.parsed_event.common_fields.content.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn depth(&self) -> i64 {
|
||||
self.parsed_event.common_fields.depth
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn hashes<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
|
||||
let dict = PyDict::new(py);
|
||||
for (key, value) in &self.parsed_event.common_fields.hashes {
|
||||
dict.set_item(&**key, &**value)?;
|
||||
}
|
||||
Ok(dict)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn origin_server_ts(&self) -> i64 {
|
||||
self.parsed_event.common_fields.origin_server_ts
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn sender(&self) -> &str {
|
||||
&self.parsed_event.common_fields.sender
|
||||
}
|
||||
|
||||
/// Deprecated alias for `sender`. Kept for backwards compatibility with
|
||||
/// modules and tests that still read `event.user_id`. This is therefore not
|
||||
/// exposed in the type stubs.
|
||||
#[getter]
|
||||
fn user_id(&self) -> &str {
|
||||
&self.parsed_event.common_fields.sender
|
||||
}
|
||||
|
||||
#[getter(state_key)]
|
||||
// We can't call this `state_key` because that would generate a
|
||||
// `get_state_key` method which already exists.
|
||||
fn state_key_attr(&self) -> PyResult<&str> {
|
||||
let Some(state_key) = self.parsed_event.common_fields.state_key.as_deref_opt() else {
|
||||
return Err(PyAttributeError::new_err("state_key"));
|
||||
};
|
||||
Ok(state_key)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn r#type(&self) -> &str {
|
||||
&self.parsed_event.common_fields.type_
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn unsigned(&self) -> Unsigned {
|
||||
self.parsed_event.unsigned.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn prev_state_events(&self) -> PyResult<Vec<String>> {
|
||||
// `prev_state_events` should only be called after validating the event
|
||||
// is of a format that supports MSC4242, so we return an AttributeError
|
||||
// for formats that don't support it.
|
||||
match &*self.parsed_event.specific_fields {
|
||||
EventFormatEnum::V1(_) | EventFormatEnum::V2V3(_) | EventFormatEnum::V4(_) => {
|
||||
Err(PyAttributeError::new_err("prev_state_events"))
|
||||
}
|
||||
EventFormatEnum::VMSC4242(format) => Ok(format.prev_state_events.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn redacts<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyAny>>> {
|
||||
let common = &self.parsed_event.common_fields;
|
||||
let value = if self.room_version.updated_redaction_rules {
|
||||
common.content.get_field(REDACTS)
|
||||
} else {
|
||||
common.other_fields.get(REDACTS)
|
||||
};
|
||||
value
|
||||
.map(|v| pythonize(py, v).map_err(Into::into))
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
fn depythonize_event_dict(
|
||||
room_version: &RoomVersion,
|
||||
event_dict: &Bound<'_, PyAny>,
|
||||
) -> PyResult<FormattedEvent> {
|
||||
let formatted_event: FormattedEvent = match room_version.event_format {
|
||||
EventFormatVersions::ROOM_V1_V2 => {
|
||||
let event_format: FormattedEvent<EventFormatV1> = depythonize(event_dict)?;
|
||||
|
||||
event_format.into()
|
||||
}
|
||||
EventFormatVersions::ROOM_V3 | EventFormatVersions::ROOM_V4_PLUS => {
|
||||
let event_format: FormattedEvent<EventFormatV2V3> = depythonize(event_dict)?;
|
||||
event_format.into()
|
||||
}
|
||||
EventFormatVersions::ROOM_V11_HYDRA_PLUS => {
|
||||
let event_format: FormattedEvent<EventFormatV4> = depythonize(event_dict)?;
|
||||
event_format.into()
|
||||
}
|
||||
EventFormatVersions::ROOM_VMSC4242 => {
|
||||
let event_format: FormattedEvent<EventFormatVMSC4242> = depythonize(event_dict)?;
|
||||
event_format.into()
|
||||
}
|
||||
_ => {
|
||||
return Err(PyValueError::new_err(format!(
|
||||
"Unsupported room version: {}",
|
||||
room_version
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
formatted_event.validate()?;
|
||||
|
||||
Ok(formatted_event)
|
||||
}
|
||||
|
||||
/// Converts an event dict as [`serde_json::Value`] into a [`FormattedEvent`].
|
||||
fn event_dict_from_json_value(
|
||||
room_version: &RoomVersion,
|
||||
event_dict: serde_json::Value,
|
||||
) -> Result<FormattedEvent, Error> {
|
||||
let formatted_event: FormattedEvent = match room_version.event_format {
|
||||
EventFormatVersions::ROOM_V1_V2 => {
|
||||
let event_format: FormattedEvent<EventFormatV1> = serde_json::from_value(event_dict)?;
|
||||
|
||||
event_format.into()
|
||||
}
|
||||
EventFormatVersions::ROOM_V3 | EventFormatVersions::ROOM_V4_PLUS => {
|
||||
let event_format: FormattedEvent<EventFormatV2V3> = serde_json::from_value(event_dict)?;
|
||||
event_format.into()
|
||||
}
|
||||
EventFormatVersions::ROOM_V11_HYDRA_PLUS => {
|
||||
let event_format: FormattedEvent<EventFormatV4> = serde_json::from_value(event_dict)?;
|
||||
event_format.into()
|
||||
}
|
||||
EventFormatVersions::ROOM_VMSC4242 => {
|
||||
let event_format: FormattedEvent<EventFormatVMSC4242> =
|
||||
serde_json::from_value(event_dict)?;
|
||||
event_format.into()
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Unsupported room version: {}",
|
||||
room_version
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
formatted_event.validate()?;
|
||||
|
||||
Ok(formatted_event)
|
||||
}
|
||||
|
||||
/// Returns a pruned version of the given event, which removes all keys we don't
|
||||
/// know about or think could potentially be dodgy.
|
||||
///
|
||||
/// Returns the redacted event as a dict.
|
||||
#[pyfunction(name = "redact_event")]
|
||||
fn redact_event_py(event: &Event) -> PyResult<Event> {
|
||||
let event_value = serde_json::to_value(&event.parsed_event).map_err(|err| {
|
||||
PyValueError::new_err(format!("Failed to serialize event for redaction: {}", err))
|
||||
})?;
|
||||
|
||||
let redacted_value = redact(&event_value, event.room_version)?;
|
||||
let redacted_formatted_event = event_dict_from_json_value(event.room_version, redacted_value)
|
||||
.map_err(|err| {
|
||||
PyValueError::new_err(format!("Failed to deserialize redacted event: {}", err))
|
||||
})?;
|
||||
|
||||
let redacted_event = Event {
|
||||
parsed_event: redacted_formatted_event,
|
||||
event_id: Arc::clone(&event.event_id),
|
||||
room_id: Arc::clone(&event.room_id),
|
||||
room_version: event.room_version,
|
||||
rejected_reason: event.rejected_reason.clone(),
|
||||
internal_metadata: event.internal_metadata.deep_copy()?,
|
||||
};
|
||||
|
||||
// Mark event as redacted
|
||||
redacted_event.internal_metadata.set_redacted(true)?;
|
||||
|
||||
Ok(redacted_event)
|
||||
}
|
||||
|
||||
/// Returns a pruned version of the given event dict, which removes all keys we
|
||||
/// don't know about or think could potentially be dodgy.
|
||||
///
|
||||
/// Returns the redacted event as a dict.
|
||||
#[pyfunction(name = "redact_event_dict")]
|
||||
fn redact_event_dict<'py>(
|
||||
py: Python<'py>,
|
||||
room_version: &RoomVersion,
|
||||
event_dict: &'py Bound<'py, PyAny>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
let event_value = depythonize(event_dict)?;
|
||||
|
||||
let redacted = redact(&event_value, room_version)?;
|
||||
|
||||
let redacted_py = pythonize(py, &redacted)?;
|
||||
|
||||
Ok(redacted_py)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::events::{
|
||||
constants::event_type::M_ROOM_CREATE,
|
||||
formats::{EventFormatV1, EventFormatV2V3, EventFormatV4, EventFormatVMSC4242},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_basic_v3_roundtrip() {
|
||||
let json = r#"{"auth_events":[],"prev_events":[],"type":"m.room.create","sender":"@anon-20260225_142731-20:localhost:8800","content":{"room_version":"10","creator":"@anon-20260225_142731-20:localhost:8800"},"depth":1,"room_id":"!qVoJSympOqdUQRUfiC:localhost:8800","state_key":"","origin_server_ts":1772029657149,"hashes":{"sha256":"RIDkn4CrExGMOfRZlHl//1weAro5QC/q2D76YcyAUqk"},"signatures":{"localhost:8800":{"ed25519:a_GMSl":"GU7WmvI2Kd5kLrXKrWpRbUfEiVKGgH0sxQNEpBMMvgF3QhHN25AubVMmIClht5r/c+Iihb1xsq1j5Sw+RGfiDg"}},"unsigned":{"age_ts":1772029657149}}"#;
|
||||
let event_value: serde_json::Value = serde_json::from_str(json).unwrap();
|
||||
|
||||
let event: FormattedEvent<EventFormatV2V3> = serde_json::from_str(json).unwrap();
|
||||
let parsed_value = serde_json::to_value(&event).unwrap();
|
||||
|
||||
// Check a couple of fields are as expected as a sanity check.
|
||||
assert_eq!(&*event.common_fields.type_, M_ROOM_CREATE);
|
||||
assert_eq!(
|
||||
&*event.specific_fields.room_id,
|
||||
"!qVoJSympOqdUQRUfiC:localhost:8800"
|
||||
);
|
||||
|
||||
assert_eq!(event_value, parsed_value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_room_id_for_create_event_format_v4() {
|
||||
let json = r#"{"auth_events":[],"prev_events":[],"type":"m.room.create","sender":"@erikj:jki.re","content":{"room_version":"12","predecessor":{"room_id":"!VuNGkDTdbMOOxSmuDa:jki.re"}},"depth":1,"state_key":"","origin_server_ts":1775568141481,"hashes":{"sha256":"qBX+glsKvogXFrvsEN0eh13pO2kpuE6o/b4yREPtOqw"},"signatures":{"jki.re":{"ed25519:auto":"n/4gHQRagk3r1r24L/7a+oaMMf9cysVfQRYdjpDZcf4ppkVym33rhTW18Vy4zMa1L5nsWLkxsBvbrRRDYUOhBQ"}},"unsigned":{"age_ts":1775568141481}}"#;
|
||||
let event_value: serde_json::Value = serde_json::from_str(json).unwrap();
|
||||
|
||||
let event: FormattedEvent<EventFormatV4> = serde_json::from_str(json).unwrap();
|
||||
|
||||
let event_id = calculate_event_id(&event_value, &RoomVersion::V12).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&*event
|
||||
.specific_fields
|
||||
.room_id(&event_id, &event.common_fields)
|
||||
.unwrap(),
|
||||
"!BeXKh925K_M46DwsuJFR0EyBpE1P7CFUDGuWW4xw55Y"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_v1_roundtrip() {
|
||||
let json = r#"{"auth_events":[["$auth1:localhost",{"sha256":"abc"}],["$auth2:localhost",{"sha256":"def"}]],"prev_events":[["$prev1:localhost",{"sha256":"ghi"}]],"type":"m.room.message","sender":"@user:localhost","content":{"body":"hello","msgtype":"m.text"},"depth":5,"room_id":"!room:localhost","event_id":"$event1:localhost","origin_server_ts":1234567890,"hashes":{"sha256":"base64hash"},"signatures":{"localhost":{"ed25519:key":"sig"}},"unsigned":{}}"#;
|
||||
let event_value: serde_json::Value = serde_json::from_str(json).unwrap();
|
||||
|
||||
let event: FormattedEvent<EventFormatV1> = serde_json::from_str(json).unwrap();
|
||||
let parsed_value = serde_json::to_value(&event).unwrap();
|
||||
|
||||
// Check a few fields are as expected as a sanity check.
|
||||
assert_eq!(&*event.common_fields.type_, "m.room.message");
|
||||
assert!(event.common_fields.state_key.is_absent());
|
||||
assert_eq!(&*event.specific_fields.room_id, "!room:localhost");
|
||||
assert_eq!(&*event.specific_fields.event_id, "$event1:localhost");
|
||||
|
||||
// Check auth/prev event extraction
|
||||
let auth_ids = event.specific_fields.auth_event_ids();
|
||||
assert_eq!(auth_ids, vec!["$auth1:localhost", "$auth2:localhost"]);
|
||||
|
||||
let prev_ids = event.specific_fields.prev_event_ids();
|
||||
assert_eq!(prev_ids, vec!["$prev1:localhost"]);
|
||||
|
||||
assert_eq!(event_value, parsed_value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_v4_roundtrip_with_room_id() {
|
||||
// A regular (non-create) V4 event has an explicit room_id.
|
||||
let json = r#"{"auth_events":["$auth1","$auth2"],"prev_events":["$prev1"],"type":"m.room.message","sender":"@user:localhost","content":{"body":"hello","msgtype":"m.text"},"depth":5,"room_id":"!room:localhost","origin_server_ts":1234567890,"hashes":{"sha256":"base64hash"},"signatures":{"localhost":{"ed25519:key":"sig"}},"unsigned":{}}"#;
|
||||
let event_value: serde_json::Value = serde_json::from_str(json).unwrap();
|
||||
|
||||
let event: FormattedEvent<EventFormatV4> = serde_json::from_str(json).unwrap();
|
||||
let parsed_value = serde_json::to_value(&event).unwrap();
|
||||
|
||||
// Check a few fields are as expected as a sanity check.
|
||||
assert_eq!(&*event.common_fields.type_, "m.room.message");
|
||||
assert_eq!(
|
||||
event.specific_fields.room_id.as_deref_opt(),
|
||||
Some("!room:localhost")
|
||||
);
|
||||
assert_eq!(
|
||||
event.specific_fields.auth_events,
|
||||
vec!["$auth1".to_string(), "$auth2".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
event.specific_fields.prev_events,
|
||||
vec!["$prev1".to_string()]
|
||||
);
|
||||
|
||||
assert_eq!(event_value, parsed_value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_v4_roundtrip_create_event() {
|
||||
// A V4 create event for a v12 room has no room_id field.
|
||||
let json = r#"{"auth_events":[],"prev_events":[],"type":"m.room.create","sender":"@erikj:jki.re","content":{"room_version":"12"},"depth":1,"state_key":"","origin_server_ts":1775568141481,"hashes":{"sha256":"qBX+glsKvogXFrvsEN0eh13pO2kpuE6o/b4yREPtOqw"},"signatures":{"jki.re":{"ed25519:auto":"sig"}},"unsigned":{}}"#;
|
||||
let event_value: serde_json::Value = serde_json::from_str(json).unwrap();
|
||||
|
||||
let event: FormattedEvent<EventFormatV4> = serde_json::from_str(json).unwrap();
|
||||
let parsed_value = serde_json::to_value(&event).unwrap();
|
||||
|
||||
// Check a few fields are as expected as a sanity check.
|
||||
assert!(event.specific_fields.room_id.is_absent());
|
||||
assert_eq!(&*event.common_fields.type_, M_ROOM_CREATE);
|
||||
|
||||
// Create events have no implicit auth events.
|
||||
assert!(event
|
||||
.specific_fields
|
||||
.auth_event_ids(&event.common_fields)
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
|
||||
assert_eq!(event_value, parsed_value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_v4_auth_event_ids_implicit_create() {
|
||||
// Non-create events implicitly include the create event (derived from
|
||||
// the room ID) in their auth chain.
|
||||
let json = r#"{"auth_events":["$auth1"],"prev_events":["$prev1"],"type":"m.room.message","sender":"@user:localhost","content":{"body":"hi","msgtype":"m.text"},"depth":5,"room_id":"!BeXKh925K_M46DwsuJFR0EyBpE1P7CFUDGuWW4xw55Y","origin_server_ts":1234567890,"hashes":{"sha256":"h"},"signatures":{"localhost":{"ed25519:k":"s"}},"unsigned":{}}"#;
|
||||
|
||||
let event: FormattedEvent<EventFormatV4> = serde_json::from_str(json).unwrap();
|
||||
|
||||
let auth_ids = event
|
||||
.specific_fields
|
||||
.auth_event_ids(&event.common_fields)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
auth_ids,
|
||||
vec![
|
||||
"$auth1".to_string(),
|
||||
"$BeXKh925K_M46DwsuJFR0EyBpE1P7CFUDGuWW4xw55Y".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_v4_validate_rejects_missing_room_id_for_non_create() {
|
||||
// A v12 non-create event without a room_id must fail validation.
|
||||
let json = r#"{"auth_events":[],"prev_events":[],"type":"m.room.message","sender":"@u:l","content":{},"depth":2,"state_key":"","origin_server_ts":1,"hashes":{"sha256":"h"},"signatures":{"l":{"ed25519:k":"s"}},"unsigned":{}}"#;
|
||||
let event: FormattedEvent<EventFormatV4> = serde_json::from_str(json).unwrap();
|
||||
assert!(event
|
||||
.specific_fields
|
||||
.validate(&event.common_fields)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_v4_validate_accepts_create_without_room_id() {
|
||||
let json = r#"{"auth_events":[],"prev_events":[],"type":"m.room.create","sender":"@u:l","content":{"room_version":"12"},"depth":1,"state_key":"","origin_server_ts":1,"hashes":{"sha256":"h"},"signatures":{"l":{"ed25519:k":"s"}},"unsigned":{}}"#;
|
||||
let event: FormattedEvent<EventFormatV4> = serde_json::from_str(json).unwrap();
|
||||
event
|
||||
.specific_fields
|
||||
.validate(&event.common_fields)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_vmsc4242_roundtrip() {
|
||||
// VMSC4242 introduces a `prev_state_events` field on top of V4.
|
||||
let json = r#"{"auth_events":["$auth1"],"prev_events":["$prev1"],"prev_state_events":["$pstate1","$pstate2"],"type":"m.room.member","sender":"@user:localhost","content":{"membership":"join"},"depth":5,"room_id":"!room:localhost","state_key":"@user:localhost","origin_server_ts":1234567890,"hashes":{"sha256":"h"},"signatures":{"localhost":{"ed25519:k":"s"}},"unsigned":{}}"#;
|
||||
let event_value: serde_json::Value = serde_json::from_str(json).unwrap();
|
||||
|
||||
let event: FormattedEvent<EventFormatVMSC4242> = serde_json::from_str(json).unwrap();
|
||||
let parsed_value = serde_json::to_value(&event).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
event.specific_fields.prev_state_events,
|
||||
vec!["$pstate1".to_string(), "$pstate2".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
event.specific_fields.room_id.as_deref_opt(),
|
||||
Some("!room:localhost")
|
||||
);
|
||||
assert_eq!(
|
||||
event.common_fields.state_key.as_deref_opt(),
|
||||
Some("@user:localhost")
|
||||
);
|
||||
|
||||
assert_eq!(event_value, parsed_value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vmsc4242_room_id_for_create_event() {
|
||||
let json = r#"{"auth_events":[],"prev_events":[],"prev_state_events":[],"type":"m.room.create","sender":"@erikj:jki.re","content":{"room_version":"12","predecessor":{"room_id":"!VuNGkDTdbMOOxSmuDa:jki.re"}},"depth":1,"state_key":"","origin_server_ts":1775568141481,"hashes":{"sha256":"qBX+glsKvogXFrvsEN0eh13pO2kpuE6o/b4yREPtOqw"},"signatures":{"jki.re":{"ed25519:auto":"n/4gHQRagk3r1r24L/7a+oaMMf9cysVfQRYdjpDZcf4ppkVym33rhTW18Vy4zMa1L5nsWLkxsBvbrRRDYUOhBQ"}},"unsigned":{"age_ts":1775568141481}}"#;
|
||||
let event_value: serde_json::Value = serde_json::from_str(json).unwrap();
|
||||
|
||||
let event: FormattedEvent<EventFormatVMSC4242> = serde_json::from_str(json).unwrap();
|
||||
|
||||
// The event_id calculation is independent of the `prev_state_events`
|
||||
// field not being present in V4, so the same event_id derivation works.
|
||||
let event_id = calculate_event_id(&event_value, &RoomVersion::V12).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&*event
|
||||
.specific_fields
|
||||
.room_id(&event_id, &event.common_fields)
|
||||
.unwrap(),
|
||||
"!BeXKh925K_M46DwsuJFR0EyBpE1P7CFUDGuWW4xw55Y"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_event_format_enum_untagged_roundtrip() {
|
||||
// The untagged EventFormatEnum serialization/deserialization is
|
||||
// driven by fields, so serializing any variant must match the
|
||||
// original JSON exactly.
|
||||
let v2v3_json = r#"{"auth_events":[],"prev_events":[],"type":"m.room.create","sender":"@a:b","content":{},"depth":1,"room_id":"!r:b","state_key":"","origin_server_ts":1,"hashes":{"sha256":"h"},"signatures":{"b":{"ed25519:k":"s"}},"unsigned":{}}"#;
|
||||
let v2v3_value: serde_json::Value = serde_json::from_str(v2v3_json).unwrap();
|
||||
let v2v3_container: FormattedEvent<EventFormatV2V3> =
|
||||
serde_json::from_str(v2v3_json).unwrap();
|
||||
assert_eq!(serde_json::to_value(&v2v3_container).unwrap(), v2v3_value);
|
||||
assert_eq!(
|
||||
serde_json::to_value(v2v3_container.into_general()).unwrap(),
|
||||
v2v3_value
|
||||
);
|
||||
|
||||
let v4_json = r#"{"auth_events":[],"prev_events":[],"type":"m.room.create","sender":"@a:b","content":{"room_version":"12"},"depth":1,"state_key":"","origin_server_ts":1,"hashes":{"sha256":"h"},"signatures":{"b":{"ed25519:k":"s"}},"unsigned":{}}"#;
|
||||
let v4_value: serde_json::Value = serde_json::from_str(v4_json).unwrap();
|
||||
let v4_container: FormattedEvent<EventFormatV4> = serde_json::from_str(v4_json).unwrap();
|
||||
assert_eq!(serde_json::to_value(&v4_container).unwrap(), v4_value);
|
||||
assert_eq!(
|
||||
serde_json::to_value(v4_container.into_general()).unwrap(),
|
||||
v4_value
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_top_level_fields_preserved_roundtrip() {
|
||||
// Extra top-level fields (e.g. unknown or experimental) are captured
|
||||
// via `other_fields` and must round-trip losslessly.
|
||||
let json = r#"{"auth_events":[],"prev_events":[],"type":"m.room.message","sender":"@a:b","content":{"body":"hi","msgtype":"m.text"},"depth":1,"room_id":"!r:b","origin_server_ts":1,"hashes":{"sha256":"h"},"signatures":{"b":{"ed25519:k":"s"}},"unsigned":{},"msc4354_sticky":{"duration_ms":5000},"some_unknown_field":"some_value"}"#;
|
||||
let event_value: serde_json::Value = serde_json::from_str(json).unwrap();
|
||||
|
||||
let event: FormattedEvent<EventFormatV4> = serde_json::from_str(json).unwrap();
|
||||
let parsed_value = serde_json::to_value(&event).unwrap();
|
||||
|
||||
assert!(event
|
||||
.common_fields
|
||||
.other_fields
|
||||
.contains_key(MSC4354_STICKY));
|
||||
assert!(event
|
||||
.common_fields
|
||||
.other_fields
|
||||
.contains_key("some_unknown_field"));
|
||||
|
||||
assert_eq!(event_value, parsed_value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,12 +30,25 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A class representing the signatures on an event.
|
||||
#[pyclass(frozen, skip_from_py_object)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(transparent)]
|
||||
pub struct Signatures {
|
||||
inner: Arc<RwLock<HashMap<String, HashMap<String, String>>>>,
|
||||
}
|
||||
|
||||
impl Signatures {
|
||||
/// Create a deep copy of this `Signatures` to allow modification without
|
||||
/// affecting other references to the same signatures. This is needed when
|
||||
/// we clone an event.
|
||||
pub fn deep_copy(&self) -> Self {
|
||||
let signatures = self.inner.read().expect("lock poisoned").clone(); // Deep copy the inner map
|
||||
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(signatures)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl Signatures {
|
||||
#[new]
|
||||
|
||||
@@ -101,6 +101,15 @@ impl Unsigned {
|
||||
.write()
|
||||
.map_err(|_| PyRuntimeError::new_err("Unsigned lock poisoned"))
|
||||
}
|
||||
|
||||
/// Create a deep copy of this `Unsigned` to allow modification without
|
||||
/// affecting other references to the same unsigned data. This is needed
|
||||
/// when we clone an event.
|
||||
pub fn deep_copy(&self) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(self.py_read().expect("lock poisoned").clone())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
@@ -268,11 +277,11 @@ impl Unsigned {
|
||||
}
|
||||
}
|
||||
|
||||
fn for_persistence<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
|
||||
pub fn for_persistence<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
|
||||
Ok(pythonize(py, &self.py_read()?.persisted_fields)?)
|
||||
}
|
||||
|
||||
fn for_event<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
|
||||
pub fn for_event<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
|
||||
Ok(pythonize(py, &*self.py_read()?)?)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* 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>.
|
||||
*
|
||||
*/
|
||||
|
||||
/// A wrapper type that represents a value that may be missing.
|
||||
///
|
||||
/// We can't necessarily use `Option<T>` for this, as we want to distinguish
|
||||
/// between a missing value and a value that is present but null (e.g.
|
||||
/// `{"field": null}` vs `{}`). Serde by default treats missing fields as
|
||||
/// `None`, so we need a custom type to capture this distinction.
|
||||
///
|
||||
/// A plain `AllowMissing<T>` is used for fields that are either present and of
|
||||
/// type `T`, or absent. An `AllowMissing<Option<T>>` is used for fields that
|
||||
/// are of type `T`, null, or absent.
|
||||
///
|
||||
/// Note, to use this type correctly, the field **MUST** be annotated with:
|
||||
///
|
||||
/// ```rust
|
||||
/// #[serde(
|
||||
/// default,
|
||||
/// with = "crate::json::allow_missing",
|
||||
/// skip_serializing_if = "AllowMissing::is_absent"
|
||||
/// )]
|
||||
/// ```
|
||||
///
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub enum AllowMissing<T> {
|
||||
Some(T),
|
||||
#[default]
|
||||
Absent,
|
||||
}
|
||||
|
||||
impl<T> AllowMissing<T> {
|
||||
/// Returns `true` if the value is present, even if it is null.
|
||||
pub fn is_some(&self) -> bool {
|
||||
matches!(self, AllowMissing::Some(_))
|
||||
}
|
||||
|
||||
/// Returns `true` if the value is absent.
|
||||
pub fn is_absent(&self) -> bool {
|
||||
matches!(self, AllowMissing::Absent)
|
||||
}
|
||||
|
||||
/// Converts to `Option<T::Target>`.
|
||||
///
|
||||
/// Useful for converting e.g. `AllowMissing<String>` to `Option<&str>`.
|
||||
pub fn as_deref_opt(&self) -> Option<&T::Target>
|
||||
where
|
||||
T: std::ops::Deref,
|
||||
{
|
||||
match self {
|
||||
AllowMissing::Some(inner) => Some(inner.deref()),
|
||||
AllowMissing::Absent => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts to `Option<&T>`.
|
||||
pub fn as_ref_opt(&self) -> Option<&T> {
|
||||
match self {
|
||||
AllowMissing::Some(inner) => Some(inner),
|
||||
AllowMissing::Absent => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A module that provides the serialization and deserialization logic for
|
||||
/// `AllowMissing<T>`.
|
||||
pub mod allow_missing {
|
||||
use serde::ser::Error as _;
|
||||
|
||||
use super::AllowMissing;
|
||||
|
||||
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<AllowMissing<T>, D::Error>
|
||||
where
|
||||
T: serde::Deserialize<'de>,
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
Ok(AllowMissing::Some(T::deserialize(deserializer)?))
|
||||
}
|
||||
|
||||
pub fn serialize<T, S>(value: &AllowMissing<T>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match value {
|
||||
AllowMissing::Some(inner) => inner.serialize(serializer),
|
||||
// We should never attempt to serialize an `AllowMissing::Absent`, as we
|
||||
// should have skipped it with `skip_serializing_if`.
|
||||
AllowMissing::Absent => Err(S::Error::custom("cannot serialize AllowMissing::Absent")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::assert_matches;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct TestStruct {
|
||||
#[serde(
|
||||
default,
|
||||
with = "crate::json::allow_missing",
|
||||
skip_serializing_if = "AllowMissing::is_absent"
|
||||
)]
|
||||
value: AllowMissing<i32>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize() {
|
||||
let json = r#"{"value":42}"#;
|
||||
let deserialized: TestStruct = serde_json::from_str(json).unwrap();
|
||||
assert!(deserialized.value.is_some());
|
||||
assert_matches!(deserialized.value, AllowMissing::Some(42));
|
||||
|
||||
let json = r#"{}"#;
|
||||
let deserialized: TestStruct = serde_json::from_str(json).unwrap();
|
||||
assert!(deserialized.value.is_absent());
|
||||
assert_matches!(deserialized.value, AllowMissing::Absent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize() {
|
||||
let value = TestStruct {
|
||||
value: AllowMissing::Some(42),
|
||||
};
|
||||
let serialized = serde_json::to_string(&value).unwrap();
|
||||
assert_eq!(serialized, r#"{"value":42}"#);
|
||||
|
||||
let value = TestStruct {
|
||||
value: AllowMissing::Absent,
|
||||
};
|
||||
let serialized = serde_json::to_string(&value).unwrap();
|
||||
assert_eq!(serialized, r#"{}"#);
|
||||
}
|
||||
|
||||
/// Test that we get an error if we attempt to serialize an
|
||||
/// `AllowMissing::Absent` without the skip_serializing_if annotation.
|
||||
#[test]
|
||||
fn test_serialize_absent_error() {
|
||||
#[derive(Serialize)]
|
||||
struct TestStructWithoutSkip {
|
||||
#[serde(default, with = "crate::json::allow_missing")]
|
||||
value: AllowMissing<i32>,
|
||||
}
|
||||
|
||||
let value = TestStructWithoutSkip {
|
||||
value: AllowMissing::Absent,
|
||||
};
|
||||
|
||||
let err = serde_json::to_string(&value).unwrap_err();
|
||||
assert_eq!(err.to_string(), "cannot serialize AllowMissing::Absent");
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct TestStructOption {
|
||||
#[serde(
|
||||
default,
|
||||
with = "crate::json::allow_missing",
|
||||
skip_serializing_if = "AllowMissing::is_absent"
|
||||
)]
|
||||
value: AllowMissing<Option<i32>>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_option() {
|
||||
let value = TestStructOption {
|
||||
value: AllowMissing::Some(Some(42)),
|
||||
};
|
||||
let serialized = serde_json::to_string(&value).unwrap();
|
||||
assert_eq!(serialized, r#"{"value":42}"#);
|
||||
|
||||
let value = TestStructOption {
|
||||
value: AllowMissing::Some(None),
|
||||
};
|
||||
let serialized = serde_json::to_string(&value).unwrap();
|
||||
assert_eq!(serialized, r#"{"value":null}"#);
|
||||
|
||||
let value = TestStructOption {
|
||||
value: AllowMissing::Absent,
|
||||
};
|
||||
let serialized = serde_json::to_string(&value).unwrap();
|
||||
assert_eq!(serialized, r#"{}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_option() {
|
||||
let json = r#"{"value":42}"#;
|
||||
let deserialized: TestStructOption = serde_json::from_str(json).unwrap();
|
||||
assert!(deserialized.value.is_some());
|
||||
assert_matches!(deserialized.value, AllowMissing::Some(Some(42)));
|
||||
|
||||
let json = r#"{"value":null}"#;
|
||||
let deserialized: TestStructOption = serde_json::from_str(json).unwrap();
|
||||
assert!(deserialized.value.is_some());
|
||||
assert_matches!(deserialized.value, AllowMissing::Some(None));
|
||||
|
||||
let json = r#"{}"#;
|
||||
let deserialized: TestStructOption = serde_json::from_str(json).unwrap();
|
||||
assert!(deserialized.value.is_absent());
|
||||
assert_matches!(deserialized.value, AllowMissing::Absent);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ pub mod events;
|
||||
pub mod http;
|
||||
pub mod http_client;
|
||||
pub mod identifier;
|
||||
pub mod json;
|
||||
pub mod matrix_const;
|
||||
pub mod msc4388_rendezvous;
|
||||
pub mod push;
|
||||
|
||||
Reference in New Issue
Block a user