diff --git a/benches/outcomes.txt b/benches/outcomes.txt index cde1855e..2de257ec 100644 --- a/benches/outcomes.txt +++ b/benches/outcomes.txt @@ -14,4 +14,26 @@ Found 3 outliers among 100 measurements (3.00%) 3 (3.00%) high mild resolve state of 10 events 3 conflicting - time: [26.858 us 26.946 us 27.037 us] \ No newline at end of file + time: [26.858 us 26.946 us 27.037 us] + +11/29/2020 BRANCH: event-trait REV: f0eb1310efd49d722979f57f20bd1ac3592b0479 +lexicographical topological sort + time: [1.7686 us 1.7738 us 1.7810 us] + change: [-3.2752% -2.4634% -1.7635%] (p = 0.00 < 0.05) + Performance has improved. +Found 1 outliers among 100 measurements (1.00%) + 1 (1.00%) high severe + +resolve state of 5 events one fork + time: [10.643 us 10.656 us 10.669 us] + change: [-4.9990% -3.8078% -2.8319%] (p = 0.00 < 0.05) + Performance has improved. +Found 1 outliers among 100 measurements (1.00%) + 1 (1.00%) high severe + +resolve state of 10 events 3 conflicting + time: [29.149 us 29.252 us 29.375 us] + change: [-0.8433% -0.3270% +0.2656%] (p = 0.25 > 0.05) + No change in performance detected. +Found 1 outliers among 100 measurements (1.00%) + 1 (1.00%) high mild \ No newline at end of file diff --git a/benches/state_res_bench.rs b/benches/state_res_bench.rs index fd85513c..ff5c37cc 100644 --- a/benches/state_res_bench.rs +++ b/benches/state_res_bench.rs @@ -10,7 +10,6 @@ use criterion::{criterion_group, criterion_main, Criterion}; use maplit::btreemap; use ruma::{ events::{ - pdu::ServerPdu, room::{ join_rules::JoinRule, member::{MemberEventContent, MembershipState}, @@ -20,7 +19,7 @@ use ruma::{ identifiers::{EventId, RoomId, RoomVersionId, UserId}, }; use serde_json::{json, Value as JsonValue}; -use state_res::{Error, Result, StateMap, StateResolution, StateStore}; +use state_res::{Error, Event, Result, StateMap, StateResolution, StateStore}; static mut SERVER_TIMESTAMP: i32 = 0; @@ -82,7 +81,7 @@ fn resolve_deeper_event_set(c: &mut Criterion) { inner.get(&event_id("PA")).unwrap(), ] .iter() - .map(|ev| ((ev.kind.clone(), ev.state_key.clone()), ev.event_id.clone())) + .map(|ev| ((ev.kind(), ev.state_key()), ev.event_id().clone())) .collect::>(); let state_set_b = [ @@ -95,13 +94,13 @@ fn resolve_deeper_event_set(c: &mut Criterion) { inner.get(&event_id("PA")).unwrap(), ] .iter() - .map(|ev| ((ev.kind.clone(), ev.state_key.clone()), ev.event_id.clone())) + .map(|ev| ((ev.kind(), ev.state_key()), ev.event_id().clone())) .collect::>(); b.iter(|| { let _resolved = match StateResolution::resolve( &room_id(), - &RoomVersionId::Version2, + &RoomVersionId::Version6, &[state_set_a.clone(), state_set_b.clone()], Some(inner.clone()), &store, @@ -115,7 +114,7 @@ fn resolve_deeper_event_set(c: &mut Criterion) { criterion_group!( benches, - // lexico_topo_sort, + lexico_topo_sort, resolution_shallow_auth_chain, resolve_deeper_event_set ); @@ -127,11 +126,11 @@ criterion_main!(benches); // IMPLEMENTATION DETAILS AHEAD // /////////////////////////////////////////////////////////////////////*/ -pub struct TestStore(BTreeMap>); +pub struct TestStore(pub BTreeMap>); #[allow(unused)] -impl StateStore for TestStore { - fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result> { +impl StateStore for TestStore { + fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result> { self.0 .get(event_id) .map(Arc::clone) @@ -139,9 +138,9 @@ impl StateStore for TestStore { } } -impl TestStore { +impl TestStore { pub fn set_up(&mut self) -> (StateMap, StateMap, StateMap) { - let create_event = Arc::new(to_pdu_event::( + let create_event = to_pdu_event::( "CREATE", alice(), EventType::RoomCreate, @@ -149,8 +148,8 @@ impl TestStore { json!({ "creator": alice() }), &[], &[], - )); - let cre = create_event.event_id.clone(); + ); + let cre = create_event.event_id().clone(); self.0.insert(cre.clone(), Arc::clone(&create_event)); let alice_mem = to_pdu_event( @@ -163,7 +162,7 @@ impl TestStore { &[cre.clone()], ); self.0 - .insert(alice_mem.event_id.clone(), Arc::clone(&alice_mem)); + .insert(alice_mem.event_id().clone(), Arc::clone(&alice_mem)); let join_rules = to_pdu_event( "IJR", @@ -171,11 +170,11 @@ impl TestStore { EventType::RoomJoinRules, Some(""), json!({ "join_rule": JoinRule::Public }), - &[cre.clone(), alice_mem.event_id.clone()], - &[alice_mem.event_id.clone()], + &[cre.clone(), alice_mem.event_id().clone()], + &[alice_mem.event_id().clone()], ); self.0 - .insert(join_rules.event_id.clone(), Arc::clone(&join_rules)); + .insert(join_rules.event_id().clone(), join_rules.clone()); // Bob and Charlie join at the same time, so there is a fork // this will be represented in the state_sets when we resolve @@ -185,11 +184,10 @@ impl TestStore { EventType::RoomMember, Some(bob().to_string().as_str()), member_content_join(), - &[cre.clone(), join_rules.event_id.clone()], - &[join_rules.event_id.clone()], + &[cre.clone(), join_rules.event_id().clone()], + &[join_rules.event_id().clone()], ); - self.0 - .insert(bob_mem.event_id.clone(), Arc::clone(&bob_mem)); + self.0.insert(bob_mem.event_id().clone(), bob_mem.clone()); let charlie_mem = to_pdu_event( "IMC", @@ -197,20 +195,20 @@ impl TestStore { EventType::RoomMember, Some(charlie().to_string().as_str()), member_content_join(), - &[cre, join_rules.event_id.clone()], - &[join_rules.event_id.clone()], + &[cre, join_rules.event_id().clone()], + &[join_rules.event_id().clone()], ); self.0 - .insert(charlie_mem.event_id.clone(), Arc::clone(&charlie_mem)); + .insert(charlie_mem.event_id().clone(), charlie_mem.clone()); let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem] .iter() - .map(|e| ((e.kind.clone(), e.state_key.clone()), e.event_id.clone())) + .map(|e| ((e.kind(), e.state_key()), e.event_id().clone())) .collect::>(); let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem] .iter() - .map(|e| ((e.kind.clone(), e.state_key.clone()), e.event_id.clone())) + .map(|e| ((e.kind(), e.state_key()), e.event_id().clone())) .collect::>(); let expected = [ @@ -221,7 +219,7 @@ impl TestStore { &charlie_mem, ] .iter() - .map(|e| ((e.kind.clone(), e.state_key.clone()), e.event_id.clone())) + .map(|e| ((e.kind(), e.state_key()), e.event_id().clone())) .collect::>(); (state_at_bob, state_at_charlie, expected) @@ -282,7 +280,7 @@ fn to_pdu_event( content: JsonValue, auth_events: &[S], prev_events: &[S], -) -> Arc +) -> Arc where S: AsRef, { @@ -345,7 +343,7 @@ where // all graphs start with these input events #[allow(non_snake_case)] -fn INITIAL_EVENTS() -> BTreeMap> { +fn INITIAL_EVENTS() -> BTreeMap> { vec![ to_pdu_event::( "CREATE", @@ -421,13 +419,13 @@ fn INITIAL_EVENTS() -> BTreeMap> { ), ] .into_iter() - .map(|ev| (ev.event_id.clone(), ev)) + .map(|ev| (ev.event_id().clone(), ev)) .collect() } // all graphs start with these input events #[allow(non_snake_case)] -fn BAN_STATE_SET() -> BTreeMap> { +fn BAN_STATE_SET() -> BTreeMap> { vec![ to_pdu_event( "PA", @@ -467,6 +465,398 @@ fn BAN_STATE_SET() -> BTreeMap> { ), ] .into_iter() - .map(|ev| (ev.event_id.clone(), ev)) + .map(|ev| (ev.event_id().clone(), ev)) .collect() } + +pub mod event { + use std::{collections::BTreeMap, time::SystemTime}; + + use ruma::{ + events::{ + from_raw_json_value, + pdu::{EventHash, Pdu}, + room::member::{MemberEventContent, MembershipState}, + EventDeHelper, EventType, + }, + serde::CanonicalJsonValue, + signatures::reference_hash, + EventId, RoomId, RoomVersionId, ServerName, UInt, UserId, + }; + use serde::{de, ser, Deserialize, Serialize}; + use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue}; + + use state_res::Event; + + impl Event for StateEvent { + fn event_id(&self) -> &EventId { + self.event_id() + } + + fn room_id(&self) -> &RoomId { + self.room_id() + } + + fn sender(&self) -> &UserId { + self.sender() + } + fn kind(&self) -> EventType { + self.kind() + } + + fn content(&self) -> serde_json::Value { + self.content() + } + fn origin_server_ts(&self) -> SystemTime { + *self.origin_server_ts() + } + + fn state_key(&self) -> Option { + self.state_key() + } + fn prev_events(&self) -> Vec { + self.prev_event_ids() + } + fn depth(&self) -> &UInt { + self.depth() + } + fn auth_events(&self) -> Vec { + self.auth_events() + } + fn redacts(&self) -> Option<&EventId> { + self.redacts() + } + fn hashes(&self) -> &EventHash { + self.hashes() + } + fn signatures(&self) -> BTreeMap, BTreeMap> { + self.signatures() + } + fn unsigned(&self) -> &BTreeMap { + self.unsigned() + } + } + + #[derive(Clone, Debug, Deserialize, Serialize)] + struct EventIdHelper { + event_id: EventId, + } + + /// This feature is turned on in conduit but off when the tests run because + /// we rely on the EventId to check the state-res. + #[cfg(feature = "gen-eventid")] + fn event_id(json: &RawJsonValue) -> Result { + use std::convert::TryFrom; + EventId::try_from(format!( + "${}", + reference_hash(&from_raw_json_value(&json)?, &RoomVersionId::Version6) + .map_err(de::Error::custom)?, + )) + .map_err(de::Error::custom) + } + + /// Only turned on for testing where we need to keep the ID. + #[cfg(not(feature = "gen-eventid"))] + fn event_id(json: &RawJsonValue) -> Result { + use std::convert::TryFrom; + Ok(match from_raw_json_value::(&json) { + Ok(id) => id.event_id, + Err(_) => { + // panic!("NOT DURING TESTS"); + EventId::try_from(format!( + "${}", + reference_hash(&from_raw_json_value(&json)?, &RoomVersionId::Version6) + .map_err(de::Error::custom)?, + )) + .map_err(de::Error::custom)? + } + }) + } + + // TODO: This no longer needs to be an enum now that PduStub is gone + #[derive(Clone, Debug)] + pub enum StateEvent { + Full(EventId, Pdu), + } + + impl Serialize for StateEvent { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + use ser::Error; + use std::convert::TryInto; + + match self { + Self::Full(id, ev) => { + // TODO: do we want to add the eventId when we + // serialize + let val: CanonicalJsonValue = serde_json::to_value(ev) + .map_err(S::Error::custom)? + .try_into() + .map_err(S::Error::custom)?; + + match val { + CanonicalJsonValue::Object(mut obj) => { + obj.insert( + "event_id".into(), + ruma::serde::to_canonical_value(id).map_err(S::Error::custom)?, + ); + obj.serialize(serializer) + } + _ => panic!("Pdu not an object"), + } + } + } + } + } + + impl<'de> de::Deserialize<'de> for StateEvent { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let json = Box::::deserialize(deserializer)?; + let EventDeHelper { + room_id, unsigned, .. + } = from_raw_json_value(&json)?; + + // TODO: do we even want to try for the existing ID + + // Determine whether the event is a full or stub + // based on the fields present. + Ok(if room_id.is_some() { + match unsigned { + Some(unsigned) if unsigned.redacted_because.is_some() => { + panic!("TODO deal with redacted events") + } + _ => StateEvent::Full( + event_id(&json)?, + Pdu::RoomV3Pdu(from_raw_json_value(&json)?), + ), + } + } else { + panic!("Found stub event") + }) + } + } + + impl StateEvent { + pub fn from_id_value(id: EventId, json: serde_json::Value) -> Result { + Ok(Self::Full( + id, + Pdu::RoomV3Pdu(serde_json::from_value(json)?), + )) + } + + pub fn from_id_canon_obj( + id: EventId, + json: ruma::serde::CanonicalJsonObject, + ) -> Result { + Ok(Self::Full( + id, + // TODO: this is unfortunate (from_value(to_value(json)))... + Pdu::RoomV3Pdu(serde_json::from_value(serde_json::to_value(json)?)?), + )) + } + + pub fn is_power_event(&self) -> bool { + match self { + Self::Full(_, any_event) => match any_event { + Pdu::RoomV1Pdu(event) => match event.kind { + EventType::RoomPowerLevels + | EventType::RoomJoinRules + | EventType::RoomCreate => event.state_key == Some("".into()), + EventType::RoomMember => { + if let Ok(content) = + // TODO fix clone + serde_json::from_value::(event.content.clone()) + { + if [MembershipState::Leave, MembershipState::Ban] + .contains(&content.membership) + { + return event.sender.as_str() + // TODO is None here a failure + != event.state_key.as_deref().unwrap_or("NOT A STATE KEY"); + } + } + + false + } + _ => false, + }, + Pdu::RoomV3Pdu(event) => event.state_key == Some("".into()), + }, + } + } + pub fn deserialize_content( + &self, + ) -> Result { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => serde_json::from_value(ev.content.clone()), + Pdu::RoomV3Pdu(ev) => serde_json::from_value(ev.content.clone()), + }, + } + } + pub fn origin_server_ts(&self) -> &SystemTime { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => &ev.origin_server_ts, + Pdu::RoomV3Pdu(ev) => &ev.origin_server_ts, + }, + } + } + pub fn event_id(&self) -> &EventId { + match self { + // TODO; make this a &EventId + Self::Full(id, _) => id, + } + } + + pub fn sender(&self) -> &UserId { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => &ev.sender, + Pdu::RoomV3Pdu(ev) => &ev.sender, + }, + } + } + + pub fn redacts(&self) -> Option<&EventId> { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.redacts.as_ref(), + Pdu::RoomV3Pdu(ev) => ev.redacts.as_ref(), + }, + } + } + + pub fn room_id(&self) -> &RoomId { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => &ev.room_id, + Pdu::RoomV3Pdu(ev) => &ev.room_id, + }, + } + } + pub fn kind(&self) -> EventType { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.kind.clone(), + Pdu::RoomV3Pdu(ev) => ev.kind.clone(), + }, + } + } + pub fn state_key(&self) -> Option { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.state_key.clone(), + Pdu::RoomV3Pdu(ev) => ev.state_key.clone(), + }, + } + } + + #[cfg(not(feature = "unstable-pre-spec"))] + pub fn origin(&self) -> String { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.origin.clone(), + Pdu::RoomV3Pdu(ev) => ev.origin.clone(), + }, + } + } + + pub fn prev_event_ids(&self) -> Vec { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.prev_events.iter().map(|(id, _)| id).cloned().collect(), + Pdu::RoomV3Pdu(ev) => ev.prev_events.clone(), + }, + } + } + + pub fn auth_events(&self) -> Vec { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.auth_events.iter().map(|(id, _)| id).cloned().collect(), + Pdu::RoomV3Pdu(ev) => ev.auth_events.to_vec(), + }, + } + } + + pub fn content(&self) -> serde_json::Value { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.content.clone(), + Pdu::RoomV3Pdu(ev) => ev.content.clone(), + }, + } + } + + pub fn unsigned(&self) -> &BTreeMap { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => &ev.unsigned, + Pdu::RoomV3Pdu(ev) => &ev.unsigned, + }, + } + } + + pub fn signatures( + &self, + ) -> BTreeMap, BTreeMap> { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(_) => maplit::btreemap! {}, + Pdu::RoomV3Pdu(ev) => ev.signatures.clone(), + }, + } + } + + pub fn hashes(&self) -> &EventHash { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => &ev.hashes, + Pdu::RoomV3Pdu(ev) => &ev.hashes, + }, + } + } + + pub fn depth(&self) -> &UInt { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => &ev.depth, + Pdu::RoomV3Pdu(ev) => &ev.depth, + }, + } + } + + pub fn is_type_and_key(&self, ev_type: EventType, state_key: &str) -> bool { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => { + ev.kind == ev_type && ev.state_key.as_deref() == Some(state_key) + } + Pdu::RoomV3Pdu(ev) => { + ev.kind == ev_type && ev.state_key.as_deref() == Some(state_key) + } + }, + } + } + + /// Returns the room version this event is formatted for. + /// + /// Currently either version 1 or 6 is returned, 6 represents + /// version 3 and above. + pub fn room_version(&self) -> RoomVersionId { + // TODO: We have to know the actual room version this is not sufficient + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(_) => RoomVersionId::Version1, + Pdu::RoomV3Pdu(_) => RoomVersionId::Version6, + }, + } + } + } +} \ No newline at end of file diff --git a/src/event_auth.rs b/src/event_auth.rs index 271a1fce..25bf4122 100644 --- a/src/event_auth.rs +++ b/src/event_auth.rs @@ -373,7 +373,7 @@ pub fn valid_membership_change( } Ok(if target_membership == MembershipState::Join { - if user.sender() != target_user_id { + if user.sender() != &target_user_id { false } else if let MembershipState::Ban = current_membership { false @@ -402,7 +402,7 @@ pub fn valid_membership_change( .is_some() } } else if target_membership == MembershipState::Leave { - if user.sender() == target_user_id { + if user.sender() == &target_user_id { current_membership == MembershipState::Join || current_membership == MembershipState::Invite } else if sender_membership != MembershipState::Join @@ -549,7 +549,7 @@ pub fn check_power_levels( } // If the current value is equal to the sender's current power level, reject - if user != &power_event.sender() && old_level.map(|int| (*int).into()) == Some(user_level) { + if user != power_event.sender() && old_level.map(|int| (*int).into()) == Some(user_level) { tracing::warn!("m.room.power_level cannot remove ops == to own"); return Some(false); // cannot remove ops level == to own } diff --git a/src/lib.rs b/src/lib.rs index 3c923d66..fde19729 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,7 @@ mod state_store; pub use error::{Error, Result}; pub use event_auth::{auth_check, auth_types_for_event}; -pub use state_event::{Event, Requester}; +pub use state_event::Event; pub use state_store::StateStore; // We want to yield to the reactor occasionally during state res when dealing @@ -154,7 +154,7 @@ impl StateResolution { .unwrap(); // update event_map to include the fetched events - event_map.extend(events.into_iter().map(|ev| (ev.event_id(), ev))); + event_map.extend(events.into_iter().map(|ev| (ev.event_id().clone(), ev))); // at this point our event_map == store there should be no missing events tracing::debug!("event map size: {}", event_map.len()); @@ -383,7 +383,7 @@ impl StateResolution { // This return value is the key used for sorting events, // events are then sorted by power level, time, // and lexically by event_id. - (-*pl, ev.origin_server_ts(), ev.event_id()) + (-*pl, ev.origin_server_ts(), ev.event_id().clone()) }) } diff --git a/src/state_event.rs b/src/state_event.rs index ab87b56f..0a81eec4 100644 --- a/src/state_event.rs +++ b/src/state_event.rs @@ -1,29 +1,21 @@ use std::{collections::BTreeMap, time::SystemTime}; use ruma::{ - events::{ - from_raw_json_value, - pdu::{EventHash, Pdu}, - room::member::{MemberEventContent, MembershipState}, - EventDeHelper, EventType, - }, - serde::CanonicalJsonValue, - signatures::{reference_hash, CanonicalJsonObject}, - EventId, RoomId, RoomVersionId, ServerName, UInt, UserId, + events::{pdu::EventHash, EventType}, + EventId, RoomId, ServerName, UInt, UserId, }; -use serde::{de, ser, Deserialize, Serialize}; -use serde_json::value::RawValue as RawJsonValue; +use serde_json::value::Value as JsonValue; /// Abstraction of a PDU so users can have their own PDU types. pub trait Event { /// The `EventId` of this event. - fn event_id(&self) -> EventId; + fn event_id(&self) -> &EventId; /// The `RoomId` of this event. - fn room_id(&self) -> RoomId; + fn room_id(&self) -> &RoomId; /// The `UserId` of this event. - fn sender(&self) -> UserId; + fn sender(&self) -> &UserId; /// The time of creation on the originating server. fn origin_server_ts(&self) -> SystemTime; @@ -41,401 +33,20 @@ pub trait Event { fn prev_events(&self) -> Vec; /// The maximum number of `prev_events` plus 1. - fn depth(&self) -> UInt; + /// + /// This is only used in state resolution version 1. + fn depth(&self) -> &UInt; /// All the authenticating events for this event. fn auth_events(&self) -> Vec; /// If this event is a redaction event this is the event it redacts. - fn redacts(&self) -> Option; + fn redacts(&self) -> Option<&EventId>; /// The `unsigned` content of this event. - fn unsigned(&self) -> CanonicalJsonObject; + fn unsigned(&self) -> &BTreeMap; fn hashes(&self) -> &EventHash; fn signatures(&self) -> BTreeMap, BTreeMap>; } - -#[derive(Clone, Debug, Deserialize, Serialize)] -struct EventIdHelper { - event_id: EventId, -} - -/// This feature is turned on in conduit but off when the tests run because -/// we rely on the EventId to check the state-res. -#[cfg(feature = "gen-eventid")] -fn event_id(json: &RawJsonValue) -> Result { - use std::convert::TryFrom; - EventId::try_from(format!( - "${}", - reference_hash(&from_raw_json_value(&json)?, &RoomVersionId::Version6) - .map_err(de::Error::custom)?, - )) - .map_err(de::Error::custom) -} - -/// Only turned on for testing where we need to keep the ID. -#[cfg(not(feature = "gen-eventid"))] -fn event_id(json: &RawJsonValue) -> Result { - use std::convert::TryFrom; - Ok(match from_raw_json_value::(&json) { - Ok(id) => id.event_id, - Err(_) => { - // panic!("NOT DURING TESTS"); - EventId::try_from(format!( - "${}", - reference_hash(&from_raw_json_value(&json)?, &RoomVersionId::Version6) - .map_err(de::Error::custom)?, - )) - .map_err(de::Error::custom)? - } - }) -} - -pub struct Requester<'a> { - pub prev_event_ids: Vec, - pub room_id: &'a RoomId, - pub content: &'a serde_json::Value, - pub state_key: Option, - pub sender: &'a UserId, -} - -// TODO: This no longer needs to be an enum now that PduStub is gone -#[derive(Clone, Debug)] -pub enum StateEvent { - Full(EventId, Pdu), -} - -impl Serialize for StateEvent { - fn serialize(&self, serializer: S) -> Result - where - S: ser::Serializer, - { - use ser::Error; - use std::convert::TryInto; - - match self { - Self::Full(id, ev) => { - // TODO: do we want to add the eventId when we - // serialize - let val: CanonicalJsonValue = serde_json::to_value(ev) - .map_err(S::Error::custom)? - .try_into() - .map_err(S::Error::custom)?; - - match val { - CanonicalJsonValue::Object(mut obj) => { - obj.insert( - "event_id".into(), - ruma::serde::to_canonical_value(id).map_err(S::Error::custom)?, - ); - obj.serialize(serializer) - } - _ => panic!("Pdu not an object"), - } - } - } - } -} - -impl<'de> de::Deserialize<'de> for StateEvent { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - let json = Box::::deserialize(deserializer)?; - let EventDeHelper { - room_id, unsigned, .. - } = from_raw_json_value(&json)?; - - // TODO: do we even want to try for the existing ID - - // Determine whether the event is a full or stub - // based on the fields present. - Ok(if room_id.is_some() { - match unsigned { - Some(unsigned) if unsigned.redacted_because.is_some() => { - panic!("TODO deal with redacted events") - } - _ => StateEvent::Full( - event_id(&json)?, - Pdu::RoomV3Pdu(from_raw_json_value(&json)?), - ), - } - } else { - panic!("Found stub event") - }) - } -} - -impl StateEvent { - pub fn from_id_value(id: EventId, json: serde_json::Value) -> Result { - Ok(Self::Full( - id, - Pdu::RoomV3Pdu(serde_json::from_value(json)?), - )) - } - - pub fn from_id_canon_obj( - id: EventId, - json: ruma::serde::CanonicalJsonObject, - ) -> Result { - Ok(Self::Full( - id, - // TODO: this is unfortunate (from_value(to_value(json)))... - Pdu::RoomV3Pdu(serde_json::from_value(serde_json::to_value(json)?)?), - )) - } - - pub fn to_requester(&self) -> Requester<'_> { - Requester { - prev_event_ids: self.prev_event_ids(), - room_id: self.room_id(), - content: self.content(), - state_key: Some(self.state_key()), - sender: self.sender(), - } - } - - pub fn is_power_event(&self) -> bool { - match self { - Self::Full(_, any_event) => match any_event { - Pdu::RoomV1Pdu(event) => match event.kind { - EventType::RoomPowerLevels - | EventType::RoomJoinRules - | EventType::RoomCreate => event.state_key == Some("".into()), - EventType::RoomMember => { - if let Ok(content) = - // TODO fix clone - serde_json::from_value::(event.content.clone()) - { - if [MembershipState::Leave, MembershipState::Ban] - .contains(&content.membership) - { - return event.sender.as_str() - // TODO is None here a failure - != event.state_key.as_deref().unwrap_or("NOT A STATE KEY"); - } - } - - false - } - _ => false, - }, - Pdu::RoomV3Pdu(event) => event.state_key == Some("".into()), - }, - } - } - pub fn deserialize_content( - &self, - ) -> Result { - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(ev) => serde_json::from_value(ev.content.clone()), - Pdu::RoomV3Pdu(ev) => serde_json::from_value(ev.content.clone()), - }, - } - } - pub fn origin_server_ts(&self) -> &SystemTime { - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(ev) => &ev.origin_server_ts, - Pdu::RoomV3Pdu(ev) => &ev.origin_server_ts, - }, - } - } - pub fn event_id(&self) -> EventId { - match self { - // TODO; make this a &EventId - Self::Full(id, _) => id.clone(), - } - } - - pub fn sender(&self) -> &UserId { - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(ev) => &ev.sender, - Pdu::RoomV3Pdu(ev) => &ev.sender, - }, - } - } - - pub fn redacts(&self) -> Option<&EventId> { - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(ev) => ev.redacts.as_ref(), - Pdu::RoomV3Pdu(ev) => ev.redacts.as_ref(), - }, - } - } - - pub fn room_id(&self) -> &RoomId { - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(ev) => &ev.room_id, - Pdu::RoomV3Pdu(ev) => &ev.room_id, - }, - } - } - pub fn kind(&self) -> EventType { - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(ev) => ev.kind.clone(), - Pdu::RoomV3Pdu(ev) => ev.kind.clone(), - }, - } - } - pub fn state_key(&self) -> String { - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(ev) => ev.state_key.clone(), - Pdu::RoomV3Pdu(ev) => ev.state_key.clone(), - }, - } - .expect("All state events have a state key") - } - - #[cfg(not(feature = "unstable-pre-spec"))] - pub fn origin(&self) -> String { - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(ev) => ev.origin.clone(), - Pdu::RoomV3Pdu(ev) => ev.origin.clone(), - }, - } - } - - pub fn prev_event_ids(&self) -> Vec { - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(ev) => ev.prev_events.iter().map(|(id, _)| id).cloned().collect(), - Pdu::RoomV3Pdu(ev) => ev.prev_events.clone(), - }, - } - } - - pub fn auth_events(&self) -> Vec { - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(ev) => ev.auth_events.iter().map(|(id, _)| id).cloned().collect(), - Pdu::RoomV3Pdu(ev) => ev.auth_events.to_vec(), - }, - } - } - - pub fn content(&self) -> &serde_json::Value { - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(ev) => &ev.content, - Pdu::RoomV3Pdu(ev) => &ev.content, - }, - } - } - - pub fn unsigned(&self) -> &BTreeMap { - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(ev) => &ev.unsigned, - Pdu::RoomV3Pdu(ev) => &ev.unsigned, - }, - } - } - - pub fn signatures( - &self, - ) -> BTreeMap, BTreeMap> { - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(_) => maplit::btreemap! {}, - Pdu::RoomV3Pdu(ev) => ev.signatures.clone(), - }, - } - } - - pub fn hashes(&self) -> &EventHash { - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(ev) => &ev.hashes, - Pdu::RoomV3Pdu(ev) => &ev.hashes, - }, - } - } - - pub fn depth(&self) -> &UInt { - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(ev) => &ev.depth, - Pdu::RoomV3Pdu(ev) => &ev.depth, - }, - } - } - - pub fn is_type_and_key(&self, ev_type: EventType, state_key: &str) -> bool { - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(ev) => { - ev.kind == ev_type && ev.state_key.as_deref() == Some(state_key) - } - Pdu::RoomV3Pdu(ev) => { - ev.kind == ev_type && ev.state_key.as_deref() == Some(state_key) - } - }, - } - } - - /// Returns the room version this event is formatted for. - /// - /// Currently either version 1 or 6 is returned, 6 represents - /// version 3 and above. - pub fn room_version(&self) -> RoomVersionId { - // TODO: We have to know the actual room version this is not sufficient - match self { - Self::Full(_, ev) => match ev { - Pdu::RoomV1Pdu(_) => RoomVersionId::Version1, - Pdu::RoomV3Pdu(_) => RoomVersionId::Version6, - }, - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn deserialize_pdu() { - let non_canonical_json = r#"{"auth_events": ["$FEKmyWTamMqoL3zkEC3mVPg3qkcXcUShxxaq5BltsCE", "$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc", "$3ImCSXY6bbWbZ5S2N6BMplHHlP7RkxWZCM9fMbdM2NY", "$8Lfs0rVCE9bHQrUztEF9kbsrT4zASnPEtpImZN4L2n8"], "content": {"membership": "join"}, "depth": 135, "hashes": {"sha256": "Q7OehFJaB32W3NINZKesQZH7+ba7xZVFuyCtuWQ2emk"}, "origin": "pc.koesters.xyz:59003", "origin_server_ts": 1599901756522, "prev_events": ["$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc"], "prev_state": [], "room_id": "!eGNyCFvnKcpsnIZiEV:koesters.xyz", "sender": "@timo:pc.koesters.xyz:59003", "state_key": "@timo:pc.koesters.xyz:59003", "type": "m.room.member", "signatures": {"koesters.xyz": {"ed25519:a_wwQy": "bb8T5haywaEXKNxUUjeNBfjYi/Qe32R/dGliduIs3Ct913WGzXYLjWh7xHqapie7viHPzkDw/KYJacpAYKvMBA"}, "pc.koesters.xyz:59003": {"ed25519:key1": "/B3tpaMZKoLNITrup4fbFhbIMWixxEKM49nS4MiKOFfyJjDGuC5nWsurw0m2eYzrffhkF5qQQ8+RlFvkqwqkBw"}}, "unsigned": {"age": 30, "replaces_state": "$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc", "prev_content": {"membership": "join"}, "prev_sender": "@timo:pc.koesters.xyz:59003"}}"#; - - let pdu = serde_json::from_str::(non_canonical_json).unwrap(); - - assert_eq!( - match &pdu { - StateEvent::Full(id, _) => id, - }, - &ruma::event_id!("$Sfx_o8eLfo4idpTO8_IGrKSPKoRMC1CmQugVw9tu_MU") - ); - } - - #[test] - fn serialize_pdu() { - let non_canonical_json = r#"{"auth_events": ["$FEKmyWTamMqoL3zkEC3mVPg3qkcXcUShxxaq5BltsCE", "$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc", "$3ImCSXY6bbWbZ5S2N6BMplHHlP7RkxWZCM9fMbdM2NY", "$8Lfs0rVCE9bHQrUztEF9kbsrT4zASnPEtpImZN4L2n8"], "content": {"membership": "join"}, "depth": 135, "hashes": {"sha256": "Q7OehFJaB32W3NINZKesQZH7+ba7xZVFuyCtuWQ2emk"}, "origin": "pc.koesters.xyz:59003", "origin_server_ts": 1599901756522, "prev_events": ["$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc"], "prev_state": [], "room_id": "!eGNyCFvnKcpsnIZiEV:koesters.xyz", "sender": "@timo:pc.koesters.xyz:59003", "state_key": "@timo:pc.koesters.xyz:59003", "type": "m.room.member", "signatures": {"koesters.xyz": {"ed25519:a_wwQy": "bb8T5haywaEXKNxUUjeNBfjYi/Qe32R/dGliduIs3Ct913WGzXYLjWh7xHqapie7viHPzkDw/KYJacpAYKvMBA"}, "pc.koesters.xyz:59003": {"ed25519:key1": "/B3tpaMZKoLNITrup4fbFhbIMWixxEKM49nS4MiKOFfyJjDGuC5nWsurw0m2eYzrffhkF5qQQ8+RlFvkqwqkBw"}}, "unsigned": {"age": 30, "replaces_state": "$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc", "prev_content": {"membership": "join"}, "prev_sender": "@timo:pc.koesters.xyz:59003"}}"#; - - let pdu = serde_json::from_str::(non_canonical_json).unwrap(); - - assert_eq!( - match &pdu { - StateEvent::Full(id, _) => id, - }, - &ruma::event_id!("$Sfx_o8eLfo4idpTO8_IGrKSPKoRMC1CmQugVw9tu_MU") - ); - - // TODO: the `origin` field is left out, though it seems it should be part of the eventId hashing - // For testing we must serialize the PDU with the `event_id` field this is probably not correct for production - // although without them we get "Invalid bytes in DB" errors in conduit - assert_eq!( - ruma::serde::to_canonical_json_string(&pdu).unwrap(), - r#"{"auth_events":["$FEKmyWTamMqoL3zkEC3mVPg3qkcXcUShxxaq5BltsCE","$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc","$3ImCSXY6bbWbZ5S2N6BMplHHlP7RkxWZCM9fMbdM2NY","$8Lfs0rVCE9bHQrUztEF9kbsrT4zASnPEtpImZN4L2n8"],"content":{"membership":"join"},"depth":135,"event_id":"$Sfx_o8eLfo4idpTO8_IGrKSPKoRMC1CmQugVw9tu_MU","hashes":{"sha256":"Q7OehFJaB32W3NINZKesQZH7+ba7xZVFuyCtuWQ2emk"},"origin_server_ts":1599901756522,"prev_events":["$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc"],"room_id":"!eGNyCFvnKcpsnIZiEV:koesters.xyz","sender":"@timo:pc.koesters.xyz:59003","signatures":{"koesters.xyz":{"ed25519:a_wwQy":"bb8T5haywaEXKNxUUjeNBfjYi/Qe32R/dGliduIs3Ct913WGzXYLjWh7xHqapie7viHPzkDw/KYJacpAYKvMBA"},"pc.koesters.xyz:59003":{"ed25519:key1":"/B3tpaMZKoLNITrup4fbFhbIMWixxEKM49nS4MiKOFfyJjDGuC5nWsurw0m2eYzrffhkF5qQQ8+RlFvkqwqkBw"}},"state_key":"@timo:pc.koesters.xyz:59003","type":"m.room.member","unsigned":{"age":30,"prev_content":{"membership":"join"},"prev_sender":"@timo:pc.koesters.xyz:59003","replaces_state":"$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc"}}"#, - ) - } -} diff --git a/src/state_store.rs b/src/state_store.rs index 9f863fca..50dad0ea 100644 --- a/src/state_store.rs +++ b/src/state_store.rs @@ -1,9 +1,6 @@ use std::{collections::BTreeSet, sync::Arc}; -use ruma::{ - events::pdu::ServerPdu, - identifiers::{EventId, RoomId}, -}; +use ruma::identifiers::{EventId, RoomId}; use crate::{Event, Result}; diff --git a/tests/event_auth.rs b/tests/event_auth.rs index 7fa976ad..fb6989b7 100644 --- a/tests/event_auth.rs +++ b/tests/event_auth.rs @@ -6,11 +6,11 @@ use state_res::{ // auth_check, auth_types_for_event, can_federate, check_power_levels, check_redaction, valid_membership_change, }, - Requester, StateMap + StateMap }; mod utils; -use utils::{alice, charlie, event_id, member_content_ban, room_id, INITIAL_EVENTS}; +use utils::{alice, charlie, event_id, member_content_ban, room_id, to_pdu_event, INITIAL_EVENTS}; #[test] fn test_ban_pass() { @@ -18,23 +18,25 @@ fn test_ban_pass() { let prev = events .values() - .find(|ev| ev.event_id.as_str().contains("IMC")) + .find(|ev| ev.event_id().as_str().contains("IMC")) .map(Arc::clone); let auth_events = events .values() - .map(|ev| ((ev.kind.clone(), ev.state_key.clone()), Arc::clone(ev))) + .map(|ev| ((ev.kind(), ev.state_key()), Arc::clone(ev))) .collect::>(); - let requester = Requester { - prev_event_ids: vec![event_id("IMC")], - room_id: &room_id(), - content: &member_content_ban(), - state_key: Some(charlie().to_string()), - sender: &alice(), - }; + let requester = to_pdu_event( + "HELLO", + alice(), + ruma::events::EventType::RoomMember, + Some(charlie().as_str()), + member_content_ban(), + &vec![], + &vec![event_id("IMC")], + ); - assert!(valid_membership_change(requester, prev, None, &auth_events).unwrap()) + assert!(valid_membership_change(&requester, prev, None, &auth_events).unwrap()) } #[test] @@ -43,21 +45,23 @@ fn test_ban_fail() { let prev = events .values() - .find(|ev| ev.event_id.as_str().contains("IMC")) + .find(|ev| ev.event_id().as_str().contains("IMC")) .map(Arc::clone); let auth_events = events .values() - .map(|ev| ((ev.kind.clone(), ev.state_key.clone()), Arc::clone(ev))) + .map(|ev| ((ev.kind(), ev.state_key()), Arc::clone(ev))) .collect::>(); - let requester = Requester { - prev_event_ids: vec![event_id("IMC")], - room_id: &room_id(), - content: &member_content_ban(), - state_key: Some(alice().to_string()), - sender: &charlie(), - }; + let requester = to_pdu_event( + "HELLO", + charlie(), + ruma::events::EventType::RoomMember, + Some(alice().as_str()), + member_content_ban(), + &vec![], + &vec![event_id("IMC")], + ); - assert!(!valid_membership_change(requester, prev, None, &auth_events).unwrap()) + assert!(!valid_membership_change(&requester, prev, None, &auth_events).unwrap()) } diff --git a/tests/event_sorting.rs b/tests/event_sorting.rs index 02d74b3a..168cf03e 100644 --- a/tests/event_sorting.rs +++ b/tests/event_sorting.rs @@ -4,7 +4,7 @@ use ruma::{ events::EventType, identifiers::{EventId, RoomVersionId}, }; -use state_res::{is_power_event, StateMap}; +use state_res::{is_power_event, Event, StateMap}; mod utils; use utils::{room_id, TestStore, INITIAL_EVENTS}; @@ -25,7 +25,7 @@ fn test_event_sort() { let event_map = events .values() - .map(|ev| ((ev.kind.clone(), ev.state_key.clone()), ev.clone())) + .map(|ev| ((ev.kind(), ev.state_key()), ev.clone())) .collect::>(); let auth_chain = &[] as &[_]; @@ -33,7 +33,7 @@ fn test_event_sort() { let power_events = event_map .values() .filter(|pdu| is_power_event(&pdu)) - .map(|pdu| pdu.event_id.clone()) + .map(|pdu| pdu.event_id().clone()) .collect::>(); // This is a TODO in conduit diff --git a/tests/res_with_auth_ids.rs b/tests/res_with_auth_ids.rs index 68a72837..cc123208 100644 --- a/tests/res_with_auth_ids.rs +++ b/tests/res_with_auth_ids.rs @@ -3,7 +3,7 @@ use std::{collections::BTreeMap, sync::Arc}; use ruma::{ - events::{pdu::ServerPdu, EventType}, + events::EventType, identifiers::{EventId, RoomVersionId}, }; use serde_json::json; @@ -12,7 +12,7 @@ use state_res::{StateMap, StateResolution}; mod utils; use utils::{ alice, bob, do_check, ella, event_id, member_content_ban, member_content_join, room_id, - to_pdu_event, zara, TestStore, INITIAL_EVENTS, + to_pdu_event, zara, StateEvent, TestStore, INITIAL_EVENTS, }; #[test] @@ -50,7 +50,7 @@ fn base_with_auth_chains() { let resolved = resolved .values() .cloned() - .chain(INITIAL_EVENTS().values().map(|e| e.event_id.clone())) + .chain(INITIAL_EVENTS().values().map(|e| e.event_id().clone())) .collect::>(); let expected = vec![ @@ -89,7 +89,7 @@ fn ban_with_auth_chains2() { inner.get(&event_id("PA")).unwrap(), ] .iter() - .map(|ev| ((ev.kind.clone(), ev.state_key.clone()), ev.event_id.clone())) + .map(|ev| ((ev.kind(), ev.state_key()), ev.event_id().clone())) .collect::>(); let state_set_b = [ @@ -102,7 +102,7 @@ fn ban_with_auth_chains2() { inner.get(&event_id("PA")).unwrap(), ] .iter() - .map(|ev| ((ev.kind.clone(), ev.state_key.clone()), ev.event_id.clone())) + .map(|ev| ((ev.kind(), ev.state_key()), ev.event_id().clone())) .collect::>(); let resolved: StateMap = match StateResolution::resolve( @@ -164,7 +164,7 @@ fn join_rule_with_auth_chain() { } #[allow(non_snake_case)] -fn BAN_STATE_SET() -> BTreeMap> { +fn BAN_STATE_SET() -> BTreeMap> { vec![ to_pdu_event( "PA", @@ -204,12 +204,12 @@ fn BAN_STATE_SET() -> BTreeMap> { ), ] .into_iter() - .map(|ev| (ev.event_id.clone(), ev)) + .map(|ev| (ev.event_id().clone(), ev)) .collect() } #[allow(non_snake_case)] -fn JOIN_RULE() -> BTreeMap> { +fn JOIN_RULE() -> BTreeMap> { vec![ to_pdu_event( "JR", @@ -231,6 +231,6 @@ fn JOIN_RULE() -> BTreeMap> { ), ] .into_iter() - .map(|ev| (ev.event_id.clone(), ev)) + .map(|ev| (ev.event_id().clone(), ev)) .collect() } diff --git a/tests/state_res.rs b/tests/state_res.rs index 4017ee0d..044d0811 100644 --- a/tests/state_res.rs +++ b/tests/state_res.rs @@ -12,7 +12,7 @@ use tracing_subscriber as tracer; mod utils; use utils::{ alice, bob, charlie, do_check, ella, event_id, member_content_ban, member_content_join, - room_id, to_init_pdu_event, to_pdu_event, zara, TestStore, LOGGER, + room_id, to_init_pdu_event, to_pdu_event, zara, StateEvent, TestStore, LOGGER, }; #[test] @@ -260,7 +260,7 @@ fn topic_setting() { #[test] fn test_event_map_none() { - let mut store = TestStore(btreemap! {}); + let mut store = TestStore::(btreemap! {}); // build up the DAG let (state_at_bob, state_at_charlie, expected) = store.set_up(); @@ -304,7 +304,7 @@ fn test_lexicographical_sort() { // A StateStore implementation for testing // // -impl TestStore { +impl TestStore { pub fn set_up(&mut self) -> (StateMap, StateMap, StateMap) { // to activate logging use `RUST_LOG=debug cargo t one_test_only` let _ = LOGGER.call_once(|| { @@ -321,7 +321,7 @@ impl TestStore { &[], &[], ); - let cre = create_event.event_id.clone(); + let cre = create_event.event_id().clone(); self.0.insert(cre.clone(), Arc::clone(&create_event)); let alice_mem = to_pdu_event( @@ -334,7 +334,7 @@ impl TestStore { &[cre.clone()], ); self.0 - .insert(alice_mem.event_id.clone(), Arc::clone(&alice_mem)); + .insert(alice_mem.event_id().clone(), Arc::clone(&alice_mem)); let join_rules = to_pdu_event( "IJR", @@ -342,11 +342,11 @@ impl TestStore { EventType::RoomJoinRules, Some(""), json!({ "join_rule": JoinRule::Public }), - &[cre.clone(), alice_mem.event_id.clone()], - &[alice_mem.event_id.clone()], + &[cre.clone(), alice_mem.event_id().clone()], + &[alice_mem.event_id().clone()], ); self.0 - .insert(join_rules.event_id.clone(), join_rules.clone()); + .insert(join_rules.event_id().clone(), join_rules.clone()); // Bob and Charlie join at the same time, so there is a fork // this will be represented in the state_sets when we resolve @@ -356,10 +356,10 @@ impl TestStore { EventType::RoomMember, Some(bob().to_string().as_str()), member_content_join(), - &[cre.clone(), join_rules.event_id.clone()], - &[join_rules.event_id.clone()], + &[cre.clone(), join_rules.event_id().clone()], + &[join_rules.event_id().clone()], ); - self.0.insert(bob_mem.event_id.clone(), bob_mem.clone()); + self.0.insert(bob_mem.event_id().clone(), bob_mem.clone()); let charlie_mem = to_pdu_event( "IMC", @@ -367,20 +367,20 @@ impl TestStore { EventType::RoomMember, Some(charlie().to_string().as_str()), member_content_join(), - &[cre, join_rules.event_id.clone()], - &[join_rules.event_id.clone()], + &[cre, join_rules.event_id().clone()], + &[join_rules.event_id().clone()], ); self.0 - .insert(charlie_mem.event_id.clone(), charlie_mem.clone()); + .insert(charlie_mem.event_id().clone(), charlie_mem.clone()); let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem] .iter() - .map(|e| ((e.kind.clone(), e.state_key.clone()), e.event_id.clone())) + .map(|e| ((e.kind(), e.state_key()), e.event_id().clone())) .collect::>(); let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem] .iter() - .map(|e| ((e.kind.clone(), e.state_key.clone()), e.event_id.clone())) + .map(|e| ((e.kind(), e.state_key()), e.event_id().clone())) .collect::>(); let expected = [ @@ -391,7 +391,7 @@ impl TestStore { &charlie_mem, ] .iter() - .map(|e| ((e.kind.clone(), e.state_key.clone()), e.event_id.clone())) + .map(|e| ((e.kind(), e.state_key()), e.event_id().clone())) .collect::>(); (state_at_bob, state_at_charlie, expected) diff --git a/tests/utils.rs b/tests/utils.rs index 3195f70f..bed80160 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -9,7 +9,6 @@ use std::{ use ruma::{ events::{ - pdu::ServerPdu, room::{ join_rules::JoinRule, member::{MemberEventContent, MembershipState}, @@ -19,15 +18,17 @@ use ruma::{ identifiers::{EventId, RoomId, RoomVersionId, UserId}, }; use serde_json::{json, Value as JsonValue}; -use state_res::{Error, Result, StateMap, StateResolution, StateStore}; +use state_res::{Error, Event, Result, StateMap, StateResolution, StateStore}; use tracing_subscriber as tracer; +pub use event::StateEvent; + pub static LOGGER: Once = Once::new(); static mut SERVER_TIMESTAMP: i32 = 0; pub fn do_check( - events: &[Arc], + events: &[Arc], edges: Vec>, expected_state_ids: Vec, ) { @@ -42,20 +43,20 @@ pub fn do_check( INITIAL_EVENTS() .values() .chain(events) - .map(|ev| (ev.event_id.clone(), ev.clone())) + .map(|ev| (ev.event_id().clone(), ev.clone())) .collect(), ); // This will be lexi_topo_sorted for resolution let mut graph = BTreeMap::new(); - // this is the same as in `resolve` event_id -> ServerPdu + // this is the same as in `resolve` event_id -> StateEvent let mut fake_event_map = BTreeMap::new(); // create the DB of events that led up to this point // TODO maybe clean up some of these clones it is just tests but... for ev in INITIAL_EVENTS().values().chain(events) { - graph.insert(ev.event_id.clone(), vec![]); - fake_event_map.insert(ev.event_id.clone(), ev.clone()); + graph.insert(ev.event_id().clone(), vec![]); + fake_event_map.insert(ev.event_id().clone(), ev.clone()); } for pair in INITIAL_EDGES().windows(2) { @@ -72,8 +73,8 @@ pub fn do_check( } } - // event_id -> ServerPdu - let mut event_map: BTreeMap> = BTreeMap::new(); + // event_id -> StateEvent + let mut event_map: BTreeMap> = BTreeMap::new(); // event_id -> StateMap let mut state_at_event: BTreeMap> = BTreeMap::new(); @@ -83,7 +84,7 @@ pub fn do_check( StateResolution::lexicographical_topological_sort(&graph, |id| (0, UNIX_EPOCH, id.clone())) { let fake_event = fake_event_map.get(&node).unwrap(); - let event_id = fake_event.event_id.clone(); + let event_id = fake_event.event_id().clone(); let prev_events = graph.get(&node).unwrap(); @@ -124,17 +125,17 @@ pub fn do_check( let mut state_after = state_before.clone(); - if fake_event.state_key.is_some() { - let ty = fake_event.kind.clone(); - let key = fake_event.state_key.clone(); + if fake_event.state_key().is_some() { + let ty = fake_event.kind(); + let key = fake_event.state_key(); state_after.insert((ty, key), event_id.clone()); } let auth_types = state_res::auth_types_for_event( - &fake_event.kind, - &fake_event.sender, - fake_event.state_key.clone(), - fake_event.content.clone(), + &fake_event.kind(), + fake_event.sender(), + fake_event.state_key(), + fake_event.content(), ); let mut auth_events = vec![]; @@ -147,13 +148,13 @@ pub fn do_check( // TODO The event is just remade, adding the auth_events and prev_events here // the `to_pdu_event` was split into `init` and the fn below, could be better let e = fake_event; - let ev_id = e.event_id.clone(); + let ev_id = e.event_id().clone(); let event = to_pdu_event( - &e.event_id.clone().to_string(), - e.sender.clone(), - e.kind.clone(), - e.state_key.as_deref(), - e.content.clone(), + e.event_id().as_str(), + e.sender().clone(), + e.kind().clone(), + e.state_key().as_deref(), + e.content(), &auth_events, prev_events, ); @@ -205,7 +206,7 @@ pub fn do_check( .collect::>(), )); - let key = (ev.kind.clone(), ev.state_key.clone()); + let key = (ev.kind(), ev.state_key()); expected_state.insert(key, node); } @@ -223,11 +224,11 @@ pub fn do_check( assert_eq!(expected_state, end_state); } -pub struct TestStore(pub BTreeMap>); +pub struct TestStore(pub BTreeMap>); #[allow(unused)] -impl StateStore for TestStore { - fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result> { +impl StateStore for TestStore { + fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result> { self.0 .get(event_id) .map(Arc::clone) @@ -290,7 +291,7 @@ pub fn to_init_pdu_event( ev_type: EventType, state_key: Option<&str>, content: JsonValue, -) -> Arc { +) -> Arc { let ts = unsafe { let ts = SERVER_TIMESTAMP; // increment the "origin_server_ts" value @@ -346,7 +347,7 @@ pub fn to_pdu_event( content: JsonValue, auth_events: &[S], prev_events: &[S], -) -> Arc +) -> Arc where S: AsRef, { @@ -409,7 +410,7 @@ where // all graphs start with these input events #[allow(non_snake_case)] -pub fn INITIAL_EVENTS() -> BTreeMap> { +pub fn INITIAL_EVENTS() -> BTreeMap> { // this is always called so we can init the logger here let _ = LOGGER.call_once(|| { tracer::fmt() @@ -492,7 +493,7 @@ pub fn INITIAL_EVENTS() -> BTreeMap> { ), ] .into_iter() - .map(|ev| (ev.event_id.clone(), ev)) + .map(|ev| (ev.event_id().clone(), ev)) .collect() } @@ -503,3 +504,437 @@ pub fn INITIAL_EDGES() -> Vec { .map(event_id) .collect::>() } + +pub mod event { + use std::{collections::BTreeMap, time::SystemTime}; + + use ruma::{ + events::{ + from_raw_json_value, + pdu::{EventHash, Pdu}, + room::member::{MemberEventContent, MembershipState}, + EventDeHelper, EventType, + }, + serde::CanonicalJsonValue, + signatures::reference_hash, + EventId, RoomId, RoomVersionId, ServerName, UInt, UserId, + }; + use serde::{de, ser, Deserialize, Serialize}; + use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue}; + + use state_res::Event; + + impl Event for StateEvent { + fn event_id(&self) -> &EventId { + self.event_id() + } + + fn room_id(&self) -> &RoomId { + self.room_id() + } + + fn sender(&self) -> &UserId { + self.sender() + } + fn kind(&self) -> EventType { + self.kind() + } + + fn content(&self) -> serde_json::Value { + self.content() + } + fn origin_server_ts(&self) -> SystemTime { + *self.origin_server_ts() + } + + fn state_key(&self) -> Option { + self.state_key() + } + fn prev_events(&self) -> Vec { + self.prev_event_ids() + } + fn depth(&self) -> &UInt { + self.depth() + } + fn auth_events(&self) -> Vec { + self.auth_events() + } + fn redacts(&self) -> Option<&EventId> { + self.redacts() + } + fn hashes(&self) -> &EventHash { + self.hashes() + } + fn signatures(&self) -> BTreeMap, BTreeMap> { + self.signatures() + } + fn unsigned(&self) -> &BTreeMap { + self.unsigned() + } + } + + #[derive(Clone, Debug, Deserialize, Serialize)] + struct EventIdHelper { + event_id: EventId, + } + + /// This feature is turned on in conduit but off when the tests run because + /// we rely on the EventId to check the state-res. + #[cfg(feature = "gen-eventid")] + fn event_id(json: &RawJsonValue) -> Result { + use std::convert::TryFrom; + EventId::try_from(format!( + "${}", + reference_hash(&from_raw_json_value(&json)?, &RoomVersionId::Version6) + .map_err(de::Error::custom)?, + )) + .map_err(de::Error::custom) + } + + /// Only turned on for testing where we need to keep the ID. + #[cfg(not(feature = "gen-eventid"))] + fn event_id(json: &RawJsonValue) -> Result { + use std::convert::TryFrom; + Ok(match from_raw_json_value::(&json) { + Ok(id) => id.event_id, + Err(_) => { + // panic!("NOT DURING TESTS"); + EventId::try_from(format!( + "${}", + reference_hash(&from_raw_json_value(&json)?, &RoomVersionId::Version6) + .map_err(de::Error::custom)?, + )) + .map_err(de::Error::custom)? + } + }) + } + + // TODO: This no longer needs to be an enum now that PduStub is gone + #[derive(Clone, Debug)] + pub enum StateEvent { + Full(EventId, Pdu), + } + + impl Serialize for StateEvent { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + use ser::Error; + use std::convert::TryInto; + + match self { + Self::Full(id, ev) => { + // TODO: do we want to add the eventId when we + // serialize + let val: CanonicalJsonValue = serde_json::to_value(ev) + .map_err(S::Error::custom)? + .try_into() + .map_err(S::Error::custom)?; + + match val { + CanonicalJsonValue::Object(mut obj) => { + obj.insert( + "event_id".into(), + ruma::serde::to_canonical_value(id).map_err(S::Error::custom)?, + ); + obj.serialize(serializer) + } + _ => panic!("Pdu not an object"), + } + } + } + } + } + + impl<'de> de::Deserialize<'de> for StateEvent { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let json = Box::::deserialize(deserializer)?; + let EventDeHelper { + room_id, unsigned, .. + } = from_raw_json_value(&json)?; + + // TODO: do we even want to try for the existing ID + + // Determine whether the event is a full or stub + // based on the fields present. + Ok(if room_id.is_some() { + match unsigned { + Some(unsigned) if unsigned.redacted_because.is_some() => { + panic!("TODO deal with redacted events") + } + _ => StateEvent::Full( + event_id(&json)?, + Pdu::RoomV3Pdu(from_raw_json_value(&json)?), + ), + } + } else { + panic!("Found stub event") + }) + } + } + + impl StateEvent { + pub fn from_id_value(id: EventId, json: serde_json::Value) -> Result { + Ok(Self::Full( + id, + Pdu::RoomV3Pdu(serde_json::from_value(json)?), + )) + } + + pub fn from_id_canon_obj( + id: EventId, + json: ruma::serde::CanonicalJsonObject, + ) -> Result { + Ok(Self::Full( + id, + // TODO: this is unfortunate (from_value(to_value(json)))... + Pdu::RoomV3Pdu(serde_json::from_value(serde_json::to_value(json)?)?), + )) + } + + pub fn is_power_event(&self) -> bool { + match self { + Self::Full(_, any_event) => match any_event { + Pdu::RoomV1Pdu(event) => match event.kind { + EventType::RoomPowerLevels + | EventType::RoomJoinRules + | EventType::RoomCreate => event.state_key == Some("".into()), + EventType::RoomMember => { + if let Ok(content) = + // TODO fix clone + serde_json::from_value::(event.content.clone()) + { + if [MembershipState::Leave, MembershipState::Ban] + .contains(&content.membership) + { + return event.sender.as_str() + // TODO is None here a failure + != event.state_key.as_deref().unwrap_or("NOT A STATE KEY"); + } + } + + false + } + _ => false, + }, + Pdu::RoomV3Pdu(event) => event.state_key == Some("".into()), + }, + } + } + pub fn deserialize_content( + &self, + ) -> Result { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => serde_json::from_value(ev.content.clone()), + Pdu::RoomV3Pdu(ev) => serde_json::from_value(ev.content.clone()), + }, + } + } + pub fn origin_server_ts(&self) -> &SystemTime { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => &ev.origin_server_ts, + Pdu::RoomV3Pdu(ev) => &ev.origin_server_ts, + }, + } + } + pub fn event_id(&self) -> &EventId { + match self { + // TODO; make this a &EventId + Self::Full(id, _) => id, + } + } + + pub fn sender(&self) -> &UserId { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => &ev.sender, + Pdu::RoomV3Pdu(ev) => &ev.sender, + }, + } + } + + pub fn redacts(&self) -> Option<&EventId> { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.redacts.as_ref(), + Pdu::RoomV3Pdu(ev) => ev.redacts.as_ref(), + }, + } + } + + pub fn room_id(&self) -> &RoomId { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => &ev.room_id, + Pdu::RoomV3Pdu(ev) => &ev.room_id, + }, + } + } + pub fn kind(&self) -> EventType { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.kind.clone(), + Pdu::RoomV3Pdu(ev) => ev.kind.clone(), + }, + } + } + pub fn state_key(&self) -> Option { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.state_key.clone(), + Pdu::RoomV3Pdu(ev) => ev.state_key.clone(), + }, + } + } + + #[cfg(not(feature = "unstable-pre-spec"))] + pub fn origin(&self) -> String { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.origin.clone(), + Pdu::RoomV3Pdu(ev) => ev.origin.clone(), + }, + } + } + + pub fn prev_event_ids(&self) -> Vec { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.prev_events.iter().map(|(id, _)| id).cloned().collect(), + Pdu::RoomV3Pdu(ev) => ev.prev_events.clone(), + }, + } + } + + pub fn auth_events(&self) -> Vec { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.auth_events.iter().map(|(id, _)| id).cloned().collect(), + Pdu::RoomV3Pdu(ev) => ev.auth_events.to_vec(), + }, + } + } + + pub fn content(&self) -> serde_json::Value { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.content.clone(), + Pdu::RoomV3Pdu(ev) => ev.content.clone(), + }, + } + } + + pub fn unsigned(&self) -> &BTreeMap { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => &ev.unsigned, + Pdu::RoomV3Pdu(ev) => &ev.unsigned, + }, + } + } + + pub fn signatures( + &self, + ) -> BTreeMap, BTreeMap> { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(_) => maplit::btreemap! {}, + Pdu::RoomV3Pdu(ev) => ev.signatures.clone(), + }, + } + } + + pub fn hashes(&self) -> &EventHash { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => &ev.hashes, + Pdu::RoomV3Pdu(ev) => &ev.hashes, + }, + } + } + + pub fn depth(&self) -> &UInt { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => &ev.depth, + Pdu::RoomV3Pdu(ev) => &ev.depth, + }, + } + } + + pub fn is_type_and_key(&self, ev_type: EventType, state_key: &str) -> bool { + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(ev) => { + ev.kind == ev_type && ev.state_key.as_deref() == Some(state_key) + } + Pdu::RoomV3Pdu(ev) => { + ev.kind == ev_type && ev.state_key.as_deref() == Some(state_key) + } + }, + } + } + + /// Returns the room version this event is formatted for. + /// + /// Currently either version 1 or 6 is returned, 6 represents + /// version 3 and above. + pub fn room_version(&self) -> RoomVersionId { + // TODO: We have to know the actual room version this is not sufficient + match self { + Self::Full(_, ev) => match ev { + Pdu::RoomV1Pdu(_) => RoomVersionId::Version1, + Pdu::RoomV3Pdu(_) => RoomVersionId::Version6, + }, + } + } + } + + #[cfg(test)] + mod test { + use super::*; + + #[test] + fn deserialize_pdu() { + let non_canonical_json = r#"{"auth_events": ["$FEKmyWTamMqoL3zkEC3mVPg3qkcXcUShxxaq5BltsCE", "$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc", "$3ImCSXY6bbWbZ5S2N6BMplHHlP7RkxWZCM9fMbdM2NY", "$8Lfs0rVCE9bHQrUztEF9kbsrT4zASnPEtpImZN4L2n8"], "content": {"membership": "join"}, "depth": 135, "hashes": {"sha256": "Q7OehFJaB32W3NINZKesQZH7+ba7xZVFuyCtuWQ2emk"}, "origin": "pc.koesters.xyz:59003", "origin_server_ts": 1599901756522, "prev_events": ["$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc"], "prev_state": [], "room_id": "!eGNyCFvnKcpsnIZiEV:koesters.xyz", "sender": "@timo:pc.koesters.xyz:59003", "state_key": "@timo:pc.koesters.xyz:59003", "type": "m.room.member", "signatures": {"koesters.xyz": {"ed25519:a_wwQy": "bb8T5haywaEXKNxUUjeNBfjYi/Qe32R/dGliduIs3Ct913WGzXYLjWh7xHqapie7viHPzkDw/KYJacpAYKvMBA"}, "pc.koesters.xyz:59003": {"ed25519:key1": "/B3tpaMZKoLNITrup4fbFhbIMWixxEKM49nS4MiKOFfyJjDGuC5nWsurw0m2eYzrffhkF5qQQ8+RlFvkqwqkBw"}}, "unsigned": {"age": 30, "replaces_state": "$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc", "prev_content": {"membership": "join"}, "prev_sender": "@timo:pc.koesters.xyz:59003"}}"#; + + let pdu = serde_json::from_str::(non_canonical_json).unwrap(); + + assert_eq!( + match &pdu { + StateEvent::Full(id, _) => id, + }, + &ruma::event_id!("$Sfx_o8eLfo4idpTO8_IGrKSPKoRMC1CmQugVw9tu_MU") + ); + } + + #[test] + fn serialize_pdu() { + let non_canonical_json = r#"{"auth_events": ["$FEKmyWTamMqoL3zkEC3mVPg3qkcXcUShxxaq5BltsCE", "$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc", "$3ImCSXY6bbWbZ5S2N6BMplHHlP7RkxWZCM9fMbdM2NY", "$8Lfs0rVCE9bHQrUztEF9kbsrT4zASnPEtpImZN4L2n8"], "content": {"membership": "join"}, "depth": 135, "hashes": {"sha256": "Q7OehFJaB32W3NINZKesQZH7+ba7xZVFuyCtuWQ2emk"}, "origin": "pc.koesters.xyz:59003", "origin_server_ts": 1599901756522, "prev_events": ["$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc"], "prev_state": [], "room_id": "!eGNyCFvnKcpsnIZiEV:koesters.xyz", "sender": "@timo:pc.koesters.xyz:59003", "state_key": "@timo:pc.koesters.xyz:59003", "type": "m.room.member", "signatures": {"koesters.xyz": {"ed25519:a_wwQy": "bb8T5haywaEXKNxUUjeNBfjYi/Qe32R/dGliduIs3Ct913WGzXYLjWh7xHqapie7viHPzkDw/KYJacpAYKvMBA"}, "pc.koesters.xyz:59003": {"ed25519:key1": "/B3tpaMZKoLNITrup4fbFhbIMWixxEKM49nS4MiKOFfyJjDGuC5nWsurw0m2eYzrffhkF5qQQ8+RlFvkqwqkBw"}}, "unsigned": {"age": 30, "replaces_state": "$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc", "prev_content": {"membership": "join"}, "prev_sender": "@timo:pc.koesters.xyz:59003"}}"#; + + let pdu = serde_json::from_str::(non_canonical_json).unwrap(); + + assert_eq!( + match &pdu { + StateEvent::Full(id, _) => id, + }, + &ruma::event_id!("$Sfx_o8eLfo4idpTO8_IGrKSPKoRMC1CmQugVw9tu_MU") + ); + + // TODO: the `origin` field is left out, though it seems it should be part of the eventId hashing + // For testing we must serialize the PDU with the `event_id` field this is probably not correct for production + // although without them we get "Invalid bytes in DB" errors in conduit + assert_eq!( + ruma::serde::to_canonical_json_string(&pdu).unwrap(), + r#"{"auth_events":["$FEKmyWTamMqoL3zkEC3mVPg3qkcXcUShxxaq5BltsCE","$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc","$3ImCSXY6bbWbZ5S2N6BMplHHlP7RkxWZCM9fMbdM2NY","$8Lfs0rVCE9bHQrUztEF9kbsrT4zASnPEtpImZN4L2n8"],"content":{"membership":"join"},"depth":135,"event_id":"$Sfx_o8eLfo4idpTO8_IGrKSPKoRMC1CmQugVw9tu_MU","hashes":{"sha256":"Q7OehFJaB32W3NINZKesQZH7+ba7xZVFuyCtuWQ2emk"},"origin_server_ts":1599901756522,"prev_events":["$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc"],"room_id":"!eGNyCFvnKcpsnIZiEV:koesters.xyz","sender":"@timo:pc.koesters.xyz:59003","signatures":{"koesters.xyz":{"ed25519:a_wwQy":"bb8T5haywaEXKNxUUjeNBfjYi/Qe32R/dGliduIs3Ct913WGzXYLjWh7xHqapie7viHPzkDw/KYJacpAYKvMBA"},"pc.koesters.xyz:59003":{"ed25519:key1":"/B3tpaMZKoLNITrup4fbFhbIMWixxEKM49nS4MiKOFfyJjDGuC5nWsurw0m2eYzrffhkF5qQQ8+RlFvkqwqkBw"}},"state_key":"@timo:pc.koesters.xyz:59003","type":"m.room.member","unsigned":{"age":30,"prev_content":{"membership":"join"},"prev_sender":"@timo:pc.koesters.xyz:59003","replaces_state":"$Oc8MYrZ3-eM4yBbhlj8YkYYluF9KHFDKU5uDpO-Ewcc"}}"#, + ) + } + } + +} \ No newline at end of file