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:
commit
6609829735
30
crates/ruma-state-res/.github/workflows/nightly.yml
vendored
Normal file
30
crates/ruma-state-res/.github/workflows/nightly.yml
vendored
Normal 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
|
28
crates/ruma-state-res/.github/workflows/stable.yml
vendored
Normal file
28
crates/ruma-state-res/.github/workflows/stable.yml
vendored
Normal 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
2
crates/ruma-state-res/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
Cargo.lock
|
37
crates/ruma-state-res/Cargo.toml
Normal file
37
crates/ruma-state-res/Cargo.toml
Normal 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
|
19
crates/ruma-state-res/LICENSE
Normal file
19
crates/ruma-state-res/LICENSE
Normal 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.
|
46
crates/ruma-state-res/README.md
Normal file
46
crates/ruma-state-res/README.md
Normal 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.
|
70
crates/ruma-state-res/architecture.md
Normal file
70
crates/ruma-state-res/architecture.md
Normal 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.
|
1
crates/ruma-state-res/benches/event_auth_bench.rs
Normal file
1
crates/ruma-state-res/benches/event_auth_bench.rs
Normal file
@ -0,0 +1 @@
|
||||
|
59
crates/ruma-state-res/benches/outcomes.txt
Normal file
59
crates/ruma-state-res/benches/outcomes.txt
Normal 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
|
812
crates/ruma-state-res/benches/state_res_bench.rs
Normal file
812
crates/ruma-state-res/benches/state_res_bench.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
crates/ruma-state-res/rustfmt.toml
Normal file
1
crates/ruma-state-res/rustfmt.toml
Normal file
@ -0,0 +1 @@
|
||||
imports_granularity="Crate"
|
35
crates/ruma-state-res/src/error.rs
Normal file
35
crates/ruma-state-res/src/error.rs
Normal 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))
|
||||
}
|
||||
}
|
885
crates/ruma-state-res/src/event_auth.rs
Normal file
885
crates/ruma-state-res/src/event_auth.rs
Normal 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 = ¤t_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 = ¤t_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 = ¤t_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
|
||||
}
|
||||
}
|
717
crates/ruma-state-res/src/lib.rs
Normal file
717
crates/ruma-state-res/src/lib.rs
Normal 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,
|
||||
}
|
||||
}
|
160
crates/ruma-state-res/src/room_version.rs
Normal file
160
crates/ruma-state-res/src/room_version.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
52
crates/ruma-state-res/src/state_event.rs
Normal file
52
crates/ruma-state-res/src/state_event.rs
Normal 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>>;
|
||||
}
|
79
crates/ruma-state-res/tests/event_auth.rs
Normal file
79
crates/ruma-state-res/tests/event_auth.rs
Normal 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())
|
||||
}
|
95
crates/ruma-state-res/tests/event_sorting.rs
Normal file
95
crates/ruma-state-res/tests/event_sorting.rs
Normal 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()
|
||||
}
|
||||
}
|
208
crates/ruma-state-res/tests/res_with_auth_ids.rs
Normal file
208
crates/ruma-state-res/tests/res_with_auth_ids.rs
Normal 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()
|
||||
}
|
408
crates/ruma-state-res/tests/state_res.rs
Normal file
408
crates/ruma-state-res/tests/state_res.rs
Normal 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)
|
||||
}
|
||||
}
|
792
crates/ruma-state-res/tests/utils.rs
Normal file
792
crates/ruma-state-res/tests/utils.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user