Initial commit sketching out ideas
This commit is contained in:
commit
61f485ea3f
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
Cargo.lock
|
15
Cargo.toml
Normal file
15
Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "state-res"
|
||||
version = "0.1.0"
|
||||
authors = ["Devin R <devin.ragotzy@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[dependencies]
|
||||
petgraph = "0.5.1"
|
||||
serde = { version = "1.0.114", features = ["derive"] }
|
||||
serde_json = "1.0.56"
|
||||
|
||||
[dependencies.ruma]
|
||||
git = "https://github.com/ruma/ruma"
|
||||
features = ["client-api", "federation-api", "appservice-api"]
|
154
src/lib.rs
Normal file
154
src/lib.rs
Normal file
@ -0,0 +1,154 @@
|
||||
use std::{collections::BTreeMap, time::SystemTime};
|
||||
|
||||
use petgraph::Graph;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{self},
|
||||
AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent, EventType,
|
||||
},
|
||||
identifiers::{EventId, RoomId, RoomVersionId},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod state_event;
|
||||
mod state_store;
|
||||
|
||||
pub use state_event::StateEvent;
|
||||
pub use state_store::StateStore;
|
||||
|
||||
pub enum ResolutionResult {
|
||||
Conflicted(Vec<StateMap<EventId>>),
|
||||
Resolved(Vec<StateMap<EventId>>),
|
||||
}
|
||||
|
||||
/// A mapping of event type and state_key to some value `T`, usually an `EventId`.
|
||||
pub type StateMap<T> = BTreeMap<(EventType, String), T>;
|
||||
|
||||
/// A mapping of `EventId` to `T`, usually a `StateEvent`.
|
||||
pub type EventMap<T> = BTreeMap<EventId, T>;
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)] // TODO make the ser/de impls useful
|
||||
pub struct StateResolution {
|
||||
// TODO remove pub after initial testing
|
||||
/// The set of resolved events over time.
|
||||
pub resolved_events: Vec<StateEvent>,
|
||||
/// The resolved state, kept to have easy access to the last resolved
|
||||
/// layer of state.
|
||||
pub state: BTreeMap<EventType, BTreeMap<String, StateEvent>>,
|
||||
/// The graph of authenticated events, kept to find the most recent auth event
|
||||
/// in a chain for incoming state sets.
|
||||
pub auth_graph: BTreeMap<EventId, Vec<StateMap<EventId>>>,
|
||||
/// The last known point in the state graph.
|
||||
pub most_recent_resolved: Option<(EventType, String)>,
|
||||
|
||||
// fields for temp storage during resolution
|
||||
pub conflicting_events: Vec<StateEvent>,
|
||||
}
|
||||
|
||||
impl StateResolution {
|
||||
/// Resolve sets of state events as they come in. Internally `StateResolution` builds a graph
|
||||
/// and an auth chain to allow for state conflict resolution.
|
||||
pub fn resolve(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
room_version: &RoomVersionId,
|
||||
state_sets: Vec<StateMap<EventId>>,
|
||||
store: &mut dyn StateStore,
|
||||
// TODO actual error handling (`thiserror`??)
|
||||
) -> Result<ResolutionResult, serde_json::Error> {
|
||||
let mut event_map = EventMap::new();
|
||||
// split non-conflicting and conflicting state
|
||||
let (clean, mut conflicting) = self.seperate(&state_sets);
|
||||
|
||||
if conflicting.is_empty() {
|
||||
return Ok(ResolutionResult::Resolved(clean));
|
||||
}
|
||||
|
||||
// the set of auth events that are not common across server forks
|
||||
let mut auth_diff = self.get_auth_chain_diff(&state_sets, &mut event_map, store)?;
|
||||
|
||||
// add the auth_diff to conflicting now we have a full set of conflicting events
|
||||
auth_diff.extend(conflicting.iter().flat_map(|map| map.values().cloned()));
|
||||
let all_conflicted = auth_diff;
|
||||
|
||||
let all_conflicted = conflicting;
|
||||
|
||||
let power_events = all_conflicted
|
||||
.iter()
|
||||
.filter(is_power_event)
|
||||
.flat_map(|map| map.values())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// sort the power events based on power_level/clock/event_id and outgoing/incoming edges
|
||||
let sorted_power_levels = self.revers_topological_power_sort(
|
||||
room_id,
|
||||
&power_events,
|
||||
&mut event_map,
|
||||
store,
|
||||
&all_conflicted,
|
||||
);
|
||||
|
||||
// sequentially auth check each event.
|
||||
let resolved = self.iterative_auth_check(
|
||||
room_id,
|
||||
room_version,
|
||||
&power_events,
|
||||
&clean,
|
||||
&mut event_map,
|
||||
store,
|
||||
);
|
||||
|
||||
// TODO return something not a place holder
|
||||
Ok(ResolutionResult::Resolved(vec![]))
|
||||
}
|
||||
|
||||
fn seperate(
|
||||
&mut self,
|
||||
state_sets: &[StateMap<EventId>],
|
||||
) -> (Vec<StateMap<EventId>>, Vec<StateMap<EventId>>) {
|
||||
panic!()
|
||||
}
|
||||
|
||||
/// Returns a Vec of deduped EventIds that appear in some chains but no others.
|
||||
fn get_auth_chain_diff(
|
||||
&mut self,
|
||||
state_sets: &[StateMap<EventId>],
|
||||
event_map: &EventMap<StateEvent>,
|
||||
store: &mut dyn StateStore,
|
||||
) -> Result<Vec<EventId>, serde_json::Error> {
|
||||
panic!()
|
||||
}
|
||||
|
||||
fn revers_topological_power_sort(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
power_events: &[EventId],
|
||||
event_map: &EventMap<StateEvent>,
|
||||
store: &mut dyn StateStore,
|
||||
conflicted_set: &[StateMap<EventId>],
|
||||
) -> Vec<StateEvent> {
|
||||
panic!()
|
||||
}
|
||||
|
||||
fn iterative_auth_check(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
room_version: &RoomVersionId,
|
||||
power_events: &[EventId],
|
||||
unconflicted_state: &[StateMap<EventId>],
|
||||
event_map: &EventMap<StateEvent>,
|
||||
store: &mut dyn StateStore,
|
||||
) -> Vec<StateEvent> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_power_event(event: &&StateMap<EventId>) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
}
|
49
src/state_event.rs
Normal file
49
src/state_event.rs
Normal file
@ -0,0 +1,49 @@
|
||||
use ruma::events::{
|
||||
from_raw_json_value, AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent, EventDeHelper,
|
||||
};
|
||||
use serde::{de, Serialize};
|
||||
use serde_json::value::RawValue as RawJsonValue;
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum StateEvent {
|
||||
Full(AnyStateEvent),
|
||||
Sync(AnySyncStateEvent),
|
||||
Stripped(AnyStrippedStateEvent),
|
||||
}
|
||||
|
||||
impl<'de> de::Deserialize<'de> for StateEvent {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
|
||||
let EventDeHelper {
|
||||
state_key,
|
||||
event_id,
|
||||
room_id,
|
||||
unsigned,
|
||||
..
|
||||
} = from_raw_json_value(&json)?;
|
||||
|
||||
// Determine whether the event is a full, sync, or stripped
|
||||
// based on the fields present.
|
||||
if room_id.is_some() {
|
||||
Ok(match unsigned {
|
||||
Some(unsigned) if unsigned.redacted_because.is_some() => {
|
||||
panic!("TODO deal with redacted events")
|
||||
}
|
||||
_ => StateEvent::Full(from_raw_json_value(&json)?),
|
||||
})
|
||||
} else if event_id.is_some() {
|
||||
Ok(match unsigned {
|
||||
Some(unsigned) if unsigned.redacted_because.is_some() => {
|
||||
panic!("TODO deal with redacted events")
|
||||
}
|
||||
_ => StateEvent::Sync(from_raw_json_value(&json)?),
|
||||
})
|
||||
} else {
|
||||
Ok(StateEvent::Stripped(from_raw_json_value(&json)?))
|
||||
}
|
||||
}
|
||||
}
|
26
src/state_store.rs
Normal file
26
src/state_store.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use std::{collections::BTreeMap, time::SystemTime};
|
||||
|
||||
use petgraph::Graph;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{self},
|
||||
AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent, EventType,
|
||||
},
|
||||
identifiers::{EventId, RoomId, RoomVersionId},
|
||||
};
|
||||
|
||||
use crate::StateEvent;
|
||||
|
||||
pub trait StateStore {
|
||||
/// Returns the events that correspond to the `event_ids` sorted in the same order.
|
||||
fn get_events(&self, event_ids: &[EventId]) -> Result<Vec<StateEvent>, serde_json::Error>;
|
||||
|
||||
/// Returns a tuple of requested state events from `event_id` and the auth chain events that
|
||||
/// relate to the.
|
||||
fn get_remote_state_for_room(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
version: &RoomVersionId,
|
||||
event_id: &EventId,
|
||||
) -> Result<(Vec<StateEvent>, Vec<StateEvent>), serde_json::Error>;
|
||||
}
|
20
state.md
Normal file
20
state.md
Normal file
@ -0,0 +1,20 @@
|
||||
Would it be possible to abstract state res into a `ruma-state-res` crate? I've been thinking about something along the lines of
|
||||
```rust
|
||||
// The would need to be Serialize/Deserialize to save state
|
||||
struct StateResV2 {
|
||||
resolved_events: Vec<Event>,
|
||||
state_graph: of indexes into the events field?,
|
||||
most_recent_resolved: index or ptr into the graph?,
|
||||
// fields for temp storage during resolution
|
||||
conflicting_events: Vec<Event>,
|
||||
}
|
||||
|
||||
impl StateResV2 {
|
||||
/// The point of this all add nonconflicting events to the graph
|
||||
/// and resolve and add conflicting events.
|
||||
fn resolve(&mut self, events: Vec<Event>) -> Vec<Event> { }
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
Now to be totally fair I have no real understanding of state res
|
184
tests/init.rs
Normal file
184
tests/init.rs
Normal file
@ -0,0 +1,184 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{self},
|
||||
AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent, EventType,
|
||||
},
|
||||
identifiers::{EventId, RoomId, RoomVersionId},
|
||||
};
|
||||
use serde_json::{from_value as from_json_value, json, Value as JsonValue};
|
||||
use state_res::{ResolutionResult, StateEvent, StateResolution, StateStore};
|
||||
|
||||
// TODO make this an array of events
|
||||
fn federated_json() -> JsonValue {
|
||||
json!({
|
||||
"content": {
|
||||
"creator": "@example:example.org",
|
||||
"m.federate": true,
|
||||
"predecessor": {
|
||||
"event_id": "$something:example.org",
|
||||
"room_id": "!oldroom:example.org"
|
||||
},
|
||||
"room_version": "6"
|
||||
},
|
||||
"event_id": "$aaa:example.org",
|
||||
"origin_server_ts": 1,
|
||||
"room_id": "!room_id:example.org",
|
||||
"sender": "@alice:example.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.create",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn room_create() -> JsonValue {
|
||||
json!({
|
||||
"content": {
|
||||
"creator": "@example:example.org",
|
||||
"m.federate": true,
|
||||
"predecessor": {
|
||||
"event_id": "$something:example.org",
|
||||
"room_id": "!oldroom:example.org"
|
||||
},
|
||||
"room_version": "6"
|
||||
},
|
||||
"event_id": "$aaa:example.org",
|
||||
"origin_server_ts": 1,
|
||||
"room_id": "!room_id:example.org",
|
||||
"sender": "@alice:example.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.create",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn join_rules() -> JsonValue {
|
||||
json!({
|
||||
"content": {
|
||||
"join_rule": "public"
|
||||
},
|
||||
"event_id": "$bbb:example.org",
|
||||
"origin_server_ts": 2,
|
||||
"room_id": "!room_id:example.org",
|
||||
"sender": "@alice:example.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.join_rules",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn join_event() -> JsonValue {
|
||||
json!({
|
||||
"content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "example",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$ccc:example.org",
|
||||
"membership": "join",
|
||||
"room_id": "!room_id:example.org",
|
||||
"origin_server_ts": 3,
|
||||
"sender": "@alice:example.org",
|
||||
"state_key": "@alice:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1,
|
||||
"replaces_state": "$151800111315tsynI:example.org",
|
||||
"prev_content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "example",
|
||||
"membership": "invite"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn power_levels() -> JsonValue {
|
||||
json!({
|
||||
"content": {
|
||||
"ban": 50,
|
||||
"events": {
|
||||
"m.room.name": 100,
|
||||
"m.room.power_levels": 100
|
||||
},
|
||||
"events_default": 0,
|
||||
"invite": 50,
|
||||
"kick": 50,
|
||||
"notifications": {
|
||||
"room": 20
|
||||
},
|
||||
"redact": 50,
|
||||
"state_default": 50,
|
||||
"users": {
|
||||
"@example:example.org": 100
|
||||
},
|
||||
"users_default": 0
|
||||
},
|
||||
"event_id": "$ddd:example.org",
|
||||
"origin_server_ts": 4,
|
||||
"room_id": "!room_id:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.power_levels",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub struct TestStore;
|
||||
|
||||
impl StateStore for TestStore {
|
||||
fn get_events(&self, events: &[EventId]) -> Result<Vec<StateEvent>, serde_json::Error> {
|
||||
Ok(vec![from_json_value(power_levels())?])
|
||||
}
|
||||
|
||||
fn get_remote_state_for_room(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
version: &RoomVersionId,
|
||||
event_id: &EventId,
|
||||
) -> Result<(Vec<StateEvent>, Vec<StateEvent>), serde_json::Error> {
|
||||
Ok((
|
||||
vec![from_json_value(federated_json())?],
|
||||
vec![from_json_value(power_levels())?],
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let mut store = TestStore;
|
||||
|
||||
let room_id = RoomId::try_from("!room_id:example.org").unwrap();
|
||||
let room_version = RoomVersionId::version_6();
|
||||
|
||||
let a = from_json_value::<StateEvent>(room_create()).unwrap();
|
||||
let b = from_json_value::<StateEvent>(join_rules()).unwrap();
|
||||
let c = from_json_value::<StateEvent>(join_event()).unwrap();
|
||||
|
||||
let mut resolver = StateResolution::default();
|
||||
|
||||
let res = resolver
|
||||
.resolve(&room_id, &room_version, vec![a.clone()], &mut store)
|
||||
.unwrap();
|
||||
assert!(if let ResolutionResult::Resolved(_) = res {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
});
|
||||
|
||||
let resolved = resolver
|
||||
.resolve(&room_id, &room_version, vec![b, c], &mut store)
|
||||
.unwrap();
|
||||
|
||||
assert!(resolver.conflicting_events.is_empty());
|
||||
assert_eq!(resolver.resolved_events.len(), 3);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user