ruma-state-res-0.8.0/.cargo_vcs_info.json0000644000000001630000000000100137120ustar { "git": { "sha1": "2c43137f5faa88b9657d8ce80a1bcdd93f459517" }, "path_in_vcs": "crates/ruma-state-res" }ruma-state-res-0.8.0/.clippy.toml000064400000000000000000000015311046102023000147550ustar 00000000000000avoid-breaking-exported-api = false disallowed-methods = [ # https://github.com/serde-rs/json/issues/160 "serde_json::from_reader", ] disallowed-types = [] enforced-import-renames = [ { path = "serde_json::from_slice", rename = "from_json_slice" }, { path = "serde_json::from_str", rename = "from_json_str" }, { path = "serde_json::from_value", rename = "from_json_value" }, { path = "serde_json::to_value", rename = "to_json_value" }, { path = "serde_json::value::to_raw_value", rename = "to_raw_json_value" }, { path = "serde_json::value::RawValue", rename = "RawJsonValue" }, { path = "serde_json::Value", rename = "JsonValue" }, ] standard-macro-braces = [ { name = "btreeset", brace = "[" }, { name = "btreemap", brace = "{" }, { name = "hashset", brace = "[" }, { name = "hashmap", brace = "{" }, ] ruma-state-res-0.8.0/CHANGELOG.md000064400000000000000000000030561046102023000143170ustar 00000000000000# [unreleased] # 0.8.0 Bug fixes: * Change default `invite` power level to `0` * The spec was determined to be wrong about the default: Improvements: * Add `m.federate` to `auth_check`: * Add `RoomVersion::V10` (MSC3604) * Deserialize stringified integers for power levels without the `compat` feature * Removes the `compat` feature # 0.7.0 Breaking changes: * `auth_check` does not require `prev_event` parameter. It was only required on some specific cases. Previous event is now calculated on demand only when it's required. # 0.6.0 Breaking changes: * Upgrade dependencies # 0.5.0 Breaking changes: * Remove some trait methods from `Event` * Update `Event::content` signature to return `&RawJsonValue` instead of `&JsonValue` * The `key_fn` in `lexicographical_topological_sort` has removed the event ID from its return type and changed to expect just the power level, not the negated power level # 0.4.1 Improvements: * Improve performance of `StateResolution::separate` # 0.4.0 Breaking changes: * Change the way events are supplied # 0.3.0 Breaking changes: * state_res::resolve auth_events type has been slightly changed and renamed to auth_chain_sets * state_res::resolve structs were changed from BTreeMap/Set to HashMap/Set * Upgrade dependencies # 0.2.0 Breaking changes: * Replace `Vec` by `BTreeSet` in parts of the API * Replace `event_map` argument with a closure to fetch events on demand # 0.1.0 Initial release ruma-state-res-0.8.0/Cargo.toml0000644000000031740000000000100117150ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.60" name = "ruma-state-res" version = "0.8.0" description = "An abstraction for Matrix state resolution." homepage = "https://www.ruma.io/" readme = "README.md" keywords = [ "matrix", "chat", "ruma", ] categories = [ "api-bindings", "web-programming", ] license = "MIT" repository = "https://github.com/ruma/ruma" [package.metadata.docs.rs] all-features = true [[bench]] name = "state_res_bench" harness = false required-features = ["criterion"] [dependencies.criterion] version = "0.3.3" optional = true [dependencies.itertools] version = "0.10.0" [dependencies.js_int] version = "0.2.0" [dependencies.ruma-common] version = "0.10.0" features = ["events"] [dependencies.serde] version = "1.0.118" features = ["derive"] [dependencies.serde_json] version = "1.0.60" [dependencies.thiserror] version = "1.0.26" [dependencies.tracing] version = "0.1.26" [dev-dependencies.maplit] version = "1.0.2" [dev-dependencies.rand] version = "0.8.3" [dev-dependencies.ruma-common] version = "0.10.0" features = ["unstable-pdu"] [dev-dependencies.tracing-subscriber] version = "0.3.3" [features] unstable-exhaustive-types = [] ruma-state-res-0.8.0/Cargo.toml.orig000064400000000000000000000020701046102023000153700ustar 00000000000000[package] name = "ruma-state-res" categories = ["api-bindings", "web-programming"] keywords = ["matrix", "chat", "ruma"] description = "An abstraction for Matrix state resolution." homepage = "https://www.ruma.io/" repository = "https://github.com/ruma/ruma" readme = "README.md" license = "MIT" version = "0.8.0" edition = "2021" rust-version = "1.60" [package.metadata.docs.rs] all-features = true [features] unstable-exhaustive-types = [] [dependencies] itertools = "0.10.0" js_int = "0.2.0" ruma-common = { version = "0.10.0", path = "../ruma-common", features = ["events"] } serde = { version = "1.0.118", features = ["derive"] } serde_json = "1.0.60" thiserror = "1.0.26" tracing = "0.1.26" # dev-dependencies can't be optional, so this is a regular dependency criterion = { version = "0.3.3", optional = true } [dev-dependencies] maplit = "1.0.2" rand = "0.8.3" ruma-common = { version = "0.10.0", path = "../ruma-common", features = ["unstable-pdu"] } tracing-subscriber = "0.3.3" [[bench]] name = "state_res_bench" harness = false required-features = ["criterion"] ruma-state-res-0.8.0/README.md000064400000000000000000000032721046102023000137650ustar 00000000000000# 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 = BTreeMap<(EventType, Option), T>; /// A mapping of `EventId` to `T`, usually a `OriginalStateEvent`. pub type EventMap = BTreeMap; 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( room_id: &RoomId, room_version: &RoomVersionId, state_sets: &[StateMap], auth_events: Vec>, event_map: &mut EventMap>, ) -> Result> {; } ``` 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. ruma-state-res-0.8.0/architecture.md000064400000000000000000000060221046102023000155060ustar 00000000000000# 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. ruma-state-res-0.8.0/benches/outcomes.txt000064400000000000000000000077231046102023000165210ustar 0000000000000011/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 6/30/2021 BRANCH: state-closure REV: 174c3e2a72232ad75b3fb14b3551f5f746f4fe84 lexicographical topological sort time: [1.5496 us 1.5536 us 1.5586 us] Found 9 outliers among 100 measurements (9.00%) 1 (1.00%) low mild 1 (1.00%) high mild 7 (7.00%) high severe resolve state of 5 events one fork time: [10.319 us 10.333 us 10.347 us] Found 2 outliers among 100 measurements (2.00%) 2 (2.00%) high severe resolve state of 10 events 3 conflicting time: [25.770 us 25.805 us 25.839 us] Found 7 outliers among 100 measurements (7.00%) 5 (5.00%) high mild 2 (2.00%) high severe 7/20/2021 BRANCH stateres-result REV: This marks the switch to HashSet/Map lexicographical topological sort time: [1.8122 us 1.8177 us 1.8233 us] change: [+15.205% +15.919% +16.502%] (p = 0.00 < 0.05) Performance has regressed. Found 7 outliers among 100 measurements (7.00%) 5 (5.00%) high mild 2 (2.00%) high severe resolve state of 5 events one fork time: [11.966 us 12.010 us 12.059 us] change: [+16.089% +16.730% +17.469%] (p = 0.00 < 0.05) Performance has regressed. Found 7 outliers among 100 measurements (7.00%) 3 (3.00%) high mild 4 (4.00%) high severe resolve state of 10 events 3 conflicting time: [29.092 us 29.201 us 29.311 us] change: [+12.447% +12.847% +13.280%] (p = 0.00 < 0.05) Performance has regressed. Found 9 outliers among 100 measurements (9.00%) 6 (6.00%) high mild 3 (3.00%) high severe ruma-state-res-0.8.0/benches/state_res_bench.rs000064400000000000000000000503071046102023000176140ustar 00000000000000// Because of criterion `cargo bench` works, // but if you use `cargo bench -- --save-baseline ` // 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 -- --save-baseline `. #![allow(clippy::exhaustive_structs)] use std::{ borrow::Borrow, collections::{HashMap, HashSet}, sync::{ atomic::{AtomicU64, Ordering::SeqCst}, Arc, }, }; use criterion::{criterion_group, criterion_main, Criterion}; use event::PduEvent; use js_int::{int, uint}; use maplit::{btreemap, hashmap, hashset}; use ruma_common::{ events::{ pdu::{EventHash, Pdu, RoomV3Pdu}, room::{ join_rules::{JoinRule, RoomJoinRulesEventContent}, member::{MembershipState, RoomMemberEventContent}, }, RoomEventType, StateEventType, }, room_id, user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, UserId, }; use ruma_state_res::{self as state_res, Error, Event, Result, StateMap}; use serde_json::{ json, value::{to_raw_value as to_raw_json_value, RawValue as RawJsonValue}, }; static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0); fn lexico_topo_sort(c: &mut Criterion) { c.bench_function("lexicographical topological sort", |b| { let graph = hashmap! { event_id("l") => hashset![event_id("o")], event_id("m") => hashset![event_id("n"), event_id("o")], event_id("n") => hashset![event_id("o")], event_id("o") => hashset![], // "o" has zero outgoing edges but 4 incoming edges event_id("p") => hashset![event_id("o")], }; b.iter(|| { let _ = state_res::lexicographical_topological_sort(&graph, |_id| { Ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0)))) }); }); }); } fn resolution_shallow_auth_chain(c: &mut Criterion) { c.bench_function("resolve state of 5 events one fork", |b| { let mut store = TestStore(hashmap! {}); // build up the DAG let (state_at_bob, state_at_charlie, _) = store.set_up(); b.iter(|| { let ev_map = store.0.clone(); let state_sets = [&state_at_bob, &state_at_charlie]; let _ = match state_res::resolve( &RoomVersionId::V6, state_sets, state_sets .iter() .map(|map| { store.auth_event_ids(room_id(), map.values().cloned().collect()).unwrap() }) .collect(), |id| ev_map.get(id).map(Arc::clone), ) { 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 mut inner = INITIAL_EVENTS(); let ban = BAN_STATE_SET(); 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.event_type().with_state_key(ev.state_key().unwrap()), ev.event_id().to_owned()) }) .collect::>(); 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.event_type().with_state_key(ev.state_key().unwrap()), ev.event_id().to_owned()) }) .collect::>(); b.iter(|| { let state_sets = [&state_set_a, &state_set_b]; let _ = match state_res::resolve( &RoomVersionId::V6, state_sets, state_sets .iter() .map(|map| { store.auth_event_ids(room_id(), map.values().cloned().collect()).unwrap() }) .collect(), |id| inner.get(id).map(Arc::clone), ) { 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 // /////////////////////////////////////////////////////////////////////*/ struct TestStore(HashMap>); #[allow(unused)] impl TestStore { fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result> { self.0 .get(event_id) .map(Arc::clone) .ok_or_else(|| Error::NotFound(format!("{} not found", event_id))) } /// Returns the events that correspond to the `event_ids` sorted in the same order. fn get_events(&self, room_id: &RoomId, event_ids: &[OwnedEventId]) -> Result>> { 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`. fn auth_event_ids(&self, room_id: &RoomId, event_ids: Vec) -> Result> { let mut result = HashSet::new(); let mut stack = event_ids; // DFS for auth event chain while !stack.is_empty() { let ev_id = stack.pop().unwrap(); if result.contains(&ev_id) { continue; } result.insert(ev_id.clone()); let event = self.get_event(room_id, ev_id.borrow())?; stack.extend(event.auth_events().map(ToOwned::to_owned)); } Ok(result) } /// Returns a vector representing the difference in auth chains of the given `events`. fn auth_chain_diff(&self, room_id: &RoomId, event_ids: Vec>) -> Result> { let mut auth_chain_sets = 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::>(); auth_chain_sets.push(chain); } if let Some(first) = auth_chain_sets.first().cloned() { let common = auth_chain_sets .iter() .skip(1) .fold(first, |a, b| a.intersection(b).cloned().collect::>()); Ok(auth_chain_sets .into_iter() .flatten() .filter(|id| !common.contains(id.borrow())) .collect()) } else { Ok(vec![]) } } } impl TestStore { #[allow(clippy::type_complexity)] fn set_up( &mut self, ) -> (StateMap, StateMap, StateMap) { let create_event = to_pdu_event::<&EventId>( "CREATE", alice(), RoomEventType::RoomCreate, Some(""), to_raw_json_value(&json!({ "creator": alice() })).unwrap(), &[], &[], ); let cre = create_event.event_id().to_owned(); self.0.insert(cre.clone(), Arc::clone(&create_event)); let alice_mem = to_pdu_event( "IMA", alice(), RoomEventType::RoomMember, Some(alice().to_string().as_str()), member_content_join(), &[cre.clone()], &[cre.clone()], ); self.0.insert(alice_mem.event_id().to_owned(), Arc::clone(&alice_mem)); let join_rules = to_pdu_event( "IJR", alice(), RoomEventType::RoomJoinRules, Some(""), to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(), &[cre.clone(), alice_mem.event_id().to_owned()], &[alice_mem.event_id().to_owned()], ); self.0.insert(join_rules.event_id().to_owned(), 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(), RoomEventType::RoomMember, Some(bob().to_string().as_str()), member_content_join(), &[cre.clone(), join_rules.event_id().to_owned()], &[join_rules.event_id().to_owned()], ); self.0.insert(bob_mem.event_id().to_owned(), bob_mem.clone()); let charlie_mem = to_pdu_event( "IMC", charlie(), RoomEventType::RoomMember, Some(charlie().to_string().as_str()), member_content_join(), &[cre, join_rules.event_id().to_owned()], &[join_rules.event_id().to_owned()], ); self.0.insert(charlie_mem.event_id().to_owned(), charlie_mem.clone()); let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem] .iter() .map(|e| { (e.event_type().with_state_key(e.state_key().unwrap()), e.event_id().to_owned()) }) .collect::>(); let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem] .iter() .map(|e| { (e.event_type().with_state_key(e.state_key().unwrap()), e.event_id().to_owned()) }) .collect::>(); let expected = [&create_event, &alice_mem, &join_rules, &bob_mem, &charlie_mem] .iter() .map(|e| { (e.event_type().with_state_key(e.state_key().unwrap()), e.event_id().to_owned()) }) .collect::>(); (state_at_bob, state_at_charlie, expected) } } fn event_id(id: &str) -> OwnedEventId { if id.contains('$') { return id.try_into().unwrap(); } format!("${}:foo", id).try_into().unwrap() } fn alice() -> &'static UserId { user_id!("@alice:foo") } fn bob() -> &'static UserId { user_id!("@bob:foo") } fn charlie() -> &'static UserId { user_id!("@charlie:foo") } fn ella() -> &'static UserId { user_id!("@ella:foo") } fn room_id() -> &'static RoomId { room_id!("!test:foo") } fn member_content_ban() -> Box { to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Ban)).unwrap() } fn member_content_join() -> Box { to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap() } fn to_pdu_event( id: &str, sender: &UserId, ev_type: RoomEventType, state_key: Option<&str>, content: Box, auth_events: &[S], prev_events: &[S], ) -> Arc where S: AsRef, { // We don't care if the addition happens in order just that it is atomic // (each event has its own value) let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst); let id = if id.contains('$') { id.to_owned() } else { format!("${}:foo", id) }; let auth_events = auth_events.iter().map(AsRef::as_ref).map(event_id).collect::>(); let prev_events = prev_events.iter().map(AsRef::as_ref).map(event_id).collect::>(); let state_key = state_key.map(ToOwned::to_owned); Arc::new(PduEvent { event_id: id.try_into().unwrap(), rest: Pdu::RoomV3Pdu(RoomV3Pdu { room_id: room_id().to_owned(), sender: sender.to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(ts.try_into().unwrap()), state_key, kind: ev_type, content, redacts: None, unsigned: btreemap! {}, auth_events, prev_events, depth: uint!(0), hashes: EventHash::new(String::new()), signatures: btreemap! {}, }), }) } // all graphs start with these input events #[allow(non_snake_case)] fn INITIAL_EVENTS() -> HashMap> { vec![ to_pdu_event::<&EventId>( "CREATE", alice(), RoomEventType::RoomCreate, Some(""), to_raw_json_value(&json!({ "creator": alice() })).unwrap(), &[], &[], ), to_pdu_event( "IMA", alice(), RoomEventType::RoomMember, Some(alice().as_str()), member_content_join(), &["CREATE"], &["CREATE"], ), to_pdu_event( "IPOWER", alice(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100 } })).unwrap(), &["CREATE", "IMA"], &["IMA"], ), to_pdu_event( "IJR", alice(), RoomEventType::RoomJoinRules, Some(""), to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(), &["CREATE", "IMA", "IPOWER"], &["IPOWER"], ), to_pdu_event( "IMB", bob(), RoomEventType::RoomMember, Some(bob().to_string().as_str()), member_content_join(), &["CREATE", "IJR", "IPOWER"], &["IJR"], ), to_pdu_event( "IMC", charlie(), RoomEventType::RoomMember, Some(charlie().to_string().as_str()), member_content_join(), &["CREATE", "IJR", "IPOWER"], &["IMB"], ), to_pdu_event::<&EventId>( "START", charlie(), RoomEventType::RoomTopic, Some(""), to_raw_json_value(&json!({})).unwrap(), &[], &[], ), to_pdu_event::<&EventId>( "END", charlie(), RoomEventType::RoomTopic, Some(""), to_raw_json_value(&json!({})).unwrap(), &[], &[], ), ] .into_iter() .map(|ev| (ev.event_id().to_owned(), ev)) .collect() } // all graphs start with these input events #[allow(non_snake_case)] fn BAN_STATE_SET() -> HashMap> { vec![ to_pdu_event( "PA", alice(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), &["CREATE", "IMA", "IPOWER"], // auth_events &["START"], // prev_events ), to_pdu_event( "PB", alice(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), &["CREATE", "IMA", "IPOWER"], &["END"], ), to_pdu_event( "MB", alice(), RoomEventType::RoomMember, Some(ella().as_str()), member_content_ban(), &["CREATE", "IMA", "PB"], &["PA"], ), to_pdu_event( "IME", ella(), RoomEventType::RoomMember, Some(ella().as_str()), member_content_join(), &["CREATE", "IJR", "PA"], &["MB"], ), ] .into_iter() .map(|ev| (ev.event_id().to_owned(), ev)) .collect() } /// Convenience trait for adding event type plus state key to state maps. trait EventTypeExt { fn with_state_key(self, state_key: impl Into) -> (StateEventType, String); } impl EventTypeExt for &RoomEventType { fn with_state_key(self, state_key: impl Into) -> (StateEventType, String) { (self.to_string().into(), state_key.into()) } } mod event { use ruma_common::{ events::{pdu::Pdu, RoomEventType}, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId, }; use ruma_state_res::Event; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue as RawJsonValue; impl Event for PduEvent { type Id = OwnedEventId; fn event_id(&self) -> &Self::Id { &self.event_id } fn room_id(&self) -> &RoomId { match &self.rest { Pdu::RoomV1Pdu(ev) => &ev.room_id, Pdu::RoomV3Pdu(ev) => &ev.room_id, #[cfg(not(feature = "unstable-exhaustive-types"))] _ => unreachable!("new PDU version"), } } fn sender(&self) -> &UserId { match &self.rest { Pdu::RoomV1Pdu(ev) => &ev.sender, Pdu::RoomV3Pdu(ev) => &ev.sender, #[cfg(not(feature = "unstable-exhaustive-types"))] _ => unreachable!("new PDU version"), } } fn event_type(&self) -> &RoomEventType { match &self.rest { Pdu::RoomV1Pdu(ev) => &ev.kind, Pdu::RoomV3Pdu(ev) => &ev.kind, #[cfg(not(feature = "unstable-exhaustive-types"))] _ => unreachable!("new PDU version"), } } fn content(&self) -> &RawJsonValue { match &self.rest { Pdu::RoomV1Pdu(ev) => &ev.content, Pdu::RoomV3Pdu(ev) => &ev.content, #[cfg(not(feature = "unstable-exhaustive-types"))] _ => unreachable!("new PDU version"), } } fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch { match &self.rest { Pdu::RoomV1Pdu(ev) => ev.origin_server_ts, Pdu::RoomV3Pdu(ev) => ev.origin_server_ts, #[cfg(not(feature = "unstable-exhaustive-types"))] _ => unreachable!("new PDU version"), } } fn state_key(&self) -> Option<&str> { match &self.rest { Pdu::RoomV1Pdu(ev) => ev.state_key.as_deref(), Pdu::RoomV3Pdu(ev) => ev.state_key.as_deref(), #[cfg(not(feature = "unstable-exhaustive-types"))] _ => unreachable!("new PDU version"), } } fn prev_events(&self) -> Box + '_> { match &self.rest { Pdu::RoomV1Pdu(ev) => Box::new(ev.prev_events.iter().map(|(id, _)| id)), Pdu::RoomV3Pdu(ev) => Box::new(ev.prev_events.iter()), #[cfg(not(feature = "unstable-exhaustive-types"))] _ => unreachable!("new PDU version"), } } fn auth_events(&self) -> Box + '_> { match &self.rest { Pdu::RoomV1Pdu(ev) => Box::new(ev.auth_events.iter().map(|(id, _)| id)), Pdu::RoomV3Pdu(ev) => Box::new(ev.auth_events.iter()), #[cfg(not(feature = "unstable-exhaustive-types"))] _ => unreachable!("new PDU version"), } } fn redacts(&self) -> Option<&Self::Id> { match &self.rest { Pdu::RoomV1Pdu(ev) => ev.redacts.as_ref(), Pdu::RoomV3Pdu(ev) => ev.redacts.as_ref(), #[cfg(not(feature = "unstable-exhaustive-types"))] _ => unreachable!("new PDU version"), } } } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct PduEvent { pub event_id: OwnedEventId, #[serde(flatten)] pub rest: Pdu, } } ruma-state-res-0.8.0/src/error.rs000064400000000000000000000016041046102023000147710ustar 00000000000000use serde_json::Error as JsonError; use thiserror::Error; /// Result type for state resolution. pub type Result = std::result::Result; /// Represents the various errors that arise when resolving state. #[derive(Error, Debug)] #[non_exhaustive] 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), } impl Error { pub fn custom(e: E) -> Self { Self::Custom(Box::new(e)) } } ruma-state-res-0.8.0/src/event_auth.rs000064400000000000000000001371441046102023000160130ustar 00000000000000use std::{borrow::Borrow, collections::BTreeSet}; use js_int::{int, Int}; use ruma_common::{ events::{ room::{ create::RoomCreateEventContent, join_rules::{JoinRule, RoomJoinRulesEventContent}, member::{MembershipState, ThirdPartyInvite}, power_levels::RoomPowerLevelsEventContent, third_party_invite::RoomThirdPartyInviteEventContent, }, RoomEventType, StateEventType, }, serde::{Base64, Raw}, OwnedUserId, RoomVersionId, UserId, }; use serde::{de::IgnoredAny, Deserialize}; use serde_json::{from_str as from_json_str, value::RawValue as RawJsonValue}; use tracing::{debug, error, info, warn}; use crate::{ power_levels::{ deserialize_power_levels, deserialize_power_levels_content_fields, deserialize_power_levels_content_invite, deserialize_power_levels_content_redact, }, room_version::RoomVersion, Error, Event, Result, }; // FIXME: field extracting could be bundled for `content` #[derive(Deserialize)] struct GetMembership { membership: MembershipState, } #[derive(Deserialize)] struct RoomMemberContentFields { membership: Option>, join_authorised_via_users_server: Option>, } /// For the given event `kind` what are the relevant auth events that are needed to authenticate /// this `content`. /// /// # Errors /// /// This function will return an error if the supplied `content` is not a JSON object. pub fn auth_types_for_event( kind: &RoomEventType, sender: &UserId, state_key: Option<&str>, content: &RawJsonValue, ) -> serde_json::Result> { if kind == &RoomEventType::RoomCreate { return Ok(vec![]); } let mut auth_types = vec![ (StateEventType::RoomPowerLevels, "".to_owned()), (StateEventType::RoomMember, sender.to_string()), (StateEventType::RoomCreate, "".to_owned()), ]; if kind == &RoomEventType::RoomMember { #[derive(Deserialize)] struct RoomMemberContentFields { membership: Option>, third_party_invite: Option>, join_authorised_via_users_server: Option>, } if let Some(state_key) = state_key { let content: RoomMemberContentFields = from_json_str(content.get())?; if let Some(Ok(membership)) = content.membership.map(|m| m.deserialize()) { if [MembershipState::Join, MembershipState::Invite, MembershipState::Knock] .contains(&membership) { let key = (StateEventType::RoomJoinRules, "".to_owned()); if !auth_types.contains(&key) { auth_types.push(key); } if let Some(Ok(u)) = content.join_authorised_via_users_server.map(|m| m.deserialize()) { let key = (StateEventType::RoomMember, u.to_string()); if !auth_types.contains(&key) { auth_types.push(key); } } } let key = (StateEventType::RoomMember, state_key.to_owned()); if !auth_types.contains(&key) { auth_types.push(key); } if membership == MembershipState::Invite { if let Some(Ok(t_id)) = content.third_party_invite.map(|t| t.deserialize()) { let key = (StateEventType::RoomThirdPartyInvite, t_id.signed.token); if !auth_types.contains(&key) { auth_types.push(key); } } } } } } Ok(auth_types) } /// Authenticate the incoming `event`. /// /// The steps of authentication are: /// /// * check that the event is being authenticated for the correct room /// * then there are checks for specific event types /// /// The `fetch_state` closure should gather state from a state snapshot. We need to know if the /// event passes auth against some state not a recursive collection of auth_events fields. pub fn auth_check( room_version: &RoomVersion, incoming_event: impl Event, current_third_party_invite: Option, fetch_state: impl Fn(&StateEventType, &str) -> Option, ) -> Result { info!( "auth_check beginning for {} ({})", incoming_event.event_id(), incoming_event.event_type() ); // [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) let sender = incoming_event.sender(); // Implementation of https://spec.matrix.org/v1.2/rooms/v1/#authorization-rules // // 1. If type is m.room.create: if *incoming_event.event_type() == RoomEventType::RoomCreate { #[derive(Deserialize)] struct RoomCreateContentFields { room_version: Option>, creator: Option>, } info!("start m.room.create check"); // If it has any previous events, reject if incoming_event.prev_events().next().is_some() { 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() != sender.server_name() { warn!("creation events server does not match sender"); return Ok(false); // creation events room id does not match senders } let content: RoomCreateContentFields = from_json_str(incoming_event.content().get())?; // If content.room_version is present and is not a recognized version, reject if content.room_version.map(|v| v.deserialize().is_err()).unwrap_or(false) { warn!("invalid room version found in m.room.create event"); return Ok(false); } // If content has no creator field, reject if content.creator.is_none() { warn!("no creator field found in m.room.create content"); return Ok(false); } info!("m.room.create event was allowed"); return Ok(true); } /* // TODO: In the past this code caused problems federating with synapse, maybe this has been // resolved already. Needs testing. // // 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, 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) { warn!("auth_events contained invalid auth event"); return Ok(false); } } */ let room_create_event = match fetch_state(&StateEventType::RoomCreate, "") { None => { warn!("no m.room.create event in auth chain"); return Ok(false); } Some(e) => e, }; // 3. If event does not have m.room.create in auth_events reject if !incoming_event.auth_events().any(|id| id.borrow() == room_create_event.event_id().borrow()) { warn!("no m.room.create event in auth events"); return Ok(false); } // If the create event content has the field m.federate set to false and the sender domain of // the event does not match the sender domain of the create event, reject. #[derive(Deserialize)] struct RoomCreateContentFederate { #[serde(rename = "m.federate", default = "ruma_common::serde::default_true")] federate: bool, } let room_create_content: RoomCreateContentFederate = from_json_str(room_create_event.content().get())?; if !room_create_content.federate && room_create_event.sender().server_name() != incoming_event.sender().server_name() { warn!("room is not federated and event's sender domain does not match create event's sender domain"); return Ok(false); } // Only in some room versions 6 and below if room_version.special_case_aliases_auth { // 4. If type is m.room.aliases if *incoming_event.event_type() == RoomEventType::RoomAliases { info!("starting m.room.aliases check"); // If sender's domain doesn't matches state_key, reject if incoming_event.state_key() != Some(sender.server_name().as_str()) { warn!("state_key does not match sender"); return Ok(false); } info!("m.room.aliases event was allowed"); return Ok(true); } } // If type is m.room.member let power_levels_event = fetch_state(&StateEventType::RoomPowerLevels, ""); let sender_member_event = fetch_state(&StateEventType::RoomMember, sender.as_str()); if *incoming_event.event_type() == RoomEventType::RoomMember { info!("starting m.room.member check"); let state_key = match incoming_event.state_key() { None => { warn!("no statekey in member event"); return Ok(false); } Some(s) => s, }; let content: RoomMemberContentFields = from_json_str(incoming_event.content().get())?; if content.membership.as_ref().and_then(|m| m.deserialize().ok()).is_none() { warn!("no valid membership field found for m.room.member event content"); return Ok(false); } let target_user = <&UserId>::try_from(state_key).map_err(|e| Error::InvalidPdu(format!("{e}")))?; let user_for_join_auth = content.join_authorised_via_users_server.as_ref().and_then(|u| u.deserialize().ok()); let user_for_join_auth_membership = user_for_join_auth .as_ref() .and_then(|auth_user| fetch_state(&StateEventType::RoomMember, auth_user.as_str())) .and_then(|mem| from_json_str::(mem.content().get()).ok()) .map(|mem| mem.membership) .unwrap_or(MembershipState::Leave); if !valid_membership_change( room_version, target_user, fetch_state(&StateEventType::RoomMember, target_user.as_str()).as_ref(), sender, sender_member_event.as_ref(), &incoming_event, current_third_party_invite, power_levels_event.as_ref(), fetch_state(&StateEventType::RoomJoinRules, "").as_ref(), user_for_join_auth.as_deref(), &user_for_join_auth_membership, room_create_event, )? { return Ok(false); } info!("m.room.member event was allowed"); return Ok(true); } // If the sender's current membership state is not join, reject let sender_member_event = match sender_member_event { Some(mem) => mem, None => { warn!("sender not found in room"); return Ok(false); } }; let sender_membership_event_content: RoomMemberContentFields = from_json_str(sender_member_event.content().get())?; let membership_state = sender_membership_event_content .membership .expect("we should test before that this field exists") .deserialize()?; if !matches!(membership_state, MembershipState::Join) { warn!("sender's membership is not join"); return Ok(false); } // If type is m.room.third_party_invite let sender_power_level = if let Some(pl) = &power_levels_event { let content = deserialize_power_levels_content_fields(pl.content().get(), room_version)?; if let Some(level) = content.users.get(sender) { *level } else { content.users_default } } else { // If no power level event found the creator gets 100 everyone else gets 0 from_json_str::(room_create_event.content().get()) .ok() .and_then(|create| (create.creator == *sender).then(|| int!(100))) .unwrap_or_default() }; // Allow if and only if sender's current power level is greater than // or equal to the invite level if *incoming_event.event_type() == RoomEventType::RoomThirdPartyInvite { let invite_level = match &power_levels_event { Some(power_levels) => { deserialize_power_levels_content_invite(power_levels.content().get(), room_version)? .invite } None => int!(0), }; if sender_power_level < invite_level { 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, power_levels_event.as_ref(), sender_power_level) { warn!("user cannot send event"); return Ok(false); } // If type is m.room.power_levels if *incoming_event.event_type() == RoomEventType::RoomPowerLevels { info!("starting m.room.power_levels check"); if let Some(required_pwr_lvl) = check_power_levels( room_version, &incoming_event, power_levels_event.as_ref(), sender_power_level, ) { if !required_pwr_lvl { warn!("power level was not allowed"); return Ok(false); } } else { warn!("power level was not allowed"); return Ok(false); } 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.event_type() == RoomEventType::RoomRedaction { let redact_level = match power_levels_event { Some(pl) => { deserialize_power_levels_content_redact(pl.content().get(), room_version)?.redact } None => int!(50), }; if !check_redaction(room_version, incoming_event, sender_power_level, redact_level)? { return Ok(false); } } 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. /// * `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. #[allow(clippy::too_many_arguments)] fn valid_membership_change( room_version: &RoomVersion, target_user: &UserId, target_user_membership_event: Option, sender: &UserId, sender_membership_event: Option, current_event: impl Event, current_third_party_invite: Option, power_levels_event: Option, join_rules_event: Option, user_for_join_auth: Option<&UserId>, user_for_join_auth_membership: &MembershipState, create_room: impl Event, ) -> Result { #[derive(Deserialize)] struct GetThirdPartyInvite { third_party_invite: Option>, } let content = current_event.content(); let target_membership = from_json_str::(content.get())?.membership; let third_party_invite = from_json_str::(content.get())?.third_party_invite; let sender_membership = match &sender_membership_event { Some(pdu) => from_json_str::(pdu.content().get())?.membership, None => MembershipState::Leave, }; let sender_is_joined = sender_membership == MembershipState::Join; let target_user_current_membership = match &target_user_membership_event { Some(pdu) => from_json_str::(pdu.content().get())?.membership, None => MembershipState::Leave, }; let power_levels: RoomPowerLevelsEventContent = match &power_levels_event { Some(ev) => from_json_str(ev.content().get())?, None => RoomPowerLevelsEventContent::default(), }; let sender_power = power_levels .users .get(sender) .or_else(|| sender_is_joined.then(|| &power_levels.users_default)); let target_power = power_levels.users.get(target_user).or_else(|| { (target_membership == MembershipState::Join).then(|| &power_levels.users_default) }); let mut join_rules = JoinRule::Invite; if let Some(jr) = &join_rules_event { join_rules = from_json_str::(jr.content().get())?.join_rule; } let power_levels_event_id = power_levels_event.as_ref().map(|e| e.event_id()); let sender_membership_event_id = sender_membership_event.as_ref().map(|e| e.event_id()); let target_user_membership_event_id = target_user_membership_event.as_ref().map(|e| e.event_id()); let user_for_join_auth_is_valid = if let Some(user_for_join_auth) = user_for_join_auth { // Is the authorised user allowed to invite users into this room let (auth_user_pl, invite_level) = if let Some(pl) = &power_levels_event { // TODO Refactor all powerlevel parsing let invite = deserialize_power_levels_content_invite(pl.content().get(), room_version)?.invite; let content = deserialize_power_levels_content_fields(pl.content().get(), room_version)?; let user_pl = if let Some(level) = content.users.get(user_for_join_auth) { *level } else { content.users_default }; (user_pl, invite) } else { (int!(0), int!(0)) }; (user_for_join_auth_membership == &MembershipState::Join) && (auth_user_pl >= invite_level) } else { // No auth user was given false }; Ok(match target_membership { MembershipState::Join => { // 1. If the only previous event is an m.room.create and the state_key is the creator, // allow let mut prev_events = current_event.prev_events(); let prev_event_is_create_event = prev_events .next() .map(|event_id| event_id.borrow() == create_room.event_id().borrow()) .unwrap_or(false); let no_more_prev_events = prev_events.next().is_none(); if prev_event_is_create_event && no_more_prev_events { let create_content = from_json_str::(create_room.content().get())?; if create_content.creator == sender && create_content.creator == target_user { return Ok(true); } } if sender != target_user { // If the sender does not match state_key, reject. warn!("Can't make other user join"); false } else if let MembershipState::Ban = target_user_current_membership { // If the sender is banned, reject. warn!(?target_user_membership_event_id, "Banned user can't join"); false } else if (join_rules == JoinRule::Invite || room_version.allow_knocking && join_rules == JoinRule::Knock) // If the join_rule is invite then allow if membership state is invite or join && (target_user_current_membership == MembershipState::Join || target_user_current_membership == MembershipState::Invite) { true } else if room_version.restricted_join_rules && matches!(join_rules, JoinRule::Restricted(_)) || room_version.knock_restricted_join_rule && matches!(join_rules, JoinRule::KnockRestricted(_)) { // If the join_rule is restricted or knock_restricted if matches!( target_user_current_membership, MembershipState::Invite | MembershipState::Join ) { // If membership state is join or invite, allow. true } else { // If the join_authorised_via_users_server key in content is not a user with // sufficient permission to invite other users, reject. // Otherwise, allow. user_for_join_auth_is_valid } } else { // If the join_rule is public, allow. // Otherwise, reject. join_rules == JoinRule::Public } } MembershipState::Invite => { // If content has third_party_invite key if let Some(tp_id) = third_party_invite.and_then(|i| i.deserialize().ok()) { if target_user_current_membership == MembershipState::Ban { warn!(?target_user_membership_event_id, "Can't invite banned user"); false } else { let allow = verify_third_party_invite( Some(target_user), sender, &tp_id, current_third_party_invite, ); if !allow { warn!("Third party invite invalid"); } allow } } else if !sender_is_joined || target_user_current_membership == MembershipState::Join || target_user_current_membership == MembershipState::Ban { warn!( ?target_user_membership_event_id, ?sender_membership_event_id, "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!( ?target_user_membership_event_id, ?power_levels_event_id, "User does not have enough power to invite", ); } allow } } MembershipState::Leave => { if sender == target_user { let allow = target_user_current_membership == MembershipState::Join || target_user_current_membership == MembershipState::Invite; if !allow { warn!(?target_user_membership_event_id, "Can't leave if not invited or joined"); } allow } else if !sender_is_joined || target_user_current_membership == MembershipState::Ban && sender_power.filter(|&p| p < &power_levels.ban).is_some() { warn!( ?target_user_membership_event_id, ?sender_membership_event_id, "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!( ?target_user_membership_event_id, ?power_levels_event_id, "User does not have enough power to kick", ); } allow } } MembershipState::Ban => { if !sender_is_joined { warn!(?sender_membership_event_id, "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!( ?target_user_membership_event_id, ?power_levels_event_id, "User does not have enough power to ban", ); } allow } } MembershipState::Knock if room_version.allow_knocking => { // 1. If the `join_rule` is anything other than `knock` or `knock_restricted`, reject. if join_rules != JoinRule::Knock || room_version.knock_restricted_join_rule && matches!(join_rules, JoinRule::KnockRestricted(_)) { warn!("Join rule is not set to knock or knock_restricted, knocking is not allowed"); false } else { // 2. If `sender` does not match `state_key`, reject. // 3. If the `sender`'s current membership is not `ban`, `invite`, or `join`, allow. // 4. Otherwise, reject. if sender != target_user { warn!( ?sender, ?target_user, "Can't make another user join, sender did not match target" ); false } else if matches!( sender_membership, MembershipState::Ban | MembershipState::Invite | MembershipState::Join ) { warn!( ?target_user_membership_event_id, "Membership state of ban, invite, or join are invalid", ); false } else { true } } } _ => { warn!("Unknown membership transition"); false } }) } /// Is the user allowed to send a specific event based on the rooms power levels. /// /// Does the event have the correct userId as its state_key if it's not the "" state_key. fn can_send_event(event: impl Event, ple: Option, user_level: Int) -> bool { let event_type_power_level = get_send_level(event.event_type(), event.state_key(), ple); debug!("{} ev_type {event_type_power_level} usr {user_level}", event.event_id()); if user_level < event_type_power_level { return false; } if event.state_key().map_or(false, |k| k.starts_with('@')) && event.state_key() != 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. fn check_power_levels( room_version: &RoomVersion, power_event: impl Event, previous_power_event: Option, user_level: Int, ) -> Option { match power_event.state_key() { Some("") => {} Some(key) => { error!("m.room.power_levels event has non-empty state key: {key}"); return None; } None => { error!("check_power_levels requires an m.room.power_levels *state* event argument"); return None; } } // - If any of the keys users_default, events_default, state_default, ban, redact, kick, or // invite in content are present and not an integer, reject. // - If either of the keys events or notifications in content are present and not a dictionary // with values that are integers, reject. // - If users key in content is not a dictionary with keys that are valid user IDs with values // that are integers, reject. let user_content: RoomPowerLevelsEventContent = deserialize_power_levels(power_event.content().get(), room_version)?; // Validation of users is done in Ruma, synapse for loops validating user_ids and integers here info!("validation of power event finished"); let current_state = match previous_power_event { Some(current_state) => current_state, // If there is no previous m.room.power_levels event in the room, allow None => return Some(true), }; let current_content: RoomPowerLevelsEventContent = deserialize_power_levels(current_state.content().get(), room_version)?; let mut user_levels_to_check = BTreeSet::new(); 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); } debug!("users to check {user_levels_to_check:?}"); let mut event_levels_to_check = BTreeSet::new(); let old_list = ¤t_content.events; let new_list = &user_content.events; for ev_id in old_list.keys().chain(new_list.keys()) { event_levels_to_check.insert(ev_id); } 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 == Some(&user_level) { 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 > Some(&user_level); let new_level_too_big = new_level > Some(&user_level); if old_level_too_big || new_level_too_big { 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 > Some(&user_level); let new_level_too_big = new_level > Some(&user_level); if old_level_too_big || new_level_too_big { 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 = old_level > user_level; let new_level_too_big = new_level > user_level; if old_level_too_big || new_level_too_big { 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 { 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<(Int, Int)> { 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. fn check_redaction( _room_version: &RoomVersion, redaction_event: impl Event, user_level: Int, redact_level: Int, ) -> Result { if user_level >= redact_level { 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().borrow().server_name() == redaction_event.redacts().as_ref().and_then(|&id| id.borrow().server_name()) { info!("redaction event allowed via room version 1 rules"); return Ok(true); } Ok(false) } /// 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. fn get_send_level( e_type: &RoomEventType, state_key: Option<&str>, power_lvl: Option, ) -> Int { power_lvl .and_then(|ple| { from_json_str::(ple.content().get()) .map(|content| { content.events.get(e_type).copied().unwrap_or_else(|| { if state_key.is_some() { content.state_default } else { content.events_default } }) }) .ok() }) .unwrap_or_else(|| if state_key.is_some() { int!(50) } else { int!(0) }) } fn verify_third_party_invite( target_user: Option<&UserId>, sender: &UserId, tp_id: &ThirdPartyInvite, current_third_party_invite: Option, ) -> bool { // 1. Check for user being banned happens before this is called // checking for mxid and token keys is done by ruma when deserializing // The state key must match the invitee if target_user != Some(&tp_id.signed.mxid) { return false; } // If there is no m.room.third_party_invite event in the current room state with state_key // matching token, reject let current_tpid = match current_third_party_invite { Some(id) => id, None => return false, }; if current_tpid.state_key() != 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 let tpid_ev = match from_json_str::(current_tpid.content().get()) { Ok(ev) => ev, Err(_) => return false, }; let decoded_invite_token = match Base64::parse(&tp_id.signed.token) { Ok(tok) => tok, // FIXME: Log a warning? Err(_) => return false, }; // A list of public keys in the public_keys field for key in tpid_ev.public_keys.unwrap_or_default() { if key.public_key == decoded_invite_token { return true; } } // A single public key in the public_key field tpid_ev.public_key == decoded_invite_token } #[cfg(test)] mod tests { use std::sync::Arc; use ruma_common::events::{ room::{ join_rules::{ AllowRule, JoinRule, Restricted, RoomJoinRulesEventContent, RoomMembership, }, member::{MembershipState, RoomMemberEventContent}, }, RoomEventType, StateEventType, }; use serde_json::value::to_raw_value as to_raw_json_value; use crate::{ event_auth::valid_membership_change, test_utils::{ alice, charlie, ella, event_id, member_content_ban, member_content_join, room_id, to_pdu_event, PduEvent, INITIAL_EVENTS, INITIAL_EVENTS_CREATE_ROOM, }, Event, EventTypeExt, RoomVersion, StateMap, }; #[test] fn test_ban_pass() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); let events = INITIAL_EVENTS(); let auth_events = events .values() .map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), Arc::clone(ev))) .collect::>(); let requester = to_pdu_event( "HELLO", alice(), RoomEventType::RoomMember, Some(charlie().as_str()), member_content_ban(), &[], &["IMC"], ); let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned(); let target_user = charlie(); let sender = alice(); assert!(valid_membership_change( &RoomVersion::V6, target_user, fetch_state(StateEventType::RoomMember, target_user.to_string()), sender, fetch_state(StateEventType::RoomMember, sender.to_string()), &requester, None::, fetch_state(StateEventType::RoomPowerLevels, "".to_owned()), fetch_state(StateEventType::RoomJoinRules, "".to_owned()), None, &MembershipState::Leave, fetch_state(StateEventType::RoomCreate, "".to_owned()).unwrap(), ) .unwrap()); } #[test] fn test_join_non_creator() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); let events = INITIAL_EVENTS_CREATE_ROOM(); let auth_events = events .values() .map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), Arc::clone(ev))) .collect::>(); let requester = to_pdu_event( "HELLO", charlie(), RoomEventType::RoomMember, Some(charlie().as_str()), member_content_join(), &["CREATE"], &["CREATE"], ); let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned(); let target_user = charlie(); let sender = charlie(); assert!(!valid_membership_change( &RoomVersion::V6, target_user, fetch_state(StateEventType::RoomMember, target_user.to_string()), sender, fetch_state(StateEventType::RoomMember, sender.to_string()), &requester, None::, fetch_state(StateEventType::RoomPowerLevels, "".to_owned()), fetch_state(StateEventType::RoomJoinRules, "".to_owned()), None, &MembershipState::Leave, fetch_state(StateEventType::RoomCreate, "".to_owned()).unwrap(), ) .unwrap()); } #[test] fn test_join_creator() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); let events = INITIAL_EVENTS_CREATE_ROOM(); let auth_events = events .values() .map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), Arc::clone(ev))) .collect::>(); let requester = to_pdu_event( "HELLO", alice(), RoomEventType::RoomMember, Some(alice().as_str()), member_content_join(), &["CREATE"], &["CREATE"], ); let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned(); let target_user = alice(); let sender = alice(); assert!(valid_membership_change( &RoomVersion::V6, target_user, fetch_state(StateEventType::RoomMember, target_user.to_string()), sender, fetch_state(StateEventType::RoomMember, sender.to_string()), &requester, None::, fetch_state(StateEventType::RoomPowerLevels, "".to_owned()), fetch_state(StateEventType::RoomJoinRules, "".to_owned()), None, &MembershipState::Leave, fetch_state(StateEventType::RoomCreate, "".to_owned()).unwrap(), ) .unwrap()); } #[test] fn test_ban_fail() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); let events = INITIAL_EVENTS(); let auth_events = events .values() .map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), Arc::clone(ev))) .collect::>(); let requester = to_pdu_event( "HELLO", charlie(), RoomEventType::RoomMember, Some(alice().as_str()), member_content_ban(), &[], &["IMC"], ); let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned(); let target_user = alice(); let sender = charlie(); assert!(!valid_membership_change( &RoomVersion::V6, target_user, fetch_state(StateEventType::RoomMember, target_user.to_string()), sender, fetch_state(StateEventType::RoomMember, sender.to_string()), &requester, None::, fetch_state(StateEventType::RoomPowerLevels, "".to_owned()), fetch_state(StateEventType::RoomJoinRules, "".to_owned()), None, &MembershipState::Leave, fetch_state(StateEventType::RoomCreate, "".to_owned()).unwrap(), ) .unwrap()); } #[test] fn test_restricted_join_rule() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); let mut events = INITIAL_EVENTS(); *events.get_mut(&event_id("IJR")).unwrap() = to_pdu_event( "IJR", alice(), RoomEventType::RoomJoinRules, Some(""), to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Restricted( Restricted::new(vec![AllowRule::RoomMembership(RoomMembership::new( room_id().to_owned(), ))]), ))) .unwrap(), &["CREATE", "IMA", "IPOWER"], &["IPOWER"], ); let mut member = RoomMemberEventContent::new(MembershipState::Join); member.join_authorized_via_users_server = Some(alice().to_owned()); let auth_events = events .values() .map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), Arc::clone(ev))) .collect::>(); let requester = to_pdu_event( "HELLO", ella(), RoomEventType::RoomMember, Some(ella().as_str()), to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap(), &["CREATE", "IJR", "IPOWER", "new"], &["new"], ); let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned(); let target_user = ella(); let sender = ella(); assert!(valid_membership_change( &RoomVersion::V9, target_user, fetch_state(StateEventType::RoomMember, target_user.to_string()), sender, fetch_state(StateEventType::RoomMember, sender.to_string()), &requester, None::, fetch_state(StateEventType::RoomPowerLevels, "".to_owned()), fetch_state(StateEventType::RoomJoinRules, "".to_owned()), Some(alice()), &MembershipState::Join, fetch_state(StateEventType::RoomCreate, "".to_owned()).unwrap(), ) .unwrap()); assert!(!valid_membership_change( &RoomVersion::V9, target_user, fetch_state(StateEventType::RoomMember, target_user.to_string()), sender, fetch_state(StateEventType::RoomMember, sender.to_string()), &requester, None::, fetch_state(StateEventType::RoomPowerLevels, "".to_owned()), fetch_state(StateEventType::RoomJoinRules, "".to_owned()), Some(ella()), &MembershipState::Leave, fetch_state(StateEventType::RoomCreate, "".to_owned()).unwrap(), ) .unwrap()); } #[test] fn test_knock() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); let mut events = INITIAL_EVENTS(); *events.get_mut(&event_id("IJR")).unwrap() = to_pdu_event( "IJR", alice(), RoomEventType::RoomJoinRules, Some(""), to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Knock)).unwrap(), &["CREATE", "IMA", "IPOWER"], &["IPOWER"], ); let auth_events = events .values() .map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), Arc::clone(ev))) .collect::>(); let requester = to_pdu_event( "HELLO", ella(), RoomEventType::RoomMember, Some(ella().as_str()), to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Knock)).unwrap(), &[], &["IMC"], ); let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned(); let target_user = ella(); let sender = ella(); assert!(valid_membership_change( &RoomVersion::V7, target_user, fetch_state(StateEventType::RoomMember, target_user.to_string()), sender, fetch_state(StateEventType::RoomMember, sender.to_string()), &requester, None::, fetch_state(StateEventType::RoomPowerLevels, "".to_owned()), fetch_state(StateEventType::RoomJoinRules, "".to_owned()), None, &MembershipState::Leave, fetch_state(StateEventType::RoomCreate, "".to_owned()).unwrap(), ) .unwrap()); } } ruma-state-res-0.8.0/src/lib.rs000064400000000000000000001264371046102023000144220ustar 00000000000000use std::{ borrow::Borrow, cmp::Reverse, collections::{BinaryHeap, HashMap, HashSet}, hash::Hash, }; use itertools::Itertools; use js_int::{int, Int}; use ruma_common::{ events::{ room::member::{MembershipState, RoomMemberEventContent}, RoomEventType, StateEventType, }, EventId, MilliSecondsSinceUnixEpoch, RoomVersionId, }; use serde_json::from_str as from_json_str; use tracing::{debug, info, trace, warn}; mod error; pub mod event_auth; mod power_levels; pub mod room_version; mod state_event; #[cfg(test)] mod test_utils; pub use error::{Error, Result}; pub use event_auth::{auth_check, auth_types_for_event}; use power_levels::PowerLevelsContentFields; pub use room_version::RoomVersion; pub use state_event::Event; /// A mapping of event type and state_key to some value `T`, usually an `EventId`. pub type StateMap = HashMap<(StateEventType, String), T>; /// 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_chain_sets` - The full recursive set of `auth_events` for each event in the /// `state_sets`. /// /// * `fetch_event` - Any event not found in the `event_map` will defer to this closure to find the /// event. /// /// ## Invariants /// /// The caller of `resolve` must ensure that all the events are from the same room. Although this /// function takes a `RoomId` it does not check that each event is part of the same room. pub fn resolve<'a, E, SetIter>( room_version: &RoomVersionId, state_sets: impl IntoIterator, auth_chain_sets: Vec>, fetch_event: impl Fn(&EventId) -> Option, ) -> Result> where E: Event + Clone, E::Id: 'a, SetIter: Iterator> + Clone, { info!("State resolution starting"); // Split non-conflicting and conflicting state let (clean, conflicting) = separate(state_sets.into_iter()); info!("non conflicting events: {}", clean.len()); trace!("{clean:?}"); if conflicting.is_empty() { info!("no conflicting state found"); return Ok(clean); } info!("conflicting events: {}", conflicting.len()); debug!("{conflicting:?}"); // `all_conflicted` contains unique items // synapse says `full_set = {eid for eid in full_conflicted_set if eid in event_map}` let all_conflicted: HashSet<_> = get_auth_chain_diff(auth_chain_sets) .chain(conflicting.into_values().flatten()) // Don't honor events we cannot "verify" .filter(|id| fetch_event(id.borrow()).is_some()) .collect(); info!("full conflicted set: {}", all_conflicted.len()); debug!("{all_conflicted:?}"); // We used to check that all events are events from the correct room // this is now a check the caller of `resolve` must make. // 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.borrow(), &fetch_event)) .cloned() .collect::>(); // Sort the control events based on power_level/clock/event_id and outgoing/incoming edges let sorted_control_levels = reverse_topological_power_sort(control_events, &all_conflicted, &fetch_event)?; debug!("sorted control events: {}", sorted_control_levels.len()); trace!("{sorted_control_levels:?}"); let room_version = RoomVersion::new(room_version)?; // Sequentially auth check each control event. let resolved_control = iterative_auth_check(&room_version, &sorted_control_levels, clean.clone(), &fetch_event)?; debug!("resolved control events: {}", resolved_control.len()); trace!("{resolved_control:?}"); // 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. let deduped_power_ev = sorted_control_levels.into_iter().collect::>(); // 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.borrow())) .cloned() .collect::>(); debug!("events left to resolve: {}", events_to_resolve.len()); trace!("{events_to_resolve:?}"); // This "epochs" power level event let power_event = resolved_control.get(&(StateEventType::RoomPowerLevels, "".into())); debug!("power event: {power_event:?}"); let sorted_left_events = mainline_sort(&events_to_resolve, power_event.cloned(), &fetch_event)?; trace!("events left, sorted: {sorted_left_events:?}"); let mut resolved_state = iterative_auth_check( &room_version, &sorted_left_events, resolved_control, // The control events are added to the final resolved state &fetch_event, )?; // 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. fn separate<'a, Id>( state_sets_iter: impl Iterator> + Clone, ) -> (StateMap, StateMap>) where Id: Clone + Eq + 'a, { let mut unconflicted_state = StateMap::new(); let mut conflicted_state = StateMap::new(); for key in state_sets_iter.clone().flat_map(|map| map.keys()).unique() { let mut event_ids = state_sets_iter.clone().map(|state_set| state_set.get(key)).collect::>(); if event_ids.iter().all_equal() { // First .unwrap() is okay because // * event_ids has the same length as state_sets // * we never enter the loop this code is in if state_sets is empty let id = event_ids.pop().unwrap().expect("unconflicting `EventId` is not None"); unconflicted_state.insert(key.clone(), id.clone()); } else { conflicted_state .insert(key.clone(), event_ids.into_iter().filter_map(|o| o.cloned()).collect()); } } (unconflicted_state, conflicted_state) } /// Returns a Vec of deduped EventIds that appear in some chains but not others. fn get_auth_chain_diff(auth_chain_sets: Vec>) -> impl Iterator where Id: Eq + Hash, { let num_sets = auth_chain_sets.len(); let mut id_counts: HashMap = HashMap::new(); for id in auth_chain_sets.into_iter().flatten() { *id_counts.entry(id).or_default() += 1; } id_counts.into_iter().filter_map(move |(id, count)| (count < num_sets).then(move || id)) } /// Events are sorted from "earliest" to "latest". /// /// They are compared using the negative power level (reverse topological ordering), the origin /// server timestamp and in case 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. fn reverse_topological_power_sort( events_to_sort: Vec, auth_diff: &HashSet, fetch_event: impl Fn(&EventId) -> Option, ) -> Result> { debug!("reverse topological sort of power events"); let mut graph = HashMap::new(); for event_id in events_to_sort { add_event_and_auth_chain_to_graph(&mut graph, event_id, auth_diff, &fetch_event); // 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 = HashMap::new(); for event_id in graph.keys() { let pl = get_power_level_for_sender(event_id.borrow(), &fetch_event)?; info!("{event_id} power level {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 } lexicographical_topological_sort(&graph, |event_id| { let ev = fetch_event(event_id).ok_or_else(|| Error::NotFound("".into()))?; let pl = *event_to_pl.get(event_id).ok_or_else(|| Error::NotFound("".into()))?; Ok((pl, ev.origin_server_ts())) }) } /// Sorts the event graph based on number of outgoing/incoming edges. /// /// `key_fn` is used as to obtain the power level and age of an event for breaking ties (together /// with the event ID). pub fn lexicographical_topological_sort( graph: &HashMap>, key_fn: F, ) -> Result> where F: Fn(&EventId) -> Result<(Int, MilliSecondsSinceUnixEpoch)>, Id: Clone + Eq + Ord + Hash + Borrow, { #[derive(PartialEq, Eq, PartialOrd, Ord)] struct TieBreaker<'a, Id> { inv_power_level: Int, age: MilliSecondsSinceUnixEpoch, event_id: &'a Id, } 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 // outdegree_map is an event referring to the events before it, the // more outdegree's the more recent the event. let mut outdegree_map = graph.clone(); // The number of events that depend on the given event (the EventId key) // How many events reference this event in the DAG as a parent let mut reverse_graph: HashMap<_, HashSet<_>> = HashMap::new(); // Vec of nodes that have zero out degree, least recent events. let mut zero_outdegree = Vec::new(); for (node, edges) in graph { if edges.is_empty() { let (power_level, age) = key_fn(node.borrow())?; // The `Reverse` is because rusts `BinaryHeap` sorts largest -> smallest we need // smallest -> largest zero_outdegree.push(Reverse(TieBreaker { inv_power_level: -power_level, age, event_id: node, })); } reverse_graph.entry(node).or_default(); for edge in edges { reverse_graph.entry(edge).or_default().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(item)) = heap.pop() { let node = item.event_id; for &parent in reverse_graph.get(node).expect("EventId in heap is also in reverse_graph") { // The number of outgoing edges this node has let out = outdegree_map .get_mut(parent.borrow()) .expect("outdegree_map knows of all referenced EventIds"); // Only push on the heap once older events have been cleared out.remove(node.borrow()); if out.is_empty() { let (power_level, age) = key_fn(node.borrow())?; heap.push(Reverse(TieBreaker { inv_power_level: -power_level, age, event_id: parent, })); } } // synapse yields we push then return the vec sorted.push(node.clone()); } Ok(sorted) } /// Find the power level for the sender of `event_id` or return a default value of zero. /// /// Do NOT use this any where but topological sort, we find the power level for the eventId /// at the eventId's generation (we walk backwards to `EventId`s most recent previous power level /// event). fn get_power_level_for_sender( event_id: &EventId, fetch_event: impl Fn(&EventId) -> Option, ) -> serde_json::Result { info!("fetch event ({event_id}) senders power level"); let event = fetch_event(event_id); let mut pl = None; for aid in event.as_ref().map(|pdu| pdu.auth_events()).into_iter().flatten() { if let Some(aev) = fetch_event(aid.borrow()) { if is_type_and_key(&aev, &RoomEventType::RoomPowerLevels, "") { pl = Some(aev); break; } } } let content: PowerLevelsContentFields = match pl { None => return Ok(int!(0)), Some(ev) => from_json_str(ev.content().get())?, }; if let Some(ev) = event { if let Some(&user_level) = content.users.get(ev.sender()) { debug!("found {} at power_level {user_level}", ev.sender()); return Ok(user_level); } } Ok(content.users_default) } /// 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 state map. /// /// For each `events_to_check` event we gather the events needed to auth it from the the /// `fetch_event` closure and verify each event using the `event_auth::auth_check` function. fn iterative_auth_check( room_version: &RoomVersion, events_to_check: &[E::Id], unconflicted_state: StateMap, fetch_event: impl Fn(&EventId) -> Option, ) -> Result> { info!("starting iterative auth check"); debug!("performing auth checks on {events_to_check:?}"); let mut resolved_state = unconflicted_state; for event_id in events_to_check { let event = fetch_event(event_id.borrow()) .ok_or_else(|| Error::NotFound(format!("Failed to find {event_id}")))?; let state_key = event .state_key() .ok_or_else(|| Error::InvalidPdu("State event had no state key".to_owned()))?; let mut auth_events = StateMap::new(); for aid in event.auth_events() { if let Some(ev) = fetch_event(aid.borrow()) { // TODO synapse check "rejected_reason" which is most likely // related to soft-failing auth_events.insert( ev.event_type().with_state_key(ev.state_key().ok_or_else(|| { Error::InvalidPdu("State event had no state key".to_owned()) })?), ev, ); } else { warn!("auth event id for {aid} is missing {event_id}"); } } for key in auth_types_for_event( event.event_type(), event.sender(), Some(state_key), event.content(), )? { if let Some(ev_id) = resolved_state.get(&key) { if let Some(event) = fetch_event(ev_id.borrow()) { // TODO synapse checks `rejected_reason` is None here auth_events.insert(key.to_owned(), event); } } } debug!("event to check {:?}", event.event_id()); // 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)| { (*pdu.event_type() == RoomEventType::RoomThirdPartyInvite).then(|| pdu) }); if auth_check(room_version, &event, current_third_party, |ty, key| { auth_events.get(&ty.with_state_key(key)) })? { // add event to resolved state map resolved_state.insert(event.event_type().with_state_key(state_key), event_id.clone()); } else { // synapse passes here on AuthError. We do not add this event to resolved_state. warn!("event {event_id} failed the authentication check"); } // 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. fn mainline_sort( to_sort: &[E::Id], resolved_power_level: Option, fetch_event: impl Fn(&EventId) -> Option, ) -> Result> { debug!("mainline sort of events"); // There are no EventId's to sort, bail. if to_sort.is_empty() { return Ok(vec![]); } let mut mainline = vec![]; let mut pl = resolved_power_level; while let Some(p) = pl { mainline.push(p.clone()); let event = fetch_event(p.borrow()) .ok_or_else(|| Error::NotFound(format!("Failed to find {p}")))?; pl = None; for aid in event.auth_events() { let ev = fetch_event(aid.borrow()) .ok_or_else(|| Error::NotFound(format!("Failed to find {aid}")))?; if is_type_and_key(&ev, &RoomEventType::RoomPowerLevels, "") { pl = Some(aid.to_owned()); 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::>(); let mut order_map = HashMap::new(); for ev_id in to_sort.iter() { if let Some(event) = fetch_event(ev_id.borrow()) { if let Ok(depth) = get_mainline_depth(Some(event), &mainline_map, &fetch_event) { order_map.insert( ev_id, (depth, fetch_event(ev_id.borrow()).map(|ev| ev.origin_server_ts()), ev_id), ); } } // 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::>(); sort_event_ids.sort_by_key(|sort_id| order_map.get(sort_id).unwrap()); Ok(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( mut event: Option, mainline_map: &HashMap, fetch_event: impl Fn(&EventId) -> Option, ) -> Result { while let Some(sort_ev) = event { debug!("mainline event_id {}", sort_ev.event_id()); let id = sort_ev.event_id(); if let Some(depth) = mainline_map.get(id.borrow()) { return Ok(*depth); } event = None; for aid in sort_ev.auth_events() { let aev = fetch_event(aid.borrow()) .ok_or_else(|| Error::NotFound(format!("Failed to find {aid}")))?; if is_type_and_key(&aev, &RoomEventType::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( graph: &mut HashMap>, event_id: E::Id, auth_diff: &HashSet, fetch_event: impl Fn(&EventId) -> Option, ) { let mut state = vec![event_id]; while let Some(eid) = state.pop() { graph.entry(eid.clone()).or_default(); // Prefer the store to event as the store filters dedups the events for aid in fetch_event(eid.borrow()).as_ref().map(|ev| ev.auth_events()).into_iter().flatten() { if auth_diff.contains(aid.borrow()) { if !graph.contains_key(aid.borrow()) { state.push(aid.to_owned()); } // We just inserted this at the start of the while loop graph.get_mut(eid.borrow()).unwrap().insert(aid.to_owned()); } } } } fn is_power_event_id(event_id: &EventId, fetch: impl Fn(&EventId) -> Option) -> bool { match fetch(event_id).as_ref() { Some(state) => is_power_event(state), _ => false, } } fn is_type_and_key(ev: impl Event, ev_type: &RoomEventType, state_key: &str) -> bool { ev.event_type() == ev_type && ev.state_key() == Some(state_key) } fn is_power_event(event: impl Event) -> bool { match event.event_type() { RoomEventType::RoomPowerLevels | RoomEventType::RoomJoinRules | RoomEventType::RoomCreate => event.state_key() == Some(""), RoomEventType::RoomMember => { if let Ok(content) = from_json_str::(event.content().get()) { if [MembershipState::Leave, MembershipState::Ban].contains(&content.membership) { return Some(event.sender().as_str()) != event.state_key(); } } false } _ => false, } } /// Convenience trait for adding event type plus state key to state maps. trait EventTypeExt { fn with_state_key(self, state_key: impl Into) -> (StateEventType, String); } impl EventTypeExt for StateEventType { fn with_state_key(self, state_key: impl Into) -> (StateEventType, String) { (self, state_key.into()) } } impl EventTypeExt for RoomEventType { fn with_state_key(self, state_key: impl Into) -> (StateEventType, String) { (self.to_string().into(), state_key.into()) } } impl EventTypeExt for &T where T: EventTypeExt + Clone, { fn with_state_key(self, state_key: impl Into) -> (StateEventType, String) { self.to_owned().with_state_key(state_key) } } #[cfg(test)] mod tests { use std::{ collections::{HashMap, HashSet}, sync::Arc, }; use js_int::{int, uint}; use maplit::{hashmap, hashset}; use rand::seq::SliceRandom; use ruma_common::{ events::{ room::join_rules::{JoinRule, RoomJoinRulesEventContent}, RoomEventType, StateEventType, }, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomVersionId, }; use serde_json::{json, value::to_raw_value as to_raw_json_value}; use tracing::debug; use crate::{ is_power_event, room_version::RoomVersion, test_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, PduEvent, TestStore, INITIAL_EVENTS, }, Event, EventTypeExt, StateMap, }; fn test_event_sort() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); let events = INITIAL_EVENTS(); let event_map = events .values() .map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone())) .collect::>(); let auth_chain: HashSet = HashSet::new(); let power_events = event_map .values() .filter(|&pdu| is_power_event(&**pdu)) .map(|pdu| pdu.event_id.clone()) .collect::>(); let sorted_power_events = crate::reverse_topological_power_sort(power_events, &auth_chain, |id| { events.get(id).map(Arc::clone) }) .unwrap(); let resolved_power = crate::iterative_auth_check( &RoomVersion::V6, &sorted_power_events, HashMap::new(), // unconflicted events |id| events.get(id).map(Arc::clone), ) .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::>(); events_to_sort.shuffle(&mut rand::thread_rng()); let power_level = resolved_power.get(&(StateEventType::RoomPowerLevels, "".to_owned())).cloned(); let sorted_event_ids = crate::mainline_sort(&events_to_sort, power_level, |id| events.get(id).map(Arc::clone)) .unwrap(); 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::>() ); } #[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(); } } #[test] fn ban_vs_power_level() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); let events = &[ to_init_pdu_event( "PA", alice(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), ), to_init_pdu_event( "MA", alice(), RoomEventType::RoomMember, Some(alice().to_string().as_str()), member_content_join(), ), to_init_pdu_event( "MB", alice(), RoomEventType::RoomMember, Some(bob().to_string().as_str()), member_content_ban(), ), to_init_pdu_event( "PB", bob(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), ), ]; let edges = vec![vec!["END", "MB", "MA", "PA", "START"], vec!["END", "PA", "PB"]] .into_iter() .map(|list| list.into_iter().map(event_id).collect::>()) .collect::>(); let expected_state_ids = vec!["PA", "MA", "MB"].into_iter().map(event_id).collect::>(); do_check(events, edges, expected_state_ids); } #[test] fn topic_basic() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); let events = &[ to_init_pdu_event( "T1", alice(), RoomEventType::RoomTopic, Some(""), to_raw_json_value(&json!({})).unwrap(), ), to_init_pdu_event( "PA1", alice(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), ), to_init_pdu_event( "T2", alice(), RoomEventType::RoomTopic, Some(""), to_raw_json_value(&json!({})).unwrap(), ), to_init_pdu_event( "PA2", alice(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 0 } })).unwrap(), ), to_init_pdu_event( "PB", bob(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), ), to_init_pdu_event( "T3", bob(), RoomEventType::RoomTopic, Some(""), to_raw_json_value(&json!({})).unwrap(), ), ]; 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::>()) .collect::>(); let expected_state_ids = vec!["PA2", "T2"].into_iter().map(event_id).collect::>(); do_check(events, edges, expected_state_ids); } #[test] fn topic_reset() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); let events = &[ to_init_pdu_event( "T1", alice(), RoomEventType::RoomTopic, Some(""), to_raw_json_value(&json!({})).unwrap(), ), to_init_pdu_event( "PA", alice(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), ), to_init_pdu_event( "T2", bob(), RoomEventType::RoomTopic, Some(""), to_raw_json_value(&json!({})).unwrap(), ), to_init_pdu_event( "MB", alice(), RoomEventType::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::>()) .collect::>(); let expected_state_ids = vec!["T1", "MB", "PA"].into_iter().map(event_id).collect::>(); do_check(events, edges, expected_state_ids); } #[test] fn join_rule_evasion() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); let events = &[ to_init_pdu_event( "JR", alice(), RoomEventType::RoomJoinRules, Some(""), to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Private)).unwrap(), ), to_init_pdu_event( "ME", ella(), RoomEventType::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::>()) .collect::>(); let expected_state_ids = vec![event_id("JR")]; do_check(events, edges, expected_state_ids); } #[test] fn offtopic_power_level() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); let events = &[ to_init_pdu_event( "PA", alice(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), ), to_init_pdu_event( "PB", bob(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50, charlie(): 50 } })) .unwrap(), ), to_init_pdu_event( "PC", charlie(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50, charlie(): 0 } })) .unwrap(), ), ]; let edges = vec![vec!["END", "PC", "PB", "PA", "START"], vec!["END", "PA"]] .into_iter() .map(|list| list.into_iter().map(event_id).collect::>()) .collect::>(); let expected_state_ids = vec!["PC"].into_iter().map(event_id).collect::>(); do_check(events, edges, expected_state_ids); } #[test] fn topic_setting() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); let events = &[ to_init_pdu_event( "T1", alice(), RoomEventType::RoomTopic, Some(""), to_raw_json_value(&json!({})).unwrap(), ), to_init_pdu_event( "PA1", alice(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), ), to_init_pdu_event( "T2", alice(), RoomEventType::RoomTopic, Some(""), to_raw_json_value(&json!({})).unwrap(), ), to_init_pdu_event( "PA2", alice(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 0 } })).unwrap(), ), to_init_pdu_event( "PB", bob(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), ), to_init_pdu_event( "T3", bob(), RoomEventType::RoomTopic, Some(""), to_raw_json_value(&json!({})).unwrap(), ), to_init_pdu_event( "MZ1", zara(), RoomEventType::RoomTopic, Some(""), to_raw_json_value(&json!({})).unwrap(), ), to_init_pdu_event( "T4", alice(), RoomEventType::RoomTopic, Some(""), to_raw_json_value(&json!({})).unwrap(), ), ]; 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::>()) .collect::>(); let expected_state_ids = vec!["T4", "PA2"].into_iter().map(event_id).collect::>(); do_check(events, edges, expected_state_ids); } #[test] fn test_event_map_none() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); let mut store = TestStore::(hashmap! {}); // build up the DAG let (state_at_bob, state_at_charlie, expected) = store.set_up(); let ev_map = store.0.clone(); let state_sets = [state_at_bob, state_at_charlie]; let resolved = match crate::resolve( &RoomVersionId::V2, &state_sets, state_sets .iter() .map(|map| { store.auth_event_ids(room_id(), map.values().cloned().collect()).unwrap() }) .collect(), |id| ev_map.get(id).map(Arc::clone), ) { Ok(state) => state, Err(e) => panic!("{e}"), }; assert_eq!(expected, resolved); } #[test] fn test_lexicographical_sort() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); let graph = hashmap! { event_id("l") => hashset![event_id("o")], event_id("m") => hashset![event_id("n"), event_id("o")], event_id("n") => hashset![event_id("o")], event_id("o") => hashset![], // "o" has zero outgoing edges but 4 incoming edges event_id("p") => hashset![event_id("o")], }; let res = crate::lexicographical_topological_sort(&graph, |_id| { Ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0)))) }) .unwrap(); assert_eq!( vec!["o", "l", "n", "m", "p"], res.iter() .map(ToString::to_string) .map(|s| s.replace('$', "").replace(":foo", "")) .collect::>() ); } #[test] fn ban_with_auth_chains() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); 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::>()) .collect::>(); let expected_state_ids = vec!["PA", "MB"].into_iter().map(event_id).collect::>(); do_check(&ban.values().cloned().collect::>(), edges, expected_state_ids); } #[test] fn ban_with_auth_chains2() { let _ = tracing::subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish()); 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.event_type().with_state_key(ev.state_key().unwrap()), ev.event_id.clone())) .collect::>(); 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.event_type().with_state_key(ev.state_key().unwrap()), ev.event_id.clone())) .collect::>(); let ev_map = store.0.clone(); let state_sets = [state_set_a, state_set_b]; let resolved = match crate::resolve( &RoomVersionId::V6, &state_sets, state_sets .iter() .map(|map| { store.auth_event_ids(room_id(), map.values().cloned().collect()).unwrap() }) .collect(), |id| ev_map.get(id).map(Arc::clone), ) { Ok(state) => state, Err(e) => panic!("{e}"), }; debug!( "{:#?}", resolved .iter() .map(|((ty, key), id)| format!("(({ty}{key:?}), {id})")) .collect::>() ); 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::>()) .collect::>(); let expected_state_ids = vec!["JR"].into_iter().map(event_id).collect::>(); do_check(&join_rule.values().cloned().collect::>(), edges, expected_state_ids); } #[allow(non_snake_case)] fn BAN_STATE_SET() -> HashMap> { vec![ to_pdu_event( "PA", alice(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), &["CREATE", "IMA", "IPOWER"], // auth_events &["START"], // prev_events ), to_pdu_event( "PB", alice(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), &["CREATE", "IMA", "IPOWER"], &["END"], ), to_pdu_event( "MB", alice(), RoomEventType::RoomMember, Some(ella().as_str()), member_content_ban(), &["CREATE", "IMA", "PB"], &["PA"], ), to_pdu_event( "IME", ella(), RoomEventType::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() -> HashMap> { vec![ to_pdu_event( "JR", alice(), RoomEventType::RoomJoinRules, Some(""), to_raw_json_value(&json!({ "join_rule": "invite" })).unwrap(), &["CREATE", "IMA", "IPOWER"], &["START"], ), to_pdu_event( "IMZ", zara(), RoomEventType::RoomPowerLevels, Some(zara().as_str()), member_content_join(), &["CREATE", "JR", "IPOWER"], &["START"], ), ] .into_iter() .map(|ev| (ev.event_id.clone(), ev)) .collect() } } ruma-state-res-0.8.0/src/power_levels.rs000064400000000000000000000134651046102023000163560ustar 00000000000000use std::collections::BTreeMap; use js_int::Int; use ruma_common::{ events::{room::power_levels::RoomPowerLevelsEventContent, RoomEventType}, power_levels::{default_power_level, NotificationPowerLevels}, serde::{btreemap_deserialize_v1_powerlevel_values, deserialize_v1_powerlevel}, OwnedUserId, }; use serde::Deserialize; use serde_json::{from_str as from_json_str, Error}; use tracing::error; use crate::RoomVersion; #[derive(Deserialize)] struct IntRoomPowerLevelsEventContent { #[serde(default = "default_power_level")] pub ban: Int, #[serde(default)] pub events: BTreeMap, #[serde(default)] pub events_default: Int, #[serde(default)] pub invite: Int, #[serde(default = "default_power_level")] pub kick: Int, #[serde(default = "default_power_level")] pub redact: Int, #[serde(default = "default_power_level")] pub state_default: Int, #[serde(default)] pub users: BTreeMap, #[serde(default)] pub users_default: Int, #[serde(default)] pub notifications: IntNotificationPowerLevels, } impl From for RoomPowerLevelsEventContent { fn from(int_pl: IntRoomPowerLevelsEventContent) -> Self { let IntRoomPowerLevelsEventContent { ban, events, events_default, invite, kick, redact, state_default, users, users_default, notifications, } = int_pl; let mut pl = Self::new(); pl.ban = ban; pl.events = events; pl.events_default = events_default; pl.invite = invite; pl.kick = kick; pl.redact = redact; pl.state_default = state_default; pl.users = users; pl.users_default = users_default; pl.notifications = notifications.into(); pl } } #[derive(Deserialize)] struct IntNotificationPowerLevels { #[serde(default = "default_power_level")] pub room: Int, } impl Default for IntNotificationPowerLevels { fn default() -> Self { Self { room: default_power_level() } } } impl From for NotificationPowerLevels { fn from(int_notif: IntNotificationPowerLevels) -> Self { let mut notif = Self::new(); notif.room = int_notif.room; notif } } pub(crate) fn deserialize_power_levels( content: &str, room_version: &RoomVersion, ) -> Option { if room_version.integer_power_levels { match from_json_str::(content) { Ok(content) => Some(content.into()), Err(_) => { error!("m.room.power_levels event is not valid with integer values"); None } } } else { match from_json_str(content) { Ok(content) => Some(content), Err(_) => { error!( "m.room.power_levels event is not valid with integer or string integer values" ); None } } } } #[derive(Deserialize)] pub(crate) struct PowerLevelsContentFields { #[serde(default, deserialize_with = "btreemap_deserialize_v1_powerlevel_values")] pub(crate) users: BTreeMap, #[serde(default, deserialize_with = "deserialize_v1_powerlevel")] pub(crate) users_default: Int, } #[derive(Deserialize)] struct IntPowerLevelsContentFields { #[serde(default)] users: BTreeMap, #[serde(default)] users_default: Int, } impl From for PowerLevelsContentFields { fn from(pl: IntPowerLevelsContentFields) -> Self { let IntPowerLevelsContentFields { users, users_default } = pl; Self { users, users_default } } } pub(crate) fn deserialize_power_levels_content_fields( content: &str, room_version: &RoomVersion, ) -> Result { if room_version.integer_power_levels { from_json_str::(content).map(|r| r.into()) } else { from_json_str(content) } } #[derive(Deserialize)] pub(crate) struct PowerLevelsContentInvite { #[serde(default, deserialize_with = "deserialize_v1_powerlevel")] pub(crate) invite: Int, } #[derive(Deserialize)] struct IntPowerLevelsContentInvite { #[serde(default)] invite: Int, } impl From for PowerLevelsContentInvite { fn from(pl: IntPowerLevelsContentInvite) -> Self { let IntPowerLevelsContentInvite { invite } = pl; Self { invite } } } pub(crate) fn deserialize_power_levels_content_invite( content: &str, room_version: &RoomVersion, ) -> Result { if room_version.integer_power_levels { from_json_str::(content).map(|r| r.into()) } else { from_json_str(content) } } #[derive(Deserialize)] pub(crate) struct PowerLevelsContentRedact { #[serde(default = "default_power_level", deserialize_with = "deserialize_v1_powerlevel")] pub(crate) redact: Int, } #[derive(Deserialize)] pub(crate) struct IntPowerLevelsContentRedact { #[serde(default = "default_power_level")] redact: Int, } impl From for PowerLevelsContentRedact { fn from(pl: IntPowerLevelsContentRedact) -> Self { let IntPowerLevelsContentRedact { redact } = pl; Self { redact } } } pub(crate) fn deserialize_power_levels_content_redact( content: &str, room_version: &RoomVersion, ) -> Result { if room_version.integer_power_levels { from_json_str::(content).map(|r| r.into()) } else { from_json_str(content) } } ruma-state-res-0.8.0/src/room_version.rs000064400000000000000000000111731046102023000163630ustar 00000000000000use ruma_common::RoomVersionId; use crate::{Error, Result}; #[derive(Debug)] #[allow(clippy::exhaustive_enums)] pub enum RoomDisposition { /// A room version that has a stable specification. Stable, /// A room version that is not yet fully specified. Unstable, } #[derive(Debug)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] 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, } #[derive(Debug)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum StateResolutionVersion { /// State resolution for rooms at version 1. V1, /// State resolution for room at version 2 or later. V2, } #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct RoomVersion { /// 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, // FIXME: not sure what this one means? 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 canonical json, 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, /// Allow knocking in event authentication. /// /// See [room v7 specification](https://spec.matrix.org/v1.2/rooms/v7/) for more information. pub allow_knocking: bool, /// Adds support for the restricted join rule. /// /// See: [MSC3289](https://github.com/matrix-org/matrix-spec-proposals/pull/3289) for more information. pub restricted_join_rules: bool, /// Adds support for the knock_restricted join rule. /// /// See: [MSC3787](https://github.com/matrix-org/matrix-spec-proposals/pull/3787) for more information. pub knock_restricted_join_rule: bool, /// Enforces integer power levels. /// /// See: [MSC3667](https://github.com/matrix-org/matrix-spec-proposals/pull/3667) for more information. pub integer_power_levels: bool, } impl RoomVersion { pub const V1: Self = Self { 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, allow_knocking: false, restricted_join_rules: false, knock_restricted_join_rule: false, integer_power_levels: false, }; pub const V2: Self = Self { state_res: StateResolutionVersion::V2, ..Self::V1 }; pub const V3: Self = Self { event_format: EventFormatVersion::V2, extra_redaction_checks: true, ..Self::V2 }; pub const V4: Self = Self { event_format: EventFormatVersion::V3, ..Self::V3 }; pub const V5: Self = Self { enforce_key_validity: true, ..Self::V4 }; pub const V6: Self = Self { special_case_aliases_auth: false, strict_canonicaljson: true, limit_notifications_power_levels: true, ..Self::V5 }; pub const V7: Self = Self { allow_knocking: true, ..Self::V6 }; pub const V8: Self = Self { restricted_join_rules: true, ..Self::V7 }; pub const V9: Self = Self::V8; pub const V10: Self = Self { knock_restricted_join_rule: true, integer_power_levels: true, ..Self::V9 }; pub fn new(version: &RoomVersionId) -> Result { Ok(match version { RoomVersionId::V1 => Self::V1, RoomVersionId::V2 => Self::V2, RoomVersionId::V3 => Self::V3, RoomVersionId::V4 => Self::V4, RoomVersionId::V5 => Self::V5, RoomVersionId::V6 => Self::V6, RoomVersionId::V7 => Self::V7, RoomVersionId::V8 => Self::V8, RoomVersionId::V9 => Self::V9, RoomVersionId::V10 => Self::V10, ver => return Err(Error::Unsupported(format!("found version `{ver}`"))), }) } } ruma-state-res-0.8.0/src/state_event.rs000064400000000000000000000063661046102023000161730ustar 00000000000000use std::{ borrow::Borrow, fmt::{Debug, Display}, hash::Hash, sync::Arc, }; use ruma_common::{events::RoomEventType, EventId, MilliSecondsSinceUnixEpoch, RoomId, UserId}; use serde_json::value::RawValue as RawJsonValue; /// Abstraction of a PDU so users can have their own PDU types. pub trait Event { type Id: Clone + Debug + Display + Eq + Ord + Hash + Borrow; /// The `EventId` of this event. fn event_id(&self) -> &Self::Id; /// 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) -> MilliSecondsSinceUnixEpoch; /// The event type. fn event_type(&self) -> &RoomEventType; /// The event's content. fn content(&self) -> &RawJsonValue; /// The state key for this event. fn state_key(&self) -> Option<&str>; /// The events before this event. // Requires GATs to avoid boxing (and TAIT for making it convenient). fn prev_events(&self) -> Box + '_>; /// All the authenticating events for this event. // Requires GATs to avoid boxing (and TAIT for making it convenient). fn auth_events(&self) -> Box + '_>; /// If this event is a redaction event this is the event it redacts. fn redacts(&self) -> Option<&Self::Id>; } impl Event for &T { type Id = T::Id; fn event_id(&self) -> &Self::Id { (*self).event_id() } fn room_id(&self) -> &RoomId { (*self).room_id() } fn sender(&self) -> &UserId { (*self).sender() } fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch { (*self).origin_server_ts() } fn event_type(&self) -> &RoomEventType { (*self).event_type() } fn content(&self) -> &RawJsonValue { (*self).content() } fn state_key(&self) -> Option<&str> { (*self).state_key() } fn prev_events(&self) -> Box + '_> { (*self).prev_events() } fn auth_events(&self) -> Box + '_> { (*self).auth_events() } fn redacts(&self) -> Option<&Self::Id> { (*self).redacts() } } impl Event for Arc { type Id = T::Id; fn event_id(&self) -> &Self::Id { (**self).event_id() } fn room_id(&self) -> &RoomId { (**self).room_id() } fn sender(&self) -> &UserId { (**self).sender() } fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch { (**self).origin_server_ts() } fn event_type(&self) -> &RoomEventType { (**self).event_type() } fn content(&self) -> &RawJsonValue { (**self).content() } fn state_key(&self) -> Option<&str> { (**self).state_key() } fn prev_events(&self) -> Box + '_> { (**self).prev_events() } fn auth_events(&self) -> Box + '_> { (**self).auth_events() } fn redacts(&self) -> Option<&Self::Id> { (**self).redacts() } } ruma-state-res-0.8.0/src/test_utils.rs000064400000000000000000000502131046102023000160370ustar 00000000000000use std::{ borrow::Borrow, collections::{BTreeMap, HashMap, HashSet}, sync::{ atomic::{AtomicU64, Ordering::SeqCst}, Arc, }, }; use js_int::{int, uint}; use ruma_common::{ event_id, events::{ pdu::{EventHash, Pdu, RoomV3Pdu}, room::{ join_rules::{JoinRule, RoomJoinRulesEventContent}, member::{MembershipState, RoomMemberEventContent}, }, RoomEventType, }, room_id, user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, UserId, }; use serde_json::{ json, value::{to_raw_value as to_raw_json_value, RawValue as RawJsonValue}, }; use tracing::info; use crate::{auth_types_for_event, Error, Event, EventTypeExt, Result, StateMap}; pub use event::PduEvent; static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0); pub fn do_check( events: &[Arc], edges: Vec>, expected_state_ids: Vec, ) { // To activate logging use `RUST_LOG=debug cargo t` let init_events = INITIAL_EVENTS(); let mut store = TestStore( init_events .values() .chain(events) .map(|ev| (ev.event_id().to_owned(), ev.clone())) .collect(), ); // This will be lexi_topo_sorted for resolution let mut graph = HashMap::new(); // This is the same as in `resolve` event_id -> OriginalStateEvent let mut fake_event_map = HashMap::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().to_owned(), HashSet::new()); fake_event_map.insert(ev.event_id().to_owned(), ev.clone()); } for pair in INITIAL_EDGES().windows(2) { if let [a, b] = &pair { graph.entry(a.to_owned()).or_insert_with(HashSet::new).insert(b.clone()); } } for edge_list in edges { for pair in edge_list.windows(2) { if let [a, b] = &pair { graph.entry(a.to_owned()).or_insert_with(HashSet::new).insert(b.clone()); } } } // event_id -> PduEvent let mut event_map: HashMap> = HashMap::new(); // event_id -> StateMap let mut state_at_event: HashMap> = HashMap::new(); // Resolve the current state and add it to the state_at_event map then continue // on in "time" for node in crate::lexicographical_topological_sort(&graph, |_id| { Ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0)))) }) .unwrap() { let fake_event = fake_event_map.get(&node).unwrap(); let event_id = fake_event.event_id().to_owned(); let prev_events = graph.get(&node).unwrap(); let state_before: StateMap = if prev_events.is_empty() { HashMap::new() } else if prev_events.len() == 1 { state_at_event.get(prev_events.iter().next().unwrap()).unwrap().clone() } else { let state_sets = prev_events.iter().filter_map(|k| state_at_event.get(k)).collect::>(); info!( "{:#?}", state_sets .iter() .map(|map| map .iter() .map(|((ty, key), id)| format!("(({ty}{key:?}), {id})")) .collect::>()) .collect::>() ); let auth_chain_sets = state_sets .iter() .map(|map| { store.auth_event_ids(room_id(), map.values().cloned().collect()).unwrap() }) .collect(); let resolved = crate::resolve(&RoomVersionId::V6, state_sets, auth_chain_sets, |id| { event_map.get(id).map(Arc::clone) }); match resolved { Ok(state) => state, Err(e) => panic!("resolution for {node} failed: {e}"), } }; let mut state_after = state_before.clone(); let ty = fake_event.event_type(); let key = fake_event.state_key().unwrap(); state_after.insert(ty.with_state_key(key), event_id.to_owned()); let auth_types = auth_types_for_event( fake_event.event_type(), fake_event.sender(), fake_event.state_key(), fake_event.content(), ) .unwrap(); 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(); let event = to_pdu_event( e.event_id().as_str(), e.sender(), e.event_type().clone(), e.state_key(), e.content().to_owned(), &auth_events, &prev_events.iter().cloned().collect::>(), ); // 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.to_owned(), event.clone()); state_at_event.insert(node, state_after); event_map.insert(event_id.to_owned(), 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).unwrap_or_else(|| { panic!( "{node} not found in {:?}", event_map.keys().map(ToString::to_string).collect::>() ) }); let key = ev.event_type().with_state_key(ev.state_key().unwrap()); 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 != ("m.room.message".into(), "dummy".to_owned()) }) .map(|(k, v)| (k.clone(), v.clone())) .collect::>(); assert_eq!(expected_state, end_state); } #[allow(clippy::exhaustive_structs)] pub struct TestStore(pub HashMap>); impl TestStore { pub fn get_event(&self, _: &RoomId, event_id: &EventId) -> Result> { self.0 .get(event_id) .map(Arc::clone) .ok_or_else(|| Error::NotFound(format!("{event_id} not found"))) } /// Returns a Vec of the related auth events to the given `event`. pub fn auth_event_ids( &self, room_id: &RoomId, event_ids: Vec, ) -> Result> { let mut result = HashSet::new(); let mut stack = event_ids; // DFS for auth event chain while let Some(ev_id) = stack.pop() { if result.contains(&ev_id) { continue; } result.insert(ev_id.clone()); let event = self.get_event(room_id, ev_id.borrow())?; stack.extend(event.auth_events().map(ToOwned::to_owned)); } Ok(result) } } // A StateStore implementation for testing #[allow(clippy::type_complexity)] impl TestStore { pub fn set_up( &mut self, ) -> (StateMap, StateMap, StateMap) { let create_event = to_pdu_event::<&EventId>( "CREATE", alice(), RoomEventType::RoomCreate, Some(""), to_raw_json_value(&json!({ "creator": alice() })).unwrap(), &[], &[], ); let cre = create_event.event_id().to_owned(); self.0.insert(cre.clone(), Arc::clone(&create_event)); let alice_mem = to_pdu_event( "IMA", alice(), RoomEventType::RoomMember, Some(alice().as_str()), member_content_join(), &[cre.clone()], &[cre.clone()], ); self.0.insert(alice_mem.event_id().to_owned(), Arc::clone(&alice_mem)); let join_rules = to_pdu_event( "IJR", alice(), RoomEventType::RoomJoinRules, Some(""), to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(), &[cre.clone(), alice_mem.event_id().to_owned()], &[alice_mem.event_id().to_owned()], ); self.0.insert(join_rules.event_id().to_owned(), 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(), RoomEventType::RoomMember, Some(bob().as_str()), member_content_join(), &[cre.clone(), join_rules.event_id().to_owned()], &[join_rules.event_id().to_owned()], ); self.0.insert(bob_mem.event_id().to_owned(), bob_mem.clone()); let charlie_mem = to_pdu_event( "IMC", charlie(), RoomEventType::RoomMember, Some(charlie().as_str()), member_content_join(), &[cre, join_rules.event_id().to_owned()], &[join_rules.event_id().to_owned()], ); self.0.insert(charlie_mem.event_id().to_owned(), charlie_mem.clone()); let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem] .iter() .map(|e| { (e.event_type().with_state_key(e.state_key().unwrap()), e.event_id().to_owned()) }) .collect::>(); let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem] .iter() .map(|e| { (e.event_type().with_state_key(e.state_key().unwrap()), e.event_id().to_owned()) }) .collect::>(); let expected = [&create_event, &alice_mem, &join_rules, &bob_mem, &charlie_mem] .iter() .map(|e| { (e.event_type().with_state_key(e.state_key().unwrap()), e.event_id().to_owned()) }) .collect::>(); (state_at_bob, state_at_charlie, expected) } } pub fn event_id(id: &str) -> OwnedEventId { if id.contains('$') { return id.try_into().unwrap(); } format!("${id}:foo").try_into().unwrap() } pub fn alice() -> &'static UserId { user_id!("@alice:foo") } pub fn bob() -> &'static UserId { user_id!("@bob:foo") } pub fn charlie() -> &'static UserId { user_id!("@charlie:foo") } pub fn ella() -> &'static UserId { user_id!("@ella:foo") } pub fn zara() -> &'static UserId { user_id!("@zara:foo") } pub fn room_id() -> &'static RoomId { room_id!("!test:foo") } pub fn member_content_ban() -> Box { to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Ban)).unwrap() } pub fn member_content_join() -> Box { to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap() } pub fn to_init_pdu_event( id: &str, sender: &UserId, ev_type: RoomEventType, state_key: Option<&str>, content: Box, ) -> Arc { let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst); let id = if id.contains('$') { id.to_owned() } else { format!("${id}:foo") }; let state_key = state_key.map(ToOwned::to_owned); Arc::new(PduEvent { event_id: id.try_into().unwrap(), rest: Pdu::RoomV3Pdu(RoomV3Pdu { room_id: room_id().to_owned(), sender: sender.to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(ts.try_into().unwrap()), state_key, kind: ev_type, content, redacts: None, unsigned: BTreeMap::new(), auth_events: vec![], prev_events: vec![], depth: uint!(0), hashes: EventHash::new("".to_owned()), signatures: BTreeMap::new(), }), }) } pub fn to_pdu_event( id: &str, sender: &UserId, ev_type: RoomEventType, state_key: Option<&str>, content: Box, auth_events: &[S], prev_events: &[S], ) -> Arc where S: AsRef, { let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst); let id = if id.contains('$') { id.to_owned() } else { format!("${id}:foo") }; let auth_events = auth_events.iter().map(AsRef::as_ref).map(event_id).collect::>(); let prev_events = prev_events.iter().map(AsRef::as_ref).map(event_id).collect::>(); let state_key = state_key.map(ToOwned::to_owned); Arc::new(PduEvent { event_id: id.try_into().unwrap(), rest: Pdu::RoomV3Pdu(RoomV3Pdu { room_id: room_id().to_owned(), sender: sender.to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(ts.try_into().unwrap()), state_key, kind: ev_type, content, redacts: None, unsigned: BTreeMap::new(), auth_events, prev_events, depth: uint!(0), hashes: EventHash::new("".to_owned()), signatures: BTreeMap::new(), }), }) } // all graphs start with these input events #[allow(non_snake_case)] pub fn INITIAL_EVENTS() -> HashMap> { vec![ to_pdu_event::<&EventId>( "CREATE", alice(), RoomEventType::RoomCreate, Some(""), to_raw_json_value(&json!({ "creator": alice() })).unwrap(), &[], &[], ), to_pdu_event( "IMA", alice(), RoomEventType::RoomMember, Some(alice().as_str()), member_content_join(), &["CREATE"], &["CREATE"], ), to_pdu_event( "IPOWER", alice(), RoomEventType::RoomPowerLevels, Some(""), to_raw_json_value(&json!({ "users": { alice(): 100 } })).unwrap(), &["CREATE", "IMA"], &["IMA"], ), to_pdu_event( "IJR", alice(), RoomEventType::RoomJoinRules, Some(""), to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(), &["CREATE", "IMA", "IPOWER"], &["IPOWER"], ), to_pdu_event( "IMB", bob(), RoomEventType::RoomMember, Some(bob().as_str()), member_content_join(), &["CREATE", "IJR", "IPOWER"], &["IJR"], ), to_pdu_event( "IMC", charlie(), RoomEventType::RoomMember, Some(charlie().as_str()), member_content_join(), &["CREATE", "IJR", "IPOWER"], &["IMB"], ), to_pdu_event::<&EventId>( "START", charlie(), RoomEventType::RoomMessage, Some("dummy"), to_raw_json_value(&json!({})).unwrap(), &[], &[], ), to_pdu_event::<&EventId>( "END", charlie(), RoomEventType::RoomMessage, Some("dummy"), to_raw_json_value(&json!({})).unwrap(), &[], &[], ), ] .into_iter() .map(|ev| (ev.event_id().to_owned(), ev)) .collect() } // all graphs start with these input events #[allow(non_snake_case)] pub fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap> { vec![to_pdu_event::<&EventId>( "CREATE", alice(), RoomEventType::RoomCreate, Some(""), to_raw_json_value(&json!({ "creator": alice() })).unwrap(), &[], &[], )] .into_iter() .map(|ev| (ev.event_id().to_owned(), ev)) .collect() } #[allow(non_snake_case)] pub fn INITIAL_EDGES() -> Vec { vec!["START", "IMC", "IMB", "IJR", "IPOWER", "IMA", "CREATE"] .into_iter() .map(event_id) .collect::>() } pub mod event { use ruma_common::{ events::{pdu::Pdu, RoomEventType}, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId, }; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue as RawJsonValue; use crate::Event; impl Event for PduEvent { type Id = OwnedEventId; fn event_id(&self) -> &Self::Id { &self.event_id } fn room_id(&self) -> &RoomId { match &self.rest { Pdu::RoomV1Pdu(ev) => &ev.room_id, Pdu::RoomV3Pdu(ev) => &ev.room_id, #[allow(unreachable_patterns)] _ => unreachable!("new PDU version"), } } fn sender(&self) -> &UserId { match &self.rest { Pdu::RoomV1Pdu(ev) => &ev.sender, Pdu::RoomV3Pdu(ev) => &ev.sender, #[allow(unreachable_patterns)] _ => unreachable!("new PDU version"), } } fn event_type(&self) -> &RoomEventType { match &self.rest { Pdu::RoomV1Pdu(ev) => &ev.kind, Pdu::RoomV3Pdu(ev) => &ev.kind, #[allow(unreachable_patterns)] _ => unreachable!("new PDU version"), } } fn content(&self) -> &RawJsonValue { match &self.rest { Pdu::RoomV1Pdu(ev) => &ev.content, Pdu::RoomV3Pdu(ev) => &ev.content, #[allow(unreachable_patterns)] _ => unreachable!("new PDU version"), } } fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch { match &self.rest { Pdu::RoomV1Pdu(ev) => ev.origin_server_ts, Pdu::RoomV3Pdu(ev) => ev.origin_server_ts, #[allow(unreachable_patterns)] _ => unreachable!("new PDU version"), } } fn state_key(&self) -> Option<&str> { match &self.rest { Pdu::RoomV1Pdu(ev) => ev.state_key.as_deref(), Pdu::RoomV3Pdu(ev) => ev.state_key.as_deref(), #[allow(unreachable_patterns)] _ => unreachable!("new PDU version"), } } fn prev_events(&self) -> Box + '_> { match &self.rest { Pdu::RoomV1Pdu(ev) => Box::new(ev.prev_events.iter().map(|(id, _)| id)), Pdu::RoomV3Pdu(ev) => Box::new(ev.prev_events.iter()), #[allow(unreachable_patterns)] _ => unreachable!("new PDU version"), } } fn auth_events(&self) -> Box + '_> { match &self.rest { Pdu::RoomV1Pdu(ev) => Box::new(ev.auth_events.iter().map(|(id, _)| id)), Pdu::RoomV3Pdu(ev) => Box::new(ev.auth_events.iter()), #[allow(unreachable_patterns)] _ => unreachable!("new PDU version"), } } fn redacts(&self) -> Option<&Self::Id> { match &self.rest { Pdu::RoomV1Pdu(ev) => ev.redacts.as_ref(), Pdu::RoomV3Pdu(ev) => ev.redacts.as_ref(), #[allow(unreachable_patterns)] _ => unreachable!("new PDU version"), } } } #[derive(Clone, Debug, Deserialize, Serialize)] #[allow(clippy::exhaustive_structs)] pub struct PduEvent { pub event_id: OwnedEventId, #[serde(flatten)] pub rest: Pdu, } }