Add 'crates/ruma-state-res/' from commit '56bf45c0235701ac6df56993c327d2f97a499ef9'

git-subtree-dir: crates/ruma-state-res
git-subtree-mainline: 719433eb5c41fb5b8099481ce7bb0a37931b260f
git-subtree-split: 56bf45c0235701ac6df56993c327d2f97a499ef9
This commit is contained in:
Jonas Platte 2021-05-08 00:20:05 +02:00
commit 6609829735
21 changed files with 4536 additions and 0 deletions

View File

@ -0,0 +1,30 @@
name: Rust Nightly
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
profile: minimal
override: true
components: rustfmt, clippy
- name: Check formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: -- --check
- name: Catch common mistakes
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-features --all-targets -- -D warnings

View File

@ -0,0 +1,28 @@
name: Rust Stable
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Run tests
uses: actions-rs/cargo@v1
with:
command: test
- name: Run tests (unstable-pre-spec)
uses: actions-rs/cargo@v1
with:
command: test
args: --features unstable-pre-spec

2
crates/ruma-state-res/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

View File

@ -0,0 +1,37 @@
[package]
name = "state-res"
version = "0.1.0"
authors = ["Devin R <devin.ragotzy@gmail.com>"]
edition = "2018"
categories = ["api-bindings", "web-programming"]
description = "An abstraction for Matrix state resolution."
homepage = "https://www.ruma.io/"
keywords = ["matrix", "chat", "state resolution", "ruma"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/ruma/state-res"
[dependencies]
itertools = "0.10.0"
serde = { version = "1.0.118", features = ["derive"] }
serde_json = "1.0.60"
maplit = "1.0.2"
thiserror = "1.0.22"
log = "0.4.11"
[features]
unstable-pre-spec = ["ruma/unstable-pre-spec"]
[dependencies.ruma]
git = "https://github.com/ruma/ruma"
rev = "8c286e78d41770fe431e7304cc2fe23e383793df"
features = ["events", "signatures"]
[dev-dependencies]
criterion = "0.3.3"
rand = "0.7.3"
tracing-subscriber = "0.2.15"
[[bench]]
name = "state_res_bench"
harness = false

View File

@ -0,0 +1,19 @@
Copyright (c) 2020 Devin Ragotzy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,46 @@
# Matrix State Resolution in Rust!
```rust
/// 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;
/// The `RoomId` of this event.
fn room_id(&self) -> &RoomId;
/// The `UserId` of this event.
fn sender(&self) -> &UserId;
// and so on...
}
/// A mapping of event type and state_key to some value `T`, usually an `EventId`.
pub type StateMap<T> = BTreeMap<(EventType, Option<String>), T>;
/// A mapping of `EventId` to `T`, usually a `StateEvent`.
pub type EventMap<T> = BTreeMap<EventId, T>;
struct StateResolution {
// For now the StateResolution struct is empty. If "caching" `event_map`
// between `resolve` calls ends up being more efficient (probably not, as this would eat memory)
// it may have an `event_map` field. The `event_map` is all the events
// `StateResolution` has to know about to resolve state.
}
impl StateResolution {
/// The point of this all, resolve the possibly conflicting sets of events.
pub fn resolve<E: Event>(
room_id: &RoomId,
room_version: &RoomVersionId,
state_sets: &[StateMap<EventId>],
auth_events: Vec<Vec<EventId>>,
event_map: &mut EventMap<Arc<E>>,
) -> Result<StateMap<EventId>> {;
}
```
The `StateStore` trait is an abstraction around what ever database your server (or maybe even client) uses to store __P__[]()ersistant __D__[]()ata __U__[]()nits.
We use `ruma`s types when deserializing any PDU or it's contents which helps avoid a lot of type checking logic [synapse](https://github.com/matrix-org/synapse) must do while authenticating event chains.

View File

@ -0,0 +1,70 @@
# Architecture
This document describes the high-level architecture of state-res.
If you want to familiarize yourself with the code base, you are just in the right place!
## Overview
The state-res crate provides all the necessary algorithms to resolve the state of a
room according to the Matrix spec. Given sets of state and the complete authorization
chain, a final resolved state is calculated.
The state sets (`BTreeMap<(EventType, StateKey), EventId>`) can be the state of a room
according to different servers or at different points in time. The authorization chain
is the recursive set of all events that authorize events that come after.
Any event that can be referenced needs to be available in the `event_map` argument,
or the call fails. The `StateResolution` struct keeps no state and is only a
collection of associated functions.
## Important Terms
- **event** In state-res this refers to a **P**ersistent **D**ata **U**nit which
represents the event and keeps metadata used for resolution
- **state resolution** The process of calculating the final state of a DAG from
conflicting input DAGs
## Code Map
This section talks briefly about important files and data structures.
### `error`
An enum representing all possible error cases in state-res. Most of the variants are
passing information of failures from other libraries except `Error::NotFound`.
The `NotFound` variant is used when an event was not in the `event_map`.
### `event_auth`
This module contains all the logic needed to authenticate and verify events.
The main function for authentication is `auth_check`. There are a few checks
that happen to every event and specific checks for some state events.
Each event is authenticated against the state before the event.
The state is built iteratively with each successive event being checked against
the current state then added.
**Note:** Any type of event can be check, not just state events.
### `state_event`
A trait called `Event` that allows the state-res library to take any PDU type the user
supplies. The main `StateResolution::resolve` function can resolve any user-defined
type that satisfies `Event`. This avoids a lot of unnecessary conversions and
gives more flexibility to users.
### `lib`
All the associated functions of `StateResolution` that are needed to resolve state live
here. The focus is `StateResolution::resolve`, given a DAG and new events
`resolve` calculates the end state of the DAG. Everything that is used by `resolve`
is exported giving users access to the pieces of the algorithm.
**Note:** only state events (events that have a state_key field) are allowed to
participate in resolution.
## Testing
state-res has three main test types: event sorting, event authentication, and state
resolution. State resolution tests the whole system. Start by setting up a room with
events and check the resolved state after adding conflicting events.
Event authentication checks that an event passes or fails based on some initial state.
Event sorting tests that given a DAG of events, the events can be predictably sorted.

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,59 @@
11/29/2020 BRANCH: timo-spec-comp REV: d2a85669cc6056679ce6ca0fde4658a879ad2b08
lexicographical topological sort
time: [1.7123 us 1.7157 us 1.7199 us]
change: [-1.7584% -1.5433% -1.3205%] (p = 0.00 < 0.05)
Performance has improved.
Found 8 outliers among 100 measurements (8.00%)
2 (2.00%) low mild
5 (5.00%) high mild
1 (1.00%) high severe
resolve state of 5 events one fork
time: [10.981 us 10.998 us 11.020 us]
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]
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
4/26/2020 BRANCH: fix-test-serde REV:
lexicographical topological sort
time: [1.6793 us 1.6823 us 1.6857 us]
Found 9 outliers among 100 measurements (9.00%)
1 (1.00%) low mild
4 (4.00%) high mild
4 (4.00%) high severe
resolve state of 5 events one fork
time: [9.9993 us 10.062 us 10.159 us]
Found 9 outliers among 100 measurements (9.00%)
7 (7.00%) high mild
2 (2.00%) high severe
resolve state of 10 events 3 conflicting
time: [26.004 us 26.092 us 26.195 us]
Found 16 outliers among 100 measurements (16.00%)
11 (11.00%) high mild
5 (5.00%) high severe

View File

@ -0,0 +1,812 @@
// Because of criterion `cargo bench` works,
// but if you use `cargo bench -- --save-baseline <name>`
// or pass any other args to it, it fails with the error
// `cargo bench unknown option --save-baseline`.
// To pass args to criterion, use this form
// `cargo bench --bench <name of the bench> -- --save-baseline <name>`.
use std::{
collections::{BTreeMap, BTreeSet},
convert::TryFrom,
sync::Arc,
time::{Duration, UNIX_EPOCH},
};
use criterion::{criterion_group, criterion_main, Criterion};
use event::StateEvent;
use maplit::btreemap;
use ruma::{
events::{
pdu::{EventHash, Pdu, RoomV3Pdu},
room::{
join_rules::JoinRule,
member::{MemberEventContent, MembershipState},
},
EventType,
},
EventId, RoomId, RoomVersionId, UserId,
};
use serde_json::{json, Value as JsonValue};
use state_res::{Error, Event, Result, StateMap, StateResolution};
static mut SERVER_TIMESTAMP: u64 = 0;
fn lexico_topo_sort(c: &mut Criterion) {
c.bench_function("lexicographical topological sort", |b| {
let graph = btreemap! {
event_id("l") => vec![event_id("o")],
event_id("m") => vec![event_id("n"), event_id("o")],
event_id("n") => vec![event_id("o")],
event_id("o") => vec![], // "o" has zero outgoing edges but 4 incoming edges
event_id("p") => vec![event_id("o")],
};
b.iter(|| {
let _ = StateResolution::lexicographical_topological_sort(&graph, |id| {
(0, UNIX_EPOCH, id.clone())
});
})
});
}
fn resolution_shallow_auth_chain(c: &mut Criterion) {
c.bench_function("resolve state of 5 events one fork", |b| {
let mut store = TestStore(btreemap! {});
// build up the DAG
let (state_at_bob, state_at_charlie, _) = store.set_up();
b.iter(|| {
let mut ev_map: state_res::EventMap<Arc<StateEvent>> = store.0.clone();
let state_sets = vec![state_at_bob.clone(), state_at_charlie.clone()];
let _ = match StateResolution::resolve::<StateEvent>(
&room_id(),
&RoomVersionId::Version2,
&state_sets,
state_sets
.iter()
.map(|map| {
store
.auth_event_ids(&room_id(), &map.values().cloned().collect::<Vec<_>>())
.unwrap()
})
.collect(),
&mut ev_map,
) {
Ok(state) => state,
Err(e) => panic!("{}", e),
};
})
});
}
fn resolve_deeper_event_set(c: &mut Criterion) {
c.bench_function("resolve state of 10 events 3 conflicting", |b| {
let init = INITIAL_EVENTS();
let ban = BAN_STATE_SET();
let mut inner = init;
inner.extend(ban);
let store = TestStore(inner.clone());
let state_set_a = [
inner.get(&event_id("CREATE")).unwrap(),
inner.get(&event_id("IJR")).unwrap(),
inner.get(&event_id("IMA")).unwrap(),
inner.get(&event_id("IMB")).unwrap(),
inner.get(&event_id("IMC")).unwrap(),
inner.get(&event_id("MB")).unwrap(),
inner.get(&event_id("PA")).unwrap(),
]
.iter()
.map(|ev| ((ev.kind(), ev.state_key().unwrap()), ev.event_id().clone()))
.collect::<StateMap<_>>();
let state_set_b = [
inner.get(&event_id("CREATE")).unwrap(),
inner.get(&event_id("IJR")).unwrap(),
inner.get(&event_id("IMA")).unwrap(),
inner.get(&event_id("IMB")).unwrap(),
inner.get(&event_id("IMC")).unwrap(),
inner.get(&event_id("IME")).unwrap(),
inner.get(&event_id("PA")).unwrap(),
]
.iter()
.map(|ev| ((ev.kind(), ev.state_key().unwrap()), ev.event_id().clone()))
.collect::<StateMap<_>>();
b.iter(|| {
let state_sets = vec![state_set_a.clone(), state_set_b.clone()];
let _ = match StateResolution::resolve::<StateEvent>(
&room_id(),
&RoomVersionId::Version2,
&state_sets,
state_sets
.iter()
.map(|map| {
store
.auth_event_ids(&room_id(), &map.values().cloned().collect::<Vec<_>>())
.unwrap()
})
.collect(),
&mut inner,
) {
Ok(state) => state,
Err(_) => panic!("resolution failed during benchmarking"),
};
})
});
}
criterion_group!(
benches,
lexico_topo_sort,
resolution_shallow_auth_chain,
resolve_deeper_event_set
);
criterion_main!(benches);
//*/////////////////////////////////////////////////////////////////////
//
// IMPLEMENTATION DETAILS AHEAD
//
/////////////////////////////////////////////////////////////////////*/
pub struct TestStore<E: Event>(pub BTreeMap<EventId, Arc<E>>);
#[allow(unused)]
impl<E: Event> TestStore<E> {
pub fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result<Arc<E>> {
self.0
.get(event_id)
.map(Arc::clone)
.ok_or_else(|| Error::NotFound(format!("{} not found", event_id.to_string())))
}
/// Returns the events that correspond to the `event_ids` sorted in the same order.
pub fn get_events(&self, room_id: &RoomId, event_ids: &[EventId]) -> Result<Vec<Arc<E>>> {
let mut events = vec![];
for id in event_ids {
events.push(self.get_event(room_id, id)?);
}
Ok(events)
}
/// Returns a Vec of the related auth events to the given `event`.
pub fn auth_event_ids(&self, room_id: &RoomId, event_ids: &[EventId]) -> Result<Vec<EventId>> {
let mut result = vec![];
let mut stack = event_ids.to_vec();
// DFS for auth event chain
while !stack.is_empty() {
let ev_id = stack.pop().unwrap();
if result.contains(&ev_id) {
continue;
}
result.push(ev_id.clone());
let event = self.get_event(room_id, &ev_id)?;
stack.extend(event.auth_events().clone());
}
Ok(result)
}
/// Returns a Vec<EventId> representing the difference in auth chains of the given `events`.
pub fn auth_chain_diff(
&self,
room_id: &RoomId,
event_ids: Vec<Vec<EventId>>,
) -> Result<Vec<EventId>> {
let mut chains = vec![];
for ids in event_ids {
// TODO state store `auth_event_ids` returns self in the event ids list
// when an event returns `auth_event_ids` self is not contained
let chain = self
.auth_event_ids(room_id, &ids)?
.into_iter()
.collect::<BTreeSet<_>>();
chains.push(chain);
}
if let Some(chain) = chains.first() {
let rest = chains.iter().skip(1).flatten().cloned().collect();
let common = chain.intersection(&rest).collect::<Vec<_>>();
Ok(chains
.iter()
.flatten()
.filter(|id| !common.contains(id))
.cloned()
.collect::<BTreeSet<_>>()
.into_iter()
.collect())
} else {
Ok(vec![])
}
}
}
impl TestStore<StateEvent> {
pub fn set_up(&mut self) -> (StateMap<EventId>, StateMap<EventId>, StateMap<EventId>) {
let create_event = to_pdu_event::<EventId>(
"CREATE",
alice(),
EventType::RoomCreate,
Some(""),
json!({ "creator": alice() }),
&[],
&[],
);
let cre = create_event.event_id().clone();
self.0.insert(cre.clone(), Arc::clone(&create_event));
let alice_mem = to_pdu_event(
"IMA",
alice(),
EventType::RoomMember,
Some(alice().to_string().as_str()),
member_content_join(),
&[cre.clone()],
&[cre.clone()],
);
self.0
.insert(alice_mem.event_id().clone(), Arc::clone(&alice_mem));
let join_rules = to_pdu_event(
"IJR",
alice(),
EventType::RoomJoinRules,
Some(""),
json!({ "join_rule": JoinRule::Public }),
&[cre.clone(), alice_mem.event_id().clone()],
&[alice_mem.event_id().clone()],
);
self.0
.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
let bob_mem = to_pdu_event(
"IMB",
bob(),
EventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_join(),
&[cre.clone(), join_rules.event_id().clone()],
&[join_rules.event_id().clone()],
);
self.0.insert(bob_mem.event_id().clone(), bob_mem.clone());
let charlie_mem = to_pdu_event(
"IMC",
charlie(),
EventType::RoomMember,
Some(charlie().to_string().as_str()),
member_content_join(),
&[cre, join_rules.event_id().clone()],
&[join_rules.event_id().clone()],
);
self.0
.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(), e.state_key().unwrap()), e.event_id().clone()))
.collect::<StateMap<_>>();
let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem]
.iter()
.map(|e| ((e.kind(), e.state_key().unwrap()), e.event_id().clone()))
.collect::<StateMap<_>>();
let expected = [
&create_event,
&alice_mem,
&join_rules,
&bob_mem,
&charlie_mem,
]
.iter()
.map(|e| ((e.kind(), e.state_key().unwrap()), e.event_id().clone()))
.collect::<StateMap<_>>();
(state_at_bob, state_at_charlie, expected)
}
}
fn event_id(id: &str) -> EventId {
if id.contains('$') {
return EventId::try_from(id).unwrap();
}
EventId::try_from(format!("${}:foo", id)).unwrap()
}
fn alice() -> UserId {
UserId::try_from("@alice:foo").unwrap()
}
fn bob() -> UserId {
UserId::try_from("@bob:foo").unwrap()
}
fn charlie() -> UserId {
UserId::try_from("@charlie:foo").unwrap()
}
fn ella() -> UserId {
UserId::try_from("@ella:foo").unwrap()
}
fn room_id() -> RoomId {
RoomId::try_from("!test:foo").unwrap()
}
fn member_content_ban() -> JsonValue {
serde_json::to_value(MemberEventContent {
membership: MembershipState::Ban,
displayname: None,
avatar_url: None,
is_direct: None,
third_party_invite: None,
})
.unwrap()
}
fn member_content_join() -> JsonValue {
serde_json::to_value(MemberEventContent {
membership: MembershipState::Join,
displayname: None,
avatar_url: None,
is_direct: None,
third_party_invite: None,
})
.unwrap()
}
pub fn to_pdu_event<S>(
id: &str,
sender: UserId,
ev_type: EventType,
state_key: Option<&str>,
content: JsonValue,
auth_events: &[S],
prev_events: &[S],
) -> Arc<StateEvent>
where
S: AsRef<str>,
{
let ts = unsafe {
let ts = SERVER_TIMESTAMP;
// increment the "origin_server_ts" value
SERVER_TIMESTAMP += 1;
ts
};
let id = if id.contains('$') {
id.to_string()
} else {
format!("${}:foo", id)
};
let auth_events = auth_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
let prev_events = prev_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
let state_key = state_key.map(ToString::to_string);
Arc::new(StateEvent {
event_id: EventId::try_from(id).unwrap(),
rest: Pdu::RoomV3Pdu(RoomV3Pdu {
room_id: room_id(),
sender,
origin_server_ts: UNIX_EPOCH + Duration::from_secs(ts),
state_key,
kind: ev_type,
content,
redacts: None,
unsigned: btreemap! {},
#[cfg(not(feature = "unstable-pre-spec"))]
origin: "foo".into(),
auth_events,
prev_events,
depth: ruma::uint!(0),
hashes: EventHash { sha256: "".into() },
signatures: btreemap! {},
}),
})
}
// all graphs start with these input events
#[allow(non_snake_case)]
fn INITIAL_EVENTS() -> BTreeMap<EventId, Arc<StateEvent>> {
vec![
to_pdu_event::<EventId>(
"CREATE",
alice(),
EventType::RoomCreate,
Some(""),
json!({ "creator": alice() }),
&[],
&[],
),
to_pdu_event(
"IMA",
alice(),
EventType::RoomMember,
Some(alice().to_string().as_str()),
member_content_join(),
&["CREATE"],
&["CREATE"],
),
to_pdu_event(
"IPOWER",
alice(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice().to_string(): 100}}),
&["CREATE", "IMA"],
&["IMA"],
),
to_pdu_event(
"IJR",
alice(),
EventType::RoomJoinRules,
Some(""),
json!({ "join_rule": JoinRule::Public }),
&["CREATE", "IMA", "IPOWER"],
&["IPOWER"],
),
to_pdu_event(
"IMB",
bob(),
EventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IJR"],
),
to_pdu_event(
"IMC",
charlie(),
EventType::RoomMember,
Some(charlie().to_string().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IMB"],
),
to_pdu_event::<EventId>(
"START",
charlie(),
EventType::RoomTopic,
Some(""),
json!({}),
&[],
&[],
),
to_pdu_event::<EventId>(
"END",
charlie(),
EventType::RoomTopic,
Some(""),
json!({}),
&[],
&[],
),
]
.into_iter()
.map(|ev| (ev.event_id().clone(), ev))
.collect()
}
// all graphs start with these input events
#[allow(non_snake_case)]
fn BAN_STATE_SET() -> BTreeMap<EventId, Arc<StateEvent>> {
vec![
to_pdu_event(
"PA",
alice(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice(): 100, bob(): 50}}),
&["CREATE", "IMA", "IPOWER"], // auth_events
&["START"], // prev_events
),
to_pdu_event(
"PB",
alice(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice(): 100, bob(): 50}}),
&["CREATE", "IMA", "IPOWER"],
&["END"],
),
to_pdu_event(
"MB",
alice(),
EventType::RoomMember,
Some(ella().as_str()),
member_content_ban(),
&["CREATE", "IMA", "PB"],
&["PA"],
),
to_pdu_event(
"IME",
ella(),
EventType::RoomMember,
Some(ella().as_str()),
member_content_join(),
&["CREATE", "IJR", "PA"],
&["MB"],
),
]
.into_iter()
.map(|ev| (ev.event_id().clone(), ev))
.collect()
}
pub mod event {
use std::{collections::BTreeMap, time::SystemTime};
use ruma::{
events::{
pdu::{EventHash, Pdu},
room::member::MembershipState,
EventType,
},
EventId, RoomId, RoomVersionId, ServerName, ServerSigningKeyId, UInt, UserId,
};
use serde::{Deserialize, Serialize};
use serde_json::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<String> {
self.state_key()
}
fn prev_events(&self) -> Vec<EventId> {
self.prev_event_ids()
}
fn depth(&self) -> &UInt {
self.depth()
}
fn auth_events(&self) -> Vec<EventId> {
self.auth_events()
}
fn redacts(&self) -> Option<&EventId> {
self.redacts()
}
fn hashes(&self) -> &EventHash {
self.hashes()
}
fn signatures(&self) -> BTreeMap<Box<ServerName>, BTreeMap<ServerSigningKeyId, String>> {
self.signatures()
}
fn unsigned(&self) -> &BTreeMap<String, JsonValue> {
self.unsigned()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct StateEvent {
pub event_id: EventId,
#[serde(flatten)]
pub rest: Pdu,
}
impl StateEvent {
pub fn from_id_value(
id: EventId,
json: serde_json::Value,
) -> Result<Self, serde_json::Error> {
Ok(Self {
event_id: id,
rest: Pdu::RoomV3Pdu(serde_json::from_value(json)?),
})
}
pub fn from_id_canon_obj(
id: EventId,
json: ruma::serde::CanonicalJsonObject,
) -> Result<Self, serde_json::Error> {
Ok(Self {
event_id: id,
// TODO: this is unfortunate (from_value(to_value(json)))...
rest: Pdu::RoomV3Pdu(serde_json::from_value(serde_json::to_value(json)?)?),
})
}
pub fn is_power_event(&self) -> bool {
match &self.rest {
Pdu::RoomV1Pdu(event) => match event.kind {
EventType::RoomPowerLevels
| EventType::RoomJoinRules
| EventType::RoomCreate => event.state_key == Some("".into()),
EventType::RoomMember => {
// TODO fix clone
if let Ok(membership) = serde_json::from_value::<MembershipState>(
event.content["membership"].clone(),
) {
[MembershipState::Leave, MembershipState::Ban].contains(&membership)
&& event.sender.as_str()
// TODO is None here a failure
!= event.state_key.as_deref().unwrap_or("NOT A STATE KEY")
} else {
false
}
}
_ => false,
},
Pdu::RoomV3Pdu(event) => event.state_key == Some("".into()),
}
}
pub fn deserialize_content<C: serde::de::DeserializeOwned>(
&self,
) -> Result<C, serde_json::Error> {
match &self.rest {
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.rest {
Pdu::RoomV1Pdu(ev) => &ev.origin_server_ts,
Pdu::RoomV3Pdu(ev) => &ev.origin_server_ts,
}
}
pub fn event_id(&self) -> &EventId {
&self.event_id
}
pub fn sender(&self) -> &UserId {
match &self.rest {
Pdu::RoomV1Pdu(ev) => &ev.sender,
Pdu::RoomV3Pdu(ev) => &ev.sender,
}
}
pub fn redacts(&self) -> Option<&EventId> {
match &self.rest {
Pdu::RoomV1Pdu(ev) => ev.redacts.as_ref(),
Pdu::RoomV3Pdu(ev) => ev.redacts.as_ref(),
}
}
pub fn room_id(&self) -> &RoomId {
match &self.rest {
Pdu::RoomV1Pdu(ev) => &ev.room_id,
Pdu::RoomV3Pdu(ev) => &ev.room_id,
}
}
pub fn kind(&self) -> EventType {
match &self.rest {
Pdu::RoomV1Pdu(ev) => ev.kind.clone(),
Pdu::RoomV3Pdu(ev) => ev.kind.clone(),
}
}
pub fn state_key(&self) -> Option<String> {
match &self.rest {
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.rest {
Pdu::RoomV1Pdu(ev) => ev.origin.clone(),
Pdu::RoomV3Pdu(ev) => ev.origin.clone(),
}
}
pub fn prev_event_ids(&self) -> Vec<EventId> {
match &self.rest {
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<EventId> {
match &self.rest {
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.rest {
Pdu::RoomV1Pdu(ev) => ev.content.clone(),
Pdu::RoomV3Pdu(ev) => ev.content.clone(),
}
}
pub fn unsigned(&self) -> &BTreeMap<String, serde_json::Value> {
match &self.rest {
Pdu::RoomV1Pdu(ev) => &ev.unsigned,
Pdu::RoomV3Pdu(ev) => &ev.unsigned,
}
}
pub fn signatures(
&self,
) -> BTreeMap<Box<ServerName>, BTreeMap<ServerSigningKeyId, String>> {
match &self.rest {
Pdu::RoomV1Pdu(_) => maplit::btreemap! {},
Pdu::RoomV3Pdu(ev) => ev.signatures.clone(),
}
}
pub fn hashes(&self) -> &EventHash {
match &self.rest {
Pdu::RoomV1Pdu(ev) => &ev.hashes,
Pdu::RoomV3Pdu(ev) => &ev.hashes,
}
}
pub fn depth(&self) -> &UInt {
match &self.rest {
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.rest {
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.rest {
Pdu::RoomV1Pdu(_) => RoomVersionId::Version1,
Pdu::RoomV3Pdu(_) => RoomVersionId::Version6,
}
}
}
}

View File

@ -0,0 +1 @@
imports_granularity="Crate"

View File

@ -0,0 +1,35 @@
use serde_json::Error as JsonError;
use thiserror::Error;
/// Result type for state resolution.
pub type Result<T> = std::result::Result<T, Error>;
/// Represents the various errors that arise when resolving state.
#[derive(Error, Debug)]
pub enum Error {
/// A deserialization error.
#[error(transparent)]
SerdeJson(#[from] JsonError),
/// The given option or version is unsupported.
#[error("Unsupported room version: {0}")]
Unsupported(String),
/// The given event was not found.
#[error("Not found error: {0}")]
NotFound(String),
/// Invalid fields in the given PDU.
#[error("Invalid PDU: {0}")]
InvalidPdu(String),
/// A custom error.
#[error("{0}")]
Custom(Box<dyn std::error::Error>),
}
impl Error {
pub fn custom<E: std::error::Error + 'static>(e: E) -> Self {
Self::Custom(Box::new(e))
}
}

View File

@ -0,0 +1,885 @@
use std::{convert::TryFrom, sync::Arc};
use log::warn;
use maplit::btreeset;
use ruma::{
events::{
room::{
create::CreateEventContent,
join_rules::{JoinRule, JoinRulesEventContent},
member::{MembershipState, ThirdPartyInvite},
power_levels::PowerLevelsEventContent,
},
EventType,
},
RoomVersionId, UserId,
};
use crate::{room_version::RoomVersion, Error, Event, Result, StateMap};
/// For the given event `kind` what are the relevant auth events
/// that are needed to authenticate this `content`.
pub fn auth_types_for_event(
kind: &EventType,
sender: &UserId,
state_key: Option<String>,
content: serde_json::Value,
) -> Vec<(EventType, String)> {
if kind == &EventType::RoomCreate {
return vec![];
}
let mut auth_types = vec![
(EventType::RoomPowerLevels, "".to_string()),
(EventType::RoomMember, sender.to_string()),
(EventType::RoomCreate, "".to_string()),
];
if kind == &EventType::RoomMember {
if let Some(state_key) = state_key {
if let Some(Ok(membership)) = content
.get("membership")
.map(|m| serde_json::from_value::<MembershipState>(m.clone()))
{
if [MembershipState::Join, MembershipState::Invite].contains(&membership) {
let key = (EventType::RoomJoinRules, "".to_string());
if !auth_types.contains(&key) {
auth_types.push(key)
}
}
let key = (EventType::RoomMember, state_key);
if !auth_types.contains(&key) {
auth_types.push(key)
}
if membership == MembershipState::Invite {
if let Some(Ok(t_id)) = content
.get("third_party_invite")
.map(|t| serde_json::from_value::<ThirdPartyInvite>(t.clone()))
{
let key = (EventType::RoomThirdPartyInvite, t_id.signed.token);
if !auth_types.contains(&key) {
auth_types.push(key)
}
}
}
}
}
}
auth_types
}
/// Authenticate the incoming `event`. The steps of authentication are:
/// * check that the event is being authenticated for the correct room
/// * check that the events signatures are valid
/// * then there are checks for specific event types
///
/// The `auth_events` that are passed to this function should be a state snapshot.
/// We need to know if the event passes auth against some state not a recursive collection
/// of auth_events fields.
///
/// ## Returns
/// This returns an `Error` only when serialization fails or some other fatal outcome.
pub fn auth_check<E: Event>(
room_version: &RoomVersion,
incoming_event: &Arc<E>,
prev_event: Option<Arc<E>>,
auth_events: &StateMap<Arc<E>>,
current_third_party_invite: Option<Arc<E>>,
) -> Result<bool> {
log::info!(
"auth_check beginning for {} ({})",
incoming_event.event_id(),
incoming_event.kind()
);
// [synapse] check that all the events are in the same room as `incoming_event`
// [synapse] do_sig_check check the event has valid signatures for member events
// TODO do_size_check is false when called by `iterative_auth_check`
// do_size_check is also mostly accomplished by ruma with the exception of checking event_type,
// state_key, and json are below a certain size (255 and 65_536 respectively)
// Implementation of https://matrix.org/docs/spec/rooms/v1#authorization-rules
//
// 1. If type is m.room.create:
if incoming_event.kind() == EventType::RoomCreate {
log::info!("start m.room.create check");
// If it has any previous events, reject
if !incoming_event.prev_events().is_empty() {
log::warn!("the room creation event had previous events");
return Ok(false);
}
// If the domain of the room_id does not match the domain of the sender, reject
if incoming_event.room_id().server_name() != incoming_event.sender().server_name() {
log::warn!("creation events server does not match sender");
return Ok(false); // creation events room id does not match senders
}
// If content.room_version is present and is not a recognized version, reject
if serde_json::from_value::<RoomVersionId>(
incoming_event
.content()
.get("room_version")
.cloned()
// TODO synapse defaults to version 1
.unwrap_or_else(|| serde_json::json!("1")),
)
.is_err()
{
log::warn!("invalid room version found in m.room.create event");
return Ok(false);
}
// If content has no creator field, reject
if incoming_event.content().get("creator").is_none() {
log::warn!("no creator field found in room create content");
return Ok(false);
}
log::info!("m.room.create event was allowed");
return Ok(true);
}
/*
// 2. Reject if auth_events
// a. auth_events cannot have duplicate keys since it's a BTree
// b. All entries are valid auth events according to spec
let expected_auth = auth_types_for_event(
incoming_event.kind,
incoming_event.sender(),
incoming_event.state_key,
incoming_event.content().clone(),
);
dbg!(&expected_auth);
for ev_key in auth_events.keys() {
// (b)
if !expected_auth.contains(ev_key) {
log::warn!("auth_events contained invalid auth event");
return Ok(false);
}
}
*/
// 3. If event does not have m.room.create in auth_events reject
if auth_events
.get(&(EventType::RoomCreate, "".to_string()))
.is_none()
{
log::warn!("no m.room.create event in auth chain");
return Ok(false);
}
// [synapse] checks for federation here
// 4. if type is m.room.aliases
if incoming_event.kind() == EventType::RoomAliases && room_version.special_case_aliases_auth {
log::info!("starting m.room.aliases check");
// If sender's domain doesn't matches state_key, reject
if incoming_event.state_key() != Some(incoming_event.sender().server_name().to_string()) {
log::warn!("state_key does not match sender");
return Ok(false);
}
log::info!("m.room.aliases event was allowed");
return Ok(true);
}
if incoming_event.kind() == EventType::RoomMember {
log::info!("starting m.room.member check");
let state_key = match incoming_event.state_key() {
None => {
log::warn!("no statekey in member event");
return Ok(false);
}
Some(s) => s,
};
let membership = incoming_event
.content()
.get("membership")
.map(|m| serde_json::from_value::<MembershipState>(m.clone()));
if !matches!(membership, Some(Ok(_))) {
log::warn!("no valid membership field found for m.room.member event content");
return Ok(false);
}
if !valid_membership_change(
&state_key,
incoming_event.sender(),
incoming_event.content(),
prev_event,
current_third_party_invite,
auth_events,
)? {
return Ok(false);
}
log::info!("m.room.member event was allowed");
return Ok(true);
}
// If the sender's current membership state is not join, reject
match check_event_sender_in_room(incoming_event.sender(), auth_events) {
Some(true) => {} // sender in room
Some(false) => {
log::warn!("sender's membership is not join");
return Ok(false);
}
None => {
log::warn!("sender not found in room");
return Ok(false);
}
}
// Allow if and only if sender's current power level is greater than
// or equal to the invite level
if incoming_event.kind() == EventType::RoomThirdPartyInvite
&& !can_send_invite(incoming_event, auth_events)?
{
log::warn!("sender's cannot send invites in this room");
return Ok(false);
}
// If the event type's required power level is greater than the sender's power level, reject
// If the event has a state_key that starts with an @ and does not match the sender, reject.
if !can_send_event(incoming_event, auth_events) {
log::warn!("user cannot send event");
return Ok(false);
}
if incoming_event.kind() == EventType::RoomPowerLevels {
log::info!("starting m.room.power_levels check");
if let Some(required_pwr_lvl) =
check_power_levels(room_version, incoming_event, auth_events)
{
if !required_pwr_lvl {
log::warn!("power level was not allowed");
return Ok(false);
}
} else {
log::warn!("power level was not allowed");
return Ok(false);
}
log::info!("power levels event allowed");
}
// Room version 3: Redaction events are always accepted (provided the event is allowed by `events` and
// `events_default` in the power levels). However, servers should not apply or send redaction's
// to clients until both the redaction event and original event have been seen, and are valid.
// Servers should only apply redaction's to events where the sender's domains match,
// or the sender of the redaction has the appropriate permissions per the power levels.
if room_version.extra_redaction_checks
&& incoming_event.kind() == EventType::RoomRedaction
&& !check_redaction(room_version, incoming_event, auth_events)?
{
return Ok(false);
}
log::info!("allowing event passed all checks");
Ok(true)
}
// TODO deserializing the member, power, join_rules event contents is done in conduit
// just before this is called. Could they be passed in?
/// Does the user who sent this member event have required power levels to do so.
///
/// * `user` - Information about the membership event and user making the request.
/// * `prev_event` - The event that occurred immediately before the `user` event or None.
/// * `auth_events` - The set of auth events that relate to a membership event.
/// this is generated by calling `auth_types_for_event` with the membership event and
/// the current State.
pub fn valid_membership_change<E: Event>(
state_key: &str,
user_sender: &UserId,
content: serde_json::Value,
prev_event: Option<Arc<E>>,
current_third_party_invite: Option<Arc<E>>,
auth_events: &StateMap<Arc<E>>,
) -> Result<bool> {
let target_membership = serde_json::from_value::<MembershipState>(
content
.get("membership")
.expect("we should test before that this field exists")
.clone(),
)?;
let third_party_invite = content
.get("third_party_invite")
.map(|t| serde_json::from_value::<ThirdPartyInvite>(t.clone()));
let target_user_id =
UserId::try_from(state_key).map_err(|e| Error::InvalidPdu(format!("{}", e)))?;
let key = (EventType::RoomMember, user_sender.to_string());
let sender = auth_events.get(&key);
let sender_membership = sender.map_or(Ok::<_, Error>(MembershipState::Leave), |pdu| {
Ok(serde_json::from_value::<MembershipState>(
pdu.content()
.get("membership")
.expect("we assume existing events are valid")
.clone(),
)?)
})?;
let key = (EventType::RoomMember, target_user_id.to_string());
let current = auth_events.get(&key);
let current_membership = current.map_or(Ok::<_, Error>(MembershipState::Leave), |pdu| {
Ok(serde_json::from_value::<MembershipState>(
pdu.content()
.get("membership")
.expect("we assume existing events are valid")
.clone(),
)?)
})?;
let key = (EventType::RoomPowerLevels, "".into());
let power_levels = auth_events.get(&key).map_or_else(
|| Ok::<_, Error>(PowerLevelsEventContent::default()),
|power_levels| {
serde_json::from_value::<PowerLevelsEventContent>(power_levels.content())
.map_err(Into::into)
},
)?;
let sender_power = power_levels.users.get(user_sender).map_or_else(
|| {
if sender_membership != MembershipState::Join {
None
} else {
Some(&power_levels.users_default)
}
},
// If it's okay, wrap with Some(_)
Some,
);
let target_power = power_levels.users.get(&target_user_id).map_or_else(
|| {
if target_membership != MembershipState::Join {
None
} else {
Some(&power_levels.users_default)
}
},
// If it's okay, wrap with Some(_)
Some,
);
let key = (EventType::RoomJoinRules, "".into());
let join_rules_event = auth_events.get(&key);
let mut join_rules = JoinRule::Invite;
if let Some(jr) = join_rules_event {
join_rules = serde_json::from_value::<JoinRulesEventContent>(jr.content())?.join_rule;
}
if let Some(prev) = prev_event {
if prev.kind() == EventType::RoomCreate && prev.prev_events().is_empty() {
return Ok(true);
}
}
Ok(if target_membership == MembershipState::Join {
if user_sender != &target_user_id {
warn!("Can't make other user join");
false
} else if let MembershipState::Ban = current_membership {
warn!("Banned user can't join");
false
} else {
let allow = join_rules == JoinRule::Invite
&& (current_membership == MembershipState::Join
|| current_membership == MembershipState::Invite)
|| join_rules == JoinRule::Public;
if !allow {
warn!("Can't join if join rules is not public and user is not invited/joined");
}
allow
}
} else if target_membership == MembershipState::Invite {
// If content has third_party_invite key
if let Some(Ok(tp_id)) = third_party_invite {
if current_membership == MembershipState::Ban {
warn!("Can't invite banned user");
false
} else {
let allow = verify_third_party_invite(
Some(state_key),
user_sender,
&tp_id,
current_third_party_invite,
);
if !allow {
warn!("Third party invite invalid");
}
allow
}
} else if sender_membership != MembershipState::Join
|| current_membership == MembershipState::Join
|| current_membership == MembershipState::Ban
{
warn!(
"Can't invite user if sender not joined or the user is currently joined or banned"
);
false
} else {
let allow = sender_power
.filter(|&p| p >= &power_levels.invite)
.is_some();
if !allow {
warn!("User does not have enough power to invite");
}
allow
}
} else if target_membership == MembershipState::Leave {
if user_sender == &target_user_id {
let allow = current_membership == MembershipState::Join
|| current_membership == MembershipState::Invite;
if !allow {
warn!("Can't leave if not invited or joined");
}
allow
} else if sender_membership != MembershipState::Join
|| current_membership == MembershipState::Ban
&& sender_power.filter(|&p| p < &power_levels.ban).is_some()
{
warn!("Can't kick if sender not joined or user is already banned");
false
} else {
let allow = sender_power.filter(|&p| p >= &power_levels.kick).is_some()
&& target_power < sender_power;
if !allow {
warn!("User does not have enough power to kick");
}
allow
}
} else if target_membership == MembershipState::Ban {
if sender_membership != MembershipState::Join {
warn!("Can't ban user if sender is not joined");
false
} else {
let allow = sender_power.filter(|&p| p >= &power_levels.ban).is_some()
&& target_power < sender_power;
if !allow {
warn!("User does not have enough power to ban");
}
allow
}
} else {
warn!("Unknown membership transition");
false
})
}
/// Is the event's sender in the room that they sent the event to.
pub fn check_event_sender_in_room<E: Event>(
sender: &UserId,
auth_events: &StateMap<Arc<E>>,
) -> Option<bool> {
let mem = auth_events.get(&(EventType::RoomMember, sender.to_string()))?;
let membership = serde_json::from_value::<MembershipState>(
mem.content()
.get("membership")
.expect("we should test before that this field exists")
.clone(),
)
.ok()?;
Some(membership == MembershipState::Join)
}
/// Is the user allowed to send a specific event based on the rooms power levels. Does the event
/// have the correct userId as it's state_key if it's not the "" state_key.
pub fn can_send_event<E: Event>(event: &Arc<E>, auth_events: &StateMap<Arc<E>>) -> bool {
let ple = auth_events.get(&(EventType::RoomPowerLevels, "".into()));
let event_type_power_level = get_send_level(&event.kind(), event.state_key(), ple);
let user_level = get_user_power_level(event.sender(), auth_events);
log::debug!(
"{} ev_type {} usr {}",
event.event_id().as_str(),
event_type_power_level,
user_level
);
if user_level < event_type_power_level {
return false;
}
if event
.state_key()
.as_ref()
.map_or(false, |k| k.starts_with('@'))
&& event.state_key().as_deref() != Some(event.sender().as_str())
{
return false; // permission required to post in this room
}
true
}
/// Confirm that the event sender has the required power levels.
pub fn check_power_levels<E: Event>(
room_version: &RoomVersion,
power_event: &Arc<E>,
auth_events: &StateMap<Arc<E>>,
) -> Option<bool> {
let power_event_state_key = power_event
.state_key()
.expect("power events have state keys");
let key = (power_event.kind(), power_event_state_key);
let current_state = if let Some(current_state) = auth_events.get(&key) {
current_state
} else {
// If there is no previous m.room.power_levels event in the room, allow
return Some(true);
};
// If users key in content is not a dictionary with keys that are valid user IDs
// with values that are integers (or a string that is an integer), reject.
let user_content =
serde_json::from_value::<PowerLevelsEventContent>(power_event.content()).unwrap();
let current_content =
serde_json::from_value::<PowerLevelsEventContent>(current_state.content()).unwrap();
// validation of users is done in Ruma, synapse for loops validating user_ids and integers here
log::info!("validation of power event finished");
let user_level = get_user_power_level(power_event.sender(), auth_events);
let mut user_levels_to_check = btreeset![];
let old_list = &current_content.users;
let user_list = &user_content.users;
for user in old_list.keys().chain(user_list.keys()) {
let user: &UserId = user;
user_levels_to_check.insert(user);
}
log::debug!("users to check {:?}", user_levels_to_check);
let mut event_levels_to_check = btreeset![];
let old_list = &current_content.events;
let new_list = &user_content.events;
for ev_id in old_list.keys().chain(new_list.keys()) {
let ev_id: &EventType = ev_id;
event_levels_to_check.insert(ev_id);
}
log::debug!("events to check {:?}", event_levels_to_check);
let old_state = &current_content;
let new_state = &user_content;
// synapse does not have to split up these checks since we can't combine UserIds and
// EventTypes we do 2 loops
// UserId loop
for user in user_levels_to_check {
let old_level = old_state.users.get(user);
let new_level = new_state.users.get(user);
if old_level.is_some() && new_level.is_some() && old_level == new_level {
continue;
}
// 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) {
log::warn!("m.room.power_level cannot remove ops == to own");
return Some(false); // cannot remove ops level == to own
}
// If the current value is higher than the sender's current power level, reject
// If the new value is higher than the sender's current power level, reject
let old_level_too_big = old_level.map(|int| (*int).into()) > Some(user_level);
let new_level_too_big = new_level.map(|int| (*int).into()) > Some(user_level);
if old_level_too_big || new_level_too_big {
log::warn!("m.room.power_level failed to add ops > than own");
return Some(false); // cannot add ops greater than own
}
}
// EventType loop
for ev_type in event_levels_to_check {
let old_level = old_state.events.get(ev_type);
let new_level = new_state.events.get(ev_type);
if old_level.is_some() && new_level.is_some() && old_level == new_level {
continue;
}
// If the current value is higher than the sender's current power level, reject
// If the new value is higher than the sender's current power level, reject
let old_level_too_big = old_level.map(|int| (*int).into()) > Some(user_level);
let new_level_too_big = new_level.map(|int| (*int).into()) > Some(user_level);
if old_level_too_big || new_level_too_big {
log::warn!("m.room.power_level failed to add ops > than own");
return Some(false); // cannot add ops greater than own
}
}
// Notifications, currently there is only @room
if room_version.limit_notifications_power_levels {
let old_level = old_state.notifications.room;
let new_level = new_state.notifications.room;
if old_level != new_level {
// If the current value is higher than the sender's current power level, reject
// If the new value is higher than the sender's current power level, reject
let old_level_too_big = i64::from(old_level) > user_level;
let new_level_too_big = i64::from(new_level) > user_level;
if old_level_too_big || new_level_too_big {
log::warn!("m.room.power_level failed to add ops > than own");
return Some(false); // cannot add ops greater than own
}
}
}
let levels = [
"users_default",
"events_default",
"state_default",
"ban",
"redact",
"kick",
"invite",
];
let old_state = serde_json::to_value(old_state).unwrap();
let new_state = serde_json::to_value(new_state).unwrap();
for lvl_name in &levels {
if let Some((old_lvl, new_lvl)) = get_deserialize_levels(&old_state, &new_state, lvl_name) {
let old_level_too_big = old_lvl > user_level;
let new_level_too_big = new_lvl > user_level;
if old_level_too_big || new_level_too_big {
log::warn!("cannot add ops > than own");
return Some(false);
}
}
}
Some(true)
}
fn get_deserialize_levels(
old: &serde_json::Value,
new: &serde_json::Value,
name: &str,
) -> Option<(i64, i64)> {
Some((
serde_json::from_value(old.get(name)?.clone()).ok()?,
serde_json::from_value(new.get(name)?.clone()).ok()?,
))
}
/// Does the event redacting come from a user with enough power to redact the given event.
pub fn check_redaction<E: Event>(
_room_version: &RoomVersion,
redaction_event: &Arc<E>,
auth_events: &StateMap<Arc<E>>,
) -> Result<bool> {
let user_level = get_user_power_level(redaction_event.sender(), auth_events);
let redact_level = get_named_level(auth_events, "redact", 50);
if user_level >= redact_level {
log::info!("redaction allowed via power levels");
return Ok(true);
}
// If the domain of the event_id of the event being redacted is the same as the
// domain of the event_id of the m.room.redaction, allow
if redaction_event.event_id().server_name()
== redaction_event
.redacts()
.as_ref()
.and_then(|id| id.server_name())
{
log::info!("redaction event allowed via room version 1 rules");
return Ok(true);
}
Ok(false)
}
/// Check that the member event matches `state`.
///
/// This function returns false instead of failing when deserialization fails.
pub fn check_membership<E: Event>(member_event: Option<Arc<E>>, state: MembershipState) -> bool {
if let Some(event) = member_event {
if let Some(Ok(membership)) = event
.content()
.get("membership")
.map(|m| serde_json::from_value::<MembershipState>(m.clone()))
{
membership == state
} else {
false
}
} else {
false
}
}
/// Can this room federate based on its m.room.create event.
pub fn can_federate<E: Event>(auth_events: &StateMap<Arc<E>>) -> bool {
let creation_event = auth_events.get(&(EventType::RoomCreate, "".into()));
if let Some(ev) = creation_event {
if let Some(fed) = ev.content().get("m.federate") {
fed == "true"
} else {
false
}
} else {
false
}
}
/// Helper function to fetch a field, `name`, from a "m.room.power_level" event's content.
/// or return `default` if no power level event is found or zero if no field matches `name`.
pub fn get_named_level<E: Event>(auth_events: &StateMap<Arc<E>>, name: &str, default: i64) -> i64 {
let power_level_event = auth_events.get(&(EventType::RoomPowerLevels, "".into()));
if let Some(pl) = power_level_event {
// TODO do this the right way and deserialize
if let Some(level) = pl.content().get(name) {
level.to_string().parse().unwrap_or(default)
} else {
0
}
} else {
default
}
}
/// Helper function to fetch a users default power level from a "m.room.power_level" event's `users`
/// object.
pub fn get_user_power_level<E: Event>(user_id: &UserId, auth_events: &StateMap<Arc<E>>) -> i64 {
if let Some(pl) = auth_events.get(&(EventType::RoomPowerLevels, "".into())) {
if let Ok(content) = serde_json::from_value::<PowerLevelsEventContent>(pl.content()) {
if let Some(level) = content.users.get(user_id) {
(*level).into()
} else {
content.users_default.into()
}
} else {
0 // TODO if this fails DB error?
}
} else {
// if no power level event found the creator gets 100 everyone else gets 0
let key = (EventType::RoomCreate, "".into());
if let Some(create) = auth_events.get(&key) {
if let Ok(c) = serde_json::from_value::<CreateEventContent>(create.content()) {
if &c.creator == user_id {
100
} else {
0
}
} else {
0
}
} else {
0
}
}
}
/// Helper function to fetch the power level needed to send an event of type
/// `e_type` based on the rooms "m.room.power_level" event.
pub fn get_send_level<E: Event>(
e_type: &EventType,
state_key: Option<String>,
power_lvl: Option<&Arc<E>>,
) -> i64 {
log::debug!("{:?} {:?}", e_type, state_key);
power_lvl
.and_then(|ple| {
serde_json::from_value::<PowerLevelsEventContent>(ple.content())
.map(|content| {
content.events.get(e_type).copied().unwrap_or_else(|| {
if state_key.is_some() {
content.state_default
} else {
content.events_default
}
})
})
.ok()
})
.map(i64::from)
.unwrap_or_else(|| if state_key.is_some() { 50 } else { 0 })
}
/// Check user can send invite.
pub fn can_send_invite<E: Event>(event: &Arc<E>, auth_events: &StateMap<Arc<E>>) -> Result<bool> {
let user_level = get_user_power_level(event.sender(), auth_events);
let key = (EventType::RoomPowerLevels, "".into());
let invite_level = auth_events
.get(&key)
.map_or_else(
|| Ok::<_, Error>(ruma::int!(50)),
|power_levels| {
serde_json::from_value::<PowerLevelsEventContent>(power_levels.content())
.map(|pl| pl.invite)
.map_err(Into::into)
},
)?
.into();
Ok(user_level >= invite_level)
}
pub fn verify_third_party_invite<E: Event>(
user_state_key: Option<&str>,
sender: &UserId,
tp_id: &ThirdPartyInvite,
current_third_party_invite: Option<Arc<E>>,
) -> bool {
// 1. check for user being banned happens before this is called
// checking for mxid and token keys is done by ruma when deserializing
if user_state_key != Some(tp_id.signed.mxid.as_str()) {
return false;
}
// If there is no m.room.third_party_invite event in the current room state
// with state_key matching token, reject
if let Some(current_tpid) = current_third_party_invite {
if current_tpid.state_key().as_ref() != Some(&tp_id.signed.token) {
return false;
}
if sender != current_tpid.sender() {
return false;
}
// If any signature in signed matches any public key in the m.room.third_party_invite event, allow
if let Ok(tpid_ev) = serde_json::from_value::<
ruma::events::room::third_party_invite::ThirdPartyInviteEventContent,
>(current_tpid.content())
{
// A list of public keys in the public_keys field
for key in tpid_ev.public_keys.unwrap_or_default() {
if key.public_key == tp_id.signed.token {
return true;
}
}
// A single public key in the public_key field
tpid_ev.public_key == tp_id.signed.token
} else {
false
}
} else {
false
}
}

View File

@ -0,0 +1,717 @@
use std::{
cmp::Reverse,
collections::{BTreeMap, BTreeSet, BinaryHeap},
sync::Arc,
time::SystemTime,
};
use maplit::btreeset;
use room_version::RoomVersion;
use ruma::{
events::{
room::{
member::{MemberEventContent, MembershipState},
power_levels::PowerLevelsEventContent,
},
EventType,
},
EventId, RoomId, RoomVersionId,
};
mod error;
pub mod event_auth;
pub mod room_version;
mod state_event;
pub use error::{Error, Result};
pub use event_auth::{auth_check, auth_types_for_event};
pub use state_event::Event;
/// 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 `ServerPdu`.
pub type EventMap<T> = BTreeMap<EventId, T>;
#[derive(Default)]
pub struct StateResolution;
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.
///
/// ## Arguments
///
/// * `state_sets` - The incoming state to resolve. Each `StateMap` represents a possible fork
/// in the state of a room.
///
/// * `auth_events` - The full recursive set of `auth_events` for each event in the `state_sets`.
///
/// * `event_map` - The `EventMap` acts as a local cache of state, any event that is not found
/// in the `event_map` will cause an unrecoverable `Error` in `resolve`.
pub fn resolve<E: Event>(
room_id: &RoomId,
room_version: &RoomVersionId,
state_sets: &[StateMap<EventId>],
auth_events: Vec<Vec<EventId>>,
event_map: &mut EventMap<Arc<E>>,
) -> Result<StateMap<EventId>> {
log::info!("State resolution starting");
// split non-conflicting and conflicting state
let (clean, conflicting) = StateResolution::separate(state_sets);
log::info!("non conflicting {:?}", clean.len());
if conflicting.is_empty() {
log::info!("no conflicting state found");
return Ok(clean);
}
log::info!("{} conflicting events", conflicting.len());
// the set of auth events that are not common across server forks
let mut auth_diff = StateResolution::get_auth_chain_diff(room_id, &auth_events)?;
log::debug!("auth diff size {:?}", auth_diff);
// add the auth_diff to conflicting now we have a full set of conflicting events
auth_diff.extend(conflicting.values().cloned().flatten());
let mut all_conflicted = auth_diff
.into_iter()
.collect::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>();
log::info!("full conflicted set is {} events", all_conflicted.len());
// we used to check that all events are events from the correct room
// this is now a check the caller of `resolve` must make.
// synapse says `full_set = {eid for eid in full_conflicted_set if eid in event_map}`
//
// don't honor events we cannot "verify"
all_conflicted.retain(|id| event_map.contains_key(id));
// get only the control events with a state_key: "" or ban/kick event (sender != state_key)
let control_events = all_conflicted
.iter()
.filter(|id| is_power_event_id(id, event_map))
.cloned()
.collect::<Vec<_>>();
// sort the control events based on power_level/clock/event_id and outgoing/incoming edges
let mut sorted_control_levels = StateResolution::reverse_topological_power_sort(
room_id,
&control_events,
event_map,
&all_conflicted,
);
log::debug!("SRTD {:?}", sorted_control_levels);
let room_version = RoomVersion::new(room_version)?;
// sequentially auth check each control event.
let resolved_control = StateResolution::iterative_auth_check(
room_id,
&room_version,
&sorted_control_levels,
&clean,
event_map,
)?;
log::debug!(
"AUTHED {:?}",
resolved_control
.iter()
.map(|(key, id)| (key, id.to_string()))
.collect::<Vec<_>>()
);
// At this point the control_events have been resolved we now have to
// sort the remaining events using the mainline of the resolved power level.
sorted_control_levels.dedup();
let deduped_power_ev = sorted_control_levels;
// This removes the control events that passed auth and more importantly those that failed auth
let events_to_resolve = all_conflicted
.iter()
.filter(|id| !deduped_power_ev.contains(id))
.cloned()
.collect::<Vec<_>>();
log::debug!(
"LEFT {:?}",
events_to_resolve
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
);
// This "epochs" power level event
let power_event = resolved_control.get(&(EventType::RoomPowerLevels, "".into()));
log::debug!("PL {:?}", power_event);
let sorted_left_events =
StateResolution::mainline_sort(room_id, &events_to_resolve, power_event, event_map);
log::debug!(
"SORTED LEFT {:?}",
sorted_left_events
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
);
let mut resolved_state = StateResolution::iterative_auth_check(
room_id,
&room_version,
&sorted_left_events,
&resolved_control, // The control events are added to the final resolved state
event_map,
)?;
// add unconflicted state to the resolved state
// We priorities the unconflicting state
resolved_state.extend(clean);
Ok(resolved_state)
}
/// Split the events that have no conflicts from those that are conflicting.
/// The return tuple looks like `(unconflicted, conflicted)`.
///
/// State is determined to be conflicting if for the given key (EventType, StateKey) there
/// is not exactly one eventId. This includes missing events, if one state_set includes an event
/// that none of the other have this is a conflicting event.
pub fn separate(
state_sets: &[StateMap<EventId>],
) -> (StateMap<EventId>, StateMap<Vec<EventId>>) {
use itertools::Itertools;
log::info!(
"seperating {} sets of events into conflicted/unconflicted",
state_sets.len()
);
let mut unconflicted_state = StateMap::new();
let mut conflicted_state = StateMap::new();
for key in state_sets.iter().flat_map(|map| map.keys()).dedup() {
let mut event_ids = state_sets
.iter()
.map(|state_set| state_set.get(key))
.dedup()
.collect::<Vec<_>>();
if event_ids.len() == 1 {
if let Some(Some(id)) = event_ids.pop() {
unconflicted_state.insert(key.clone(), id.clone());
} else {
panic!()
}
} else {
conflicted_state.insert(
key.clone(),
event_ids.into_iter().flatten().cloned().collect::<Vec<_>>(),
);
}
}
(unconflicted_state, conflicted_state)
}
/// Returns a Vec of deduped EventIds that appear in some chains but not others.
pub fn get_auth_chain_diff(
_room_id: &RoomId,
auth_event_ids: &[Vec<EventId>],
) -> Result<Vec<EventId>> {
use itertools::Itertools;
let mut chains = vec![];
for ids in auth_event_ids {
// TODO state store `auth_event_ids` returns self in the event ids list
// when an event returns `auth_event_ids` self is not contained
let chain = ids.iter().cloned().collect::<BTreeSet<_>>();
chains.push(chain);
}
if let Some(chain) = chains.first().cloned() {
let rest = chains.iter().skip(1).flatten().cloned().collect();
let common = chain.intersection(&rest).collect::<Vec<_>>();
Ok(chains
.into_iter()
.flatten()
.filter(|id| !common.contains(&id))
.dedup()
.collect())
} else {
Ok(vec![])
}
}
/// Events are sorted from "earliest" to "latest". They are compared using
/// the negative power level (reverse topological ordering), the
/// origin server timestamp and incase of a tie the `EventId`s
/// are compared lexicographically.
///
/// The power level is negative because a higher power level is equated to an
/// earlier (further back in time) origin server timestamp.
pub fn reverse_topological_power_sort<E: Event>(
room_id: &RoomId,
events_to_sort: &[EventId],
event_map: &mut EventMap<Arc<E>>,
auth_diff: &[EventId],
) -> Vec<EventId> {
log::debug!("reverse topological sort of power events");
let mut graph = BTreeMap::new();
for event_id in events_to_sort.iter() {
StateResolution::add_event_and_auth_chain_to_graph(
room_id, &mut graph, event_id, event_map, auth_diff,
);
// TODO: if these functions are ever made async here
// is a good place to yield every once in a while so other
// tasks can make progress
}
// this is used in the `key_fn` passed to the lexico_topo_sort fn
let mut event_to_pl = BTreeMap::new();
for event_id in graph.keys() {
let pl = StateResolution::get_power_level_for_sender(room_id, event_id, event_map);
log::info!("{} power level {}", event_id.to_string(), pl);
event_to_pl.insert(event_id.clone(), pl);
// TODO: if these functions are ever made async here
// is a good place to yield every once in a while so other
// tasks can make progress
}
StateResolution::lexicographical_topological_sort(&graph, |event_id| {
// log::debug!("{:?}", event_map.get(event_id).unwrap().origin_server_ts());
let ev = event_map.get(event_id).unwrap();
let pl = event_to_pl.get(event_id).unwrap();
log::debug!("{:?}", (-*pl, ev.origin_server_ts(), &ev.event_id()));
// 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().clone())
})
}
/// Sorts the event graph based on number of outgoing/incoming edges, where
/// `key_fn` is used as a tie breaker. The tie breaker happens based on
/// power level, age, and event_id.
pub fn lexicographical_topological_sort<F>(
graph: &BTreeMap<EventId, Vec<EventId>>,
key_fn: F,
) -> Vec<EventId>
where
F: Fn(&EventId) -> (i64, SystemTime, EventId),
{
log::info!("starting lexicographical topological sort");
// NOTE: an event that has no incoming edges happened most recently,
// and an event that has no outgoing edges happened least recently.
// NOTE: this is basically Kahn's algorithm except we look at nodes with no
// outgoing edges, c.f.
// https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm
// TODO make the BTreeSet conversion cleaner ??
// outdegree_map is an event referring to the events before it, the
// more outdegree's the more recent the event.
let mut outdegree_map: BTreeMap<EventId, BTreeSet<EventId>> = graph
.iter()
.map(|(k, v)| (k.clone(), v.iter().cloned().collect()))
.collect();
// The number of events that depend on the given event (the eventId key)
let mut reverse_graph = BTreeMap::new();
// Vec of nodes that have zero out degree, least recent events.
let mut zero_outdegree = vec![];
for (node, edges) in graph.iter() {
if edges.is_empty() {
// the `Reverse` is because rusts `BinaryHeap` sorts largest -> smallest we need
// smallest -> largest
zero_outdegree.push(Reverse((key_fn(node), node)));
}
reverse_graph.entry(node).or_insert(btreeset![]);
for edge in edges {
reverse_graph
.entry(edge)
.or_insert(btreeset![])
.insert(node);
}
}
let mut heap = BinaryHeap::from(zero_outdegree);
// we remove the oldest node (most incoming edges) and check against all other
let mut sorted = vec![];
// destructure the `Reverse` and take the smallest `node` each time
while let Some(Reverse((_, node))) = heap.pop() {
let node: &EventId = node;
for parent in reverse_graph.get(node).unwrap() {
// the number of outgoing edges this node has
let out = outdegree_map.get_mut(parent).unwrap();
// only push on the heap once older events have been cleared
out.remove(node);
if out.is_empty() {
heap.push(Reverse((key_fn(parent), parent)));
}
}
// synapse yields we push then return the vec
sorted.push(node.clone());
}
sorted
}
/// Find the power level for the sender of `event_id` or return a default value of zero.
fn get_power_level_for_sender<E: Event>(
room_id: &RoomId,
event_id: &EventId,
event_map: &mut EventMap<Arc<E>>,
) -> i64 {
log::info!("fetch event ({}) senders power level", event_id.to_string());
let event = StateResolution::get_or_load_event(room_id, event_id, event_map);
let mut pl = None;
// TODO store.auth_event_ids returns "self" with the event ids is this ok
// event.auth_event_ids does not include its own event id ?
for aid in event
.as_ref()
.map(|pdu| pdu.auth_events())
.unwrap_or_default()
{
if let Ok(aev) = StateResolution::get_or_load_event(room_id, &aid, event_map) {
if is_type_and_key(&aev, EventType::RoomPowerLevels, "") {
pl = Some(aev);
break;
}
}
}
if pl.is_none() {
return 0;
}
if let Some(content) =
pl.and_then(|pl| serde_json::from_value::<PowerLevelsEventContent>(pl.content()).ok())
{
if let Ok(ev) = event {
if let Some(user) = content.users.get(ev.sender()) {
log::debug!("found {} at power_level {}", ev.sender().as_str(), user);
return (*user).into();
}
}
content.users_default.into()
} else {
0
}
}
/// Check the that each event is authenticated based on the events before it.
///
/// ## Returns
///
/// The `unconflicted_state` combined with the newly auth'ed events. So any event that
/// fails the `event_auth::auth_check` will be excluded from the returned `StateMap<EventId>`.
///
/// For each `events_to_check` event we gather the events needed to auth it from the
/// `event_map` or `store` and verify each event using the `event_auth::auth_check`
/// function.
pub fn iterative_auth_check<E: Event>(
room_id: &RoomId,
room_version: &RoomVersion,
events_to_check: &[EventId],
unconflicted_state: &StateMap<EventId>,
event_map: &mut EventMap<Arc<E>>,
) -> Result<StateMap<EventId>> {
log::info!("starting iterative auth check");
log::debug!(
"performing auth checks on {:?}",
events_to_check
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
);
let mut resolved_state = unconflicted_state.clone();
for event_id in events_to_check.iter() {
let event = StateResolution::get_or_load_event(room_id, event_id, event_map)?;
let state_key = event
.state_key()
.ok_or_else(|| Error::InvalidPdu("State event had no state key".to_owned()))?;
let mut auth_events = BTreeMap::new();
for aid in &event.auth_events() {
if let Ok(ev) = StateResolution::get_or_load_event(room_id, aid, event_map) {
// TODO synapse check "rejected_reason", I'm guessing this is redacted_because in ruma ??
auth_events.insert(
(
ev.kind(),
ev.state_key().ok_or_else(|| {
Error::InvalidPdu("State event had no state key".to_owned())
})?,
),
ev,
);
} else {
log::warn!("auth event id for {} is missing {}", aid, event_id);
}
}
for key in auth_types_for_event(
&event.kind(),
event.sender(),
Some(state_key.clone()),
event.content(),
) {
if let Some(ev_id) = resolved_state.get(&key) {
if let Ok(event) = StateResolution::get_or_load_event(room_id, ev_id, event_map)
{
// TODO synapse checks `rejected_reason` is None here
auth_events.insert(key.clone(), event);
}
}
}
log::debug!("event to check {:?}", event.event_id().as_str());
let most_recent_prev_event = event
.prev_events()
.iter()
.filter_map(|id| StateResolution::get_or_load_event(room_id, id, event_map).ok())
.next_back();
// The key for this is (eventType + a state_key of the signed token not sender) so search
// for it
let current_third_party = auth_events.iter().find_map(|(_, pdu)| {
if pdu.kind() == EventType::RoomThirdPartyInvite {
Some(pdu.clone()) // TODO no clone, auth_events is borrowed while moved
} else {
None
}
});
if auth_check(
room_version,
&event,
most_recent_prev_event,
&auth_events,
current_third_party,
)? {
// add event to resolved state map
resolved_state.insert((event.kind(), state_key), event_id.clone());
} else {
// synapse passes here on AuthError. We do not add this event to resolved_state.
log::warn!(
"event {} failed the authentication check",
event_id.to_string()
);
}
// TODO: if these functions are ever made async here
// is a good place to yield every once in a while so other
// tasks can make progress
}
Ok(resolved_state)
}
/// Returns the sorted `to_sort` list of `EventId`s based on a mainline sort using
/// the depth of `resolved_power_level`, the server timestamp, and the eventId.
///
/// The depth of the given event is calculated based on the depth of it's closest "parent"
/// power_level event. If there have been two power events the after the most recent are
/// depth 0, the events before (with the first power level as a parent) will be marked
/// as depth 1. depth 1 is "older" than depth 0.
pub fn mainline_sort<E: Event>(
room_id: &RoomId,
to_sort: &[EventId],
resolved_power_level: Option<&EventId>,
event_map: &mut EventMap<Arc<E>>,
) -> Vec<EventId> {
log::debug!("mainline sort of events");
// There are no EventId's to sort, bail.
if to_sort.is_empty() {
return vec![];
}
let mut mainline = vec![];
let mut pl = resolved_power_level.cloned();
while let Some(p) = pl {
mainline.push(p.clone());
let event = StateResolution::get_or_load_event(room_id, &p, event_map).unwrap();
let auth_events = &event.auth_events();
pl = None;
for aid in auth_events {
let ev = StateResolution::get_or_load_event(room_id, aid, event_map).unwrap();
if is_type_and_key(&ev, EventType::RoomPowerLevels, "") {
pl = Some(aid.clone());
break;
}
}
// TODO: if these functions are ever made async here
// is a good place to yield every once in a while so other
// tasks can make progress
}
let mainline_map = mainline
.iter()
.rev()
.enumerate()
.map(|(idx, eid)| ((*eid).clone(), idx))
.collect::<BTreeMap<_, _>>();
let mut order_map = BTreeMap::new();
for ev_id in to_sort.iter() {
if let Ok(event) = StateResolution::get_or_load_event(room_id, ev_id, event_map) {
if let Ok(depth) = StateResolution::get_mainline_depth(
room_id,
Some(event),
&mainline_map,
event_map,
) {
order_map.insert(
ev_id,
(
depth,
event_map.get(ev_id).map(|ev| ev.origin_server_ts()),
ev_id, // TODO should this be a &str to sort lexically??
),
);
}
}
// TODO: if these functions are ever made async here
// is a good place to yield every once in a while so other
// tasks can make progress
}
// sort the event_ids by their depth, timestamp and EventId
// unwrap is OK order map and sort_event_ids are from to_sort (the same Vec)
let mut sort_event_ids = order_map.keys().map(|&k| k.clone()).collect::<Vec<_>>();
sort_event_ids.sort_by_key(|sort_id| order_map.get(sort_id).unwrap());
sort_event_ids
}
/// Get the mainline depth from the `mainline_map` or finds a power_level event
/// that has an associated mainline depth.
fn get_mainline_depth<E: Event>(
room_id: &RoomId,
mut event: Option<Arc<E>>,
mainline_map: &EventMap<usize>,
event_map: &mut EventMap<Arc<E>>,
) -> Result<usize> {
while let Some(sort_ev) = event {
log::debug!("mainline event_id {}", sort_ev.event_id().to_string());
let id = &sort_ev.event_id();
if let Some(depth) = mainline_map.get(id) {
return Ok(*depth);
}
// dbg!(&sort_ev);
let auth_events = &sort_ev.auth_events();
event = None;
for aid in auth_events {
// dbg!(&aid);
let aev = StateResolution::get_or_load_event(room_id, aid, event_map)?;
if is_type_and_key(&aev, EventType::RoomPowerLevels, "") {
event = Some(aev);
break;
}
}
}
// Did not find a power level event so we default to zero
Ok(0)
}
fn add_event_and_auth_chain_to_graph<E: Event>(
room_id: &RoomId,
graph: &mut BTreeMap<EventId, Vec<EventId>>,
event_id: &EventId,
event_map: &mut EventMap<Arc<E>>,
auth_diff: &[EventId],
) {
let mut state = vec![event_id.clone()];
while !state.is_empty() {
// we just checked if it was empty so unwrap is fine
let eid = state.pop().unwrap();
graph.entry(eid.clone()).or_insert_with(Vec::new);
// prefer the store to event as the store filters dedups the events
// otherwise it seems we can loop forever
for aid in &StateResolution::get_or_load_event(room_id, &eid, event_map)
.unwrap()
.auth_events()
{
if auth_diff.contains(aid) {
if !graph.contains_key(aid) {
state.push(aid.clone());
}
// we just inserted this at the start of the while loop
graph.get_mut(&eid).unwrap().push(aid.clone());
}
}
}
}
/// Uses the `event_map` to return the full PDU or fails.
fn get_or_load_event<E: Event>(
_room_id: &RoomId,
ev_id: &EventId,
event_map: &EventMap<Arc<E>>,
) -> Result<Arc<E>> {
event_map.get(ev_id).map_or_else(
|| Err(Error::NotFound(format!("EventId: {:?} not found", ev_id))),
|e| Ok(Arc::clone(e)),
)
}
}
pub fn is_power_event_id<E: Event>(event_id: &EventId, event_map: &EventMap<Arc<E>>) -> bool {
match event_map.get(event_id) {
Some(state) => is_power_event(state),
_ => false,
}
}
pub fn is_type_and_key<E: Event>(ev: &Arc<E>, ev_type: EventType, state_key: &str) -> bool {
ev.kind() == ev_type && ev.state_key().as_deref() == Some(state_key)
}
pub fn is_power_event<E: Event>(event: &Arc<E>) -> bool {
match event.kind() {
EventType::RoomPowerLevels | EventType::RoomJoinRules | EventType::RoomCreate => {
event.state_key() == Some("".into())
}
EventType::RoomMember => {
if let Ok(content) = serde_json::from_value::<MemberEventContent>(event.content()) {
if [MembershipState::Leave, MembershipState::Ban].contains(&content.membership) {
return Some(event.sender().as_str()) != event.state_key().as_deref();
}
}
false
}
_ => false,
}
}

View File

@ -0,0 +1,160 @@
use ruma::RoomVersionId;
use crate::{Error, Result};
pub enum RoomDisposition {
/// A room version that has a stable specification.
Stable,
/// A room version that is not yet fully specified.
#[allow(dead_code)]
Unstable,
}
pub enum EventFormatVersion {
/// $id:server event id format
V1,
/// MSC1659-style $hash event id format: introduced for room v3
V2,
/// MSC1884-style $hash format: introduced for room v4
V3,
}
pub enum StateResolutionVersion {
/// State resolution for rooms at version 1.
V1,
/// State resolution for room at version 2 or later.
V2,
}
pub struct RoomVersion {
/// The version this room is set to.
pub version: RoomVersionId,
/// The stability of this room.
pub disposition: RoomDisposition,
/// The format of the EventId.
pub event_format: EventFormatVersion,
/// Which state resolution algorithm is used.
pub state_res: StateResolutionVersion,
/// not sure
pub enforce_key_validity: bool,
/// `m.room.aliases` had special auth rules and redaction rules
/// before room version 6.
///
/// before MSC2261/MSC2432,
pub special_case_aliases_auth: bool,
/// Strictly enforce canonicaljson, do not allow:
/// * Integers outside the range of [-2 ^ 53 + 1, 2 ^ 53 - 1]
/// * Floats
/// * NaN, Infinity, -Infinity
pub strict_canonicaljson: bool,
/// Verify notifications key while checking m.room.power_levels.
///
/// bool: MSC2209: Check 'notifications'
pub limit_notifications_power_levels: bool,
/// Extra rules when verifying redaction events.
pub extra_redaction_checks: bool,
}
impl RoomVersion {
pub fn new(version: &RoomVersionId) -> Result<Self> {
Ok(match version {
RoomVersionId::Version1 => Self::version_1(),
RoomVersionId::Version2 => Self::version_2(),
RoomVersionId::Version3 => Self::version_3(),
RoomVersionId::Version4 => Self::version_4(),
RoomVersionId::Version5 => Self::version_5(),
RoomVersionId::Version6 => Self::version_6(),
ver => {
return Err(Error::Unsupported(format!(
"found version `{}`",
ver.as_str()
)))
}
})
}
pub fn version_1() -> Self {
Self {
version: RoomVersionId::Version1,
disposition: RoomDisposition::Stable,
event_format: EventFormatVersion::V1,
state_res: StateResolutionVersion::V1,
enforce_key_validity: false,
special_case_aliases_auth: true,
strict_canonicaljson: false,
limit_notifications_power_levels: false,
extra_redaction_checks: false,
}
}
pub fn version_2() -> Self {
Self {
version: RoomVersionId::Version2,
disposition: RoomDisposition::Stable,
event_format: EventFormatVersion::V1,
state_res: StateResolutionVersion::V2,
enforce_key_validity: false,
special_case_aliases_auth: true,
strict_canonicaljson: false,
limit_notifications_power_levels: false,
extra_redaction_checks: false,
}
}
pub fn version_3() -> Self {
Self {
version: RoomVersionId::Version3,
disposition: RoomDisposition::Stable,
event_format: EventFormatVersion::V2,
state_res: StateResolutionVersion::V2,
enforce_key_validity: false,
special_case_aliases_auth: true,
strict_canonicaljson: false,
limit_notifications_power_levels: false,
extra_redaction_checks: true,
}
}
pub fn version_4() -> Self {
Self {
version: RoomVersionId::Version4,
disposition: RoomDisposition::Stable,
event_format: EventFormatVersion::V3,
state_res: StateResolutionVersion::V2,
enforce_key_validity: false,
special_case_aliases_auth: true,
strict_canonicaljson: false,
limit_notifications_power_levels: false,
extra_redaction_checks: true,
}
}
pub fn version_5() -> Self {
Self {
version: RoomVersionId::Version5,
disposition: RoomDisposition::Stable,
event_format: EventFormatVersion::V3,
state_res: StateResolutionVersion::V2,
enforce_key_validity: true,
special_case_aliases_auth: true,
strict_canonicaljson: false,
limit_notifications_power_levels: false,
extra_redaction_checks: true,
}
}
pub fn version_6() -> Self {
Self {
version: RoomVersionId::Version6,
disposition: RoomDisposition::Stable,
event_format: EventFormatVersion::V3,
state_res: StateResolutionVersion::V2,
enforce_key_validity: true,
special_case_aliases_auth: false,
strict_canonicaljson: true,
limit_notifications_power_levels: true,
extra_redaction_checks: true,
}
}
}

View File

@ -0,0 +1,52 @@
use std::{collections::BTreeMap, time::SystemTime};
use ruma::{
events::{pdu::EventHash, EventType},
EventId, RoomId, ServerName, UInt, UserId,
};
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;
/// The `RoomId` of this event.
fn room_id(&self) -> &RoomId;
/// The `UserId` of this event.
fn sender(&self) -> &UserId;
/// The time of creation on the originating server.
fn origin_server_ts(&self) -> SystemTime;
/// The kind of event.
fn kind(&self) -> EventType;
/// The `UserId` of this PDU.
fn content(&self) -> serde_json::Value;
/// The state key for this event.
fn state_key(&self) -> Option<String>;
/// The events before this event.
fn prev_events(&self) -> Vec<EventId>;
/// The maximum number of `prev_events` plus 1.
///
/// 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<EventId>;
/// If this event is a redaction event this is the event it redacts.
fn redacts(&self) -> Option<&EventId>;
/// The `unsigned` content of this event.
fn unsigned(&self) -> &BTreeMap<String, JsonValue>;
fn hashes(&self) -> &EventHash;
fn signatures(&self) -> BTreeMap<Box<ServerName>, BTreeMap<ruma::ServerSigningKeyId, String>>;
}

View File

@ -0,0 +1,79 @@
use std::sync::Arc;
use state_res::{event_auth::valid_membership_change, StateMap};
// use state_res::event_auth:::{
// auth_check, auth_types_for_event, can_federate, check_power_levels, check_redaction,
// };
mod utils;
use utils::{alice, charlie, event_id, member_content_ban, to_pdu_event, INITIAL_EVENTS};
#[test]
fn test_ban_pass() {
let events = INITIAL_EVENTS();
let prev = events
.values()
.find(|ev| ev.event_id().as_str().contains("IMC"))
.map(Arc::clone);
let auth_events = events
.values()
.map(|ev| ((ev.kind(), ev.state_key()), Arc::clone(ev)))
.collect::<StateMap<_>>();
let requester = to_pdu_event(
"HELLO",
alice(),
ruma::events::EventType::RoomMember,
Some(charlie().as_str()),
member_content_ban(),
&[],
&[event_id("IMC")],
);
assert!(valid_membership_change(
&requester.state_key(),
requester.sender(),
requester.content(),
prev,
None,
&auth_events
)
.unwrap())
}
#[test]
fn test_ban_fail() {
let events = INITIAL_EVENTS();
let prev = events
.values()
.find(|ev| ev.event_id().as_str().contains("IMC"))
.map(Arc::clone);
let auth_events = events
.values()
.map(|ev| ((ev.kind(), ev.state_key()), Arc::clone(ev)))
.collect::<StateMap<_>>();
let requester = to_pdu_event(
"HELLO",
charlie(),
ruma::events::EventType::RoomMember,
Some(alice().as_str()),
member_content_ban(),
&[],
&[event_id("IMC")],
);
assert!(!valid_membership_change(
&requester.state_key(),
requester.sender(),
requester.content(),
prev,
None,
&auth_events
)
.unwrap())
}

View File

@ -0,0 +1,95 @@
use std::collections::BTreeMap;
use ruma::{events::EventType, EventId};
use state_res::{is_power_event, room_version::RoomVersion, StateMap};
mod utils;
use utils::{room_id, INITIAL_EVENTS};
fn shuffle(list: &mut [EventId]) {
use rand::Rng;
let mut rng = rand::thread_rng();
for i in 1..list.len() {
let j = rng.gen_range(0, list.len());
list.swap(i, j);
}
}
fn test_event_sort() {
let mut events = INITIAL_EVENTS();
let event_map = events
.values()
.map(|ev| ((ev.kind(), ev.state_key()), ev.clone()))
.collect::<StateMap<_>>();
let auth_chain = &[] as &[_];
let power_events = event_map
.values()
.filter(|pdu| is_power_event(pdu))
.map(|pdu| pdu.event_id().clone())
.collect::<Vec<_>>();
// This is a TODO in conduit
// TODO these events are not guaranteed to be sorted but they are resolved, do
// we need the auth_chain
let sorted_power_events = state_res::StateResolution::reverse_topological_power_sort(
&room_id(),
&power_events,
&mut events,
auth_chain,
);
// This is a TODO in conduit
// TODO we may be able to skip this since they are resolved according to spec
let resolved_power = state_res::StateResolution::iterative_auth_check(
&room_id(),
&RoomVersion::version_6(),
&sorted_power_events,
&BTreeMap::new(), // unconflicted events
&mut events,
)
.expect("iterative auth check failed on resolved events");
// don't remove any events so we know it sorts them all correctly
let mut events_to_sort = events.keys().cloned().collect::<Vec<_>>();
shuffle(&mut events_to_sort);
let power_level = resolved_power.get(&(EventType::RoomPowerLevels, "".to_string()));
let sorted_event_ids = state_res::StateResolution::mainline_sort(
&room_id(),
&events_to_sort,
power_level,
&mut events,
);
assert_eq!(
vec![
"$CREATE:foo",
"$IMA:foo",
"$IPOWER:foo",
"$IJR:foo",
"$IMB:foo",
"$IMC:foo",
"$START:foo",
"$END:foo"
],
sorted_event_ids
.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
)
}
#[test]
fn test_sort() {
for _ in 0..20 {
// since we shuffle the eventIds before we sort them introducing randomness
// seems like we should test this a few times
test_event_sort()
}
}

View File

@ -0,0 +1,208 @@
#![allow(clippy::or_fun_call, clippy::expect_fun_call)]
use std::{collections::BTreeMap, sync::Arc};
use ruma::{events::EventType, EventId, RoomVersionId};
use serde_json::json;
use state_res::{EventMap, 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, StateEvent, TestStore, INITIAL_EVENTS,
};
#[test]
fn ban_with_auth_chains() {
let ban = BAN_STATE_SET();
let edges = vec![vec!["END", "MB", "PA", "START"], vec!["END", "IME", "MB"]]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["PA", "MB"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(
&ban.values().cloned().collect::<Vec<_>>(),
edges,
expected_state_ids,
);
}
#[test]
fn ban_with_auth_chains2() {
let init = INITIAL_EVENTS();
let ban = BAN_STATE_SET();
let mut inner = init.clone();
inner.extend(ban);
let store = TestStore(inner.clone());
let state_set_a = [
inner.get(&event_id("CREATE")).unwrap(),
inner.get(&event_id("IJR")).unwrap(),
inner.get(&event_id("IMA")).unwrap(),
inner.get(&event_id("IMB")).unwrap(),
inner.get(&event_id("IMC")).unwrap(),
inner.get(&event_id("MB")).unwrap(),
inner.get(&event_id("PA")).unwrap(),
]
.iter()
.map(|ev| ((ev.kind(), ev.state_key()), ev.event_id().clone()))
.collect::<BTreeMap<_, _>>();
let state_set_b = [
inner.get(&event_id("CREATE")).unwrap(),
inner.get(&event_id("IJR")).unwrap(),
inner.get(&event_id("IMA")).unwrap(),
inner.get(&event_id("IMB")).unwrap(),
inner.get(&event_id("IMC")).unwrap(),
inner.get(&event_id("IME")).unwrap(),
inner.get(&event_id("PA")).unwrap(),
]
.iter()
.map(|ev| ((ev.kind(), ev.state_key()), ev.event_id().clone()))
.collect::<StateMap<_>>();
let mut ev_map: EventMap<Arc<StateEvent>> = store.0.clone();
let state_sets = vec![state_set_a, state_set_b];
let resolved = match StateResolution::resolve::<StateEvent>(
&room_id(),
&RoomVersionId::Version2,
&state_sets,
state_sets
.iter()
.map(|map| {
store
.auth_event_ids(&room_id(), &map.values().cloned().collect::<Vec<_>>())
.unwrap()
})
.collect(),
&mut ev_map,
) {
Ok(state) => state,
Err(e) => panic!("{}", e),
};
log::debug!(
"{:#?}",
resolved
.iter()
.map(|((ty, key), id)| format!("(({}{:?}), {})", ty, key, id))
.collect::<Vec<_>>()
);
let expected = vec![
"$CREATE:foo",
"$IJR:foo",
"$PA:foo",
"$IMA:foo",
"$IMB:foo",
"$IMC:foo",
"$MB:foo",
];
for id in expected.iter().map(|i| event_id(i)) {
// make sure our resolved events are equal to the expected list
assert!(
resolved.values().any(|eid| eid == &id) || init.contains_key(&id),
"{}",
id
)
}
assert_eq!(expected.len(), resolved.len())
}
#[test]
fn join_rule_with_auth_chain() {
let join_rule = JOIN_RULE();
let edges = vec![vec!["END", "JR", "START"], vec!["END", "IMZ", "START"]]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["JR"].into_iter().map(event_id).collect::<Vec<_>>();
do_check(
&join_rule.values().cloned().collect::<Vec<_>>(),
edges,
expected_state_ids,
);
}
#[allow(non_snake_case)]
fn BAN_STATE_SET() -> BTreeMap<EventId, Arc<StateEvent>> {
vec![
to_pdu_event(
"PA",
alice(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice(): 100, bob(): 50}}),
&["CREATE", "IMA", "IPOWER"], // auth_events
&["START"], // prev_events
),
to_pdu_event(
"PB",
alice(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice(): 100, bob(): 50}}),
&["CREATE", "IMA", "IPOWER"],
&["END"],
),
to_pdu_event(
"MB",
alice(),
EventType::RoomMember,
Some(ella().as_str()),
member_content_ban(),
&["CREATE", "IMA", "PB"],
&["PA"],
),
to_pdu_event(
"IME",
ella(),
EventType::RoomMember,
Some(ella().as_str()),
member_content_join(),
&["CREATE", "IJR", "PA"],
&["MB"],
),
]
.into_iter()
.map(|ev| (ev.event_id().clone(), ev))
.collect()
}
#[allow(non_snake_case)]
fn JOIN_RULE() -> BTreeMap<EventId, Arc<StateEvent>> {
vec![
to_pdu_event(
"JR",
alice(),
EventType::RoomJoinRules,
Some(""),
json!({"join_rule": "invite"}),
&["CREATE", "IMA", "IPOWER"],
&["START"],
),
to_pdu_event(
"IMZ",
zara(),
EventType::RoomPowerLevels,
Some(zara().as_str()),
member_content_join(),
&["CREATE", "JR", "IPOWER"],
&["START"],
),
]
.into_iter()
.map(|ev| (ev.event_id().clone(), ev))
.collect()
}

View File

@ -0,0 +1,408 @@
use std::{sync::Arc, time::UNIX_EPOCH};
use maplit::btreemap;
use ruma::{
events::{room::join_rules::JoinRule, EventType},
EventId, RoomVersionId,
};
use serde_json::json;
use state_res::{StateMap, StateResolution};
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, StateEvent, TestStore, LOGGER,
};
#[test]
fn ban_vs_power_level() {
let events = &[
to_init_pdu_event(
"PA",
alice(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice(): 100, bob(): 50}}),
),
to_init_pdu_event(
"MA",
alice(),
EventType::RoomMember,
Some(alice().to_string().as_str()),
member_content_join(),
),
to_init_pdu_event(
"MB",
alice(),
EventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_ban(),
),
to_init_pdu_event(
"PB",
bob(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice(): 100, bob(): 50}}),
),
];
let edges = vec![
vec!["END", "MB", "MA", "PA", "START"],
vec!["END", "PA", "PB"],
]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["PA", "MA", "MB"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(events, edges, expected_state_ids)
}
#[test]
fn topic_basic() {
let events = &[
to_init_pdu_event("T1", alice(), EventType::RoomTopic, Some(""), json!({})),
to_init_pdu_event(
"PA1",
alice(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice(): 100, bob(): 50}}),
),
to_init_pdu_event("T2", alice(), EventType::RoomTopic, Some(""), json!({})),
to_init_pdu_event(
"PA2",
alice(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice(): 100, bob(): 0}}),
),
to_init_pdu_event(
"PB",
bob(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice(): 100, bob(): 50}}),
),
to_init_pdu_event("T3", bob(), EventType::RoomTopic, Some(""), json!({})),
];
let edges = vec![
vec!["END", "PA2", "T2", "PA1", "T1", "START"],
vec!["END", "T3", "PB", "PA1"],
]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["PA2", "T2"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(events, edges, expected_state_ids)
}
#[test]
fn topic_reset() {
let events = &[
to_init_pdu_event("T1", alice(), EventType::RoomTopic, Some(""), json!({})),
to_init_pdu_event(
"PA",
alice(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice(): 100, bob(): 50}}),
),
to_init_pdu_event("T2", bob(), EventType::RoomTopic, Some(""), json!({})),
to_init_pdu_event(
"MB",
alice(),
EventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_ban(),
),
];
let edges = vec![
vec!["END", "MB", "T2", "PA", "T1", "START"],
vec!["END", "T1"],
]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["T1", "MB", "PA"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(events, edges, expected_state_ids)
}
#[test]
fn join_rule_evasion() {
let events = &[
to_init_pdu_event(
"JR",
alice(),
EventType::RoomJoinRules,
Some(""),
json!({ "join_rule": JoinRule::Private }),
),
to_init_pdu_event(
"ME",
ella(),
EventType::RoomMember,
Some(ella().to_string().as_str()),
member_content_join(),
),
];
let edges = vec![vec!["END", "JR", "START"], vec!["END", "ME", "START"]]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec![event_id("JR")];
do_check(events, edges, expected_state_ids)
}
#[test]
fn offtopic_power_level() {
let events = &[
to_init_pdu_event(
"PA",
alice(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice(): 100, bob(): 50}}),
),
to_init_pdu_event(
"PB",
bob(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice(): 100, bob(): 50, charlie(): 50}}),
),
to_init_pdu_event(
"PC",
charlie(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice(): 100, bob(): 50, charlie(): 0}}),
),
];
let edges = vec![vec!["END", "PC", "PB", "PA", "START"], vec!["END", "PA"]]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["PC"].into_iter().map(event_id).collect::<Vec<_>>();
do_check(events, edges, expected_state_ids)
}
#[test]
fn topic_setting() {
let events = &[
to_init_pdu_event("T1", alice(), EventType::RoomTopic, Some(""), json!({})),
to_init_pdu_event(
"PA1",
alice(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice(): 100, bob(): 50}}),
),
to_init_pdu_event("T2", alice(), EventType::RoomTopic, Some(""), json!({})),
to_init_pdu_event(
"PA2",
alice(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice(): 100, bob(): 0}}),
),
to_init_pdu_event(
"PB",
bob(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice(): 100, bob(): 50}}),
),
to_init_pdu_event("T3", bob(), EventType::RoomTopic, Some(""), json!({})),
to_init_pdu_event("MZ1", zara(), EventType::RoomTopic, Some(""), json!({})),
to_init_pdu_event("T4", alice(), EventType::RoomTopic, Some(""), json!({})),
];
let edges = vec![
vec!["END", "T4", "MZ1", "PA2", "T2", "PA1", "T1", "START"],
vec!["END", "MZ1", "T3", "PB", "PA1"],
]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["T4", "PA2"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(events, edges, expected_state_ids)
}
#[test]
fn test_event_map_none() {
let mut store = TestStore::<StateEvent>(btreemap! {});
// build up the DAG
let (state_at_bob, state_at_charlie, expected) = store.set_up();
let mut ev_map: state_res::EventMap<Arc<StateEvent>> = store.0.clone();
let state_sets = vec![state_at_bob, state_at_charlie];
let resolved = match StateResolution::resolve::<StateEvent>(
&room_id(),
&RoomVersionId::Version2,
&state_sets,
state_sets
.iter()
.map(|map| {
store
.auth_event_ids(&room_id(), &map.values().cloned().collect::<Vec<_>>())
.unwrap()
})
.collect(),
&mut ev_map,
) {
Ok(state) => state,
Err(e) => panic!("{}", e),
};
assert_eq!(expected, resolved)
}
#[test]
fn test_lexicographical_sort() {
let graph = btreemap! {
event_id("l") => vec![event_id("o")],
event_id("m") => vec![event_id("n"), event_id("o")],
event_id("n") => vec![event_id("o")],
event_id("o") => vec![], // "o" has zero outgoing edges but 4 incoming edges
event_id("p") => vec![event_id("o")],
};
let res =
StateResolution::lexicographical_topological_sort(&graph, |id| (0, UNIX_EPOCH, id.clone()));
assert_eq!(
vec!["o", "l", "n", "m", "p"],
res.iter()
.map(ToString::to_string)
.map(|s| s.replace("$", "").replace(":foo", ""))
.collect::<Vec<_>>()
)
}
// A StateStore implementation for testing
//
//
impl TestStore<StateEvent> {
pub fn set_up(&mut self) -> (StateMap<EventId>, StateMap<EventId>, StateMap<EventId>) {
// to activate logging use `RUST_LOG=debug cargo t one_test_only`
let _ = LOGGER.call_once(|| {
tracer::fmt()
.with_env_filter(tracer::EnvFilter::from_default_env())
.init()
});
let create_event = to_pdu_event::<EventId>(
"CREATE",
alice(),
EventType::RoomCreate,
Some(""),
json!({ "creator": alice() }),
&[],
&[],
);
let cre = create_event.event_id().clone();
self.0.insert(cre.clone(), Arc::clone(&create_event));
let alice_mem = to_pdu_event(
"IMA",
alice(),
EventType::RoomMember,
Some(alice().to_string().as_str()),
member_content_join(),
&[cre.clone()],
&[cre.clone()],
);
self.0
.insert(alice_mem.event_id().clone(), Arc::clone(&alice_mem));
let join_rules = to_pdu_event(
"IJR",
alice(),
EventType::RoomJoinRules,
Some(""),
json!({ "join_rule": JoinRule::Public }),
&[cre.clone(), alice_mem.event_id().clone()],
&[alice_mem.event_id().clone()],
);
self.0
.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
let bob_mem = to_pdu_event(
"IMB",
bob(),
EventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_join(),
&[cre.clone(), join_rules.event_id().clone()],
&[join_rules.event_id().clone()],
);
self.0.insert(bob_mem.event_id().clone(), bob_mem.clone());
let charlie_mem = to_pdu_event(
"IMC",
charlie(),
EventType::RoomMember,
Some(charlie().to_string().as_str()),
member_content_join(),
&[cre, join_rules.event_id().clone()],
&[join_rules.event_id().clone()],
);
self.0
.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(), e.state_key()), e.event_id().clone()))
.collect::<StateMap<_>>();
let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem]
.iter()
.map(|e| ((e.kind(), e.state_key()), e.event_id().clone()))
.collect::<StateMap<_>>();
let expected = [
&create_event,
&alice_mem,
&join_rules,
&bob_mem,
&charlie_mem,
]
.iter()
.map(|e| ((e.kind(), e.state_key()), e.event_id().clone()))
.collect::<StateMap<_>>();
(state_at_bob, state_at_charlie, expected)
}
}

View File

@ -0,0 +1,792 @@
#![allow(clippy::or_fun_call, clippy::expect_fun_call, dead_code)]
use std::{
collections::{BTreeMap, BTreeSet},
convert::TryFrom,
sync::{Arc, Once},
time::{Duration, UNIX_EPOCH},
};
use maplit::btreemap;
use ruma::{
events::{
pdu::{EventHash, Pdu, RoomV3Pdu},
room::{
join_rules::JoinRule,
member::{MemberEventContent, MembershipState},
},
EventType,
},
EventId, RoomId, RoomVersionId, UserId,
};
use serde_json::{json, Value as JsonValue};
use state_res::{Error, Event, Result, StateMap, StateResolution};
use tracing_subscriber as tracer;
pub use event::StateEvent;
pub static LOGGER: Once = Once::new();
static mut SERVER_TIMESTAMP: u64 = 0;
pub fn do_check(
events: &[Arc<StateEvent>],
edges: Vec<Vec<EventId>>,
expected_state_ids: Vec<EventId>,
) {
// to activate logging use `RUST_LOG=debug cargo t`
let _ = LOGGER.call_once(|| {
tracer::fmt()
.with_env_filter(tracer::EnvFilter::from_default_env())
.init()
});
let init_events = INITIAL_EVENTS();
let mut store = TestStore(
init_events
.values()
.chain(events)
.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 -> 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 init_events.values().chain(events) {
graph.insert(ev.event_id().clone(), vec![]);
fake_event_map.insert(ev.event_id().clone(), ev.clone());
}
for pair in INITIAL_EDGES().windows(2) {
if let [a, b] = &pair {
graph.entry(a.clone()).or_insert(vec![]).push(b.clone());
}
}
for edge_list in edges {
for pair in edge_list.windows(2) {
if let [a, b] = &pair {
graph.entry(a.clone()).or_insert(vec![]).push(b.clone());
}
}
}
// event_id -> StateEvent
let mut event_map: BTreeMap<EventId, Arc<StateEvent>> = BTreeMap::new();
// event_id -> StateMap<EventId>
let mut state_at_event: BTreeMap<EventId, StateMap<EventId>> = BTreeMap::new();
// resolve the current state and add it to the state_at_event map then continue
// on in "time"
for node in
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 prev_events = graph.get(&node).unwrap();
let state_before: StateMap<EventId> = if prev_events.is_empty() {
BTreeMap::new()
} else if prev_events.len() == 1 {
state_at_event.get(&prev_events[0]).unwrap().clone()
} else {
let state_sets = prev_events
.iter()
.filter_map(|k| state_at_event.get(k))
.cloned()
.collect::<Vec<_>>();
log::info!(
"{:#?}",
state_sets
.iter()
.map(|map| map
.iter()
.map(|((ty, key), id)| format!("(({}{:?}), {})", ty, key, id))
.collect::<Vec<_>>())
.collect::<Vec<_>>()
);
let resolved = StateResolution::resolve(
&room_id(),
&RoomVersionId::Version6,
&state_sets,
state_sets
.iter()
.map(|map| {
store
.auth_event_ids(&room_id(), &map.values().cloned().collect::<Vec<_>>())
.unwrap()
})
.collect(),
&mut event_map,
);
match resolved {
Ok(state) => state,
Err(e) => panic!("resolution for {} failed: {}", node, e),
}
};
let mut state_after = state_before.clone();
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(),
Some(fake_event.state_key()),
fake_event.content(),
);
let mut auth_events = vec![];
for key in auth_types {
if state_before.contains_key(&key) {
auth_events.push(state_before[&key].clone())
}
}
// 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 event = to_pdu_event(
e.event_id().as_str(),
e.sender().clone(),
e.kind().clone(),
Some(&e.state_key()),
e.content(),
&auth_events,
prev_events,
);
// we have to update our store, an actual user of this lib would
// be giving us state from a DB.
store.0.insert(ev_id.clone(), event.clone());
state_at_event.insert(node, state_after);
event_map.insert(event_id.clone(), Arc::clone(store.0.get(&ev_id).unwrap()));
}
let mut expected_state = StateMap::new();
for node in expected_state_ids {
let ev = event_map.get(&node).expect(&format!(
"{} not found in {:?}",
node.to_string(),
event_map
.keys()
.map(ToString::to_string)
.collect::<Vec<_>>(),
));
let key = (ev.kind(), ev.state_key());
expected_state.insert(key, node);
}
let start_state = state_at_event.get(&event_id("$START:foo")).unwrap();
let end_state = state_at_event
.get(&event_id("$END:foo"))
.unwrap()
.iter()
.filter(|(k, v)| {
expected_state.contains_key(k)
|| start_state.get(k) != Some(*v)
// Filter out the dummy messages events.
// These act as points in time where there should be a known state to
// test against.
&& k != &&(EventType::RoomMessage, "dummy".to_string())
})
.map(|(k, v)| (k.clone(), v.clone()))
.collect::<StateMap<EventId>>();
assert_eq!(expected_state, end_state);
}
pub struct TestStore<E: Event>(pub BTreeMap<EventId, Arc<E>>);
#[allow(unused)]
impl<E: Event> TestStore<E> {
pub fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result<Arc<E>> {
self.0
.get(event_id)
.map(Arc::clone)
.ok_or_else(|| Error::NotFound(format!("{} not found", event_id.to_string())))
}
/// Returns the events that correspond to the `event_ids` sorted in the same order.
pub fn get_events(&self, room_id: &RoomId, event_ids: &[EventId]) -> Result<Vec<Arc<E>>> {
let mut events = vec![];
for id in event_ids {
events.push(self.get_event(room_id, id)?);
}
Ok(events)
}
/// Returns a Vec of the related auth events to the given `event`.
pub fn auth_event_ids(&self, room_id: &RoomId, event_ids: &[EventId]) -> Result<Vec<EventId>> {
let mut result = vec![];
let mut stack = event_ids.to_vec();
// DFS for auth event chain
while !stack.is_empty() {
let ev_id = stack.pop().unwrap();
if result.contains(&ev_id) {
continue;
}
result.push(ev_id.clone());
let event = self.get_event(room_id, &ev_id)?;
stack.extend(event.auth_events().clone());
}
Ok(result)
}
/// Returns a Vec<EventId> representing the difference in auth chains of the given `events`.
pub fn auth_chain_diff(
&self,
room_id: &RoomId,
event_ids: Vec<Vec<EventId>>,
) -> Result<Vec<EventId>> {
use itertools::Itertools;
let mut chains = vec![];
for ids in event_ids {
// TODO state store `auth_event_ids` returns self in the event ids list
// when an event returns `auth_event_ids` self is not contained
let chain = self
.auth_event_ids(room_id, &ids)?
.into_iter()
.collect::<BTreeSet<_>>();
chains.push(chain);
}
if let Some(chain) = chains.first().cloned() {
let rest = chains.iter().skip(1).flatten().cloned().collect();
let common = chain.intersection(&rest).collect::<Vec<_>>();
Ok(chains
.into_iter()
.flatten()
.filter(|id| !common.contains(&id))
.dedup()
.collect())
} else {
Ok(vec![])
}
}
}
pub fn event_id(id: &str) -> EventId {
if id.contains('$') {
return EventId::try_from(id).unwrap();
}
EventId::try_from(format!("${}:foo", id)).unwrap()
}
pub fn alice() -> UserId {
UserId::try_from("@alice:foo").unwrap()
}
pub fn bob() -> UserId {
UserId::try_from("@bob:foo").unwrap()
}
pub fn charlie() -> UserId {
UserId::try_from("@charlie:foo").unwrap()
}
pub fn ella() -> UserId {
UserId::try_from("@ella:foo").unwrap()
}
pub fn zara() -> UserId {
UserId::try_from("@zara:foo").unwrap()
}
pub fn room_id() -> RoomId {
RoomId::try_from("!test:foo").unwrap()
}
pub fn member_content_ban() -> JsonValue {
serde_json::to_value(MemberEventContent {
membership: MembershipState::Ban,
displayname: None,
avatar_url: None,
is_direct: None,
third_party_invite: None,
})
.unwrap()
}
pub fn member_content_join() -> JsonValue {
serde_json::to_value(MemberEventContent {
membership: MembershipState::Join,
displayname: None,
avatar_url: None,
is_direct: None,
third_party_invite: None,
})
.unwrap()
}
pub fn to_init_pdu_event(
id: &str,
sender: UserId,
ev_type: EventType,
state_key: Option<&str>,
content: JsonValue,
) -> Arc<StateEvent> {
let ts = unsafe {
let ts = SERVER_TIMESTAMP;
// increment the "origin_server_ts" value
SERVER_TIMESTAMP += 1;
ts
};
let id = if id.contains('$') {
id.to_string()
} else {
format!("${}:foo", id)
};
let state_key = state_key.map(ToString::to_string);
Arc::new(StateEvent {
event_id: EventId::try_from(id).unwrap(),
rest: Pdu::RoomV3Pdu(RoomV3Pdu {
room_id: room_id(),
sender,
origin_server_ts: UNIX_EPOCH + Duration::from_secs(ts),
state_key,
kind: ev_type,
content,
redacts: None,
unsigned: btreemap! {},
#[cfg(not(feature = "unstable-pre-spec"))]
origin: "foo".into(),
auth_events: vec![],
prev_events: vec![],
depth: ruma::uint!(0),
hashes: EventHash { sha256: "".into() },
signatures: btreemap! {},
}),
})
}
pub fn to_pdu_event<S>(
id: &str,
sender: UserId,
ev_type: EventType,
state_key: Option<&str>,
content: JsonValue,
auth_events: &[S],
prev_events: &[S],
) -> Arc<StateEvent>
where
S: AsRef<str>,
{
let ts = unsafe {
let ts = SERVER_TIMESTAMP;
// increment the "origin_server_ts" value
SERVER_TIMESTAMP += 1;
ts
};
let id = if id.contains('$') {
id.to_string()
} else {
format!("${}:foo", id)
};
let auth_events = auth_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
let prev_events = prev_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
let state_key = state_key.map(ToString::to_string);
Arc::new(StateEvent {
event_id: EventId::try_from(id).unwrap(),
rest: Pdu::RoomV3Pdu(RoomV3Pdu {
room_id: room_id(),
sender,
origin_server_ts: UNIX_EPOCH + Duration::from_secs(ts),
state_key,
kind: ev_type,
content,
redacts: None,
unsigned: btreemap! {},
#[cfg(not(feature = "unstable-pre-spec"))]
origin: "foo".into(),
auth_events,
prev_events,
depth: ruma::uint!(0),
hashes: EventHash { sha256: "".into() },
signatures: btreemap! {},
}),
})
}
// all graphs start with these input events
#[allow(non_snake_case)]
pub fn INITIAL_EVENTS() -> BTreeMap<EventId, Arc<StateEvent>> {
// this is always called so we can init the logger here
let _ = LOGGER.call_once(|| {
tracer::fmt()
.with_env_filter(tracer::EnvFilter::from_default_env())
.init()
});
vec![
to_pdu_event::<EventId>(
"CREATE",
alice(),
EventType::RoomCreate,
Some(""),
json!({ "creator": alice() }),
&[],
&[],
),
to_pdu_event(
"IMA",
alice(),
EventType::RoomMember,
Some(alice().to_string().as_str()),
member_content_join(),
&["CREATE"],
&["CREATE"],
),
to_pdu_event(
"IPOWER",
alice(),
EventType::RoomPowerLevels,
Some(""),
json!({"users": {alice().to_string(): 100}}),
&["CREATE", "IMA"],
&["IMA"],
),
to_pdu_event(
"IJR",
alice(),
EventType::RoomJoinRules,
Some(""),
json!({ "join_rule": JoinRule::Public }),
&["CREATE", "IMA", "IPOWER"],
&["IPOWER"],
),
to_pdu_event(
"IMB",
bob(),
EventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IJR"],
),
to_pdu_event(
"IMC",
charlie(),
EventType::RoomMember,
Some(charlie().to_string().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IMB"],
),
to_pdu_event::<EventId>(
"START",
charlie(),
EventType::RoomMessage,
Some("dummy"),
json!({}),
&[],
&[],
),
to_pdu_event::<EventId>(
"END",
charlie(),
EventType::RoomMessage,
Some("dummy"),
json!({}),
&[],
&[],
),
]
.into_iter()
.map(|ev| (ev.event_id().clone(), ev))
.collect()
}
#[allow(non_snake_case)]
pub fn INITIAL_EDGES() -> Vec<EventId> {
vec!["START", "IMC", "IMB", "IJR", "IPOWER", "IMA", "CREATE"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>()
}
pub mod event {
use std::{collections::BTreeMap, time::SystemTime};
use ruma::{
events::{
pdu::{EventHash, Pdu},
room::member::{MemberEventContent, MembershipState},
EventType,
},
EventId, RoomId, RoomVersionId, ServerName, UInt, UserId,
};
use serde::{Deserialize, Serialize};
use serde_json::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<String> {
Some(self.state_key())
}
fn prev_events(&self) -> Vec<EventId> {
self.prev_event_ids()
}
fn depth(&self) -> &UInt {
self.depth()
}
fn auth_events(&self) -> Vec<EventId> {
self.auth_events()
}
fn redacts(&self) -> Option<&EventId> {
self.redacts()
}
fn hashes(&self) -> &EventHash {
self.hashes()
}
fn signatures(
&self,
) -> BTreeMap<Box<ServerName>, BTreeMap<ruma::ServerSigningKeyId, String>> {
self.signatures()
}
fn unsigned(&self) -> &BTreeMap<String, JsonValue> {
self.unsigned()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct StateEvent {
pub event_id: EventId,
#[serde(flatten)]
pub rest: Pdu,
}
impl StateEvent {
pub fn from_id_value(
id: EventId,
json: serde_json::Value,
) -> Result<Self, serde_json::Error> {
Ok(Self {
event_id: id,
rest: Pdu::RoomV3Pdu(serde_json::from_value(json)?),
})
}
pub fn from_id_canon_obj(
id: EventId,
json: ruma::serde::CanonicalJsonObject,
) -> Result<Self, serde_json::Error> {
Ok(Self {
event_id: id,
// TODO: this is unfortunate (from_value(to_value(json)))...
rest: Pdu::RoomV3Pdu(serde_json::from_value(serde_json::to_value(json)?)?),
})
}
pub fn is_power_event(&self) -> bool {
match &self.rest {
Pdu::RoomV1Pdu(event) => match event.kind {
EventType::RoomPowerLevels
| EventType::RoomJoinRules
| EventType::RoomCreate => event.state_key == Some("".into()),
EventType::RoomMember => {
// TODO fix clone
if let Ok(content) =
serde_json::from_value::<MemberEventContent>(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<C: serde::de::DeserializeOwned>(
&self,
) -> Result<C, serde_json::Error> {
match &self.rest {
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.rest {
Pdu::RoomV1Pdu(ev) => &ev.origin_server_ts,
Pdu::RoomV3Pdu(ev) => &ev.origin_server_ts,
}
}
pub fn event_id(&self) -> &EventId {
&self.event_id
}
pub fn sender(&self) -> &UserId {
match &self.rest {
Pdu::RoomV1Pdu(ev) => &ev.sender,
Pdu::RoomV3Pdu(ev) => &ev.sender,
}
}
pub fn redacts(&self) -> Option<&EventId> {
match &self.rest {
Pdu::RoomV1Pdu(ev) => ev.redacts.as_ref(),
Pdu::RoomV3Pdu(ev) => ev.redacts.as_ref(),
}
}
pub fn room_id(&self) -> &RoomId {
match &self.rest {
Pdu::RoomV1Pdu(ev) => &ev.room_id,
Pdu::RoomV3Pdu(ev) => &ev.room_id,
}
}
pub fn kind(&self) -> EventType {
match &self.rest {
Pdu::RoomV1Pdu(ev) => ev.kind.clone(),
Pdu::RoomV3Pdu(ev) => ev.kind.clone(),
}
}
pub fn state_key(&self) -> String {
match &self.rest {
Pdu::RoomV1Pdu(ev) => ev.state_key.clone().unwrap(),
Pdu::RoomV3Pdu(ev) => ev.state_key.clone().unwrap(),
}
}
#[cfg(not(feature = "unstable-pre-spec"))]
pub fn origin(&self) -> String {
match &self.rest {
Pdu::RoomV1Pdu(ev) => ev.origin.clone(),
Pdu::RoomV3Pdu(ev) => ev.origin.clone(),
}
}
pub fn prev_event_ids(&self) -> Vec<EventId> {
match &self.rest {
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<EventId> {
match &self.rest {
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.rest {
Pdu::RoomV1Pdu(ev) => ev.content.clone(),
Pdu::RoomV3Pdu(ev) => ev.content.clone(),
}
}
pub fn unsigned(&self) -> &BTreeMap<String, serde_json::Value> {
match &self.rest {
Pdu::RoomV1Pdu(ev) => &ev.unsigned,
Pdu::RoomV3Pdu(ev) => &ev.unsigned,
}
}
pub fn signatures(
&self,
) -> BTreeMap<Box<ServerName>, BTreeMap<ruma::ServerSigningKeyId, String>> {
match &self.rest {
Pdu::RoomV1Pdu(_) => maplit::btreemap! {},
Pdu::RoomV3Pdu(ev) => ev.signatures.clone(),
}
}
pub fn hashes(&self) -> &EventHash {
match &self.rest {
Pdu::RoomV1Pdu(ev) => &ev.hashes,
Pdu::RoomV3Pdu(ev) => &ev.hashes,
}
}
pub fn depth(&self) -> &UInt {
match &self.rest {
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.rest {
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.rest {
Pdu::RoomV1Pdu(_) => RoomVersionId::Version1,
Pdu::RoomV3Pdu(_) => RoomVersionId::Version6,
}
}
}
}