feat: Start on PDU builders

ran out of creative juices so this doesn't work but i'll find a way later
This commit is contained in:
timedout committed 2026-07-12 03:51:28 +01:00
1 parent b1a3bcfb88
commit 9aacbf2650
8 files changed
+261 -15

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
# Roadmap for PDU versioning
Currently, PDUs are one monolithic `PduEvent` struct which applies a one-size-fits-all approach to PDUs, however as we
support a wide range of room versions, this only just about currently works, and will cause more and more problems as
time progresses. This branch will split the `PduEvent` struct into multiple versioned structs that represent the proper
format for appropriate room versions, and will drop the `Event` trait in favour of Ruma's `ruma::state_res::Event`
trait.
This will require a lot of work, and since I forget everything the moment I look out the window, here be a list (waow):
- [x] Split `PduEvent` into multiple versioned structs, implementing `ruma::state_res::Event` for each of them.
- [ ] Refactor out all references to `PduEvent`.
- [ ] Refactor out all references to `conduwuit_core::event::Event` trait in favour of `ruma::state_res::Event`.
- [ ] Centralise PDU struct creation in a way that allows us to greater control the deserialization process.
- Developers can't be trusted to read documentation so we can't store the PDU metadata (things like event ID and
rejection status) in the PDU struct itself, as this may end up being leaked to consumers. Instead, PDU metadata
should be stored seprately from the PDU struct itself, but needs to be strongly associated with it (i.e. fetched
at the same time, always returned together, etc). This may take a form similar to
`StoredPdu<T: Event> { pdu: T, metadata: PduMetadata }`.
- [ ] Stop storing generated data in the `unsigned` field, but instead calculate it on demand(?).
- Storing generated data in the `unsigned` field results in:
- History visibility cannot be enforced on `prev_content` or `redacted_because`.
- `age` is manually calculated but only sometimes.
- `membership` is not provided at all.
- `prev_content` can leak redacted state event content ([#1103]).
- `prev_content` cannot easily be populated for backfilled events, leaving them contextless.
- It is easy to accidentally leak `transaction_id`.
Once `PduEvent` is gone, we can then do the following lovely things:
- Drop our local state resolution logic and event authorisation logic and instead use Ruma's, which naturally receives
more love and attention. Our current `Event` trait and `PduEvent` implementations are incompatible with what Ruma
needs, and do not store the appropriate information required.
- There's something else but I forgot just trust me
+11 -4
View File
@@ -1,9 +1,11 @@
use std::collections::HashSet;
use assign::assign;
use axum::extract::State;
use conduwuit::{
Err, Pdu, Result, debug_info, debug_warn, err,
matrix::{event::gen_event_id, pdu::PartialPdu},
pdu::{AnyPDU, metadata::PduMetadata, new_pdu},
utils::{self, FutureBoolExt, future::ReadyEqExt},
warn,
};
@@ -216,7 +218,7 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
room_id: &RoomId,
reason: Option<String>,
mut servers: HashSet<OwnedServerName, S>,
) -> Result<Pdu> {
) -> Result<AnyPDU> {
let mut make_leave_response_and_server =
Err!(BadServerResponse("No remote server available to assist in leaving {room_id}."));
@@ -406,9 +408,14 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
.outlier
.add_pdu_outlier(&event_id, &leave_event);
let leave_pdu = Pdu::from_id_val(&event_id, leave_event).map_err(|e| {
err!(BadServerResponse("Invalid leave PDU received during federated leave: {e:?}"))
})?;
let leave_pdu = new_pdu(
room_version_rules.into(),
PduMetadata::new(event_id),
serde_json::to_value(leave_event)?,
)?;
// let leave_pdu = Pdu::from_id_val(&event_id, leave_event).map_err(|e| {
// err!(BadServerResponse("Invalid leave PDU received during federated leave:
// {e:?}")) })?;
Ok(leave_pdu)
}
+191 -6
View File
@@ -13,11 +13,13 @@ pub mod v3;
use std::cmp::Ordering;
use assign::assign;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId,
OwnedRoomId, OwnedServerName, OwnedUserId, RoomId, UInt, UserId, events::TimelineEventType,
OwnedRoomId, OwnedServerName, OwnedUserId, RoomId, ServerSignatures, UInt, UserId, event_id,
events::TimelineEventType, room_version_rules::RoomVersionRules, state_res::Event,
};
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::value::RawValue as RawJsonValue;
pub use self::{
@@ -27,10 +29,193 @@ pub use self::{
partial::PartialPdu,
raw_id::*,
};
use super::{Event, StateKey};
use crate::Result;
use super::StateKey;
use crate::{
Err, Result,
pdu::{hashes::Hashes, metadata::PduMetadata},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EventFormatVersion {
/// V1 represents the PDU format for rooms v1 and v2.
V1,
/// V2 represents the PDU format for rooms v3-v11.
V2,
/// V3 represents the PDU format for rooms v12+.
V3,
}
impl From<RoomVersionRules> for EventFormatVersion {
fn from(rules: RoomVersionRules) -> Self {
// TODO: this is hacky because ruma doesn't actually define event versions in an
// enum, but does define consts with the appropriate rules.
if rules.event_format.require_event_id {
Self::V1
} else if rules.event_format.require_room_create_room_id {
Self::V2
} else {
Self::V3
}
}
}
/// Trait that allows conversion from a generic PDU to a concrete PDU type.
pub trait ConcretePDU: ruma::state_res::Event<Id = OwnedEventId> {
/// The concrete type for this PDU (`v2::PDU`, `v3::PDU`, etc)
type Concrete;
/// Get a reference to the concrete PDU type.
fn to_concrete(&self) -> &Self::Concrete;
/// Get the value of the concrete PDU type, consuming.
fn into_concrete(self) -> Self::Concrete;
}
pub fn new_pdu<E, C, P, JSON>(
format: EventFormatVersion,
metadata: PduMetadata,
json: serde_json::Value,
) -> Result<E>
where
E: ConcretePDU<Concrete = C>,
JSON: DeserializeOwned,
{
match format {
| EventFormatVersion::V2 =>
Ok(assign!(v2::PDU::try_from(json)?, {internal_metadata: metadata})),
| EventFormatVersion::V3 =>
Ok(assign!(v3::PDU::try_from(json)?, {internal_metadata: metadata})),
| _ => Err!("Unsupported event format: {format:?}"),
}
}
/// A builder type that specifies every field present on any PDU type, allowing
/// it to be used to create any PDU version with the appropriate function.
///
/// See `create_pdu`.
#[derive(Clone, Debug, Default)]
pub struct CommonPDUBuilder {
pub auth_events: Option<Vec<OwnedEventId>>,
pub content: Option<Box<RawJsonValue>>,
pub depth: Option<UInt>,
pub hashes: Option<Hashes>,
pub origin_server_ts: Option<MilliSecondsSinceUnixEpoch>,
pub prev_events: Option<Vec<OwnedEventId>>,
pub redacts: Option<OwnedEventId>,
pub room_id: Option<OwnedRoomId>,
pub sender: Option<OwnedUserId>,
pub signatures: Option<ServerSignatures>,
pub state_key: Option<String>,
pub event_type: Option<TimelineEventType>,
pub unsigned: Option<Box<RawJsonValue>>,
pub internal_metadata: Option<PduMetadata>,
}
/// Creates a new PDU based on the given version and builder.
///
/// ## Example
///
/// ```rust
/// use conduwuit_core::pdu::{CommonPDUBuilder, create_pdu}
///
/// let room_version_rules = RoomVersionRules::V10; // This usually comes dynamically.
/// let builder = CommonPDUBuilder {
/// auth_events: Some(vec![event_id!("$auth_event_id").to_owned()]),
/// content: Some(Box::new(serde_json::json!({"key": "value"}))),
/// depth: Some(1),
/// origin_server_ts: Some(MilliSecondsSinceUnixEpoch(123456789)),
/// prev_events: Some(vec![event_id!("$prev_event_id").to_owned()]),
/// room_id: Some(room_id!("!room_id").to_owned()),
/// sender: Some(user_id!("@sender:example.com").to_owned()),
/// event_type: Some(TimelineEventType::RoomMessage),
/// ..Default::default()
/// };
/// let pdu = create_pdu(
/// room_version_rules.into(),
/// builder,
/// ).expect("PDU creation must succeed");
/// ```
pub fn create_pdu<C>(version: EventFormatVersion, builder: CommonPDUBuilder) -> Result<C> {
match version {
| EventFormatVersion::V2 => Ok(create_v2_pdu(builder)),
| EventFormatVersion::V3 => Ok(create_v3_pdu(builder)),
| _ => Err!("Unsupported event format: {version:?}"),
}
}
fn create_v2_pdu<C>(builder: CommonPDUBuilder) -> v2::PDU {
v2::PDU {
auth_events: builder
.auth_events
.expect("auth_events must be provided for a V2 PDU"),
content: builder
.content
.expect("content must be provided for a V2 PDU"),
depth: builder.depth.expect("depth must be provided for a V2 PDU"),
hashes: builder.hashes.unwrap_or_default(), /* We allow default here since we might not
* have hashed yet */
origin_server_ts: builder
.origin_server_ts
.expect("origin_server_ts must be provided for a V2 PDU"),
prev_events: builder
.prev_events
.expect("prev_events must be provided for a V2 PDU"),
redacts: builder.redacts,
room_id: builder
.room_id
.expect("room_id must be provided for a V2 PDU"),
sender: builder
.sender
.expect("sender must be provided for a V2 PDU"),
signatures: builder.signatures.unwrap_or_default(), /* We allow default here since we
* might not have signed yet */
state_key: builder.state_key,
event_type: builder
.event_type
.expect("event_type must be provided for a V2 PDU"),
unsigned: builder.unsigned,
internal_metadata: builder
.internal_metadata
.unwrap_or_else(|| PduMetadata::new(event_id!("$unknown").to_owned())),
}
}
fn create_v3_pdu(builder: CommonPDUBuilder) -> v3::PDU {
v3::PDU {
auth_events: builder
.auth_events
.expect("auth_events must be provided for a V3 PDU"),
content: builder
.content
.expect("content must be provided for a V3 PDU"),
depth: builder.depth.expect("depth must be provided for a V3 PDU"),
hashes: builder.hashes.unwrap_or_default(), /* We allow default here since we might not
* have hashed yet */
origin_server_ts: builder
.origin_server_ts
.expect("origin_server_ts must be provided for a V3 PDU"),
prev_events: builder
.prev_events
.expect("prev_events must be provided for a V3 PDU"),
room_id: builder.room_id,
sender: builder
.sender
.expect("sender must be provided for a V3 PDU"),
signatures: builder.signatures.unwrap_or_default(), /* We allow default here since we
* might not have signed yet */
state_key: builder.state_key,
event_type: builder
.event_type
.expect("event_type must be provided for a V3 PDU"),
unsigned: builder.unsigned,
internal_metadata: builder
.internal_metadata
.unwrap_or_else(|| PduMetadata::new(event_id!("$unknown").to_owned())),
}
}
/// Persistent Data Unit (Event)
#[deprecated(note = "Use `v2::PDU` or `v3::PDU` or `ruma::state_res::Event` instead")]
#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct Pdu {
pub event_id: OwnedEventId,
@@ -89,7 +274,7 @@ impl Pdu {
}
}
impl Event for Pdu {
impl crate::Event for Pdu {
#[inline]
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_ {
self.auth_events.iter().map(AsRef::as_ref)
@@ -157,7 +342,7 @@ impl Event for Pdu {
fn into_pdu(self) -> Pdu { self }
}
impl Event for &Pdu {
impl crate::Event for &Pdu {
#[inline]
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_ {
self.auth_events.iter().map(AsRef::as_ref)
+2 -1
View File
@@ -3,9 +3,10 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
/// The content hash map for a PDU.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
pub struct Hashes {
/// The SHA256 content hash.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub sha256: String,
/// Any other hashes present, if any.
+6 -1
View File
@@ -1,4 +1,4 @@
use ruma::OwnedEventId;
use ruma::{EventId, OwnedEventId};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -9,3 +9,8 @@ pub struct PduMetadata {
/// Indicates whether the event has been rejected under auth rules.
pub rejected: bool,
}
impl PduMetadata {
/// Creates a new PduMetadata instance with the minimum required fields.
pub fn new(event_id: OwnedEventId) -> Self { Self { event_id, rejected: false } }
}
+8
View File
@@ -67,6 +67,14 @@ impl PDU {
pub fn internal_metadata(&self) -> &PduMetadata { &self.internal_metadata }
}
impl crate::pdu::ConcretePDU for PDU {
type Concrete = PDU;
fn to_concrete(&self) -> &Self::Concrete { &self }
fn into_concrete(self) -> Self::Concrete { self }
}
impl ruma::state_res::Event for PDU {
type Id = OwnedEventId;
+6
View File
@@ -65,6 +65,12 @@ impl PDU {
pub fn internal_metadata(&self) -> &PduMetadata { &self.internal_metadata }
}
impl crate::pdu::ConcretePDU<PDU> for PDU {
fn to_concrete(&self) -> &PDU { &self }
fn into_concrete(self) -> PDU { self }
}
impl ruma::state_res::Event for PDU {
type Id = OwnedEventId;
+3 -3
View File
@@ -1,6 +1,6 @@
use std::{cmp, collections::HashMap};
use conduwuit::{smallstr::SmallString, trace};
use conduwuit::{pdu::AnyPDU, smallstr::SmallString, trace};
use conduwuit_core::{
Err, Error, Result, err,
matrix::{
@@ -84,7 +84,7 @@ impl super::Service {
sender: &UserId,
room_id: Option<&RoomId>,
_mutex_lock: &RoomMutexGuard,
) -> Result<(PduEvent, RoomVersionRules)> {
) -> Result<AnyPDU> {
let PartialPdu {
event_type,
content,
@@ -282,7 +282,7 @@ impl super::Service {
sender: &UserId,
room_id: Option<&RoomId>,
mutex_lock: &RoomMutexGuard,
) -> Result<(PduEvent, CanonicalJsonObject)> {
) -> Result<AnyPDU> {
if !self.services.globals.user_is_local(sender) {
return Err!(Request(Forbidden("Sender must be a local user")));
}