Move state_res from tuwunel_core to tuwunel_service.

Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
Jason Volk
2026-02-16 05:43:03 +00:00
parent 6a550baf5f
commit 9ede830ffe
73 changed files with 134 additions and 131 deletions

View File

@@ -116,7 +116,3 @@ similar.workspace = true
[lints]
workspace = true
[[bench]]
name = "state_res"
harness = false

View File

@@ -1,594 +0,0 @@
use std::{
borrow::Borrow,
collections::HashMap,
sync::atomic::{AtomicU64, Ordering::SeqCst},
};
use criterion::{Criterion, async_executor::FuturesExecutor, criterion_group, criterion_main};
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, UserId,
events::{
TimelineEventType,
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
},
},
int, room_id, uint, user_id,
};
use serde_json::{
json,
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
};
use tuwunel_core::{
Result, err,
matrix::{
Event, EventHash, PduEvent,
event::TypeExt,
state_res::{AuthSet, StateMap},
},
utils::stream::IterStream,
};
criterion_group!(
benches,
lexico_topo_sort,
resolution_shallow_auth_chain,
resolve_deeper_event_set
);
criterion_main!(benches);
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
#[expect(
clippy::iter_on_single_items,
clippy::iter_on_empty_collections
)]
fn lexico_topo_sort(c: &mut Criterion) {
c.bench_function("lexico_topo_sort", |c| {
use maplit::hashmap;
let graph = hashmap! {
event_id("l") => [event_id("o")].into_iter().collect(),
event_id("m") => [event_id("n"), event_id("o")].into_iter().collect(),
event_id("n") => [event_id("o")].into_iter().collect(),
event_id("o") => [].into_iter().collect(), // "o" has zero outgoing edges but 4 incoming edges
event_id("p") => [event_id("o")].into_iter().collect(),
};
c.to_async(FuturesExecutor).iter(async || {
_ = tuwunel_core::matrix::state_res::topological_sort(&graph, &async |_id| {
Ok((int!(0).into(), MilliSecondsSinceUnixEpoch(uint!(0))))
})
.await;
});
});
}
fn resolution_shallow_auth_chain(c: &mut Criterion) {
c.bench_function("resolution_shallow_auth_chain", |c| {
let mut store = TestStore(maplit::hashmap! {});
// build up the DAG
let (state_at_bob, state_at_charlie, _) = store.set_up();
let rules = RoomVersionId::V6.rules().unwrap();
let ev_map = store.0.clone();
let state_sets = [state_at_bob, state_at_charlie];
let auth_chains = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect::<Vec<_>>();
let func = async || {
if let Err(e) = tuwunel_core::matrix::state_res::resolve(
&rules,
state_sets.clone().into_iter().stream(),
auth_chains.clone().into_iter().stream(),
&async |id| {
ev_map
.get(&id)
.cloned()
.ok_or(err!(Request(NotFound("Not Found"))))
},
&async |id| ev_map.contains_key(&id),
false,
)
.await
{
panic!("{e}")
}
};
c.to_async(FuturesExecutor).iter(async || {
func().await;
});
});
}
fn resolve_deeper_event_set(c: &mut Criterion) {
c.bench_function("resolver_deeper_event_set", |c| {
let mut inner = INITIAL_EVENTS();
let ban = BAN_STATE_SET();
inner.extend(ban);
let store = TestStore(inner.clone());
let state_set_a = [
&inner[&event_id("CREATE")],
&inner[&event_id("IJR")],
&inner[&event_id("IMA")],
&inner[&event_id("IMB")],
&inner[&event_id("IMC")],
&inner[&event_id("MB")],
&inner[&event_id("PA")],
]
.iter()
.map(|ev| {
(
ev.event_type()
.with_state_key(ev.state_key().unwrap()),
ev.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
let state_set_b = [
&inner[&event_id("CREATE")],
&inner[&event_id("IJR")],
&inner[&event_id("IMA")],
&inner[&event_id("IMB")],
&inner[&event_id("IMC")],
&inner[&event_id("IME")],
&inner[&event_id("PA")],
]
.iter()
.map(|ev| {
(
ev.event_type()
.with_state_key(ev.state_key().unwrap()),
ev.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
let rules = RoomVersionId::V6.rules().unwrap();
let state_sets = [state_set_a, state_set_b];
let auth_chains = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect::<Vec<_>>();
let func = async || {
if let Err(e) = tuwunel_core::matrix::state_res::resolve(
&rules,
state_sets.clone().into_iter().stream(),
auth_chains.clone().into_iter().stream(),
&async |id| {
inner
.get(&id)
.cloned()
.ok_or(err!(Request(NotFound("Not Found"))))
},
&async |id| inner.contains_key(&id),
false,
)
.await
{
panic!("{e}")
}
};
c.to_async(FuturesExecutor).iter(async || {
func().await;
});
});
}
//*/////////////////////////////////////////////////////////////////////
//
// IMPLEMENTATION DETAILS AHEAD
//
/////////////////////////////////////////////////////////////////////*/
struct TestStore<E: Event>(HashMap<OwnedEventId, E>);
#[expect(unused)]
impl<E: Event> TestStore<E> {
fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result<E> {
self.0
.get(event_id)
.cloned()
.ok_or(err!(Request(NotFound("Not Found"))))
}
/// 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<Vec<E>> {
let mut events = vec![];
for id in event_ids {
events.push(self.get_event(room_id, id)?);
}
Ok(events)
}
/// Returns a Vec of the related auth events to the given `event`.
fn auth_event_ids(
&self,
room_id: &RoomId,
event_ids: Vec<OwnedEventId>,
) -> Result<AuthSet<OwnedEventId>> {
let mut result = AuthSet::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)
}
/// Returns a vector representing the difference in auth chains of the given
/// `events`.
fn auth_chain_diff(
&self,
room_id: &RoomId,
event_ids: Vec<Vec<OwnedEventId>>,
) -> Result<Vec<OwnedEventId>> {
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::<AuthSet<_>>();
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::<AuthSet<_>>());
Ok(auth_chain_sets
.into_iter()
.flatten()
.filter(|id| !common.contains(id))
.collect())
} else {
Ok(vec![])
}
}
}
impl TestStore<PduEvent> {
fn set_up(
&mut self,
) -> (StateMap<OwnedEventId>, StateMap<OwnedEventId>, StateMap<OwnedEventId>) {
let create_event = to_pdu_event::<&EventId>(
"CREATE",
alice(),
TimelineEventType::RoomCreate,
Some(""),
to_raw_json_value(&json!({ "creator": alice() })).unwrap(),
&[],
&[],
);
let cre = create_event.event_id();
self.0
.insert(cre.to_owned(), create_event.clone());
let alice_mem = to_pdu_event(
"IMA",
alice(),
TimelineEventType::RoomMember,
Some(alice().to_string().as_str()),
member_content_join(),
&[cre.to_owned()],
&[cre.to_owned()],
);
self.0
.insert(alice_mem.event_id().to_owned(), alice_mem.clone());
let join_rules = to_pdu_event(
"IJR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
&[cre.to_owned(), 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(),
TimelineEventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_join(),
&[cre.to_owned(), 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(),
TimelineEventType::RoomMember,
Some(charlie().to_string().as_str()),
member_content_join(),
&[cre.to_owned(), 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::<StateMap<_>>();
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::<StateMap<_>>();
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::<StateMap<_>>();
(state_at_bob, state_at_charlie, expected)
}
}
fn event_id(id: &str) -> OwnedEventId {
if id.contains('$') {
return id.try_into().unwrap();
}
format!("${id}:foo").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<RawJsonValue> {
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Ban)).unwrap()
}
fn member_content_join() -> Box<RawJsonValue> {
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap()
}
fn to_pdu_event<S>(
id: &str,
sender: &UserId,
ev_type: TimelineEventType,
state_key: Option<&str>,
content: Box<RawJsonValue>,
auth_events: &[S],
prev_events: &[S],
) -> PduEvent
where
S: AsRef<str>,
{
// 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!("${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);
PduEvent {
event_id: id.try_into().unwrap(),
room_id: room_id().to_owned(),
sender: sender.to_owned(),
origin: None,
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
kind: ev_type,
content,
redacts: None,
unsigned: None,
auth_events,
prev_events,
depth: uint!(0),
hashes: EventHash::default(),
signatures: None,
//#[cfg(test)]
//rejected: false,
}
}
// all graphs start with these input events
#[expect(non_snake_case)]
fn INITIAL_EVENTS() -> HashMap<OwnedEventId, PduEvent> {
vec![
to_pdu_event::<&EventId>(
"CREATE",
alice(),
TimelineEventType::RoomCreate,
Some(""),
to_raw_json_value(&json!({ "creator": alice() })).unwrap(),
&[],
&[],
),
to_pdu_event(
"IMA",
alice(),
TimelineEventType::RoomMember,
Some(alice().as_str()),
member_content_join(),
&["CREATE"],
&["CREATE"],
),
to_pdu_event(
"IPOWER",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100 } })).unwrap(),
&["CREATE", "IMA"],
&["IMA"],
),
to_pdu_event(
"IJR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["IPOWER"],
),
to_pdu_event(
"IMB",
bob(),
TimelineEventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IJR"],
),
to_pdu_event(
"IMC",
charlie(),
TimelineEventType::RoomMember,
Some(charlie().to_string().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IMB"],
),
to_pdu_event::<&EventId>(
"START",
charlie(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
&[],
&[],
),
to_pdu_event::<&EventId>(
"END",
charlie(),
TimelineEventType::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
#[expect(non_snake_case)]
fn BAN_STATE_SET() -> HashMap<OwnedEventId, PduEvent> {
vec![
to_pdu_event(
"PA",
alice(),
TimelineEventType::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(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["END"],
),
to_pdu_event(
"MB",
alice(),
TimelineEventType::RoomMember,
Some(ella().as_str()),
member_content_ban(),
&["CREATE", "IMA", "PB"],
&["PA"],
),
to_pdu_event(
"IME",
ella(),
TimelineEventType::RoomMember,
Some(ella().as_str()),
member_content_join(),
&["CREATE", "IJR", "PA"],
&["MB"],
),
]
.into_iter()
.map(|ev| (ev.event_id().to_owned(), ev))
.collect()
}

View File

@@ -3,12 +3,10 @@
pub mod event;
pub mod pdu;
pub mod room_version;
pub mod state_res;
pub use event::{Event, StateKey, TypeExt as EventTypeExt, TypeStateKey, state_key};
pub use pdu::{EventHash, Pdu, PduBuilder, PduCount, PduEvent, PduId, RawPduId};
pub use room_version::{RoomVersion, RoomVersionRules};
pub use state_res::{StateMap, events};
pub type ShortStateKey = ShortId;
pub type ShortEventId = ShortId;

View File

@@ -22,6 +22,7 @@ pub use self::{
Count as PduCount, Id as PduId, Pdu as PduEvent, RawId as RawPduId,
builder::{Builder, Builder as PduBuilder},
count::Count,
format::check::check_pdu_format,
hashes::EventHashes as EventHash,
id::Id,
raw_id::*,

View File

@@ -1,3 +1,5 @@
pub(super) mod check;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, RoomId, RoomVersionId,
room_version_rules::{EventsReferenceFormatVersion, RoomVersionRules},
@@ -6,7 +8,6 @@ use ruma::{
use crate::{
Result, extract_variant, is_equal_to,
matrix::{PduEvent, room_version},
state_res::{self},
};
pub fn into_outgoing_federation(
@@ -94,7 +95,7 @@ pub fn from_incoming_federation(
pdu_json.insert("event_id".into(), CanonicalJsonValue::String(event_id.into()));
}
state_res::check_pdu_format(pdu_json, &room_rules.event_format)?;
check::check_pdu_format(pdu_json, &room_rules.event_format)?;
PduEvent::from_val(pdu_json)
}

View File

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

View File

@@ -1,707 +0,0 @@
mod auth_types;
mod room_member;
#[cfg(test)]
mod tests;
use futures::{
FutureExt, TryStreamExt,
future::{join3, try_join},
};
use ruma::{
EventId, Int, OwnedEventId, OwnedUserId,
api::client::error::ErrorKind::InvalidParam,
events::{
StateEventType, TimelineEventType,
room::{member::MembershipState, power_levels::UserPowerLevel},
},
room_version_rules::{AuthorizationRules, RoomVersionRules},
};
pub use self::auth_types::{AuthTypes, auth_types_for_event};
use self::room_member::check_room_member;
#[cfg(test)]
use super::test_utils;
use super::{
FetchStateExt, TypeStateKey, events,
events::{
RoomCreateEvent, RoomMemberEvent, RoomPowerLevelsEvent,
power_levels::{self, RoomPowerLevelsEventOptionExt, RoomPowerLevelsIntField},
},
};
use crate::{
Err, Error, Result, err,
matrix::{Event, StateKey},
trace,
utils::stream::{IterStream, TryReadyExt},
};
#[tracing::instrument(
level = "debug",
skip_all,
fields(
event_id = ?incoming_event.event_id(),
)
)]
pub async fn auth_check<FetchEvent, EventFut, FetchState, StateFut, Pdu>(
rules: &RoomVersionRules,
incoming_event: &Pdu,
fetch_event: &FetchEvent,
fetch_state: &FetchState,
) -> Result
where
FetchEvent: Fn(OwnedEventId) -> EventFut + Sync,
EventFut: Future<Output = Result<Pdu>> + Send,
FetchState: Fn(StateEventType, StateKey) -> StateFut + Sync,
StateFut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let dependent = check_state_dependent_auth_rules(rules, incoming_event, fetch_state);
let independent = check_state_independent_auth_rules(rules, incoming_event, fetch_event);
match try_join(independent, dependent).await {
| Err(e) if matches!(e, Error::Request(InvalidParam, ..)) => Err(e),
| Err(e) => Err!(Request(Forbidden("Auth check failed: {e}"))),
| Ok(_) => Ok(()),
}
}
/// Check whether the incoming event passes the state-independent [authorization
/// rules] for the given room version rules.
///
/// The state-independent rules are the first few authorization rules that check
/// an incoming `m.room.create` event (which cannot have `auth_events`), and the
/// list of `auth_events` of other events.
///
/// This method only needs to be called once, when the event is received.
///
/// # Errors
///
/// If the check fails, this returns an `Err(_)` with a description of the check
/// that failed.
///
/// [authorization rules]: https://spec.matrix.org/latest/server-server-api/#authorization-rules
#[tracing::instrument(
name = "independent",
level = "debug",
skip_all,
fields(
sender = ?incoming_event.sender(),
)
)]
pub(super) async fn check_state_independent_auth_rules<Fetch, Fut, Pdu>(
rules: &RoomVersionRules,
incoming_event: &Pdu,
fetch_event: &Fetch,
) -> Result
where
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
// Since v1, if type is m.room.create:
if *incoming_event.event_type() == TimelineEventType::RoomCreate {
let room_create_event = RoomCreateEvent::new(incoming_event.clone());
return check_room_create(&room_create_event, &rules.authorization);
}
let expected_auth_types = auth_types_for_event(
incoming_event.event_type(),
incoming_event.sender(),
incoming_event.state_key(),
incoming_event.content(),
&rules.authorization,
false,
)?;
// Since v1, considering auth_events:
let seen_auth_types = Vec::with_capacity(expected_auth_types.len());
let seen_auth_types = incoming_event
.auth_events()
.try_stream()
.and_then(async |event_id: &EventId| match fetch_event(event_id.to_owned()).await {
| Ok(auth_event) => Ok(auth_event),
| Err(e) if e.is_not_found() => Err!(Request(NotFound("auth event {event_id}: {e}"))),
| Err(e) => Err(e),
})
.ready_try_fold(seen_auth_types, |mut seen_auth_types, auth_event| {
let event_id = auth_event.event_id();
// The auth event must be in the same room as the incoming event.
if auth_event.room_id() != incoming_event.room_id() {
return Err!("auth event {event_id} not in the same room");
}
let state_key = auth_event
.state_key()
.ok_or_else(|| err!("auth event {event_id} has no `state_key`"))?;
let event_type = auth_event.event_type();
let key: TypeStateKey = (event_type.to_cow_str().into(), state_key.into());
// Since v1, if there are duplicate entries for a given type and state_key pair,
// reject.
if seen_auth_types.contains(&key) {
return Err!(
"duplicate auth event {event_id} for ({event_type}, {state_key}) pair"
);
}
// Since v1, if there are entries whose type and state_key dont match those
// specified by the auth events selection algorithm described in the server
// specification, reject.
if !expected_auth_types.contains(&key) {
return Err!(
"unexpected auth event {event_id} with ({event_type}, {state_key}) pair"
);
}
// Since v1, if there are entries which were themselves rejected under the
// checks performed on receipt of a PDU, reject.
if auth_event.rejected() {
return Err!("rejected auth event {event_id}");
}
seen_auth_types.push(key);
Ok(seen_auth_types)
})
.await?;
// Since v1, if there is no m.room.create event among the entries, reject.
if !rules
.authorization
.room_create_event_id_as_room_id
&& !seen_auth_types
.iter()
.any(|(event_type, _)| *event_type == StateEventType::RoomCreate)
{
return Err!("no `m.room.create` event in auth events");
}
// Since `org.matrix.hydra.11`, the room_id must be the reference hash of an
// accepted m.room.create event.
if rules
.authorization
.room_create_event_id_as_room_id
{
let room_create_event_id = incoming_event
.room_id()
.as_event_id()
.map_err(|e| {
err!(Request(InvalidParam(
"could not construct `m.room.create` event ID from room ID: {e}"
)))
})?;
let Ok(room_create_event) = fetch_event(room_create_event_id.clone()).await else {
return Err!(Request(NotFound(
"failed to find `m.room.create` event {room_create_event_id}"
)));
};
if room_create_event.rejected() {
return Err!("rejected `m.room.create` event {room_create_event_id}");
}
}
Ok(())
}
/// Check whether the incoming event passes the state-dependent [authorization
/// rules] for the given room version rules.
///
/// The state-dependent rules are all the remaining rules not checked by
/// [`check_state_independent_auth_rules()`].
///
/// This method should be called several times for an event, to perform the
/// [checks on receipt of a PDU].
///
/// 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.
///
/// This assumes that `ruma_signatures::verify_event()` was called previously,
/// as some authorization rules depend on the signatures being valid on the
/// event.
///
/// # Errors
///
/// If the check fails, this returns an `Err(_)` with a description of the check
/// that failed.
///
/// [authorization rules]: https://spec.matrix.org/latest/server-server-api/#authorization-rules
/// [checks on receipt of a PDU]: https://spec.matrix.org/latest/server-server-api/#checks-performed-on-receipt-of-a-pdu
#[tracing::instrument(
name = "dependent",
level = "debug",
skip_all,
fields(
sender = ?incoming_event.sender(),
)
)]
pub(super) async fn check_state_dependent_auth_rules<Fetch, Fut, Pdu>(
rules: &RoomVersionRules,
incoming_event: &Pdu,
fetch_state: &Fetch,
) -> Result
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
// There are no state-dependent auth rules for create events.
if *incoming_event.event_type() == TimelineEventType::RoomCreate {
trace!("allowing `m.room.create` event");
return Ok(());
}
let sender = incoming_event.sender();
let (room_create_event, sender_membership, current_room_power_levels_event) = join3(
fetch_state.room_create_event(),
fetch_state.user_membership(sender),
fetch_state.room_power_levels_event(),
)
.await;
// Since v1, 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.
let room_create_event = room_create_event?;
let federate = room_create_event.federate()?;
if !federate
&& room_create_event.sender().server_name() != incoming_event.sender().server_name()
{
return Err!(
"room is not federated and event's sender domain does not match `m.room.create` \
event's sender domain"
);
}
// v1-v5, if type is m.room.aliases:
if rules.authorization.special_case_room_aliases
&& *incoming_event.event_type() == TimelineEventType::RoomAliases
{
trace!("starting m.room.aliases check");
// v1-v5, if event has no state_key, reject.
//
// v1-v5, if sender's domain doesn't match state_key, reject.
if incoming_event.state_key() != Some(sender.server_name().as_str()) {
return Err!(
"server name of the `state_key` of `m.room.aliases` event does not match the \
server name of the sender"
);
}
// Otherwise, allow.
trace!("`m.room.aliases` event was allowed");
return Ok(());
}
// Since v1, if type is m.room.member:
if *incoming_event.event_type() == TimelineEventType::RoomMember {
let room_member_event = RoomMemberEvent::new(incoming_event.clone());
return check_room_member(
&room_member_event,
&rules.authorization,
&room_create_event,
fetch_state,
)
.boxed()
.await;
}
// Since v1, if the sender's current membership state is not join, reject.
let sender_membership = sender_membership?;
if sender_membership != MembershipState::Join {
return Err!("sender's membership `{sender_membership}` is not `join`");
}
let creators = room_create_event.creators(&rules.authorization)?;
let sender_power_level = current_room_power_levels_event.user_power_level(
sender,
creators.clone(),
&rules.authorization,
)?;
// Since v1, if type is m.room.third_party_invite:
if *incoming_event.event_type() == TimelineEventType::RoomThirdPartyInvite {
// Since v1, allow if and only if sender's current power level is greater than
// or equal to the invite level.
let invite_power_level = current_room_power_levels_event
.get_as_int_or_default(RoomPowerLevelsIntField::Invite, &rules.authorization)?;
if sender_power_level < invite_power_level {
return Err!(
"sender does not have enough power ({sender_power_level:?}) to send invites \
({invite_power_level}) in this room"
);
}
trace!("`m.room.third_party_invite` event was allowed");
return Ok(());
}
// Since v1, if the event type's required power level is greater than the
// sender's power level, reject.
let event_type_power_level = current_room_power_levels_event.event_power_level(
incoming_event.event_type(),
incoming_event.state_key(),
&rules.authorization,
)?;
if sender_power_level < event_type_power_level {
return Err!(
"sender does not have enough power ({sender_power_level:?}) for `{}` event type \
({event_type_power_level})",
incoming_event.event_type()
);
}
// Since v1, if the event has a state_key that starts with an @ and does not
// match the sender, reject.
if incoming_event
.state_key()
.is_some_and(|k| k.starts_with('@'))
&& incoming_event.state_key() != Some(incoming_event.sender().as_str())
{
return Err!("sender cannot send event with `state_key` matching another user's ID");
}
// If type is m.room.power_levels
if *incoming_event.event_type() == TimelineEventType::RoomPowerLevels {
let room_power_levels_event = RoomPowerLevelsEvent::new(incoming_event.clone());
return check_room_power_levels(
&room_power_levels_event,
current_room_power_levels_event.as_ref(),
&rules.authorization,
sender_power_level,
creators,
);
}
// v1-v2, if type is m.room.redaction:
if rules.authorization.special_case_room_redaction
&& *incoming_event.event_type() == TimelineEventType::RoomRedaction
{
return check_room_redaction(
incoming_event,
current_room_power_levels_event.as_ref(),
&rules.authorization,
sender_power_level,
);
}
// Otherwise, allow.
trace!("allowing event passed all checks");
Ok(())
}
/// Check whether the given event passes the `m.room.create` authorization
/// rules.
#[tracing::instrument(level = "trace", skip_all)]
fn check_room_create<Pdu>(
room_create_event: &RoomCreateEvent<Pdu>,
rules: &AuthorizationRules,
) -> Result
where
Pdu: Event,
{
// Since v1, if it has any previous events, reject.
if room_create_event.prev_events().next().is_some() {
return Err!("`m.room.create` event cannot have previous events");
}
if rules.room_create_event_id_as_room_id {
let Ok(room_create_event_id) = room_create_event.room_id().as_event_id() else {
return Err!(Request(InvalidParam(
"Failed to create `event_id` out of `m.room.create` synthetic `room_id`"
)));
};
if room_create_event_id != room_create_event.event_id() {
return Err!(Request(InvalidParam(
"`m.room.create` has mismatching synthetic `room_id` and `event_id`"
)));
}
} else {
// v1-v11, if the domain of the room_id does not match the domain of the sender,
// reject.
let Some(room_id_server_name) = room_create_event.room_id().server_name() else {
return Err!("Invalid `ServerName` for `room_id` in `m.room.create` event");
};
if room_id_server_name != room_create_event.sender().server_name() {
return Err!(
"Mismatched `ServerName` for `room_id` in `m.room.create` with `sender`"
);
}
}
// Since v1, if `content.room_version` is present and is not a recognized
// version, reject.
//
// This check is assumed to be done before calling auth_check because we have an
// AuthorizationRules, which means that we recognized the version.
// v1-v10, if content has no creator field, reject.
if !rules.use_room_create_sender && !room_create_event.has_creator()? {
return Err!("missing `creator` field in `m.room.create` event");
}
// Otherwise, allow.
trace!("`m.room.create` event was allowed");
Ok(())
}
/// Check whether the given event passes the `m.room.power_levels` authorization
/// rules.
#[tracing::instrument(level = "trace", skip_all)]
fn check_room_power_levels<Creators, Pdu>(
room_power_levels_event: &RoomPowerLevelsEvent<Pdu>,
current_room_power_levels_event: Option<&RoomPowerLevelsEvent<Pdu>>,
rules: &AuthorizationRules,
sender_power_level: impl Into<UserPowerLevel>,
mut room_creators: Creators,
) -> Result
where
Creators: Iterator<Item = OwnedUserId> + Clone,
Pdu: Event,
{
let sender_power_level = sender_power_level.into();
// Since v10, if any of the properties users_default, events_default,
// state_default, ban, redact, kick, or invite in content are present and not
// an integer, reject.
let new_int_fields = room_power_levels_event.int_fields_map(rules)?;
// Since v10, if either of the properties events or notifications in content are
// present and not a dictionary with values that are integers, reject.
let new_events = room_power_levels_event.events(rules)?;
let new_notifications = room_power_levels_event.notifications(rules)?;
// v1-v9, If the users property in content is not an object with keys that are
// valid user IDs with values that are integers (or a string that is an
// integer), reject. Since v10, if the users property in content is not an
// object with keys that are valid user IDs with values that are integers,
// reject.
let new_users = room_power_levels_event.users(rules)?;
// Since `org.matrix.hydra.11`, if the `users` property in `content` contains
// the `sender` of
// the `m.room.create` event or any of the user IDs in the create event's
// `content.additional_creators`, reject.
if rules.explicitly_privilege_room_creators
&& new_users.as_ref().is_some_and(|new_users| {
room_creators.any(|creator| power_levels::contains_key(new_users, &creator))
}) {
return Err!(Request(InvalidParam(
"creator user IDs are not allowed in the `users` field"
)));
}
trace!("validation of power event finished");
// Since v1, if there is no previous m.room.power_levels event in the room,
// allow.
let Some(current_room_power_levels_event) = current_room_power_levels_event else {
trace!("initial m.room.power_levels event allowed");
return Ok(());
};
// Since v1, for the properties users_default, events_default, state_default,
// ban, redact, kick, invite check if they were added, changed or removed. For
// each found alteration:
for field in RoomPowerLevelsIntField::ALL {
let current_power_level = current_room_power_levels_event.get_as_int(*field, rules)?;
let new_power_level = power_levels::get_value(&new_int_fields, field).copied();
if current_power_level == new_power_level {
continue;
}
// Since v1, if the current value is higher than the senders current power
// level, reject.
let current_power_level_too_big =
current_power_level.unwrap_or_else(|| field.default_value()) > sender_power_level;
// Since v1, if the new value is higher than the senders current power level,
// reject.
let new_power_level_too_big =
new_power_level.unwrap_or_else(|| field.default_value()) > sender_power_level;
if current_power_level_too_big || new_power_level_too_big {
return Err!(
"sender does not have enough power to change the power level of `{field}`"
);
}
}
// Since v1, for each entry being added to, or changed in, the events property:
// - Since v1, if the new value is higher than the sender's current power level,
// reject.
let current_events = current_room_power_levels_event.events(rules)?;
check_power_level_maps(
current_events.as_deref(),
new_events.as_deref(),
sender_power_level,
|_, current_power_level| {
// Since v1, for each entry being changed in, or removed from, the events
// property:
// - Since v1, if the current value is higher than the sender's current power
// level, reject.
current_power_level > sender_power_level
},
|ev_type| {
err!(
"sender does not have enough power to change the `{ev_type}` event type power \
level"
)
},
)?;
// Since v6, for each entry being added to, or changed in, the notifications
// property:
// - Since v6, if the new value is higher than the sender's current power level,
// reject.
if rules.limit_notifications_power_levels {
let current_notifications = current_room_power_levels_event.notifications(rules)?;
check_power_level_maps(
current_notifications.as_deref(),
new_notifications.as_deref(),
sender_power_level,
|_, current_power_level| {
// Since v6, for each entry being changed in, or removed from, the notifications
// property:
// - Since v6, if the current value is higher than the sender's current power
// level, reject.
current_power_level > sender_power_level
},
|key| {
err!(
"sender does not have enough power to change the `{key}` notification power \
level"
)
},
)?;
}
// Since v1, for each entry being added to, or changed in, the users property:
// - Since v1, if the new value is greater than the senders current power
// level, reject.
let current_users = current_room_power_levels_event.users(rules)?;
check_power_level_maps(
current_users.as_deref(),
new_users.as_deref(),
sender_power_level,
|user_id, current_power_level| {
// Since v1, for each entry being changed in, or removed from, the users
// property, other than the senders own entry:
// - Since v1, if the current value is greater than or equal to the senders
// current power level, reject.
user_id != room_power_levels_event.sender()
&& current_power_level >= sender_power_level
},
|user_id| err!("sender does not have enough power to change `{user_id}`'s power level"),
)?;
// Otherwise, allow.
trace!("m.room.power_levels event allowed");
Ok(())
}
/// Check the power levels changes between the current and the new maps.
///
/// # Arguments
///
/// * `current`: the map with the current power levels.
/// * `new`: the map with the new power levels.
/// * `sender_power_level`: the power level of the sender of the new map.
/// * `reject_current_power_level_change_fn`: the function to check if a power
/// level change or removal must be rejected given its current value.
///
/// The arguments to the method are the key of the power level and the current
/// value of the power level. It must return `true` if the change or removal
/// is rejected.
///
/// Note that another check is done after this one to check if the change is
/// allowed given the new value of the power level.
/// * `error_fn`: the function to generate an error when the change for the
/// given key is not allowed.
fn check_power_level_maps<'a, K>(
current: Option<&'a [(K, Int)]>,
new: Option<&'a [(K, Int)]>,
sender_power_level: UserPowerLevel,
reject_current_power_level_change_fn: impl FnOnce(&K, Int) -> bool + Copy,
error_fn: impl FnOnce(&K) -> Error,
) -> Result
where
K: Ord,
{
let keys_to_check = current
.iter()
.flat_map(|m| m.iter().map(|(k, _)| k))
.chain(new.iter().flat_map(|m| m.iter().map(|(k, _)| k)));
for key in keys_to_check {
let current_power_level = current.and_then(|m| power_levels::get_value(m, key));
let new_power_level = new.and_then(|m| power_levels::get_value(m, key));
if current_power_level == new_power_level {
continue;
}
// For each entry being changed in, or removed from, the property.
let current_power_level_change_rejected = current_power_level
.is_some_and(|power_level| reject_current_power_level_change_fn(key, *power_level));
// For each entry being added to, or changed in, the property:
// - If the new value is higher than the sender's current power level, reject.
let new_power_level_too_big =
new_power_level.is_some_and(|&new_power_level| new_power_level > sender_power_level);
if current_power_level_change_rejected || new_power_level_too_big {
return Err(error_fn(key));
}
}
Ok(())
}
/// Check whether the given event passes the `m.room.redaction` authorization
/// rules.
fn check_room_redaction<Pdu>(
room_redaction_event: &Pdu,
current_room_power_levels_event: Option<&RoomPowerLevelsEvent<Pdu>>,
rules: &AuthorizationRules,
sender_level: UserPowerLevel,
) -> Result
where
Pdu: Event,
{
let redact_level = current_room_power_levels_event
.cloned()
.get_as_int_or_default(RoomPowerLevelsIntField::Redact, rules)?;
// v1-v2, if the senders power level is greater than or equal to the redact
// level, allow.
if sender_level >= redact_level {
trace!("`m.room.redaction` event allowed via power levels");
return Ok(());
}
// v1-v2, 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 room_redaction_event.event_id().server_name()
== room_redaction_event
.redacts()
.as_ref()
.and_then(|&id| id.server_name())
{
trace!("`m.room.redaction` event allowed via room version 1 rules");
return Ok(());
}
// Otherwise, reject.
Err!("`m.room.redaction` event did not pass any of the allow rules")
}

View File

@@ -1,119 +0,0 @@
use ruma::{
UserId,
events::{StateEventType, TimelineEventType, room::member::MembershipState},
room_version_rules::AuthorizationRules,
};
use serde_json::value::RawValue as RawJsonValue;
use super::super::{TypeStateKey, events::member::RoomMemberEventContent};
use crate::{Err, Result, arrayvec::ArrayVec, matrix::pdu::MAX_AUTH_EVENTS};
pub type AuthTypes = ArrayVec<TypeStateKey, MAX_AUTH_EVENTS>;
/// Get the list of [relevant auth events] required to authorize the event of
/// the given type.
///
/// Returns a list of `(event_type, state_key)` tuples.
///
/// # Errors
///
/// Returns an `Err(_)` if a field could not be deserialized because `content`
/// does not respect the expected format for the `event_type`.
///
/// [relevant auth events]: https://spec.matrix.org/latest/server-server-api/#auth-events-selection
pub fn auth_types_for_event(
event_type: &TimelineEventType,
sender: &UserId,
state_key: Option<&str>,
content: &RawJsonValue,
rules: &AuthorizationRules,
always_create: bool,
) -> Result<AuthTypes> {
let mut auth_types = AuthTypes::new();
// The `auth_events` for the `m.room.create` event in a room is empty.
// For other events, it should be the following subset of the room state:
//
// - The `m.room.create` event.
// - The current `m.room.power_levels` event, if any.
// - The senders current `m.room.member` event, if any.
if *event_type != TimelineEventType::RoomCreate {
// v1-v11, the `m.room.create` event.
if !rules.room_create_event_id_as_room_id || always_create {
auth_types.push((StateEventType::RoomCreate, "".into()));
}
auth_types.push((StateEventType::RoomPowerLevels, "".into()));
auth_types.push((StateEventType::RoomMember, sender.as_str().into()));
}
// If type is `m.room.member`:
if *event_type == TimelineEventType::RoomMember {
auth_types_for_member_event(&mut auth_types, state_key, content, rules)?;
}
Ok(auth_types)
}
fn auth_types_for_member_event(
auth_types: &mut AuthTypes,
state_key: Option<&str>,
content: &RawJsonValue,
rules: &AuthorizationRules,
) -> Result {
// The targets current `m.room.member` event, if any.
let Some(state_key) = state_key else {
return Err!("missing `state_key` field for `m.room.member` event");
};
let key = (StateEventType::RoomMember, state_key.into());
if !auth_types.contains(&key) {
auth_types.push(key);
}
let content = RoomMemberEventContent::new(content);
let membership = content.membership()?;
// If `membership` is `join`, `invite` or `knock`, the current
// `m.room.join_rules` event, if any.
if matches!(
membership,
MembershipState::Join | MembershipState::Invite | MembershipState::Knock
) {
let key = (StateEventType::RoomJoinRules, "".into());
if !auth_types.contains(&key) {
auth_types.push(key);
}
}
// If `membership` is `invite` and `content` contains a `third_party_invite`
// property, the current `m.room.third_party_invite` event with `state_key`
// matching `content.third_party_invite.signed.token`, if any.
if membership == MembershipState::Invite {
let third_party_invite = content.third_party_invite()?;
if let Some(third_party_invite) = third_party_invite {
let token = third_party_invite.token()?.into();
let key = (StateEventType::RoomThirdPartyInvite, token);
if !auth_types.contains(&key) {
auth_types.push(key);
}
}
}
// If `content.join_authorised_via_users_server` is present, and the room
// version supports restricted rooms, then the `m.room.member` event with
// `state_key` matching `content.join_authorised_via_users_server`.
//
// Note: And the membership is join (https://github.com/matrix-org/matrix-spec/pull/2100)
if membership == MembershipState::Join && rules.restricted_join_rule {
let join_authorised_via_users_server = content.join_authorised_via_users_server()?;
if let Some(user_id) = join_authorised_via_users_server {
let key = (StateEventType::RoomMember, user_id.as_str().into());
if !auth_types.contains(&key) {
auth_types.push(key);
}
}
}
Ok(())
}

View File

@@ -1,585 +0,0 @@
use std::borrow::Borrow;
use futures::future::{join, join3};
use ruma::{
AnyKeyName, SigningKeyId, UserId,
events::{StateEventType, room::member::MembershipState},
room_version_rules::AuthorizationRules,
serde::{Base64, base64::Standard},
signatures::verify_canonical_json_bytes,
};
use crate::{
Err, Result, err, is_equal_to,
matrix::{Event, StateKey},
};
#[cfg(test)]
mod tests;
#[cfg(test)]
use super::test_utils;
use super::{
FetchStateExt,
events::{
JoinRule, RoomCreateEvent, RoomMemberEvent, RoomPowerLevelsIntField,
member::ThirdPartyInvite, power_levels::RoomPowerLevelsEventOptionExt,
},
};
/// Check whether the given event passes the `m.room.roomber` authorization
/// rules.
///
/// This assumes that `ruma_signatures::verify_event()` was called previously,
/// as some authorization rules depend on the signatures being valid on the
/// event.
#[tracing::instrument(level = "trace", skip_all)]
pub(super) async fn check_room_member<Fetch, Fut, Pdu>(
room_member_event: &RoomMemberEvent<Pdu>,
rules: &AuthorizationRules,
room_create_event: &RoomCreateEvent<Pdu>,
fetch_state: &Fetch,
) -> Result
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
// Since v1, if there is no state_key property, or no membership property in
// content, reject.
let Some(state_key) = room_member_event.state_key() else {
return Err!("missing `state_key` field in `m.room.member` event");
};
let target_user = <&UserId>::try_from(state_key)
.map_err(|e| err!("invalid `state_key` field in `m.room.member` event: {e}"))?;
let target_membership = room_member_event.membership()?;
// These checks are done `in ruma_signatures::verify_event()`:
//
// Since v8, if content has a join_authorised_via_users_server property:
//
// - Since v8, if the event is not validly signed by the homeserver of the user
// ID denoted by the key, reject.
match target_membership {
// Since v1, if membership is join:
| MembershipState::Join =>
check_room_member_join(
room_member_event,
target_user,
rules,
room_create_event,
fetch_state,
)
.await,
// Since v1, if membership is invite:
| MembershipState::Invite =>
check_room_member_invite(
room_member_event,
target_user,
rules,
room_create_event,
fetch_state,
)
.await,
// Since v1, if membership is leave:
| MembershipState::Leave =>
check_room_member_leave(
room_member_event,
target_user,
rules,
room_create_event,
fetch_state,
)
.await,
// Since v1, if membership is ban:
| MembershipState::Ban =>
check_room_member_ban(
room_member_event,
target_user,
rules,
room_create_event,
fetch_state,
)
.await,
// Since v7, if membership is knock:
| MembershipState::Knock if rules.knocking =>
check_room_member_knock(room_member_event, target_user, rules, fetch_state).await,
// Since v1, otherwise, the membership is unknown. Reject.
| _ => Err!("unknown membership"),
}
}
/// Check whether the given event passes the `m.room.member` authorization rules
/// with a membership of `join`.
#[tracing::instrument(level = "trace", skip_all)]
async fn check_room_member_join<Fetch, Fut, Pdu>(
room_member_event: &RoomMemberEvent<Pdu>,
target_user: &UserId,
rules: &AuthorizationRules,
room_create_event: &RoomCreateEvent<Pdu>,
fetch_state: &Fetch,
) -> Result
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let mut creators = room_create_event.creators(rules)?;
let mut prev_events = room_member_event.prev_events();
let prev_event_is_room_create_event = prev_events
.next()
.is_some_and(|event_id| event_id.borrow() == room_create_event.event_id().borrow());
let prev_event_is_only_room_create_event =
prev_event_is_room_create_event && prev_events.next().is_none();
// v1-v10, if the only previous event is an m.room.create and the state_key is
// the creator, allow.
// Since v11, if the only previous event is an m.room.create and the state_key
// is the sender of the m.room.create, allow.
if prev_event_is_only_room_create_event && creators.any(is_equal_to!(*target_user)) {
return Ok(());
}
// Since v1, if the sender does not match state_key, reject.
if room_member_event.sender() != target_user {
return Err!("sender of join event must match target user");
}
let (current_membership, join_rule) =
join(fetch_state.user_membership(target_user), fetch_state.join_rule()).await;
// Since v1, if the sender is banned, reject.
let current_membership = current_membership?;
if current_membership == MembershipState::Ban {
return Err!("banned user cannot join room");
}
// v1-v6, if the join_rule is invite then allow if membership state is invite or
// join.
// Since v7, if the join_rule is invite or knock then allow if membership state
// is invite or join.
let join_rule = join_rule?;
if (join_rule == JoinRule::Invite || rules.knocking && join_rule == JoinRule::Knock)
&& matches!(current_membership, MembershipState::Invite | MembershipState::Join)
{
return Ok(());
}
// v8-v9, if the join_rule is restricted:
// Since v10, if the join_rule is restricted or knock_restricted:
if rules.restricted_join_rule && matches!(join_rule, JoinRule::Restricted)
|| rules.knock_restricted_join_rule && matches!(join_rule, JoinRule::KnockRestricted)
{
// Since v8, if membership state is join or invite, allow.
if matches!(current_membership, MembershipState::Join | MembershipState::Invite) {
return Ok(());
}
// Since v8, if the join_authorised_via_users_server key in content is not a
// user with sufficient permission to invite other users, reject.
//
// Otherwise, allow.
let Some(authorized_via_user) = room_member_event.join_authorised_via_users_server()?
else {
// The field is absent, we cannot authorize.
return Err!(
"cannot join restricted room without `join_authorised_via_users_server` field \
if not invited"
);
};
// The member needs to be in the room to have any kind of permission.
let authorized_via_user_membership = fetch_state
.user_membership(&authorized_via_user)
.await?;
if authorized_via_user_membership != MembershipState::Join {
return Err!("`join_authorised_via_users_server` is not joined");
}
let room_power_levels_event = fetch_state.room_power_levels_event().await;
let authorized_via_user_power_level =
room_power_levels_event.user_power_level(&authorized_via_user, creators, rules)?;
let invite_power_level = room_power_levels_event
.get_as_int_or_default(RoomPowerLevelsIntField::Invite, rules)?;
if authorized_via_user_power_level < invite_power_level {
return Err!("`join_authorised_via_users_server` does not have enough power");
}
return Ok(());
}
// Since v1, if the join_rule is public, allow. Otherwise, reject.
if join_rule != JoinRule::Public {
return Err!("cannot join a room that is not `public`");
}
Ok(())
}
/// Check whether the given event passes the `m.room.member` authorization rules
/// with a membership of `invite`.
#[tracing::instrument(level = "trace", skip_all)]
async fn check_room_member_invite<Fetch, Fut, Pdu>(
room_member_event: &RoomMemberEvent<Pdu>,
target_user: &UserId,
rules: &AuthorizationRules,
room_create_event: &RoomCreateEvent<Pdu>,
fetch_state: &Fetch,
) -> Result
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let third_party_invite = room_member_event.third_party_invite()?;
// Since v1, if content has a third_party_invite property:
if let Some(third_party_invite) = third_party_invite {
return check_third_party_invite(
room_member_event,
&third_party_invite,
target_user,
fetch_state,
)
.await;
}
let sender_user = room_member_event.sender();
let (sender_membership, current_target_user_membership, room_power_levels_event) = join3(
fetch_state.user_membership(sender_user),
fetch_state.user_membership(target_user),
fetch_state.room_power_levels_event(),
)
.await;
// Since v1, if the senders current membership state is not join, reject.
let sender_membership = sender_membership?;
if sender_membership != MembershipState::Join {
return Err!("cannot invite user if sender is not joined");
}
// Since v1, if target users current membership state is join or ban, reject.
let current_target_user_membership = current_target_user_membership?;
if matches!(current_target_user_membership, MembershipState::Join | MembershipState::Ban) {
return Err!("cannot invite user that is joined or banned");
}
let creators = room_create_event.creators(rules)?;
let sender_power_level =
room_power_levels_event.user_power_level(room_member_event.sender(), creators, rules)?;
let invite_power_level =
room_power_levels_event.get_as_int_or_default(RoomPowerLevelsIntField::Invite, rules)?;
// Since v1, if the senders power level is greater than or equal to the invite
// level, allow. Otherwise, reject.
if sender_power_level < invite_power_level {
return Err!("sender does not have enough power to invite");
}
Ok(())
}
/// Check whether the `third_party_invite` from the `m.room.member` event passes
/// the authorization rules.
#[tracing::instrument(level = "trace", skip_all)]
async fn check_third_party_invite<Fetch, Fut, Pdu>(
room_member_event: &RoomMemberEvent<Pdu>,
third_party_invite: &ThirdPartyInvite,
target_user: &UserId,
fetch_state: &Fetch,
) -> Result
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let current_target_user_membership = fetch_state.user_membership(target_user).await?;
// Since v1, if target user is banned, reject.
if current_target_user_membership == MembershipState::Ban {
return Err!("cannot invite user that is banned");
}
// Since v1, if content.third_party_invite does not have a signed property,
// reject. Since v1, if signed does not have mxid and token properties, reject.
let third_party_invite_token = third_party_invite.token()?;
let third_party_invite_mxid = third_party_invite.mxid()?;
// Since v1, if mxid does not match state_key, reject.
if target_user != third_party_invite_mxid {
return Err!("third-party invite mxid does not match target user");
}
// Since v1, if there is no m.room.third_party_invite event in the current room
// state with state_key matching token, reject.
let Some(room_third_party_invite_event) = fetch_state
.room_third_party_invite_event(third_party_invite_token)
.await
else {
return Err!("no `m.room.third_party_invite` in room state matches the token");
};
// Since v1, if sender does not match sender of the m.room.third_party_invite,
// reject.
if room_member_event.sender() != room_third_party_invite_event.sender() {
return Err!(
"sender of `m.room.third_party_invite` does not match sender of `m.room.member`"
);
}
let signatures = third_party_invite.signatures()?;
let public_keys = room_third_party_invite_event.public_keys()?;
let signed_canonical_json = third_party_invite.signed_canonical_json()?;
// Since v1, if any signature in signed matches any public key in the
// m.room.third_party_invite event, allow.
for entity_signatures_value in signatures.values() {
let Some(entity_signatures) = entity_signatures_value.as_object() else {
return Err!(Request(InvalidParam(
"unexpected format of `signatures` field in `third_party_invite.signed` of \
`m.room.member` event: expected a map of string to object, got \
{entity_signatures_value:?}"
)));
};
// We will ignore any error from now on, we just want to find a signature that
// can be verified from a public key.
for (key_id, signature_value) in entity_signatures {
let Ok(parsed_key_id) = <&SigningKeyId<AnyKeyName>>::try_from(key_id.as_str()) else {
continue;
};
let Some(signature_str) = signature_value.as_str() else {
continue;
};
let Ok(signature) = Base64::<Standard>::parse(signature_str) else {
continue;
};
let algorithm = parsed_key_id.algorithm();
for encoded_public_key in &public_keys {
let Ok(public_key) = encoded_public_key.decode() else {
continue;
};
if verify_canonical_json_bytes(
&algorithm,
&public_key,
signature.as_bytes(),
signed_canonical_json.as_bytes(),
)
.is_ok()
{
return Ok(());
}
}
}
}
// Otherwise, reject.
Err!(
"no signature on third-party invite matches a public key in `m.room.third_party_invite` \
event"
)
}
/// Check whether the given event passes the `m.room.member` authorization rules
/// with a membership of `leave`.
#[tracing::instrument(level = "trace", skip_all)]
async fn check_room_member_leave<Fetch, Fut, Pdu>(
room_member_event: &RoomMemberEvent<Pdu>,
target_user: &UserId,
rules: &AuthorizationRules,
room_create_event: &RoomCreateEvent<Pdu>,
fetch_state: &Fetch,
) -> Result
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let (sender_membership, room_power_levels_event, current_target_user_membership) = join3(
fetch_state.user_membership(room_member_event.sender()),
fetch_state.room_power_levels_event(),
fetch_state.user_membership(target_user),
)
.await;
let sender_membership = sender_membership?;
// v1-v6, if the sender matches state_key, allow if and only if that users
// current membership state is invite or join.
// Since v7, if the sender matches state_key, allow if and only if that users
// current membership state is invite, join, or knock.
if room_member_event.sender() == target_user {
let membership_is_invite_or_join =
matches!(sender_membership, MembershipState::Join | MembershipState::Invite);
let membership_is_knock = rules.knocking && sender_membership == MembershipState::Knock;
return if membership_is_invite_or_join || membership_is_knock {
Ok(())
} else {
Err!("cannot leave if not joined, invited or knocked")
};
}
// Since v1, if the senders current membership state is not join, reject.
if sender_membership != MembershipState::Join {
return Err!("cannot kick if sender is not joined");
}
let creators = room_create_event.creators(rules)?;
let current_target_user_membership = current_target_user_membership?;
let sender_power_level = room_power_levels_event.user_power_level(
room_member_event.sender(),
creators.clone(),
rules,
)?;
let ban_power_level =
room_power_levels_event.get_as_int_or_default(RoomPowerLevelsIntField::Ban, rules)?;
// Since v1, if the target users current membership state is ban, and the
// senders power level is less than the ban level, reject.
if current_target_user_membership == MembershipState::Ban
&& sender_power_level < ban_power_level
{
return Err!("sender does not have enough power to unban");
}
let kick_power_level =
room_power_levels_event.get_as_int_or_default(RoomPowerLevelsIntField::Kick, rules)?;
let target_user_power_level =
room_power_levels_event.user_power_level(target_user, creators, rules)?;
// Since v1, if the senders power level is greater than or equal to the kick
// level, and the target users power level is less than the senders power
// level, allow.
//
// Otherwise, reject.
if sender_power_level >= kick_power_level && target_user_power_level < sender_power_level {
Ok(())
} else {
Err!("sender does not have enough power to kick target user")
}
}
/// Check whether the given event passes the `m.room.member` authorization rules
/// with a membership of `ban`.
#[tracing::instrument(level = "trace", skip_all)]
async fn check_room_member_ban<Fetch, Fut, Pdu>(
room_member_event: &RoomMemberEvent<Pdu>,
target_user: &UserId,
rules: &AuthorizationRules,
room_create_event: &RoomCreateEvent<Pdu>,
fetch_state: &Fetch,
) -> Result
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let (sender_membership, room_power_levels_event) = join(
fetch_state.user_membership(room_member_event.sender()),
fetch_state.room_power_levels_event(),
)
.await;
// Since v1, if the senders current membership state is not join, reject.
let sender_membership = sender_membership?;
if sender_membership != MembershipState::Join {
return Err!("cannot ban if sender is not joined");
}
let creators = room_create_event.creators(rules)?;
let sender_power_level = room_power_levels_event.user_power_level(
room_member_event.sender(),
creators.clone(),
rules,
)?;
let ban_power_level =
room_power_levels_event.get_as_int_or_default(RoomPowerLevelsIntField::Ban, rules)?;
let target_user_power_level =
room_power_levels_event.user_power_level(target_user, creators, rules)?;
// If the senders power level is greater than or equal to the ban level, and
// the target users power level is less than the senders power level, allow.
//
// Otherwise, reject.
if sender_power_level >= ban_power_level && target_user_power_level < sender_power_level {
Ok(())
} else {
Err!("sender does not have enough power to ban target user")
}
}
/// Check whether the given event passes the `m.room.member` authorization rules
/// with a membership of `knock`.
#[tracing::instrument(level = "trace", skip_all)]
async fn check_room_member_knock<Fetch, Fut, Pdu>(
room_member_event: &RoomMemberEvent<Pdu>,
target_user: &UserId,
rules: &AuthorizationRules,
fetch_state: &Fetch,
) -> Result
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let sender = room_member_event.sender();
let (join_rule, sender_membership) =
join(fetch_state.join_rule(), fetch_state.user_membership(sender)).await;
// v7-v9, if the join_rule is anything other than knock, reject.
// Since v10, if the join_rule is anything other than knock or knock_restricted,
// reject.
let join_rule = join_rule?;
if join_rule != JoinRule::Knock
&& (rules.knock_restricted_join_rule && !matches!(join_rule, JoinRule::KnockRestricted))
{
return Err!(
"join rule is not set to knock or knock_restricted, knocking is not allowed"
);
}
// Since v7, if sender does not match state_key, reject.
if room_member_event.sender() != target_user {
return Err!("cannot make another user knock, sender does not match target user");
}
// Since v7, if the senders current membership is not ban, invite, or join,
// allow. Otherwise, reject.
let sender_membership = sender_membership?;
if !matches!(
sender_membership,
MembershipState::Ban | MembershipState::Invite | MembershipState::Join
) {
Ok(())
} else {
Err!("cannot knock if user is banned, invited or joined")
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +0,0 @@
//! Helper traits and types to work with events (aka PDUs).
pub mod create;
pub mod join_rules;
pub mod member;
pub mod power_levels;
pub mod third_party_invite;
pub use self::{
create::RoomCreateEvent,
join_rules::{JoinRule, RoomJoinRulesEvent},
member::{RoomMemberEvent, RoomMemberEventContent},
power_levels::{RoomPowerLevelsEvent, RoomPowerLevelsIntField},
third_party_invite::RoomThirdPartyInviteEvent,
};
/// Whether the given event is a power event.
///
/// Definition in the spec:
///
/// > A power event is a state event with type `m.room.power_levels` or
/// > `m.room.join_rules`, or a
/// > state event with type `m.room.member` where the `membership` is `leave` or
/// > `ban` and the
/// > `sender` does not match the `state_key`. The idea behind this is that
/// > power events are events
/// > that might remove someones ability to do something in the room.
pub(super) fn is_power_event<Pdu>(event: &Pdu) -> bool
where
Pdu: crate::matrix::Event,
{
use ruma::events::{TimelineEventType, room::member::MembershipState};
match event.event_type() {
| TimelineEventType::RoomPowerLevels
| TimelineEventType::RoomJoinRules
| TimelineEventType::RoomCreate => event.state_key() == Some(""),
| TimelineEventType::RoomMember => {
let content = RoomMemberEventContent::new(event.content());
if content.membership().is_ok_and(|membership| {
matches!(membership, MembershipState::Leave | MembershipState::Ban)
}) {
return Some(event.sender().as_str()) != event.state_key();
}
false
},
| _ => false,
}
}

View File

@@ -1,150 +0,0 @@
//! Types to deserialize `m.room.create` events.
use std::{borrow::Cow, iter, ops::Deref};
use ruma::{
OwnedUserId, RoomVersionId, UserId, room_version_rules::AuthorizationRules,
serde::from_raw_json_value,
};
use serde::{Deserialize, de::IgnoredAny};
use crate::{Error, Result, err, matrix::Event};
/// A helper type for an [`Event`] of type `m.room.create`.
///
/// This is a type that deserializes each field lazily, when requested.
#[derive(Debug)]
pub struct RoomCreateEvent<E: Event>(E);
impl<E: Event> RoomCreateEvent<E> {
/// Construct a new `RoomCreateEvent` around the given event.
#[inline]
pub fn new(event: E) -> Self { Self(event) }
/// The version of the room.
pub fn room_version(&self) -> Result<RoomVersionId> {
#[derive(Deserialize)]
struct RoomCreateContentRoomVersion {
room_version: Option<RoomVersionId>,
}
let content: RoomCreateContentRoomVersion =
from_raw_json_value(self.content()).map_err(|err: Error| {
err!("invalid `room_version` field in `m.room.create` event: {err}")
})?;
Ok(content.room_version.unwrap_or(RoomVersionId::V1))
}
/// Whether the room is federated.
pub fn federate(&self) -> Result<bool> {
#[derive(Deserialize)]
struct RoomCreateContentFederate {
#[serde(rename = "m.federate")]
federate: Option<bool>,
}
let content: RoomCreateContentFederate =
from_raw_json_value(self.content()).map_err(|err: Error| {
err!("invalid `m.federate` field in `m.room.create` event: {err}")
})?;
Ok(content.federate.unwrap_or(true))
}
/// The creator of the room.
///
/// If the `use_room_create_sender` field of `AuthorizationRules` is set,
/// the creator is the sender of this `m.room.create` event, otherwise it
/// is deserialized from the `creator` field of this event's content.
pub fn creator(&self, rules: &AuthorizationRules) -> Result<Cow<'_, UserId>> {
#[derive(Deserialize)]
struct RoomCreateContentCreator {
creator: OwnedUserId,
}
if rules.use_room_create_sender {
Ok(Cow::Borrowed(self.sender()))
} else {
let content: RoomCreateContentCreator =
from_raw_json_value(self.content()).map_err(|err: Error| {
err!("missing or invalid `creator` field in `m.room.create` event: {err}")
})?;
Ok(Cow::Owned(content.creator))
}
}
/// The creators of the room.
///
/// If the `use_room_create_sender` field of `AuthorizationRules` is set,
/// the creator is the sender of this `m.room.create` event, otherwise it
/// is deserialized from the `creator` field of this event's content.
/// Additionally if the `explicitly_privilege_room_creators`
/// field of `AuthorizationRules` is set, any additional user IDs in
/// `additional_creators`, if present, will also be considered creators.
pub fn creators<'a>(
&'a self,
rules: &'a AuthorizationRules,
) -> Result<impl Iterator<Item = OwnedUserId> + Clone + use<'a, E>> {
let initial = self.creator(rules)?.into_owned();
let additional = self.additional_creators(rules)?;
Ok(iter::once(initial).chain(additional))
}
/// The additional creators of the room (if any).
///
/// If the `explicitly_privilege_room_creators`
/// field of `AuthorizationRules` is set, any additional user IDs in
/// `additional_creators`, if present, will also be considered creators.
///
/// This function ignores the primary room creator, and should only be used
/// in `check_room_member_join`. Otherwise, you should use `creators`
/// instead.
pub(super) fn additional_creators(
&self,
rules: &AuthorizationRules,
) -> Result<impl Iterator<Item = OwnedUserId> + Clone> {
#[derive(Deserialize)]
struct RoomCreateContentAdditionalCreators {
#[serde(default)]
additional_creators: Vec<OwnedUserId>,
}
Ok(if rules.additional_room_creators {
let mut content: RoomCreateContentAdditionalCreators =
from_raw_json_value(self.content()).map_err(|err: serde_json::Error| {
err!("invalid `additional_creators` field in `m.room.create` event: {err}")
})?;
content.additional_creators.sort();
content.additional_creators.dedup();
content.additional_creators.into_iter()
} else {
Vec::new().into_iter()
})
}
/// Whether the event has a `creator` field.
pub fn has_creator(&self) -> Result<bool> {
#[derive(Deserialize)]
struct RoomCreateContentCreator {
creator: Option<IgnoredAny>,
}
let content: RoomCreateContentCreator =
from_raw_json_value(self.content()).map_err(|err: Error| {
err!("invalid `creator` field in `m.room.create` event: {err}")
})?;
Ok(content.creator.is_some())
}
}
impl<E: Event> Deref for RoomCreateEvent<E> {
type Target = E;
#[inline]
fn deref(&self) -> &Self::Target { &self.0 }
}

View File

@@ -1,74 +0,0 @@
//! Types to deserialize `m.room.join_rules` events.
use std::ops::Deref;
use ruma::serde::{PartialEqAsRefStr, StringEnum, from_raw_json_value};
use serde::Deserialize;
use crate::{Error, Result, err, matrix::Event};
/// A helper type for an [`Event`] of type `m.room.join_rules`.
///
/// This is a type that deserializes each field lazily, when requested.
#[derive(Debug, Clone)]
pub struct RoomJoinRulesEvent<E: Event>(E);
impl<E: Event> RoomJoinRulesEvent<E> {
/// Construct a new `RoomJoinRulesEvent` around the given event.
#[inline]
pub fn new(event: E) -> Self { Self(event) }
/// The join rule of the room.
pub fn join_rule(&self) -> Result<JoinRule> {
#[derive(Deserialize)]
struct RoomJoinRulesContentJoinRule {
join_rule: JoinRule,
}
let content: RoomJoinRulesContentJoinRule =
from_raw_json_value(self.content()).map_err(|err: Error| {
err!("missing or invalid `join_rule` field in `m.room.join_rules` event: {err}")
})?;
Ok(content.join_rule)
}
}
impl<E: Event> Deref for RoomJoinRulesEvent<E> {
type Target = E;
#[inline]
fn deref(&self) -> &Self::Target { &self.0 }
}
/// The possible values for the join rule of a room.
#[derive(Clone, StringEnum, PartialEqAsRefStr)]
#[ruma_enum(rename_all = "snake_case")]
#[non_exhaustive]
pub enum JoinRule {
/// `public`
Public,
/// `invite`
Invite,
/// `knock`
Knock,
/// `restricted`
Restricted,
/// `KnockRestricted`
KnockRestricted,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
impl Eq for JoinRule {}
// Wrapper around `Box<str>` that cannot be used in a meaningful way outside of
// this crate. Used for string enums because their `_Custom` variant can't be
// truly private (only `#[doc(hidden)]`).
#[derive(Debug, Clone)]
pub struct PrivOwnedStr(Box<str>);

View File

@@ -1,207 +0,0 @@
//! Types to deserialize `m.room.member` events.
use std::ops::Deref;
use ruma::{
CanonicalJsonObject, OwnedUserId, events::room::member::MembershipState,
serde::from_raw_json_value, signatures::canonical_json,
};
use serde::Deserialize;
use serde_json::value::RawValue as RawJsonValue;
use crate::{Err, Error, Result, debug_error, err, matrix::Event};
/// A helper type for an [`Event`] of type `m.room.member`.
///
/// This is a type that deserializes each field lazily, as requested.
#[derive(Debug, Clone)]
pub struct RoomMemberEvent<E: Event>(E);
impl<E: Event> RoomMemberEvent<E> {
/// Construct a new `RoomMemberEvent` around the given event.
#[inline]
pub fn new(event: E) -> Self { Self(event) }
/// The membership of the user.
#[inline]
pub fn membership(&self) -> Result<MembershipState> {
RoomMemberEventContent(self.content()).membership()
}
/// If this is a `join` event, the ID of a user on the homeserver that
/// authorized it.
#[inline]
pub fn join_authorised_via_users_server(&self) -> Result<Option<OwnedUserId>> {
RoomMemberEventContent(self.content()).join_authorised_via_users_server()
}
/// If this is an `invite` event, details about the third-party invite that
/// resulted in this event.
#[inline]
pub fn third_party_invite(&self) -> Result<Option<ThirdPartyInvite>> {
RoomMemberEventContent(self.content()).third_party_invite()
}
}
impl<E: Event> Deref for RoomMemberEvent<E> {
type Target = E;
#[inline]
fn deref(&self) -> &Self::Target { &self.0 }
}
/// Helper trait for `Option<RoomMemberEvent<E>>`.
pub(crate) trait RoomMemberEventResultExt {
/// The membership of the user.
///
/// Defaults to `leave` if there is no `m.room.member` event.
fn membership(&self) -> Result<MembershipState>;
}
impl<E: Event> RoomMemberEventResultExt for Result<RoomMemberEvent<E>> {
fn membership(&self) -> Result<MembershipState> {
match self {
| Ok(event) => event.membership(),
| Err(e) if e.is_not_found() => Ok(MembershipState::Leave),
| Err(e) if cfg!(test) => panic!("membership(): unexpected: {e}"),
| Err(e) => {
debug_error!("membership(): unexpected: {e}");
Ok(MembershipState::Leave)
},
}
}
}
/// A helper type for the raw JSON content of an event of type `m.room.member`.
pub struct RoomMemberEventContent<'a>(&'a RawJsonValue);
impl<'a> RoomMemberEventContent<'a> {
/// Construct a new `RoomMemberEventContent` around the given raw JSON
/// content.
#[inline]
#[must_use]
pub fn new(content: &'a RawJsonValue) -> Self { Self(content) }
}
impl RoomMemberEventContent<'_> {
/// The membership of the user.
pub fn membership(&self) -> Result<MembershipState> {
#[derive(Deserialize)]
struct RoomMemberContentMembership {
membership: MembershipState,
}
let content: RoomMemberContentMembership =
from_raw_json_value(self.0).map_err(|err: Error| {
err!(Request(InvalidParam(
"missing or invalid `membership` field in `m.room.member` event: {err}"
)))
})?;
Ok(content.membership)
}
/// If this is a `join` event, the ID of a user on the homeserver that
/// authorized it.
pub fn join_authorised_via_users_server(&self) -> Result<Option<OwnedUserId>> {
#[derive(Deserialize)]
struct RoomMemberContentJoinAuthorizedViaUsersServer {
join_authorised_via_users_server: Option<OwnedUserId>,
}
let content: RoomMemberContentJoinAuthorizedViaUsersServer = from_raw_json_value(self.0)
.map_err(|err: Error| {
err!(Request(InvalidParam(
"invalid `join_authorised_via_users_server` field in `m.room.member` event: \
{err}"
)))
})?;
Ok(content.join_authorised_via_users_server)
}
/// If this is an `invite` event, details about the third-party invite that
/// resulted in this event.
pub fn third_party_invite(&self) -> Result<Option<ThirdPartyInvite>> {
#[derive(Deserialize)]
struct RoomMemberContentThirdPartyInvite {
third_party_invite: Option<ThirdPartyInvite>,
}
let content: RoomMemberContentThirdPartyInvite =
from_raw_json_value(self.0).map_err(|err: Error| {
err!(Request(InvalidParam(
"invalid `third_party_invite` field in `m.room.member` event: {err}"
)))
})?;
Ok(content.third_party_invite)
}
}
/// Details about a third-party invite.
#[derive(Deserialize)]
pub struct ThirdPartyInvite {
/// Signed details about the third-party invite.
signed: CanonicalJsonObject,
}
impl ThirdPartyInvite {
/// The unique identifier for the third-party invite.
pub fn token(&self) -> Result<&str> {
let Some(token_value) = self.signed.get("token") else {
return Err!(Request(InvalidParam(
"missing `token` field in `third_party_invite.signed` of `m.room.member` event"
)));
};
token_value.as_str().ok_or_else(|| {
err!(Request(InvalidParam(
"unexpected format of `token` field in `third_party_invite.signed` of \
`m.room.member` event: expected string, got {token_value:?}"
)))
})
}
/// The Matrix ID of the user that was invited.
pub fn mxid(&self) -> Result<&str> {
let Some(mxid_value) = self.signed.get("mxid") else {
return Err!(Request(InvalidParam(
"missing `mxid` field in `third_party_invite.signed` of `m.room.member` event"
)));
};
mxid_value.as_str().ok_or_else(|| {
err!(Request(InvalidParam(
"unexpected format of `mxid` field in `third_party_invite.signed` of \
`m.room.member` event: expected string, got {mxid_value:?}"
)))
})
}
/// The signatures of the event.
pub fn signatures(&self) -> Result<&CanonicalJsonObject> {
let Some(signatures_value) = self.signed.get("signatures") else {
return Err!(Request(InvalidParam(
"missing `signatures` field in `third_party_invite.signed` of `m.room.member` \
event"
)));
};
signatures_value.as_object().ok_or_else(|| {
err!(Request(InvalidParam(
"unexpected format of `signatures` field in `third_party_invite.signed` of \
`m.room.member` event: expected object, got {signatures_value:?}"
)))
})
}
/// The `signed` object as canonical JSON string to verify the signatures.
pub fn signed_canonical_json(&self) -> Result<String> {
canonical_json(&self.signed).map_err(|error| {
err!(Request(InvalidParam(
"invalid `third_party_invite.signed` field in `m.room.member` event: {error}"
)))
})
}
}

View File

@@ -1,401 +0,0 @@
//! Types to deserialize `m.room.power_levels` events.
use std::ops::Deref;
use ruma::{
Int, OwnedUserId, UserId,
events::{TimelineEventType, room::power_levels::UserPowerLevel},
int,
room_version_rules::AuthorizationRules,
serde::{
DebugAsRefStr, DisplayAsRefStr, JsonObject, OrdAsRefStr, PartialEqAsRefStr,
PartialOrdAsRefStr, deserialize_v1_powerlevel, from_raw_json_value,
vec_deserialize_int_powerlevel_values, vec_deserialize_v1_powerlevel_values,
},
};
use serde::de::DeserializeOwned;
use serde_json::{Error, from_value as from_json_value};
use crate::{Result, err, is_equal_to, matrix::Event, ref_at};
/// The default value of the creator's power level.
const DEFAULT_CREATOR_POWER_LEVEL: i32 = 100;
/// A helper type for an [`Event`] of type `m.room.power_levels`.
#[derive(Clone, Debug)]
pub struct RoomPowerLevelsEvent<E: Event>(E);
impl<E: Event> RoomPowerLevelsEvent<E> {
/// Construct a new `RoomPowerLevelsEvent` around the given event.
#[inline]
pub fn new(event: E) -> Self { Self(event) }
/// The deserialized content of the event.
fn deserialized_content(&self) -> Result<JsonObject> {
from_raw_json_value(self.content()).map_err(|error: Error| {
err!(Request(InvalidParam("malformed `m.room.power_levels` content: {error}")))
})
}
/// Get the value of a field that should contain an integer, if any.
///
/// The deserialization of this field is cached in memory.
pub(crate) fn get_as_int(
&self,
field: RoomPowerLevelsIntField,
rules: &AuthorizationRules,
) -> Result<Option<Int>> {
let content = self.deserialized_content()?;
let Some(value) = content.get(field.as_str()) else {
return Ok(None);
};
let res = if rules.integer_power_levels {
from_json_value(value.clone())
} else {
deserialize_v1_powerlevel(value)
};
let power_level = res.map(Some).map_err(|error| {
err!(Request(InvalidParam(
"unexpected format of `{field}` field in `content` of `m.room.power_levels` \
event: {error}"
)))
})?;
Ok(power_level)
}
/// Get the value of a field that should contain an integer, or its default
/// value if it is absent.
#[inline]
pub(crate) fn get_as_int_or_default(
&self,
field: RoomPowerLevelsIntField,
rules: &AuthorizationRules,
) -> Result<Int> {
Ok(self
.get_as_int(field, rules)?
.unwrap_or_else(|| field.default_value()))
}
/// Get the value of a field that should contain a map of any value to
/// integer, if any.
fn get_as_int_map<T: Ord + DeserializeOwned>(
&self,
field: &str,
rules: &AuthorizationRules,
) -> Result<Option<Vec<(T, Int)>>> {
let content = self.deserialized_content()?;
let Some(value) = content.get(field) else {
return Ok(None);
};
let res = if rules.integer_power_levels {
vec_deserialize_int_powerlevel_values(value)
} else {
vec_deserialize_v1_powerlevel_values(value)
};
res.map(Some).map_err(|error| {
err!(Request(InvalidParam(
"unexpected format of `{field}` field in `content` of `m.room.power_levels` \
event: {error}"
)))
})
}
/// Get the power levels required to send events, if any.
#[inline]
pub(crate) fn events(
&self,
rules: &AuthorizationRules,
) -> Result<Option<Vec<(TimelineEventType, Int)>>> {
self.get_as_int_map("events", rules)
}
/// Get the power levels required to trigger notifications, if any.
#[inline]
pub(crate) fn notifications(
&self,
rules: &AuthorizationRules,
) -> Result<Option<Vec<(String, Int)>>> {
self.get_as_int_map("notifications", rules)
}
/// Get the power levels of the users, if any.
///
/// The deserialization of this field is cached in memory.
#[inline]
pub(crate) fn users(
&self,
rules: &AuthorizationRules,
) -> Result<Option<Vec<(OwnedUserId, Int)>>> {
self.get_as_int_map("users", rules)
}
/// Get the power level of the user with the given ID.
///
/// Calling this method several times should be cheap because the necessary
/// deserialization results are cached.
pub(crate) fn user_power_level(
&self,
user_id: &UserId,
rules: &AuthorizationRules,
) -> Result<UserPowerLevel> {
let power_level = if let Some(power_level) = self
.users(rules)?
.as_ref()
.and_then(|users| get_value(users, user_id))
{
Ok(*power_level)
} else {
self.get_as_int_or_default(RoomPowerLevelsIntField::UsersDefault, rules)
};
power_level.map(Into::into)
}
/// Get the power level required to send an event of the given type.
pub(crate) fn event_power_level(
&self,
event_type: &TimelineEventType,
state_key: Option<&str>,
rules: &AuthorizationRules,
) -> Result<Int> {
let events = self.events(rules)?;
if let Some(power_level) = events
.as_ref()
.and_then(|events| get_value(events, event_type))
{
return Ok(*power_level);
}
let default_field = if state_key.is_some() {
RoomPowerLevelsIntField::StateDefault
} else {
RoomPowerLevelsIntField::EventsDefault
};
self.get_as_int_or_default(default_field, rules)
}
/// Get a map of all the fields with an integer value in the `content` of an
/// `m.room.power_levels` event.
pub(crate) fn int_fields_map(
&self,
rules: &AuthorizationRules,
) -> Result<Vec<(RoomPowerLevelsIntField, Int)>> {
RoomPowerLevelsIntField::ALL
.iter()
.copied()
.filter_map(|field| match self.get_as_int(field, rules) {
| Ok(value) => value.map(|value| Ok((field, value))),
| Err(error) => Some(Err(error)),
})
.collect()
}
}
impl<E: Event> Deref for RoomPowerLevelsEvent<E> {
type Target = E;
#[inline]
fn deref(&self) -> &Self::Target { &self.0 }
}
/// Helper trait for `Option<RoomPowerLevelsEvent<E>>`.
pub(crate) trait RoomPowerLevelsEventOptionExt {
/// Get the power level of the user with the given ID.
fn user_power_level(
&self,
user_id: &UserId,
creators: impl Iterator<Item = OwnedUserId>,
rules: &AuthorizationRules,
) -> Result<UserPowerLevel>;
/// Get the value of a field that should contain an integer, or its default
/// value if it is absent.
fn get_as_int_or_default(
&self,
field: RoomPowerLevelsIntField,
rules: &AuthorizationRules,
) -> Result<Int>;
/// Get the power level required to send an event of the given type.
fn event_power_level(
&self,
event_type: &TimelineEventType,
state_key: Option<&str>,
rules: &AuthorizationRules,
) -> Result<Int>;
}
impl<E> RoomPowerLevelsEventOptionExt for Option<RoomPowerLevelsEvent<E>>
where
E: Event,
{
fn user_power_level(
&self,
user_id: &UserId,
mut creators: impl Iterator<Item = OwnedUserId>,
rules: &AuthorizationRules,
) -> Result<UserPowerLevel> {
if rules.explicitly_privilege_room_creators && creators.any(is_equal_to!(user_id)) {
Ok(UserPowerLevel::Infinite)
} else if let Some(room_power_levels_event) = self {
room_power_levels_event.user_power_level(user_id, rules)
} else {
let power_level = if creators.any(is_equal_to!(user_id)) {
DEFAULT_CREATOR_POWER_LEVEL.into()
} else {
RoomPowerLevelsIntField::UsersDefault.default_value()
};
Ok(power_level.into())
}
}
fn get_as_int_or_default(
&self,
field: RoomPowerLevelsIntField,
rules: &AuthorizationRules,
) -> Result<Int> {
if let Some(room_power_levels_event) = self {
room_power_levels_event.get_as_int_or_default(field, rules)
} else {
Ok(field.default_value())
}
}
fn event_power_level(
&self,
event_type: &TimelineEventType,
state_key: Option<&str>,
rules: &AuthorizationRules,
) -> Result<Int> {
if let Some(room_power_levels_event) = self {
room_power_levels_event.event_power_level(event_type, state_key, rules)
} else {
let default_field = if state_key.is_some() {
RoomPowerLevelsIntField::StateDefault
} else {
RoomPowerLevelsIntField::EventsDefault
};
Ok(default_field.default_value())
}
}
}
#[inline]
pub(crate) fn get_value<'a, K, V, B>(vec: &'a [(K, V)], key: &'a B) -> Option<&'a V>
where
&'a K: PartialEq<&'a B>,
B: ?Sized,
{
position(vec, key)
.and_then(|i| vec.get(i))
.map(ref_at!(1))
}
#[inline]
pub(crate) fn contains_key<'a, K, V, B>(vec: &'a [(K, V)], key: &'a B) -> bool
where
&'a K: PartialEq<&'a B>,
B: ?Sized,
{
position(vec, key).is_some()
}
fn position<'a, K, V, B>(vec: &'a [(K, V)], key: &'a B) -> Option<usize>
where
&'a K: PartialEq<&'a B>,
B: ?Sized,
{
vec.iter()
.map(ref_at!(0))
.position(is_equal_to!(key))
}
/// Fields in the `content` of an `m.room.power_levels` event with an integer
/// value.
#[derive(
DebugAsRefStr,
Clone,
Copy,
DisplayAsRefStr,
PartialEqAsRefStr,
Eq,
PartialOrdAsRefStr,
OrdAsRefStr,
)]
#[non_exhaustive]
pub enum RoomPowerLevelsIntField {
/// `users_default`
UsersDefault,
/// `events_default`
EventsDefault,
/// `state_default`
StateDefault,
/// `ban`
Ban,
/// `redact`
Redact,
/// `kick`
Kick,
/// `invite`
Invite,
}
impl RoomPowerLevelsIntField {
/// A slice containing all the variants.
pub const ALL: &[Self] = &[
Self::UsersDefault,
Self::EventsDefault,
Self::StateDefault,
Self::Ban,
Self::Redact,
Self::Kick,
Self::Invite,
];
/// The string representation of this field.
#[inline]
#[must_use]
pub fn as_str(&self) -> &str { self.as_ref() }
/// The default value for this field if it is absent.
#[inline]
#[must_use]
pub fn default_value(self) -> Int {
match self {
| Self::UsersDefault | Self::EventsDefault | Self::Invite => int!(0),
| Self::StateDefault | Self::Kick | Self::Ban | Self::Redact => int!(50),
}
}
}
impl AsRef<str> for RoomPowerLevelsIntField {
#[inline]
fn as_ref(&self) -> &'static str {
match self {
| Self::UsersDefault => "users_default",
| Self::EventsDefault => "events_default",
| Self::StateDefault => "state_default",
| Self::Ban => "ban",
| Self::Redact => "redact",
| Self::Kick => "kick",
| Self::Invite => "invite",
}
}
}

View File

@@ -1,61 +0,0 @@
//! Types to deserialize `m.room.third_party_invite` events.
use std::{collections::BTreeSet, ops::Deref};
use ruma::{serde::from_raw_json_value, third_party_invite::IdentityServerBase64PublicKey};
use serde::Deserialize;
use crate::{Error, Result, err, matrix::Event};
/// A helper type for an [`Event`] of type `m.room.third_party_invite`.
///
/// This is a type that deserializes each field lazily, when requested.
#[derive(Debug, Clone)]
pub struct RoomThirdPartyInviteEvent<E: Event>(E);
impl<E: Event> RoomThirdPartyInviteEvent<E> {
/// Construct a new `RoomThirdPartyInviteEvent` around the given event.
pub fn new(event: E) -> Self { Self(event) }
/// The public keys of the identity server that might be used to sign the
/// third-party invite.
pub(crate) fn public_keys(&self) -> Result<BTreeSet<IdentityServerBase64PublicKey>> {
#[derive(Deserialize)]
struct RoomThirdPartyInviteContentPublicKeys {
public_key: Option<IdentityServerBase64PublicKey>,
#[serde(default)]
public_keys: Vec<PublicKey>,
}
#[derive(Deserialize)]
struct PublicKey {
public_key: IdentityServerBase64PublicKey,
}
let content: RoomThirdPartyInviteContentPublicKeys = from_raw_json_value(self.content())
.map_err(|err: Error| {
err!(
"invalid `public_key` or `public_keys` in `m.room.third_party_invite` \
event: {err}"
)
})?;
let public_keys = content
.public_keys
.into_iter()
.map(|k| k.public_key);
Ok(content
.public_key
.into_iter()
.chain(public_keys)
.collect())
}
}
impl<E: Event> Deref for RoomThirdPartyInviteEvent<E> {
type Target = E;
#[inline]
fn deref(&self) -> &Self::Target { &self.0 }
}

View File

@@ -1,74 +0,0 @@
use ruma::{
UserId,
events::{StateEventType, room::member::MembershipState},
};
use super::events::{
JoinRule, RoomCreateEvent, RoomJoinRulesEvent, RoomMemberEvent, RoomPowerLevelsEvent,
RoomThirdPartyInviteEvent, member::RoomMemberEventResultExt,
};
use crate::{
Result, err,
matrix::{Event, StateKey},
};
pub(super) trait FetchStateExt<Pdu: Event> {
async fn room_create_event(&self) -> Result<RoomCreateEvent<Pdu>>;
async fn user_membership(&self, user_id: &UserId) -> Result<MembershipState>;
async fn room_power_levels_event(&self) -> Option<RoomPowerLevelsEvent<Pdu>>;
async fn join_rule(&self) -> Result<JoinRule>;
async fn room_third_party_invite_event(
&self,
token: &str,
) -> Option<RoomThirdPartyInviteEvent<Pdu>>;
}
impl<Fetch, Fut, Pdu> FetchStateExt<Pdu> for &Fetch
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>>,
Pdu: Event,
{
async fn room_create_event(&self) -> Result<RoomCreateEvent<Pdu>> {
self(StateEventType::RoomCreate, "".into())
.await
.map(RoomCreateEvent::new)
.map_err(|e| err!("no `m.room.create` event in current state: {e}"))
}
async fn user_membership(&self, user_id: &UserId) -> Result<MembershipState> {
self(StateEventType::RoomMember, user_id.as_str().into())
.await
.map(RoomMemberEvent::new)
.membership()
}
async fn room_power_levels_event(&self) -> Option<RoomPowerLevelsEvent<Pdu>> {
self(StateEventType::RoomPowerLevels, "".into())
.await
.map(RoomPowerLevelsEvent::new)
.ok()
}
async fn join_rule(&self) -> Result<JoinRule> {
self(StateEventType::RoomJoinRules, "".into())
.await
.map(RoomJoinRulesEvent::new)
.map_err(|e| err!("no `m.room.join_rules` event in current state: {e}"))?
.join_rule()
}
async fn room_third_party_invite_event(
&self,
token: &str,
) -> Option<RoomThirdPartyInviteEvent<Pdu>> {
self(StateEventType::RoomThirdPartyInvite, token.into())
.await
.ok()
.map(RoomThirdPartyInviteEvent::new)
}
}

View File

@@ -1,77 +0,0 @@
//! State resolution and checks on incoming PDUs according to the [Matrix](https://matrix.org/) specification.
//!
//! When creating or receiving a PDU (or event), a server must check whether it
//! is valid and how it affects the room state. The purpose of this crate is to
//! provide functions that solve that.
//!
//! # Checks performed on receipt of a PDU
//!
//! This crate used with [ruma-signatures] should allow to perform all the
//! [necessary checks on receipt of a PDU].
//!
//! To respect the Matrix specification, the following functions should be
//! called for a PDU:
//!
//! 1. [`check_pdu_format()`] - The event should be dropped on error.
//! 2. [`ruma_signatures::verify_event()`] - The event should be dropped on
//! error. The PDU should be redacted before checking the authorization rules
//! if the result is `Verified::Signatures`.
//! 3. [`check_state_independent_auth_rules()`] - The event should be rejected
//! on error.
//! 4. [`check_state_dependent_auth_rules()`] - This function must be called 3
//! times:
//! 1. With the `auth_events` for the state, the event should be rejected on
//! error.
//! 2. With the state before the event, the event should be rejected on
//! error.
//! 3. With the current state of the room, the event should be "soft failed"
//! on error.
//!
//! # Room State Resolution
//!
//! Because of the distributed nature of Matrix, homeservers might not receive
//! all events in the same order, which might cause the room state to diverge
//! temporarily between homeservers. The purpose of [state resolution] is to
//! make sure that all homeservers arrive at the same room state given the same
//! events.
//!
//! The [`resolve()`] function allows to do that. It takes an iterator of state
//! maps and applies the proper state resolution algorithm for the current room
//! version to output the map of events in the current room state.
//!
//! # Event helper types
//!
//! The types from [ruma-events] use strict deserialization rules according to
//! their definition in the specification, which means that they also validate
//! fields that are not checked when receiving a PDU. Since it is not
//! appropriate for servers to reject an event that passes those checks, this
//! crate provides helper types in the [`events`] module, built around the
//! [`Event`] trait, to deserialize lazily a handful of fields from the most
//! common state events. Since these are the same types used for checking the
//! authorization rules, they are guaranteed to not perform extra validation on
//! unvalidated fields.
//!
//! The types from ruma-events are still appropriate to be used to create a new
//! event, or to validate an event received from a client.
//!
//! [ruma-signatures]: https://crates.io/crates/ruma-signatures
//! [necessary checks on receipt of a PDU]: https://spec.matrix.org/latest/server-server-api/#checks-performed-on-receipt-of-a-pdu
//! [ruma-events]: https://crates.io/crates/ruma-events
mod event_auth;
mod event_format;
pub mod events;
mod fetch_state;
mod resolve;
#[cfg(test)]
mod test_utils;
pub mod topological_sort;
use self::{event_auth::check_state_dependent_auth_rules, fetch_state::FetchStateExt};
pub use self::{
event_auth::{AuthTypes, auth_check, auth_types_for_event},
event_format::check_pdu_format,
resolve::{AuthSet, ConflictMap, StateMap, resolve},
topological_sort::topological_sort,
};
use crate::matrix::TypeStateKey;

View File

@@ -1,104 +0,0 @@
11/29/2020 BRANCH: timo-spec-comp REV: d2a85669cc6056679ce6ca0fde4658a879ad2b08
lexicographical topological sort
time: [1.7123 us 1.7157 us 1.7199 us]
change: [-1.7584% -1.5433% -1.3205%] (p = 0.00 < 0.05)
Performance has improved.
Found 8 outliers among 100 measurements (8.00%)
2 (2.00%) low mild
5 (5.00%) high mild
1 (1.00%) high severe
resolve state of 5 events one fork
time: [10.981 us 10.998 us 11.020 us]
Found 3 outliers among 100 measurements (3.00%)
3 (3.00%) high mild
resolve state of 10 events 3 conflicting
time: [26.858 us 26.946 us 27.037 us]
11/29/2020 BRANCH: event-trait REV: f0eb1310efd49d722979f57f20bd1ac3592b0479
lexicographical topological sort
time: [1.7686 us 1.7738 us 1.7810 us]
change: [-3.2752% -2.4634% -1.7635%] (p = 0.00 < 0.05)
Performance has improved.
Found 1 outliers among 100 measurements (1.00%)
1 (1.00%) high severe
resolve state of 5 events one fork
time: [10.643 us 10.656 us 10.669 us]
change: [-4.9990% -3.8078% -2.8319%] (p = 0.00 < 0.05)
Performance has improved.
Found 1 outliers among 100 measurements (1.00%)
1 (1.00%) high severe
resolve state of 10 events 3 conflicting
time: [29.149 us 29.252 us 29.375 us]
change: [-0.8433% -0.3270% +0.2656%] (p = 0.25 > 0.05)
No change in performance detected.
Found 1 outliers among 100 measurements (1.00%)
1 (1.00%) high mild
4/26/2020 BRANCH: fix-test-serde REV:
lexicographical topological sort
time: [1.6793 us 1.6823 us 1.6857 us]
Found 9 outliers among 100 measurements (9.00%)
1 (1.00%) low mild
4 (4.00%) high mild
4 (4.00%) high severe
resolve state of 5 events one fork
time: [9.9993 us 10.062 us 10.159 us]
Found 9 outliers among 100 measurements (9.00%)
7 (7.00%) high mild
2 (2.00%) high severe
resolve state of 10 events 3 conflicting
time: [26.004 us 26.092 us 26.195 us]
Found 16 outliers among 100 measurements (16.00%)
11 (11.00%) high mild
5 (5.00%) high severe
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

View File

@@ -1,272 +0,0 @@
#[cfg(test)]
mod tests;
mod auth_difference;
mod conflicted_subgraph;
mod iterative_auth_check;
mod mainline_sort;
mod power_sort;
mod split_conflicted;
use std::{
collections::{BTreeMap, BTreeSet, HashSet},
ops::Deref,
};
use futures::{FutureExt, Stream, StreamExt, TryFutureExt};
use itertools::Itertools;
use ruma::{OwnedEventId, events::StateEventType, room_version_rules::RoomVersionRules};
use self::{
auth_difference::auth_difference, conflicted_subgraph::conflicted_subgraph_dfs,
iterative_auth_check::iterative_auth_check, mainline_sort::mainline_sort,
power_sort::power_sort, split_conflicted::split_conflicted_state,
};
#[cfg(test)]
use super::test_utils;
use crate::{
Result, debug,
matrix::{Event, TypeStateKey},
smallvec::SmallVec,
trace,
utils::{
BoolExt,
stream::{BroadbandExt, IterStream},
},
};
/// A mapping of event type and state_key to some value `T`, usually an
/// `EventId`.
pub type StateMap<Id> = BTreeMap<TypeStateKey, Id>;
/// Full recursive set of `auth_events` for each event in a StateMap.
pub type AuthSet<Id> = BTreeSet<Id>;
/// ConflictMap of OwnedEventId specifically.
pub type ConflictMap<Id> = StateMap<ConflictVec<Id>>;
/// List of conflicting event_ids
type ConflictVec<Id> = SmallVec<[Id; 2]>;
/// Apply the [state resolution] algorithm introduced in room version 2 to
/// resolve the state of a room.
///
/// ## Arguments
///
/// * `rules` - The rules to apply for the version of the current room.
///
/// * `state_maps` - The incoming states to resolve. Each `StateMap` represents
/// a possible fork in the state of a room.
///
/// * `auth_chains` - The list of full recursive sets of `auth_events` for each
/// event in the `state_maps`.
///
/// * `fetch_event` - Function to fetch an event in the room given its event ID.
///
/// ## Invariants
///
/// The caller of `resolve` must ensure that all the events are from the same
/// room.
///
/// ## Returns
///
/// The resolved room state.
///
/// [state resolution]: https://spec.matrix.org/latest/rooms/v2/#state-resolution
#[tracing::instrument(level = "debug", skip_all)]
pub async fn resolve<States, AuthSets, FetchExists, ExistsFut, FetchEvent, EventFut, Pdu>(
rules: &RoomVersionRules,
state_maps: States,
auth_sets: AuthSets,
fetch: &FetchEvent,
exists: &FetchExists,
backport_css: bool,
) -> Result<StateMap<OwnedEventId>>
where
States: Stream<Item = StateMap<OwnedEventId>> + Send,
AuthSets: Stream<Item = AuthSet<OwnedEventId>> + Send,
FetchExists: Fn(OwnedEventId) -> ExistsFut + Sync,
ExistsFut: Future<Output = bool> + Send,
FetchEvent: Fn(OwnedEventId) -> EventFut + Sync,
EventFut: Future<Output = Result<Pdu>> + Send,
Pdu: Event + Clone,
{
// Split the unconflicted state map and the conflicted state set.
let (unconflicted_state, conflicted_states) = split_conflicted_state(state_maps).await;
debug!(
unconflicted = unconflicted_state.len(),
conflicted_states = conflicted_states.len(),
conflicted_events = conflicted_states
.values()
.fold(0_usize, |a, s| a.saturating_add(s.len())),
"unresolved states"
);
trace!(
?unconflicted_state,
?conflicted_states,
unconflicted = unconflicted_state.len(),
conflicted_states = conflicted_states.len(),
"unresolved states"
);
if conflicted_states.is_empty() {
return Ok(unconflicted_state.into_iter().collect());
}
// 0. The full conflicted set is the union of the conflicted state set and the
// auth difference. Don't honor events that don't exist.
let full_conflicted_set =
full_conflicted_set(rules, conflicted_states, auth_sets, fetch, exists, backport_css)
.await;
// 1. Select the set X of all power events that appear in the full conflicted
// set. For each such power event P, enlarge X by adding the events in the
// auth chain of P which also belong to the full conflicted set. Sort X into
// a list using the reverse topological power ordering.
let sorted_power_set: Vec<_> = power_sort(rules, &full_conflicted_set, fetch)
.inspect_ok(|list| debug!(count = list.len(), "sorted power events"))
.inspect_ok(|list| trace!(?list, "sorted power events"))
.boxed()
.await?;
let power_set_event_ids: Vec<_> = sorted_power_set.iter().sorted().collect();
let sorted_power_set = sorted_power_set
.iter()
.stream()
.map(AsRef::as_ref);
let start_with_incoming_state = rules
.state_res
.v2_rules()
.is_none_or(|r| !r.begin_iterative_auth_checks_with_empty_state_map);
let initial_state = start_with_incoming_state
.then(|| unconflicted_state.clone())
.unwrap_or_default();
// 2. Apply the iterative auth checks algorithm, starting from the unconflicted
// state map, to the list of events from the previous step to get a partially
// resolved state.
let partially_resolved_state =
iterative_auth_check(rules, sorted_power_set, initial_state, fetch)
.inspect_ok(|map| debug!(count = map.len(), "partially resolved power state"))
.inspect_ok(|map| trace!(?map, "partially resolved power state"))
.boxed()
.await?;
// This "epochs" power level event
let power_ty_sk = (StateEventType::RoomPowerLevels, "".into());
let power_event = partially_resolved_state.get(&power_ty_sk);
debug!(event_id = ?power_event, "epoch power event");
let remaining_events: Vec<_> = full_conflicted_set
.into_iter()
.filter(|id| power_set_event_ids.binary_search(&id).is_err())
.collect();
debug!(count = remaining_events.len(), "remaining events");
trace!(list = ?remaining_events, "remaining events");
let have_remaining_events = !remaining_events.is_empty();
let remaining_events = remaining_events
.iter()
.stream()
.map(AsRef::as_ref);
// 3. Take all remaining events that werent picked in step 1 and order them by
// the mainline ordering based on the power level in the partially resolved
// state obtained in step 2.
let sorted_remaining_events = have_remaining_events
.then_async(move || mainline_sort(power_event.cloned(), remaining_events, fetch))
.boxed();
let sorted_remaining_events = sorted_remaining_events
.await
.unwrap_or(Ok(Vec::new()))?;
debug!(count = sorted_remaining_events.len(), "sorted remaining events");
trace!(list = ?sorted_remaining_events, "sorted remaining events");
let sorted_remaining_events = sorted_remaining_events
.iter()
.stream()
.map(AsRef::as_ref);
// 4. Apply the iterative auth checks algorithm on the partial resolved state
// and the list of events from the previous step.
let mut resolved_state =
iterative_auth_check(rules, sorted_remaining_events, partially_resolved_state, fetch)
.boxed()
.await?;
// 5. Update the result by replacing any event with the event with the same key
// from the unconflicted state map, if such an event exists, to get the final
// resolved state.
resolved_state.extend(unconflicted_state);
debug!(resolved_state = resolved_state.len(), "resolved state");
trace!(?resolved_state, "resolved state");
Ok(resolved_state)
}
#[tracing::instrument(
name = "conflicted",
level = "debug",
skip_all,
fields(
states = conflicted_states.len(),
events = conflicted_states.values().flatten().count()
),
)]
async fn full_conflicted_set<AuthSets, FetchExists, ExistsFut, FetchEvent, EventFut, Pdu>(
rules: &RoomVersionRules,
conflicted_states: ConflictMap<OwnedEventId>,
auth_sets: AuthSets,
fetch: &FetchEvent,
exists: &FetchExists,
backport_css: bool,
) -> HashSet<OwnedEventId>
where
AuthSets: Stream<Item = AuthSet<OwnedEventId>> + Send,
FetchExists: Fn(OwnedEventId) -> ExistsFut + Sync,
ExistsFut: Future<Output = bool> + Send,
FetchEvent: Fn(OwnedEventId) -> EventFut + Sync,
EventFut: Future<Output = Result<Pdu>> + Send,
Pdu: Event + Clone,
{
let consider_conflicted_subgraph = rules
.state_res
.v2_rules()
.is_some_and(|rules| rules.consider_conflicted_state_subgraph)
|| backport_css;
let conflicted_state_set: HashSet<_> = conflicted_states.values().flatten().collect();
// Since `org.matrix.hydra.11`, fetch the conflicted state subgraph.
let conflicted_subgraph = consider_conflicted_subgraph
.then_async(async || conflicted_subgraph_dfs(&conflicted_state_set, fetch))
.map(Option::into_iter)
.map(IterStream::stream)
.flatten_stream()
.flatten()
.boxed();
let conflicted_state_ids = conflicted_state_set
.iter()
.map(Deref::deref)
.cloned()
.stream();
auth_difference(auth_sets)
.chain(conflicted_state_ids)
.broad_filter_map(async |id| exists(id.clone()).await.then_some(id))
.chain(conflicted_subgraph)
.collect::<HashSet<_>>()
.inspect(|set| debug!(count = set.len(), "full conflicted set"))
.inspect(|set| trace!(?set, "full conflicted set"))
.await
}

View File

@@ -1,41 +0,0 @@
use std::borrow::Borrow;
use futures::{FutureExt, Stream};
use ruma::EventId;
use super::AuthSet;
use crate::utils::stream::{IterStream, ReadyExt};
/// Get the auth difference for the given auth chains.
///
/// Definition in the specification:
///
/// The auth difference is calculated by first calculating the full auth chain
/// for each state _Si_, that is the union of the auth chains for each event in
/// _Si_, and then taking every event that doesnt appear in every auth chain.
/// If _Ci_ is the full auth chain of _Si_, then the auth difference is _Ci_
/// ∩_Ci_.
///
/// ## Arguments
///
/// * `auth_chains` - The list of full recursive sets of `auth_events`. Inputs
/// must be sorted.
///
/// ## Returns
///
/// Outputs the event IDs that are not present in all the auth chains.
#[tracing::instrument(level = "debug", skip_all)]
pub(super) fn auth_difference<'a, AuthSets, Id>(auth_sets: AuthSets) -> impl Stream<Item = Id>
where
AuthSets: Stream<Item = AuthSet<Id>>,
Id: Borrow<EventId> + Clone + Eq + Ord + Send + 'a,
{
auth_sets
.ready_fold_default(|ret: AuthSet<Id>, set| {
ret.symmetric_difference(&set)
.cloned()
.collect::<AuthSet<Id>>()
})
.map(|set: AuthSet<Id>| set.into_iter().stream())
.flatten_stream()
}

View File

@@ -1,147 +0,0 @@
use std::{
collections::HashSet as Set,
iter::once,
mem::take,
sync::{Arc, Mutex},
};
use futures::{Future, FutureExt, Stream, StreamExt};
use ruma::OwnedEventId;
use crate::{
Result, debug,
matrix::{Event, pdu::AuthEvents},
smallvec::SmallVec,
utils::stream::{IterStream, automatic_width},
};
#[derive(Default)]
struct Global {
subgraph: Mutex<Set<OwnedEventId>>,
seen: Mutex<Set<OwnedEventId>>,
}
#[derive(Default, Debug)]
struct Local {
path: Path,
stack: Stack,
}
type Path = SmallVec<[OwnedEventId; PATH_INLINE]>;
type Stack = SmallVec<[Frame; STACK_INLINE]>;
type Frame = AuthEvents;
const PATH_INLINE: usize = 48;
const STACK_INLINE: usize = 48;
#[tracing::instrument(name = "subgraph_dfs", level = "debug", skip_all)]
pub(super) fn conflicted_subgraph_dfs<Fetch, Fut, Pdu>(
conflicted_event_ids: &Set<&OwnedEventId>,
fetch: &Fetch,
) -> impl Stream<Item = OwnedEventId> + Send
where
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let state = Arc::new(Global::default());
let state_ = state.clone();
conflicted_event_ids
.iter()
.stream()
.enumerate()
.map(move |(i, event_id)| (state_.clone(), event_id, i))
.for_each_concurrent(automatic_width(), async |(state, event_id, i)| {
subgraph_descent(conflicted_event_ids, fetch, &state, event_id, i)
.await
.expect("only mutex errors expected");
})
.map(move |()| {
let seen = state.seen.lock().expect("locked");
let mut state = state.subgraph.lock().expect("locked");
debug!(
input_events = conflicted_event_ids.len(),
seen_events = seen.len(),
output_events = state.len(),
"conflicted subgraph state"
);
take(&mut *state).into_iter().stream()
})
.flatten_stream()
}
#[tracing::instrument(
name = "descent",
level = "trace",
skip_all,
fields(
event_ids = conflicted_event_ids.len(),
event_id = %conflicted_event_id,
%i,
)
)]
async fn subgraph_descent<Fetch, Fut, Pdu>(
conflicted_event_ids: &Set<&OwnedEventId>,
fetch: &Fetch,
state: &Arc<Global>,
conflicted_event_id: &OwnedEventId,
i: usize,
) -> Result
where
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let Global { subgraph, seen } = &**state;
let mut local = Local {
path: once(conflicted_event_id.clone()).collect(),
stack: once(once(conflicted_event_id.clone()).collect()).collect(),
};
while let Some(event_id) = pop(&mut local) {
if subgraph.lock()?.contains(&event_id) {
if local.path.len() > 1 {
subgraph
.lock()?
.extend(local.path.iter().cloned());
}
local.path.pop();
continue;
}
if !seen.lock()?.insert(event_id.clone()) {
continue;
}
if local.path.len() > 1 && conflicted_event_ids.contains(&event_id) {
subgraph
.lock()?
.extend(local.path.iter().cloned());
}
if let Ok(event) = fetch(event_id).await {
local
.stack
.push(event.auth_events_into().into_iter().collect());
}
}
Ok(())
}
fn pop(local: &mut Local) -> Option<OwnedEventId> {
let Local { path, stack } = local;
while stack.last().is_some_and(Frame::is_empty) {
stack.pop();
path.pop();
}
stack
.last_mut()
.and_then(Frame::pop)
.inspect(|event_id| path.push(event_id.clone()))
}

View File

@@ -1,209 +0,0 @@
use futures::{Stream, StreamExt, TryFutureExt, TryStreamExt};
use ruma::{
EventId, OwnedEventId,
events::{StateEventType, TimelineEventType},
room_version_rules::RoomVersionRules,
};
use super::{
super::{auth_types_for_event, check_state_dependent_auth_rules},
StateMap,
};
use crate::{
Error, Result, debug_warn, err, error,
matrix::{Event, EventTypeExt, StateKey},
smallvec::SmallVec,
trace,
utils::stream::{IterStream, ReadyExt, TryReadyExt, TryWidebandExt},
};
/// Perform the iterative auth checks to the given list of events.
///
/// Definition in the specification:
///
/// The iterative auth checks algorithm takes as input an initial room state and
/// a sorted list of state events, and constructs a new room state by iterating
/// through the event list and applying the state event to the room state if the
/// state event is allowed by the authorization rules. If the state event is not
/// allowed by the authorization rules, then the event is ignored. If a
/// (event_type, state_key) key that is required for checking the authorization
/// rules is not present in the state, then the appropriate state event from the
/// events auth_events is used if the auth event is not rejected.
///
/// ## Arguments
///
/// * `rules` - The authorization rules for the current room version.
/// * `events` - The sorted state events to apply to the `partial_state`.
/// * `state` - The current state that was partially resolved for the room.
/// * `fetch_event` - Function to fetch an event in the room given its event ID.
///
/// ## Returns
///
/// Returns the partially resolved state, or an `Err(_)` if one of the state
/// events in the room has an unexpected format.
#[tracing::instrument(
name = "iterative_auth",
level = "debug",
skip_all,
fields(
states = ?state.len(),
)
)]
pub(super) async fn iterative_auth_check<'b, SortedPowerEvents, Fetch, Fut, Pdu>(
rules: &RoomVersionRules,
events: SortedPowerEvents,
state: StateMap<OwnedEventId>,
fetch: &Fetch,
) -> Result<StateMap<OwnedEventId>>
where
SortedPowerEvents: Stream<Item = &'b EventId> + Send,
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
events
.map(Ok)
.wide_and_then(async |event_id| {
let event = fetch(event_id.to_owned()).await?;
let state_key: StateKey = event
.state_key()
.ok_or_else(|| err!(Request(InvalidParam("Missing state_key"))))?
.into();
Ok((event_id, state_key, event))
})
.try_fold(state, |state, (event_id, state_key, event)| {
auth_check(rules, state, event_id, state_key, event, fetch)
})
.await
}
#[tracing::instrument(
name = "check",
level = "trace",
skip_all,
fields(
%event_id,
?state_key,
)
)]
async fn auth_check<Fetch, Fut, Pdu>(
rules: &RoomVersionRules,
mut state: StateMap<OwnedEventId>,
event_id: &EventId,
state_key: StateKey,
event: Pdu,
fetch: &Fetch,
) -> Result<StateMap<OwnedEventId>>
where
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let Ok(auth_types) = auth_types_for_event(
event.event_type(),
event.sender(),
Some(&state_key),
event.content(),
&rules.authorization,
true,
)
.inspect_err(|e| error!("failed to get auth types for event: {e}")) else {
return Ok(state);
};
let auth_types_events = auth_types
.stream()
.ready_filter_map(|key| {
state
.get(&key)
.map(move |auth_event_id| (auth_event_id, key))
})
.filter_map(async |(id, key)| {
fetch(id.clone())
.inspect_err(|e| debug_warn!(%id, "missing auth event: {e}"))
.inspect_err(|e| debug_assert!(!cfg!(test), "missing auth {id:?}: {e:?}"))
.map_ok(move |auth_event| (key, auth_event))
.await
.ok()
})
.ready_filter_map(|(key, auth_event)| {
auth_event
.rejected()
.eq(&false)
.then_some((key, auth_event))
})
.map(Ok);
// If the `m.room.create` event is not in the auth events, we need to add it,
// because it's always part of the state and required in the auth rules.
let also_need_create_event = *event.event_type() != TimelineEventType::RoomCreate
&& rules
.authorization
.room_create_event_id_as_room_id;
let also_create_id: Option<OwnedEventId> = also_need_create_event
.then(|| event.room_id().as_event_id().ok())
.flatten();
let auth_events = event
.auth_events()
.chain(also_create_id.as_deref().into_iter())
.stream()
.filter_map(async |id| {
fetch(id.to_owned())
.inspect_err(|e| debug_warn!(%id, "missing auth event: {e}"))
.inspect_err(|e| debug_assert!(!cfg!(test), "missing auth {id:?}: {e:?}"))
.await
.ok()
})
.map(Result::<Pdu, Error>::Ok)
.ready_try_filter_map(|auth_event| {
let state_key = auth_event
.state_key()
.ok_or_else(|| err!(Request(InvalidParam("Missing state_key"))))?;
let key_val = auth_event
.rejected()
.eq(&false)
.then_some((auth_event.event_type().with_state_key(state_key), auth_event));
Ok(key_val)
});
let auth_events = auth_events
.chain(auth_types_events)
.try_collect()
.map_ok(|mut vec: SmallVec<[_; 4]>| {
vec.sort_by(|a, b| a.0.cmp(&b.0));
vec.reverse();
vec.dedup_by(|a, b| a.0.eq(&b.0));
vec
})
.await?;
let fetch_state = async |ty: StateEventType, key: StateKey| -> Result<Pdu> {
trace!(?ty, ?key, auth_events = auth_events.len(), "fetch state");
auth_events
.binary_search_by(|a| ty.cmp(&a.0.0).then(key.cmp(&a.0.1)))
.map(|i| auth_events[i].1.clone())
.map_err(|_| err!(Request(NotFound("Missing auth_event {ty:?},{key:?}"))))
};
// Add authentic event to the partially resolved state.
if check_state_dependent_auth_rules(rules, &event, &fetch_state)
.await
.inspect_err(|e| {
debug_warn!(
event_type = ?event.event_type(), ?state_key, %event_id,
"event failed auth check: {e}"
);
})
.is_ok()
{
let key = event.event_type().with_state_key(state_key);
state.insert(key, event_id.to_owned());
}
Ok(state)
}

View File

@@ -1,193 +0,0 @@
use futures::{
FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt, pin_mut, stream::try_unfold,
};
use ruma::{EventId, OwnedEventId, events::TimelineEventType};
use crate::{
Error, Result, at, is_equal_to,
matrix::Event,
trace,
utils::stream::{BroadbandExt, IterStream, TryReadyExt},
};
/// Perform mainline ordering of the given events.
///
/// Definition in the spec:
/// Given mainline positions calculated from P, the mainline ordering based on P
/// of a set of events is the ordering, from smallest to largest, using the
/// following comparison relation on events: for events x and y, x < y if
///
/// 1. the mainline position of x is greater than the mainline position of y
/// (i.e. the auth chain of x is based on an earlier event in the mainline
/// than y); or
/// 2. the mainline positions of the events are the same, but xs
/// origin_server_ts is less than ys origin_server_ts; or
/// 3. the mainline positions of the events are the same and the events have the
/// same origin_server_ts, but xs event_id is less than ys event_id.
///
/// ## Arguments
///
/// * `events` - The list of event IDs to sort.
/// * `power_level` - The power level event in the current state.
/// * `fetch_event` - Function to fetch an event in the room given its event ID.
///
/// ## Returns
///
/// Returns the sorted list of event IDs, or an `Err(_)` if one the event in the
/// room has an unexpected format.
#[tracing::instrument(
level = "debug",
skip_all,
fields(
power_levels = power_level_event_id
.as_deref()
.map(EventId::as_str)
.unwrap_or_default(),
)
)]
pub(super) async fn mainline_sort<'a, RemainingEvents, Fetch, Fut, Pdu>(
power_level_event_id: Option<OwnedEventId>,
events: RemainingEvents,
fetch: &Fetch,
) -> Result<Vec<OwnedEventId>>
where
RemainingEvents: Stream<Item = &'a EventId> + Send,
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
// Populate the mainline of the power level.
let mainline: Vec<_> = try_unfold(power_level_event_id, async |power_level_event_id| {
let Some(power_level_event_id) = power_level_event_id else {
return Ok::<_, Error>(None);
};
let power_level_event = fetch(power_level_event_id).await?;
let this_event_id = power_level_event.event_id().to_owned();
let next_event_id = get_power_levels_auth_event(&power_level_event, fetch)
.map_ok(|event| {
event
.as_ref()
.map(Event::event_id)
.map(ToOwned::to_owned)
})
.await?;
trace!(?this_event_id, ?next_event_id, "mainline descent",);
Ok(Some((this_event_id, next_event_id)))
})
.try_collect()
.await?;
let mainline = mainline.iter().rev().map(AsRef::as_ref);
events
.map(ToOwned::to_owned)
.broad_filter_map(async |event_id| {
let event = fetch(event_id.clone()).await.ok()?;
let origin_server_ts = event.origin_server_ts();
let position = mainline_position(Some(event), &mainline, fetch)
.await
.ok()?;
Some((event_id, (position, origin_server_ts)))
})
.inspect(|(event_id, (position, origin_server_ts))| {
trace!(position, ?origin_server_ts, ?event_id, "mainline position");
})
.collect()
.map(|mut vec: Vec<_>| {
vec.sort_by(|a, b| {
let (a_pos, a_ots) = &a.1;
let (b_pos, b_ots) = &b.1;
a_pos
.cmp(b_pos)
.then(a_ots.cmp(b_ots))
.then(a.cmp(b))
});
vec.into_iter().map(at!(0)).collect()
})
.map(Ok)
.await
}
/// Get the mainline position of the given event from the given mainline map.
///
/// ## Arguments
///
/// * `event` - The event to compute the mainline position of.
/// * `mainline_map` - The mainline map of the m.room.power_levels event.
/// * `fetch` - Function to fetch an event in the room given its event ID.
///
/// ## Returns
///
/// Returns the mainline position of the event, or an `Err(_)` if one of the
/// events in the auth chain of the event was not found.
#[tracing::instrument(
name = "position",
level = "trace",
ret(level = "trace"),
skip_all,
fields(
mainline = mainline.clone().count(),
event = ?current_event.as_ref().map(Event::event_id).map(ToOwned::to_owned),
)
)]
async fn mainline_position<'a, Mainline, Fetch, Fut, Pdu>(
mut current_event: Option<Pdu>,
mainline: &Mainline,
fetch: &Fetch,
) -> Result<usize>
where
Mainline: Iterator<Item = &'a EventId> + Clone + Send + Sync,
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
while let Some(event) = current_event {
trace!(
event_id = ?event.event_id(),
"mainline position search",
);
// If the current event is in the mainline map, return its position.
if let Some(position) = mainline
.clone()
.position(is_equal_to!(event.event_id()))
{
return Ok(position);
}
// Look for the power levels event in the auth events.
current_event = get_power_levels_auth_event(&event, fetch).await?;
}
// Did not find a power level event so we default to zero.
Ok(0)
}
#[expect(clippy::redundant_closure)]
#[tracing::instrument(level = "trace", skip_all)]
async fn get_power_levels_auth_event<Fetch, Fut, Pdu>(
event: &Pdu,
fetch: &Fetch,
) -> Result<Option<Pdu>>
where
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let power_level_event = event
.auth_events()
.try_stream()
.map_ok(ToOwned::to_owned)
.and_then(|auth_event_id| fetch(auth_event_id))
.ready_try_skip_while(|auth_event| {
Ok(!auth_event.is_type_and_state_key(&TimelineEventType::RoomPowerLevels, ""))
});
pin_mut!(power_level_event);
power_level_event.try_next().await
}

View File

@@ -1,273 +0,0 @@
use std::{
borrow::Borrow,
collections::{HashMap, HashSet},
};
use futures::{StreamExt, TryFutureExt, TryStreamExt};
use ruma::{
EventId, OwnedEventId,
events::{TimelineEventType, room::power_levels::UserPowerLevel},
room_version_rules::RoomVersionRules,
};
use super::super::{
events::{
RoomCreateEvent, RoomPowerLevelsEvent, RoomPowerLevelsIntField, is_power_event,
power_levels::RoomPowerLevelsEventOptionExt,
},
topological_sort,
topological_sort::ReferencedIds,
};
use crate::{
Result, err,
matrix::Event,
utils::stream::{BroadbandExt, IterStream, TryBroadbandExt},
};
/// Enlarge the given list of conflicted power events by adding the events in
/// their auth chain that are in the full conflicted set, and sort it using
/// reverse topological power ordering.
///
/// ## Arguments
///
/// * `conflicted_power_events` - The list of power events in the full
/// conflicted set.
///
/// * `full_conflicted_set` - The full conflicted set.
///
/// * `rules` - The authorization rules for the current room version.
///
/// * `fetch` - Function to fetch an event in the room given its event ID.
///
/// ## Returns
///
/// Returns the ordered list of event IDs from earliest to latest.
#[tracing::instrument(
level = "debug",
skip_all,
fields(
conflicted = full_conflicted_set.len(),
)
)]
pub(super) async fn power_sort<Fetch, Fut, Pdu>(
rules: &RoomVersionRules,
full_conflicted_set: &HashSet<OwnedEventId>,
fetch: &Fetch,
) -> Result<Vec<OwnedEventId>>
where
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
// A representation of the DAG, a map of event ID to its list of auth events
// that are in the full conflicted set. Fill the graph.
let graph = full_conflicted_set
.iter()
.stream()
.broad_filter_map(async |id| is_power_event_id(id, fetch).await.then_some(id))
.fold(HashMap::new(), |graph, event_id| {
add_event_auth_chain(graph, full_conflicted_set, event_id, fetch)
})
.await;
// The map of event ID to the power level of the sender of the event.
// Get the power level of the sender of each event in the graph.
let event_to_power_level: HashMap<_, _> = graph
.keys()
.try_stream()
.map_ok(AsRef::as_ref)
.broad_and_then(|event_id| {
power_level_for_sender(event_id, rules, fetch)
.map_ok(move |sender_power| (event_id, sender_power))
.map_err(|e| err!(Request(NotFound("Missing PL for sender: {e}"))))
})
.try_collect()
.await?;
let query = async |event_id: OwnedEventId| {
let power_level = *event_to_power_level
.get(&event_id.borrow())
.ok_or_else(|| err!(Request(NotFound("Missing PL event: {event_id}"))))?;
let event = fetch(event_id).await?;
Ok((power_level, event.origin_server_ts()))
};
topological_sort(&graph, &query).await
}
/// Add the event with the given event ID and all the events in its auth chain
/// that are in the full conflicted set to the graph.
#[tracing::instrument(
name = "auth_chain",
level = "trace",
skip_all,
fields(
?event_id,
graph = graph.len(),
)
)]
async fn add_event_auth_chain<Fetch, Fut, Pdu>(
mut graph: HashMap<OwnedEventId, ReferencedIds>,
full_conflicted_set: &HashSet<OwnedEventId>,
event_id: &EventId,
fetch: &Fetch,
) -> HashMap<OwnedEventId, ReferencedIds>
where
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let mut state = vec![event_id.to_owned()];
// Iterate through the auth chain of the event.
while let Some(event_id) = state.pop() {
// Iterate through the auth events of this event.
let event = fetch(event_id.clone()).await.ok();
// Add the current event to the graph.
graph.entry(event_id).or_default();
event
.as_ref()
.map(Event::auth_events)
.into_iter()
.flatten()
.map(ToOwned::to_owned)
.filter(|auth_event_id| full_conflicted_set.contains(auth_event_id))
.for_each(|auth_event_id| {
// If the auth event ID is not in the graph, check its auth events later.
if !graph.contains_key(&auth_event_id) {
state.push(auth_event_id.clone());
}
let event_id = event
.as_ref()
.expect("event is Some if there are auth_events")
.event_id();
// Add the auth event ID to the list of incoming edges.
let references = graph
.get_mut(event_id)
.expect("event_id must be added to graph");
if !references.contains(&auth_event_id) {
references.push(auth_event_id);
}
});
}
graph
}
/// Find the power level for the sender of the event of the given event ID or
/// return a default value of zero.
///
/// We find the most recent `m.room.power_levels` by walking backwards in the
/// auth chain of the event.
///
/// Do NOT use this anywhere but topological sort.
///
/// ## Arguments
///
/// * `event_id` - The event ID of the event to get the power level of the
/// sender of.
///
/// * `rules` - The authorization rules for the current room version.
///
/// * `fetch` - Function to fetch an event in the room given its event ID.
///
/// ## Returns
///
/// Returns the power level of the sender of the event or an `Err(_)` if one of
/// the auth events if malformed.
#[tracing::instrument(
name = "sender_power",
level = "trace",
skip_all,
fields(
?event_id,
)
)]
async fn power_level_for_sender<Fetch, Fut, Pdu>(
event_id: &EventId,
rules: &RoomVersionRules,
fetch: &Fetch,
) -> Result<UserPowerLevel>
where
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let mut room_create_event = None;
let mut room_power_levels_event = None;
let event = fetch(event_id.to_owned()).await;
if let Ok(event) = &event
&& rules
.authorization
.room_create_event_id_as_room_id
{
let create_id = event.room_id().as_event_id()?;
let fetched = fetch(create_id).await?;
room_create_event = Some(RoomCreateEvent::new(fetched));
}
for auth_event_id in event
.as_ref()
.map(Event::auth_events)
.into_iter()
.flatten()
{
if let Ok(auth_event) = fetch(auth_event_id.to_owned()).await {
if auth_event.is_type_and_state_key(&TimelineEventType::RoomPowerLevels, "") {
room_power_levels_event = Some(RoomPowerLevelsEvent::new(auth_event));
} else if !rules
.authorization
.room_create_event_id_as_room_id
&& auth_event.is_type_and_state_key(&TimelineEventType::RoomCreate, "")
{
room_create_event = Some(RoomCreateEvent::new(auth_event));
}
if room_power_levels_event.is_some() && room_create_event.is_some() {
break;
}
}
}
let auth_rules = &rules.authorization;
let creators = room_create_event
.as_ref()
.and_then(|event| event.creators(auth_rules).ok());
if let Some((event, creators)) = event.ok().zip(creators) {
room_power_levels_event.user_power_level(event.sender(), creators, auth_rules)
} else {
room_power_levels_event
.get_as_int_or_default(RoomPowerLevelsIntField::UsersDefault, auth_rules)
.map(Into::into)
}
}
/// Whether the given event ID belongs to a power event.
///
/// See the docs of `is_power_event()` for the definition of a power event.
#[tracing::instrument(
name = "is_power_event",
level = "trace",
skip_all,
fields(
?event_id,
)
)]
async fn is_power_event_id<Fetch, Fut, Pdu>(event_id: &EventId, fetch: &Fetch) -> bool
where
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
match fetch(event_id.to_owned()).await {
| Ok(state) => is_power_event(&state),
| _ => false,
}
}

View File

@@ -1,82 +0,0 @@
use std::{collections::HashMap, hash::Hash, iter::IntoIterator};
use futures::{Stream, StreamExt};
use super::{ConflictMap, StateMap};
use crate::validated;
/// Split the unconflicted state map and the conflicted state set.
///
/// Definition in the specification:
///
/// If a given key _K_ is present in every _Si_ with the same value _V_ in each
/// state map, then the pair (_K_, _V_) belongs to the unconflicted state map.
/// Otherwise, _V_ belongs to the conflicted state set.
///
/// It means that, for a given (event type, state key) tuple, if all state maps
/// have the same event ID, it lands in the unconflicted state map, otherwise
/// the event IDs land in the conflicted state set.
///
/// ## Arguments
///
/// * `state_maps` - The incoming states to resolve. Each `StateMap` represents
/// a possible fork in the state of a room.
///
/// ## Returns
///
/// Returns an `(unconflicted_state, conflicted_states)` tuple.
#[tracing::instrument(name = "split", level = "debug", skip_all)]
pub(super) async fn split_conflicted_state<'a, Maps, Id>(
state_maps: Maps,
) -> (StateMap<Id>, ConflictMap<Id>)
where
Maps: Stream<Item = StateMap<Id>>,
Id: Clone + Eq + Hash + Ord + Send + Sync + 'a,
{
let state_maps: Vec<_> = state_maps.collect().await;
let state_ids_est = state_maps.iter().flatten().count();
let state_set_count = state_maps
.iter()
.fold(0_usize, |acc, _| validated!(acc + 1));
let mut occurrences = HashMap::<_, HashMap<_, usize>>::with_capacity(state_ids_est);
for (k, v) in state_maps
.into_iter()
.flat_map(IntoIterator::into_iter)
{
let acc = occurrences
.entry(k.clone())
.or_default()
.entry(v.clone())
.or_default();
*acc = acc.saturating_add(1);
}
let mut unconflicted_state_map = StateMap::new();
let mut conflicted_state_set = ConflictMap::new();
for (k, v) in occurrences {
for (id, occurrence_count) in v {
if occurrence_count == state_set_count {
unconflicted_state_map.insert((k.0.clone(), k.1.clone()), id.clone());
} else {
let conflicts = conflicted_state_set
.entry((k.0.clone(), k.1.clone()))
.or_default();
debug_assert!(
!conflicts.contains(&id),
"Unexpected duplicate conflicted state event"
);
conflicts.push(id.clone());
}
}
}
(unconflicted_state_map, conflicted_state_set)
}

View File

@@ -1,843 +0,0 @@
use std::{collections::HashMap, iter::once};
use futures::StreamExt;
use maplit::hashmap;
use rand::seq::SliceRandom;
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedEventId,
events::{
StateEventType, TimelineEventType,
room::join_rules::{JoinRule, RoomJoinRulesEventContent},
},
int,
room_version_rules::RoomVersionRules,
uint,
};
use serde_json::{json, value::to_raw_value as to_raw_json_value};
use super::{
StateMap,
test_utils::{
INITIAL_EVENTS, TestStore, alice, bob, charlie, do_check, ella, event_id,
member_content_ban, member_content_join, not_found, room_id, to_init_pdu_event,
to_pdu_event, zara,
},
};
use crate::{
debug,
matrix::{Event, EventTypeExt, PduEvent},
utils::stream::IterStream,
};
async fn test_event_sort() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let rules = RoomVersionRules::V6;
let events = INITIAL_EVENTS();
let auth_chain = Default::default();
let sorted_power_events = super::power_sort(&rules, &auth_chain, &async |id| {
events.get(&id).cloned().ok_or_else(not_found)
})
.await
.unwrap();
let sorted_power_events = sorted_power_events
.iter()
.stream()
.map(AsRef::as_ref);
let resolved_power =
super::iterative_auth_check(&rules, sorted_power_events, StateMap::new(), &async |id| {
events.get(&id).cloned().ok_or_else(not_found)
})
.await
.expect("iterative auth check failed on resolved events");
// don't remove any events so we know it sorts them all correctly
let mut events_to_sort = events.keys().cloned().collect::<Vec<_>>();
events_to_sort.shuffle(&mut rand::thread_rng());
let power_level = resolved_power
.get(&(StateEventType::RoomPowerLevels, "".into()))
.cloned();
let events_to_sort = events_to_sort.iter().stream().map(AsRef::as_ref);
let sorted_event_ids = super::mainline_sort(power_level, events_to_sort, &async |id| {
events.get(&id).cloned().ok_or_else(not_found)
})
.await
.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(ToString::to_string)
.collect::<Vec<_>>()
);
}
#[tokio::test]
async 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().await;
}
}
#[tokio::test]
async fn ban_vs_power_level() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let events = &[
to_init_pdu_event(
"PA",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
),
to_init_pdu_event(
"MA",
alice(),
TimelineEventType::RoomMember,
Some(alice().to_string().as_str()),
member_content_join(),
),
to_init_pdu_event(
"MB",
alice(),
TimelineEventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_ban(),
),
to_init_pdu_event(
"PB",
bob(),
TimelineEventType::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::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["PA", "MA", "MB"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(events, edges, expected_state_ids).await;
}
#[tokio::test]
async fn topic_basic() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let events = vec![
to_init_pdu_event(
"T1",
alice(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
to_init_pdu_event(
"PA1",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
),
to_init_pdu_event(
"T2",
alice(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
to_init_pdu_event(
"PA2",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 0 } })).unwrap(),
),
to_init_pdu_event(
"PB",
bob(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
),
to_init_pdu_event(
"T3",
bob(),
TimelineEventType::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::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["PA2", "T2"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(&events, edges, expected_state_ids).await;
}
#[tokio::test]
async fn topic_reset() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let events = &[
to_init_pdu_event(
"T1",
alice(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
to_init_pdu_event(
"PA",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
),
to_init_pdu_event(
"T2",
bob(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
to_init_pdu_event(
"MB",
alice(),
TimelineEventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_ban(),
),
];
let edges = vec![vec!["END", "MB", "T2", "PA", "T1", "START"], vec!["END", "T1"]]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["T1", "MB", "PA"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(events, edges, expected_state_ids).await;
}
#[tokio::test]
async fn join_rule_evasion() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let events = &[
to_init_pdu_event(
"JR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Private)).unwrap(),
),
to_init_pdu_event(
"ME",
ella(),
TimelineEventType::RoomMember,
Some(ella().to_string().as_str()),
member_content_join(),
),
];
let edges = vec![vec!["END", "JR", "START"], vec!["END", "ME", "START"]]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec![event_id("JR")];
do_check(events, edges, expected_state_ids).await;
}
#[tokio::test]
async fn offtopic_power_level() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let events = &[
to_init_pdu_event(
"PA",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
),
to_init_pdu_event(
"PB",
bob(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50, charlie(): 50 } }))
.unwrap(),
),
to_init_pdu_event(
"PC",
charlie(),
TimelineEventType::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::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["PC"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(events, edges, expected_state_ids).await;
}
#[tokio::test]
async fn topic_setting() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let events = vec![
to_init_pdu_event(
"T1",
alice(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
to_init_pdu_event(
"PA1",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
),
to_init_pdu_event(
"T2",
alice(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
to_init_pdu_event(
"PA2",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 0 } })).unwrap(),
),
to_init_pdu_event(
"PB",
bob(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
),
to_init_pdu_event(
"T3",
bob(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
to_init_pdu_event(
"MZ1",
zara(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
to_init_pdu_event(
"T4",
alice(),
TimelineEventType::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::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["T4", "PA2"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(&events, edges, expected_state_ids).await;
}
#[tokio::test]
async fn test_event_map_none() {
_ = 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 auth_chains = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect::<Vec<_>>();
let rules = RoomVersionRules::V1;
let resolved = match super::resolve(
&rules,
state_sets.into_iter().stream(),
auth_chains.into_iter().stream(),
&async |id| ev_map.get(&id).cloned().ok_or_else(not_found),
&async |id| ev_map.contains_key(&id),
false,
)
.await
{
| Ok(state) => state,
| Err(e) => panic!("{e}"),
};
assert_eq!(expected, resolved);
}
#[tokio::test]
#[expect(
clippy::iter_on_single_items,
clippy::iter_on_empty_collections
)]
async fn test_reverse_topological_power_sort() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let graph = hashmap! {
event_id("l") => [event_id("o")].into_iter().collect(),
event_id("m") => [event_id("n"), event_id("o")].into_iter().collect(),
event_id("n") => [event_id("o")].into_iter().collect(),
event_id("o") => [].into_iter().collect(), // "o" has zero outgoing edges but 4 incoming edges
event_id("p") => [event_id("o")].into_iter().collect(),
};
let res = super::super::topological_sort(&graph, &async |_id| {
Ok((int!(0).into(), MilliSecondsSinceUnixEpoch(uint!(0))))
})
.await
.unwrap();
assert_eq!(
vec!["o", "l", "n", "m", "p"],
res.iter()
.map(ToString::to_string)
.map(|s| s.replace('$', "").replace(":foo", ""))
.collect::<Vec<_>>()
);
}
#[tokio::test]
async fn ban_with_auth_chains() {
_ = 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::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["PA", "MB"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(&ban.values().cloned().collect::<Vec<_>>(), edges, expected_state_ids).await;
}
#[tokio::test]
async fn ban_with_auth_chains2() {
_ = 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[&event_id("CREATE")],
&inner[&event_id("IJR")],
&inner[&event_id("IMA")],
&inner[&event_id("IMB")],
&inner[&event_id("IMC")],
&inner[&event_id("MB")],
&inner[&event_id("PA")],
]
.iter()
.map(|ev| {
(
ev.event_type()
.with_state_key(ev.state_key().unwrap()),
ev.event_id.clone(),
)
})
.collect::<StateMap<_>>();
let state_set_b = [
&inner[&event_id("CREATE")],
&inner[&event_id("IJR")],
&inner[&event_id("IMA")],
&inner[&event_id("IMB")],
&inner[&event_id("IMC")],
&inner[&event_id("IME")],
&inner[&event_id("PA")],
]
.iter()
.map(|ev| {
(
ev.event_type()
.with_state_key(ev.state_key().unwrap()),
ev.event_id.clone(),
)
})
.collect::<StateMap<_>>();
let ev_map = &store.0;
let state_sets = [state_set_a, state_set_b];
let auth_chains = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect::<Vec<_>>();
let resolved = match super::resolve(
&RoomVersionRules::V6,
state_sets.into_iter().stream(),
auth_chains.into_iter().stream(),
&async |id| ev_map.get(&id).cloned().ok_or_else(not_found),
&async |id| ev_map.contains_key(&id),
false,
)
.await
{
| Ok(state) => state,
| Err(e) => panic!("{e}"),
};
debug!(
resolved = ?resolved
.iter()
.map(|((ty, key), id)| format!("(({ty}{key:?}), {id})"))
.collect::<Vec<_>>(),
"resolved state",
);
let expected = [
"$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());
}
#[tokio::test]
async fn join_rule_with_auth_chain() {
let join_rule = JOIN_RULE();
let edges = vec![vec!["END", "JR", "START"], vec!["END", "IMZ", "START"]]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["JR"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(&join_rule.values().cloned().collect::<Vec<_>>(), edges, expected_state_ids).await;
}
#[expect(non_snake_case)]
fn BAN_STATE_SET() -> HashMap<OwnedEventId, PduEvent> {
vec![
to_pdu_event(
"PA",
alice(),
TimelineEventType::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(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["END"],
),
to_pdu_event(
"MB",
alice(),
TimelineEventType::RoomMember,
Some(ella().as_str()),
member_content_ban(),
&["CREATE", "IMA", "PB"],
&["PA"],
),
to_pdu_event(
"IME",
ella(),
TimelineEventType::RoomMember,
Some(ella().as_str()),
member_content_join(),
&["CREATE", "IJR", "PA"],
&["MB"],
),
]
.into_iter()
.map(|ev| (ev.event_id.clone(), ev))
.collect()
}
#[expect(non_snake_case)]
fn JOIN_RULE() -> HashMap<OwnedEventId, PduEvent> {
vec![
to_pdu_event(
"JR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&json!({ "join_rule": "invite" })).unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["START"],
),
to_pdu_event(
"IMZ",
zara(),
TimelineEventType::RoomPowerLevels,
Some(zara().as_str()),
member_content_join(),
&["CREATE", "JR", "IPOWER"],
&["START"],
),
]
.into_iter()
.map(|ev| (ev.event_id.clone(), ev))
.collect()
}
macro_rules! state_set {
($($kind:expr => $key:expr => $id:expr),* $(,)?) => {{
let mut x = StateMap::new();
$(
x.insert(($kind, $key.into()), $id);
)*
x
}};
}
#[tokio::test]
async fn split_conflicted_state_set_conflicted_unique_state_keys() {
let (unconflicted, conflicted) = super::split_conflicted_state(
[
state_set![StateEventType::RoomMember => "@a:hs1" => 0],
state_set![StateEventType::RoomMember => "@b:hs1" => 1],
state_set![StateEventType::RoomMember => "@c:hs1" => 2],
]
.into_iter()
.stream(),
)
.await;
let (unconflicted, conflicted): (StateMap<_>, StateMap<_>) =
(unconflicted.into_iter().collect(), conflicted.into_iter().collect());
assert_eq!(unconflicted, StateMap::new());
assert_eq!(conflicted, state_set![
StateEventType::RoomMember => "@a:hs1" => once(0).collect(),
StateEventType::RoomMember => "@b:hs1" => once(1).collect(),
StateEventType::RoomMember => "@c:hs1" => once(2).collect(),
],);
}
#[tokio::test]
async fn split_conflicted_state_set_conflicted_same_state_key() {
let (unconflicted, conflicted) = super::split_conflicted_state(
[
state_set![StateEventType::RoomMember => "@a:hs1" => 0],
state_set![StateEventType::RoomMember => "@a:hs1" => 1],
state_set![StateEventType::RoomMember => "@a:hs1" => 2],
]
.into_iter()
.stream(),
)
.await;
let (unconflicted, mut conflicted): (StateMap<_>, StateMap<_>) =
(unconflicted.into_iter().collect(), conflicted.into_iter().collect());
// HashMap iteration order is random, so sort this before asserting on it
for v in conflicted.values_mut() {
v.sort_unstable();
}
assert_eq!(unconflicted, StateMap::new());
assert_eq!(conflicted, state_set![
StateEventType::RoomMember => "@a:hs1" => [0, 1, 2].into_iter().collect(),
],);
}
#[tokio::test]
async fn split_conflicted_state_set_unconflicted() {
let (unconflicted, conflicted) = super::split_conflicted_state(
[
state_set![StateEventType::RoomMember => "@a:hs1" => 0],
state_set![StateEventType::RoomMember => "@a:hs1" => 0],
state_set![StateEventType::RoomMember => "@a:hs1" => 0],
]
.into_iter()
.stream(),
)
.await;
let (unconflicted, conflicted): (StateMap<_>, StateMap<_>) =
(unconflicted.into_iter().collect(), conflicted.into_iter().collect());
assert_eq!(unconflicted, state_set![
StateEventType::RoomMember => "@a:hs1" => 0,
],);
assert_eq!(conflicted, StateMap::new());
}
#[tokio::test]
async fn split_conflicted_state_set_mixed() {
let (unconflicted, conflicted) = super::split_conflicted_state(
[
state_set![StateEventType::RoomMember => "@a:hs1" => 0],
state_set![
StateEventType::RoomMember => "@a:hs1" => 0,
StateEventType::RoomMember => "@b:hs1" => 1,
],
state_set![
StateEventType::RoomMember => "@a:hs1" => 0,
StateEventType::RoomMember => "@c:hs1" => 2,
],
]
.into_iter()
.stream(),
)
.await;
let (unconflicted, conflicted): (StateMap<_>, StateMap<_>) =
(unconflicted.into_iter().collect(), conflicted.into_iter().collect());
assert_eq!(unconflicted, state_set![
StateEventType::RoomMember => "@a:hs1" => 0,
],);
assert_eq!(conflicted, state_set![
StateEventType::RoomMember => "@b:hs1" => once(1).collect(),
StateEventType::RoomMember => "@c:hs1" => once(2).collect(),
],);
}

View File

@@ -1,923 +0,0 @@
use std::{
borrow::Borrow,
collections::HashMap,
pin::Pin,
slice,
sync::{
Arc,
atomic::{AtomicU64, Ordering::SeqCst},
},
};
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId, event_id,
events::{
StateEventType, TimelineEventType,
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
},
},
int, room_id,
room_version_rules::{AuthorizationRules, RoomVersionRules},
uint, user_id,
};
use serde_json::{
json,
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
};
use super::{AuthSet, StateMap, auth_types_for_event, events::RoomCreateEvent};
use crate::{
Error, Result, err, info,
matrix::{
Event, EventHash, EventTypeExt, PduEvent, StateKey,
state_res::topological_sort::ReferencedIds,
},
utils::stream::IterStream,
};
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
pub(super) fn not_found() -> Error { err!(Request(NotFound("Test event not found"))) }
pub(super) fn event_not_found(event_id: &EventId) -> Error {
err!(Request(NotFound("Test event not found: {event_id:?}")))
}
pub(super) fn state_not_found(ty: &StateEventType, sk: &str) -> Error {
err!(Request(NotFound("Test state not found: ({ty:?},{sk:?})")))
}
pub(super) async fn do_check(
events: &[PduEvent],
edges: Vec<Vec<OwnedEventId>>,
expected_state_ids: Vec<OwnedEventId>,
) {
// 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::<OwnedEventId, ReferencedIds>::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(), Default::default());
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_default()
.push(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_default()
.push(b.clone());
}
}
}
// event_id -> PduEvent
let mut event_map: HashMap<OwnedEventId, PduEvent> = HashMap::new();
// event_id -> StateMap<OwnedEventId>
let mut state_at_event: HashMap<OwnedEventId, StateMap<OwnedEventId>> = HashMap::new();
// Resolve the current state and add it to the state_at_event map then continue
// on in "time"
for node in super::topological_sort(&graph, &async |_id| {
Ok((int!(0).into(), MilliSecondsSinceUnixEpoch(uint!(0))))
})
.await
.unwrap()
{
let fake_event = &fake_event_map[&node];
let event_id = fake_event.event_id().to_owned();
let prev_events = &graph[&node];
let state_before: StateMap<OwnedEventId> = if prev_events.is_empty() {
StateMap::new()
} else if prev_events.len() == 1 {
state_at_event[prev_events.iter().next().unwrap()].clone()
} else {
let state_sets = prev_events
.iter()
.filter_map(|k| state_at_event.get(k).cloned())
.collect::<Vec<_>>();
info!(
"{:#?}",
state_sets
.iter()
.map(|map| map
.iter()
.map(|((ty, key), id)| format!("(({ty}{key:?}), {id})"))
.collect::<Vec<_>>())
.collect::<Vec<_>>()
);
let auth_chain_sets = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect::<Vec<_>>();
let state_sets = state_sets.into_iter().stream();
let rules = RoomVersionRules::V6;
let resolved = super::resolve(
&rules,
state_sets,
auth_chain_sets.into_iter().stream(),
&async |id| event_map.get(&id).cloned().ok_or_else(not_found),
&async |id| event_map.contains_key(&id),
false,
)
.await;
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.clone());
let auth_types = auth_types_for_event(
fake_event.event_type(),
fake_event.sender(),
fake_event.state_key(),
fake_event.content(),
&AuthorizationRules::V6,
false,
)
.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::<Vec<_>>(),
);
// 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.clone(), store.0[ev_id].clone());
}
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::<Vec<_>>()
)
});
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".into())
})
.map(|(k, v)| (k.clone(), v.clone()))
.collect::<StateMap<OwnedEventId>>();
assert_eq!(expected_state, end_state);
}
pub(super) struct TestStore(pub(super) HashMap<OwnedEventId, PduEvent>);
impl TestStore {
pub(super) fn get_event(&self, _: &RoomId, event_id: &EventId) -> Result<PduEvent> {
self.0
.get(event_id)
.cloned()
.ok_or_else(|| event_not_found(event_id))
}
/// Returns a Vec of the related auth events to the given `event`.
pub(super) fn auth_event_ids(
&self,
room_id: &RoomId,
event_ids: Vec<OwnedEventId>,
) -> Result<AuthSet<OwnedEventId>> {
let mut result = AuthSet::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
impl TestStore {
pub(super) fn set_up(
&mut self,
) -> (StateMap<OwnedEventId>, StateMap<OwnedEventId>, StateMap<OwnedEventId>) {
let create_event = to_pdu_event::<&EventId>(
"CREATE",
alice(),
TimelineEventType::RoomCreate,
Some(""),
to_raw_json_value(&json!({ "creator": alice() })).unwrap(),
&[],
&[],
);
let cre = create_event.event_id().to_owned();
self.0.insert(cre.clone(), create_event.clone());
let alice_mem = to_pdu_event(
"IMA",
alice(),
TimelineEventType::RoomMember,
Some(alice().as_str()),
member_content_join(),
slice::from_ref(&cre),
slice::from_ref(&cre),
);
self.0
.insert(alice_mem.event_id().to_owned(), alice_mem.clone());
let join_rules = to_pdu_event(
"IJR",
alice(),
TimelineEventType::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(),
TimelineEventType::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(),
TimelineEventType::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::<StateMap<_>>();
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::<StateMap<_>>();
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::<StateMap<_>>();
(state_at_bob, state_at_charlie, expected)
}
}
pub(super) fn event_id(id: &str) -> OwnedEventId {
if id.contains('$') {
return id.try_into().unwrap();
}
format!("${id}:foo").try_into().unwrap()
}
pub(super) fn alice() -> &'static UserId { user_id!("@alice:foo") }
pub(super) fn bob() -> &'static UserId { user_id!("@bob:foo") }
pub(super) fn charlie() -> &'static UserId { user_id!("@charlie:foo") }
pub(super) fn ella() -> &'static UserId { user_id!("@ella:foo") }
pub(super) fn zara() -> &'static UserId { user_id!("@zara:foo") }
pub(super) fn room_id() -> &'static RoomId { room_id!("!test:foo") }
pub(crate) fn hydra_room_id() -> &'static RoomId { room_id!("!CREATE") }
pub(super) fn member_content_ban() -> Box<RawJsonValue> {
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Ban)).unwrap()
}
pub(super) fn member_content_join() -> Box<RawJsonValue> {
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap()
}
pub(super) fn to_init_pdu_event(
id: &str,
sender: &UserId,
ev_type: TimelineEventType,
state_key: Option<&str>,
content: Box<RawJsonValue>,
) -> PduEvent {
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
let state_key = state_key.map(ToOwned::to_owned);
let id = if id.contains('$') {
id.to_owned()
} else {
format!("${id}:foo")
};
PduEvent {
event_id: id.try_into().unwrap(),
room_id: room_id().to_owned(),
sender: sender.to_owned(),
origin: None,
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
kind: ev_type,
content,
redacts: None,
unsigned: None,
auth_events: Default::default(),
prev_events: Default::default(),
depth: uint!(0),
hashes: EventHash::default(),
signatures: None,
rejected: false,
}
}
pub(super) fn to_pdu_event<S>(
id: &str,
sender: &UserId,
ev_type: TimelineEventType,
state_key: Option<&str>,
content: Box<RawJsonValue>,
auth_events: &[S],
prev_events: &[S],
) -> PduEvent
where
S: AsRef<str>,
{
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
let state_key = state_key.map(ToOwned::to_owned);
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();
PduEvent {
event_id: id.try_into().unwrap(),
room_id: room_id().to_owned(),
sender: sender.to_owned(),
origin: None,
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
kind: ev_type,
content,
redacts: None,
unsigned: None,
auth_events,
prev_events,
depth: uint!(0),
hashes: EventHash::default(),
signatures: None,
rejected: false,
}
}
/// Same as `to_pdu_event()`, but uses the default m.room.create event ID to
/// generate the room ID.
pub(super) fn to_hydra_pdu_event<S>(
id: &str,
sender: &UserId,
ev_type: TimelineEventType,
state_key: Option<&str>,
content: Box<RawJsonValue>,
auth_events: &[S],
prev_events: &[S],
) -> PduEvent
where
S: AsRef<str>,
{
fn event_id(id: &str) -> OwnedEventId {
if id.contains('$') {
id.try_into().unwrap()
} else {
format!("${id}").try_into().unwrap()
}
}
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
let state_key = state_key.map(ToOwned::to_owned);
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();
PduEvent {
event_id: event_id(id),
room_id: hydra_room_id().to_owned(),
sender: sender.to_owned(),
origin: None,
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
kind: ev_type,
content,
redacts: None,
unsigned: None,
auth_events,
prev_events,
depth: uint!(0),
hashes: EventHash::default(),
signatures: None,
rejected: false,
}
}
pub(super) fn room_redaction_pdu_event<S>(
id: &str,
sender: &UserId,
redacts: OwnedEventId,
content: Box<RawJsonValue>,
auth_events: &[S],
prev_events: &[S],
) -> PduEvent
where
S: AsRef<str>,
{
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();
PduEvent {
event_id: id.try_into().unwrap(),
room_id: room_id().to_owned(),
sender: sender.to_owned(),
origin: None,
origin_server_ts: ts.try_into().unwrap(),
state_key: None,
kind: TimelineEventType::RoomRedaction,
content,
redacts: Some(redacts),
unsigned: None,
auth_events,
prev_events,
depth: uint!(0),
hashes: EventHash::default(),
signatures: None,
rejected: false,
}
}
pub(super) fn room_create_hydra_pdu_event(
id: &str,
sender: &UserId,
content: Box<RawJsonValue>,
) -> PduEvent {
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
let eid = if id.contains('$') {
id.to_owned()
} else {
format!("${id}")
};
let rid = if id.contains('!') {
id.to_owned()
} else {
format!("!{id}")
};
PduEvent {
event_id: eid.try_into().unwrap(),
room_id: rid.try_into().unwrap(),
sender: sender.to_owned(),
origin: None,
origin_server_ts: ts.try_into().unwrap(),
state_key: Some(StateKey::new()),
kind: TimelineEventType::RoomCreate,
content,
redacts: None,
unsigned: None,
auth_events: Default::default(),
prev_events: Default::default(),
depth: uint!(0),
hashes: EventHash::default(),
signatures: None,
rejected: false,
}
}
// all graphs start with these input events
#[expect(non_snake_case)]
pub(super) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, PduEvent> {
vec![
to_pdu_event::<&EventId>(
"CREATE",
alice(),
TimelineEventType::RoomCreate,
Some(""),
to_raw_json_value(&json!({ "creator": alice() })).unwrap(),
&[],
&[],
),
to_pdu_event(
"IMA",
alice(),
TimelineEventType::RoomMember,
Some(alice().as_str()),
member_content_join(),
&["CREATE"],
&["CREATE"],
),
to_pdu_event(
"IPOWER",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100 } })).unwrap(),
&["CREATE", "IMA"],
&["IMA"],
),
to_pdu_event(
"IJR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["IPOWER"],
),
to_pdu_event(
"IMB",
bob(),
TimelineEventType::RoomMember,
Some(bob().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IJR"],
),
to_pdu_event(
"IMC",
charlie(),
TimelineEventType::RoomMember,
Some(charlie().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IMB"],
),
to_pdu_event::<&EventId>(
"START",
charlie(),
TimelineEventType::RoomMessage,
Some("dummy"),
to_raw_json_value(&json!({})).unwrap(),
&[],
&[],
),
to_pdu_event::<&EventId>(
"END",
charlie(),
TimelineEventType::RoomMessage,
Some("dummy"),
to_raw_json_value(&json!({})).unwrap(),
&[],
&[],
),
]
.into_iter()
.map(|ev| (ev.event_id().to_owned(), ev))
.collect()
}
/// Batch of initial events to use for incoming events from room version
/// `org.matrix.hydra.11` onwards.
#[expect(non_snake_case)]
pub(super) fn INITIAL_HYDRA_EVENTS() -> HashMap<OwnedEventId, PduEvent> {
vec![
room_create_hydra_pdu_event(
"CREATE",
alice(),
to_raw_json_value(&json!({ "room_version": "org.matrix.hydra.11" })).unwrap(),
),
to_hydra_pdu_event(
"IMA",
alice(),
TimelineEventType::RoomMember,
Some(alice().as_str()),
member_content_join(),
&["CREATE"],
&["CREATE"],
),
to_hydra_pdu_event(
"IPOWER",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
&["CREATE", "IMA"],
&["IMA"],
),
to_hydra_pdu_event(
"IJR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["IPOWER"],
),
to_hydra_pdu_event(
"IMB",
bob(),
TimelineEventType::RoomMember,
Some(bob().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IJR"],
),
to_hydra_pdu_event(
"IMC",
charlie(),
TimelineEventType::RoomMember,
Some(charlie().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IMB"],
),
to_hydra_pdu_event::<&EventId>(
"START",
charlie(),
TimelineEventType::RoomMessage,
Some("dummy"),
to_raw_json_value(&json!({})).unwrap(),
&[],
&[],
),
to_hydra_pdu_event::<&EventId>(
"END",
charlie(),
TimelineEventType::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
#[expect(non_snake_case)]
pub(super) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap<OwnedEventId, PduEvent> {
vec![to_pdu_event::<&EventId>(
"CREATE",
alice(),
TimelineEventType::RoomCreate,
Some(""),
to_raw_json_value(&json!({ "creator": alice() })).unwrap(),
&[],
&[],
)]
.into_iter()
.map(|ev| (ev.event_id().to_owned(), ev))
.collect()
}
#[expect(non_snake_case)]
pub(super) fn INITIAL_EDGES() -> Vec<OwnedEventId> {
vec!["START", "IMC", "IMB", "IJR", "IPOWER", "IMA", "CREATE"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>()
}
pub(super) fn init_subscriber() -> tracing::dispatcher::DefaultGuard {
tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
)
}
/// Wrapper around a state map.
pub(super) struct TestStateMap(HashMap<StateEventType, HashMap<String, PduEvent>>);
impl TestStateMap {
/// Construct a `TestStateMap` from the given event map.
pub(super) fn new(events: &HashMap<OwnedEventId, PduEvent>) -> Arc<Self> {
let mut state_map: HashMap<StateEventType, HashMap<String, PduEvent>> = HashMap::new();
for event in events.values() {
let event_type = StateEventType::from(event.event_type().to_string());
state_map
.entry(event_type)
.or_default()
.insert(event.state_key().unwrap().to_owned(), event.clone());
}
Arc::new(Self(state_map))
}
/// Get the event with the given event type and state key.
pub(super) fn get(
self: &Arc<Self>,
event_type: &StateEventType,
state_key: &str,
) -> Result<PduEvent> {
self.0
.get(event_type)
.ok_or_else(|| state_not_found(event_type, state_key))?
.get(state_key)
.cloned()
.ok_or_else(|| state_not_found(event_type, state_key))
}
/// A function to get a state event from this map.
pub(super) fn fetch_state_fn(
self: &Arc<Self>,
) -> impl Fn(StateEventType, StateKey) -> Pin<Box<dyn Future<Output = Result<PduEvent>> + Send>>
{
move |event_type: StateEventType, state_key: StateKey| {
let s = self.clone();
Box::pin(async move { s.get(&event_type, state_key.as_str()) })
}
}
/// The `m.room.create` event contained in this map.
///
/// Panics if there is no `m.room.create` event in this map.
pub(super) fn room_create_event(self: &Arc<Self>) -> RoomCreateEvent<PduEvent> {
RoomCreateEvent::new(self.get(&StateEventType::RoomCreate, "").unwrap())
}
}
/// Create an `m.room.third_party_invite` event with the given sender.
pub(super) fn room_third_party_invite(sender: &UserId) -> PduEvent {
let content = json!({
"display_name": "o...@g...",
"key_validity_url": "https://identity.local/_matrix/identity/v2/pubkey/isvalid",
"public_key": "Gb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE",
"public_keys": [
{
"key_validity_url": "https://identity.local/_matrix/identity/v2/pubkey/isvalid",
"public_key": "Gb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE"
},
{
"key_validity_url": "https://identity.local/_matrix/identity/v2/pubkey/ephemeral/isvalid",
"public_key": "Kxdvv7lo0O6JVI7yimFgmYPfpLGnctcpYjuypP5zx/c"
}
]
});
to_pdu_event(
"THIRDPARTY",
sender,
TimelineEventType::RoomThirdPartyInvite,
Some("somerandomtoken"),
to_raw_json_value(&content).unwrap(),
&["CREATE", "IJR", "IPOWER"],
&["IPOWER"],
)
}

View File

@@ -1,173 +0,0 @@
//! Sorts the given event graph using reverse topological power ordering.
//!
//! Definition in the specification:
//!
//! The reverse topological power ordering of a set of events is the
//! lexicographically smallest topological ordering based on the DAG formed by
//! referenced events (prev or auth, determined by caller). The reverse
//! topological power ordering is ordered from earliest event to latest. For
//! comparing two equal topological orderings to determine which is the
//! lexicographically smallest, the following comparison relation on events is
//! used: for events x and y, x < y if
//!
//! 1. xs sender has greater power level than ys sender, when looking at their
//! respective referenced events; or
//! 2. the senders have the same power level, but xs origin_server_ts is less
//! than ys origin_server_ts; or
//! 3. the senders have the same power level and the events have the same
//! origin_server_ts, but xs event_id is less than ys event_id.
//!
//! The reverse topological power ordering can be found by sorting the events
//! using Kahns algorithm for topological sorting, and at each step selecting,
//! among all the candidate vertices, the smallest vertex using the above
//! comparison relation.
use std::{
cmp::{Ordering, Reverse},
collections::{BinaryHeap, HashMap},
};
use futures::TryStreamExt;
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedEventId, events::room::power_levels::UserPowerLevel,
};
use crate::{
Error, Result, is_not_equal_to, smallvec::SmallVec, utils::stream::IterStream, validated,
};
pub type ReferencedIds = SmallVec<[OwnedEventId; 3]>;
type PduInfo = (UserPowerLevel, MilliSecondsSinceUnixEpoch);
#[derive(PartialEq, Eq)]
struct TieBreaker {
power_level: UserPowerLevel,
origin_server_ts: MilliSecondsSinceUnixEpoch,
event_id: OwnedEventId,
}
// NOTE: the power level comparison is "backwards" intentionally.
impl Ord for TieBreaker {
fn cmp(&self, other: &Self) -> Ordering {
other
.power_level
.cmp(&self.power_level)
.then(self.origin_server_ts.cmp(&other.origin_server_ts))
.then(self.event_id.cmp(&other.event_id))
}
}
impl PartialOrd for TieBreaker {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
/// Sorts the given event graph using reverse topological power ordering.
///
/// ## Arguments
///
/// * `graph` - The graph to sort. A map of event ID to its referenced events
/// that are in the full conflicted set.
///
/// * `query` - Function to obtain a (power level, origin_server_ts) of an event
/// for breaking ties.
///
/// ## Returns
///
/// Returns the ordered list of event IDs from earliest to latest.
#[tracing::instrument(
level = "debug",
skip_all,
fields(
graph = graph.len(),
)
)]
#[expect(clippy::implicit_hasher, clippy::or_fun_call)]
pub async fn topological_sort<Query, Fut>(
graph: &HashMap<OwnedEventId, ReferencedIds>,
query: &Query,
) -> Result<Vec<OwnedEventId>>
where
Query: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<PduInfo>> + Send,
{
let query = async |event_id: OwnedEventId| {
let (power_level, origin_server_ts) = query(event_id.clone()).await?;
Ok::<_, Error>(TieBreaker { power_level, origin_server_ts, event_id })
};
let max_edges = graph
.values()
.map(ReferencedIds::len)
.fold(graph.len(), |a, c| validated!(a + c));
// We consider that the DAG is directed from most recent events to oldest
// events, so an event is an incoming edge to its referenced events.
// zero_outdegs: Vec of events that have an outdegree of zero (no outgoing
// edges), i.e. the oldest events. incoming_edges_map: Map of event to the list
// of events that reference it in its referenced events.
let init = (
Vec::with_capacity(max_edges),
HashMap::<OwnedEventId, ReferencedIds>::with_capacity(max_edges),
);
// Populate the list of events with an outdegree of zero, and the map of
// incoming edges.
let (zero_outdeg, incoming_edges) = graph
.iter()
.try_stream()
.try_fold(
init,
async |(mut zero_outdeg, mut incoming_edges), (event_id, outgoing_edges)| {
if outgoing_edges.is_empty() {
// `Reverse` because `BinaryHeap` sorts largest -> smallest and we need
// smallest -> largest.
zero_outdeg.push(Reverse(query(event_id.clone()).await?));
}
for referenced_event_id in outgoing_edges {
let references = incoming_edges
.entry(referenced_event_id.into())
.or_default();
if !references.contains(event_id) {
references.push(event_id.into());
}
}
Ok((zero_outdeg, incoming_edges))
},
)
.await?;
// Map of event to the list of events in its referenced events.
let mut outgoing_edges_map = graph.clone();
// Use a BinaryHeap to keep the events with an outdegree of zero sorted.
let mut heap = BinaryHeap::from(zero_outdeg);
let mut sorted = Vec::with_capacity(max_edges);
// Apply Kahn's algorithm.
// https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm
while let Some(Reverse(item)) = heap.pop() {
for parent_id in incoming_edges
.get(&item.event_id)
.unwrap_or(&ReferencedIds::new())
{
let outgoing_edges = outgoing_edges_map
.get_mut(parent_id)
.expect("outgoing_edges should contain all event_ids");
outgoing_edges.retain(is_not_equal_to!(&item.event_id));
if !outgoing_edges.is_empty() {
continue;
}
// Push on the heap once all the outgoing edges have been removed.
heap.push(Reverse(query(parent_id.clone()).await?));
}
sorted.push(item.event_id);
}
Ok(sorted)
}

View File

@@ -25,9 +25,7 @@ pub use ::tracing;
pub use config::Config;
pub use error::Error;
pub use info::{rustc_flags_capture, version, version::version};
pub use matrix::{
Event, EventTypeExt, Pdu, PduCount, PduEvent, PduId, RoomVersion, pdu, state_res,
};
pub use matrix::{Event, EventTypeExt, Pdu, PduCount, PduEvent, PduId, RoomVersion, pdu};
pub use server::Server;
pub use utils::{ctor, dtor, implement, result, result::Result};

View File

@@ -1,214 +0,0 @@
[
{
"test-comments": [
"NOTE: Unlike the v11 pdus, alice is never in `m.room.power_levels`.",
"This is due to MSC4289 forbidding room creators from being in the",
"`users` field of `m.room.power_levels`."
],
"event_id": "$00-m-room-create",
"room_id": "!00-m-room-create",
"sender": "@alice:example.com",
"type": "m.room.create",
"content": {
"room_version": "12"
},
"state_key": "",
"origin_server_ts": 0,
"depth": 0,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [],
"auth_events": []
},
{
"event_id": "$00-m-room-member-join-alice",
"room_id": "!00-m-room-create",
"sender": "@alice:example.com",
"type": "m.room.member",
"content": {
"displayname": "alice",
"membership": "join"
},
"state_key": "@alice:example.com",
"origin_server_ts": 1,
"depth": 1,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-create"
],
"auth_events": []
},
{
"event_id": "$00-m-room-power_levels",
"room_id": "!00-m-room-create",
"sender": "@alice:example.com",
"type": "m.room.power_levels",
"content": {},
"state_key": "",
"origin_server_ts": 2,
"depth": 2,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-member-join-alice"
],
"auth_events": [
"$00-m-room-member-join-alice"
]
},
{
"event_id": "$00-m-room-join_rules",
"room_id": "!00-m-room-create",
"sender": "@alice:example.com",
"type": "m.room.join_rules",
"content": {
"join_rule": "public"
},
"state_key": "",
"origin_server_ts": 3,
"depth": 3,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-power_levels"
],
"auth_events": [
"$00-m-room-member-join-alice",
"$00-m-room-power_levels"
]
},
{
"event_id": "$00-m-room-member-join-bob",
"room_id": "!00-m-room-create",
"sender": "@bob:example.com",
"type": "m.room.member",
"content": {
"displayname": "bob",
"membership": "join"
},
"state_key": "@bob:example.com",
"origin_server_ts": 4,
"depth": 4,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-join_rules"
],
"auth_events": [
"$00-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$00-m-room-member-join-charlie",
"room_id": "!00-m-room-create",
"sender": "@charlie:example.com",
"type": "m.room.member",
"content": {
"displayname": "charlie",
"membership": "join"
},
"state_key": "@charlie:example.com",
"origin_server_ts": 5,
"depth": 5,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-member-join-bob"
],
"auth_events": [
"$00-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$01-m-room-join_rules",
"room_id": "!00-m-room-create",
"sender": "@alice:example.com",
"type": "m.room.join_rules",
"content": {
"join_rule": "invite"
},
"state_key": "",
"origin_server_ts": 6,
"depth": 6,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-member-join-charlie"
],
"auth_events": [
"$00-m-room-power_levels",
"$00-m-room-member-join-alice"
]
},
{
"event_id": "$01-m-room-member-leave-alice",
"room_id": "!00-m-room-create",
"sender": "@alice:example.com",
"type": "m.room.member",
"content": {
"displayname": "alice",
"membership": "leave"
},
"state_key": "@alice:example.com",
"origin_server_ts": 7,
"depth": 7,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$01-m-room-join_rules"
],
"auth_events": [
"$00-m-room-power_levels",
"$00-m-room-member-join-alice"
]
},
{
"event_id": "$01-m-room-member-change-display-name-bob",
"room_id": "!00-m-room-create",
"sender": "@bob:example.com",
"type": "m.room.member",
"content": {
"displayname": "bob++",
"membership": "join"
},
"state_key": "@bob:example.com",
"origin_server_ts": 8,
"depth": 8,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$01-m-room-member-leave-alice"
],
"auth_events": [
"$00-m-room-power_levels",
"$00-m-room-member-join-bob",
"$01-m-room-join_rules"
]
},
{
"event_id": "$01-m-room-member-change-display-name-charlie",
"room_id": "!00-m-room-create",
"sender": "@charlie:example.com",
"type": "m.room.member",
"content": {
"displayname": "charlie++",
"membership": "join"
},
"state_key": "@charlie:example.com",
"origin_server_ts": 9,
"depth": 9,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$01-m-room-member-leave-alice"
],
"auth_events": [
"$00-m-room-power_levels",
"$00-m-room-member-join-charlie",
"$01-m-room-join_rules"
]
}
]

View File

@@ -1,223 +0,0 @@
[
{
"event_id": "$00-m-room-create",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.create",
"content": {
"room_version": "11"
},
"state_key": "",
"origin_server_ts": 0,
"depth": 0,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [],
"auth_events": []
},
{
"event_id": "$00-m-room-member-join-alice",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.member",
"content": {
"displayname": "alice",
"membership": "join"
},
"state_key": "@alice:example.com",
"origin_server_ts": 1,
"depth": 1,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-create"
],
"auth_events": [
"$00-m-room-create"
]
},
{
"event_id": "$00-m-room-power_levels",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.power_levels",
"content": {
"users": {
"@alice:example.com": 100
}
},
"state_key": "",
"origin_server_ts": 2,
"depth": 2,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-member-join-alice"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-member-join-alice"
]
},
{
"event_id": "$00-m-room-join_rules",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.join_rules",
"content": {
"join_rule": "public"
},
"state_key": "",
"origin_server_ts": 3,
"depth": 3,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-power_levels"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-member-join-alice",
"$00-m-room-power_levels"
]
},
{
"event_id": "$00-m-room-member-join-bob",
"room_id": "!room:example.com",
"sender": "@bob:example.com",
"type": "m.room.member",
"content": {
"displayname": "bob",
"membership": "join"
},
"state_key": "@bob:example.com",
"origin_server_ts": 4,
"depth": 4,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-join_rules"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$00-m-room-member-join-charlie",
"room_id": "!room:example.com",
"sender": "@charlie:example.com",
"type": "m.room.member",
"content": {
"displayname": "charlie",
"membership": "join"
},
"state_key": "@charlie:example.com",
"origin_server_ts": 5,
"depth": 5,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-member-join-bob"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$01-m-room-join_rules",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.join_rules",
"content": {
"join_rule": "invite"
},
"state_key": "",
"origin_server_ts": 6,
"depth": 6,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-member-join-charlie"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-member-join-alice"
]
},
{
"event_id": "$01-m-room-member-leave-alice",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.member",
"content": {
"displayname": "alice",
"membership": "leave"
},
"state_key": "@alice:example.com",
"origin_server_ts": 7,
"depth": 7,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$01-m-room-join_rules"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-member-join-alice"
]
},
{
"event_id": "$01-m-room-member-change-display-name-bob",
"room_id": "!room:example.com",
"sender": "@bob:example.com",
"type": "m.room.member",
"content": {
"displayname": "bob++",
"membership": "join"
},
"state_key": "@bob:example.com",
"origin_server_ts": 8,
"depth": 8,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$01-m-room-member-leave-alice"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-member-join-bob",
"$01-m-room-join_rules"
]
},
{
"event_id": "$01-m-room-member-change-display-name-charlie",
"room_id": "!room:example.com",
"sender": "@charlie:example.com",
"type": "m.room.member",
"content": {
"displayname": "charlie++",
"membership": "join"
},
"state_key": "@charlie:example.com",
"origin_server_ts": 9,
"depth": 9,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$01-m-room-member-leave-alice"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-member-join-charlie",
"$01-m-room-join_rules"
]
}
]

View File

@@ -1,8 +0,0 @@
[
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-member-join-charlie",
"$01-m-room-join_rules",
"$01-m-room-member-leave-alice",
"$01-m-room-member-change-display-name-bob"
]

View File

@@ -1,8 +0,0 @@
[
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-join_rules",
"$00-m-room-member-join-bob",
"$01-m-room-member-leave-alice",
"$01-m-room-member-change-display-name-charlie"
]

View File

@@ -1,239 +0,0 @@
[
{
"test-comments": [
"NOTE: Unlike the v11 pdus, alice is never in `m.room.power_levels`.",
"This is due to MSC4289 forbidding room creators from being in the",
"`users` field of `m.room.power_levels`."
],
"event_id": "$00-m-room-create",
"room_id": "!00-m-room-create",
"sender": "@alice:example.com",
"type": "m.room.create",
"content": {
"room_version": "12"
},
"state_key": "",
"origin_server_ts": 0,
"depth": 0,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [],
"auth_events": []
},
{
"event_id": "$00-m-room-member-join-alice",
"room_id": "!00-m-room-create",
"sender": "@alice:example.com",
"type": "m.room.member",
"content": {
"displayname": "alice",
"membership": "join"
},
"state_key": "@alice:example.com",
"origin_server_ts": 1,
"depth": 1,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-create"
],
"auth_events": []
},
{
"event_id": "$00-m-room-power_levels",
"room_id": "!00-m-room-create",
"sender": "@alice:example.com",
"type": "m.room.power_levels",
"content": {},
"state_key": "",
"origin_server_ts": 2,
"depth": 2,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-member-join-alice"
],
"auth_events": [
"$00-m-room-member-join-alice"
]
},
{
"event_id": "$00-m-room-join_rules",
"room_id": "!00-m-room-create",
"sender": "@alice:example.com",
"type": "m.room.join_rules",
"content": {
"join_rule": "public"
},
"state_key": "",
"origin_server_ts": 3,
"depth": 3,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-power_levels"
],
"auth_events": [
"$00-m-room-member-join-alice",
"$00-m-room-power_levels"
]
},
{
"event_id": "$00-m-room-member-join-bob",
"room_id": "!00-m-room-create",
"sender": "@bob:example.com",
"type": "m.room.member",
"content": {
"displayname": "bob",
"membership": "join"
},
"state_key": "@bob:example.com",
"origin_server_ts": 4,
"depth": 4,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-join_rules"
],
"auth_events": [
"$00-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$00-m-room-member-join-charlie",
"room_id": "!00-m-room-create",
"sender": "@charlie:example.com",
"type": "m.room.member",
"content": {
"displayname": "charlie",
"membership": "join"
},
"state_key": "@charlie:example.com",
"origin_server_ts": 5,
"depth": 5,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-member-join-bob"
],
"auth_events": [
"$00-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$01-m-room-power_levels",
"room_id": "!00-m-room-create",
"sender": "@alice:example.com",
"type": "m.room.power_levels",
"content": {
"users": {
"@bob:example.com": 50
}
},
"state_key": "",
"origin_server_ts": 6,
"depth": 6,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-member-join-charlie"
],
"auth_events": [
"$00-m-room-power_levels",
"$00-m-room-member-join-alice"
]
},
{
"event_id": "$02-m-room-power_levels",
"room_id": "!00-m-room-create",
"sender": "@bob:example.com",
"type": "m.room.power_levels",
"content": {
"users": {
"@bob:example.com": 50,
"@charlie:example.com": 50
}
},
"state_key": "",
"origin_server_ts": 7,
"depth": 7,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$01-m-room-power_levels"
],
"auth_events": [
"$01-m-room-power_levels",
"$00-m-room-member-join-bob"
]
},
{
"event_id": "$00-m-room-member-join-zara",
"room_id": "!00-m-room-create",
"sender": "@zara:example.com",
"type": "m.room.member",
"content": {
"displayname": "zara",
"membership": "join"
},
"state_key": "@zara:example.com",
"origin_server_ts": 8,
"depth": 8,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$02-m-room-power_levels"
],
"auth_events": [
"$02-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$00-m-room-member-join-eve",
"room_id": "!00-m-room-create",
"sender": "@eve:example.com",
"type": "m.room.member",
"content": {
"displayname": "eve",
"membership": "join"
},
"state_key": "@eve:example.com",
"origin_server_ts": 9,
"depth": 9,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$02-m-room-power_levels"
],
"auth_events": [
"$02-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$01-m-room-member-change-display-name-eve",
"room_id": "!00-m-room-create",
"sender": "@eve:example.com",
"type": "m.room.member",
"content": {
"displayname": "eve++",
"membership": "join"
},
"state_key": "@eve:example.com",
"origin_server_ts": 9,
"depth": 9,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-member-join-eve"
],
"auth_events": [
"$02-m-room-power_levels",
"$00-m-room-member-join-eve",
"$00-m-room-join_rules"
]
}
]

View File

@@ -1,251 +0,0 @@
[
{
"event_id": "$00-m-room-create",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.create",
"content": {
"room_version": "11"
},
"state_key": "",
"origin_server_ts": 0,
"depth": 0,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [],
"auth_events": []
},
{
"event_id": "$00-m-room-member-join-alice",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.member",
"content": {
"displayname": "alice",
"membership": "join"
},
"state_key": "@alice:example.com",
"origin_server_ts": 1,
"depth": 1,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-create"
],
"auth_events": [
"$00-m-room-create"
]
},
{
"event_id": "$00-m-room-power_levels",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.power_levels",
"content": {
"users": {
"@alice:example.com": 100
}
},
"state_key": "",
"origin_server_ts": 2,
"depth": 2,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-member-join-alice"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-member-join-alice"
]
},
{
"event_id": "$00-m-room-join_rules",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.join_rules",
"content": {
"join_rule": "public"
},
"state_key": "",
"origin_server_ts": 3,
"depth": 3,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-power_levels"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-member-join-alice",
"$00-m-room-power_levels"
]
},
{
"event_id": "$00-m-room-member-join-bob",
"room_id": "!room:example.com",
"sender": "@bob:example.com",
"type": "m.room.member",
"content": {
"displayname": "bob",
"membership": "join"
},
"state_key": "@bob:example.com",
"origin_server_ts": 4,
"depth": 4,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-join_rules"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$00-m-room-member-join-charlie",
"room_id": "!room:example.com",
"sender": "@charlie:example.com",
"type": "m.room.member",
"content": {
"displayname": "charlie",
"membership": "join"
},
"state_key": "@charlie:example.com",
"origin_server_ts": 5,
"depth": 5,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-member-join-bob"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$01-m-room-power_levels",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.power_levels",
"content": {
"users": {
"@alice:example.com": 100,
"@bob:example.com": 50
}
},
"state_key": "",
"origin_server_ts": 6,
"depth": 6,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-member-join-charlie"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-member-join-alice"
]
},
{
"event_id": "$02-m-room-power_levels",
"room_id": "!room:example.com",
"sender": "@bob:example.com",
"type": "m.room.power_levels",
"content": {
"users": {
"@alice:example.com": 100,
"@bob:example.com": 50,
"@charlie:example.com": 50
}
},
"state_key": "",
"origin_server_ts": 7,
"depth": 7,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$01-m-room-power_levels"
],
"auth_events": [
"$00-m-room-create",
"$01-m-room-power_levels",
"$00-m-room-member-join-bob"
]
},
{
"event_id": "$00-m-room-member-join-zara",
"room_id": "!room:example.com",
"sender": "@zara:example.com",
"type": "m.room.member",
"content": {
"displayname": "zara",
"membership": "join"
},
"state_key": "@zara:example.com",
"origin_server_ts": 8,
"depth": 8,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$02-m-room-power_levels"
],
"auth_events": [
"$00-m-room-create",
"$02-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$00-m-room-member-join-eve",
"room_id": "!room:example.com",
"sender": "@eve:example.com",
"type": "m.room.member",
"content": {
"displayname": "eve",
"membership": "join"
},
"state_key": "@eve:example.com",
"origin_server_ts": 9,
"depth": 9,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$02-m-room-power_levels"
],
"auth_events": [
"$00-m-room-create",
"$02-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$01-m-room-member-change-display-name-eve",
"room_id": "!room:example.com",
"sender": "@eve:example.com",
"type": "m.room.member",
"content": {
"displayname": "eve++",
"membership": "join"
},
"state_key": "@eve:example.com",
"origin_server_ts": 9,
"depth": 9,
"signatures": {},
"hashes": {"sha256": ""},
"prev_events": [
"$00-m-room-member-join-eve"
],
"auth_events": [
"$00-m-room-create",
"$02-m-room-power_levels",
"$00-m-room-member-join-eve",
"$00-m-room-join_rules"
]
}
]

View File

@@ -1,9 +0,0 @@
[
"$00-m-room-create",
"$00-m-room-member-join-alice",
"$00-m-room-power_levels",
"$00-m-room-join_rules",
"$00-m-room-member-join-bob",
"$00-m-room-member-join-charlie",
"$01-m-room-member-change-display-name-eve"
]

View File

@@ -1,9 +0,0 @@
[
"$00-m-room-create",
"$00-m-room-member-join-alice",
"$00-m-room-join_rules",
"$00-m-room-member-join-bob",
"$00-m-room-member-join-charlie",
"$02-m-room-power_levels",
"$00-m-room-member-join-zara"
]

View File

@@ -1,129 +0,0 @@
[
{
"event_id": "$00-m-room-create",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.create",
"content": {
"creator": "@alice:example.com",
"room_version": "10"
},
"state_key": "",
"origin_server_ts": 0,
"depth": 0,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": [],
"auth_events": []
},
{
"event_id": "$00-m-room-member-join-alice",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.member",
"content": {
"displayname": "alice",
"membership": "join"
},
"state_key": "@alice:example.com",
"origin_server_ts": 1,
"depth": 1,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": [
"$00-m-room-create"
],
"auth_events": [
"$00-m-room-create"
]
},
{
"event_id": "$00-m-room-power_levels",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.power_levels",
"content": {
"users": {
"@alice:example.com": 100
}
},
"state_key": "",
"origin_server_ts": 2,
"depth": 2,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": [
"$00-m-room-member-join-alice"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-member-join-alice"
]
},
{
"event_id": "$00-m-room-join_rules",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.join_rules",
"content": {
"join_rule": "invite"
},
"state_key": "",
"origin_server_ts": 3,
"depth": 3,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": [
"$00-m-room-power_levels"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-member-join-alice",
"$00-m-room-power_levels"
]
},
{
"event_id": "$00-m-room-history_visibility",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.history_visibility",
"content": {
"history_visibility": "shared"
},
"state_key": "",
"origin_server_ts": 4,
"depth": 4,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": [
"$00-m-room-join_rules"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-member-join-alice",
"$00-m-room-power_levels"
]
},
{
"event_id": "$00-m-room-guest_access",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.guest_access",
"content": {
"guest_access": "can_join"
},
"state_key": "",
"origin_server_ts": 5,
"depth": 5,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": [
"$00-m-room-history_visibility"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-member-join-alice",
"$00-m-room-power_levels"
]
}
]

View File

@@ -1,64 +0,0 @@
[
{
"test-comments": [
"NOTE: It is very important that the `event_id` of this PDU is ",
"lexicographically larger than the `event_id` of the following PDU, to ",
"ensure that the tiebreaking is done by the `origin_server_ts` field ",
"and not by the `event_id` field."
],
"event_id": "$02-m-room-join_rules",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.join_rules",
"content": {
"join_rule": "restricted",
"allow": [
{
"room_id": "!other:example.com",
"type": "m.room_membership"
}
]
},
"state_key": "",
"origin_server_ts": 6,
"depth": 6,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": [
"$00-m-room-guest_access"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-member-join-alice",
"$00-m-room-power_levels"
]
},
{
"test-comments": [
"NOTE: It is very important that the `event_id` of this PDU is ",
"lexicographically smaller than the `event_id` of the previous PDU, to ",
"ensure that the tiebreaking is done by the `origin_server_ts` field ",
"and not by the `event_id` field."
],
"event_id": "$01-m-room-join_rules",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.join_rules",
"content": {
"join_rule": "public"
},
"state_key": "",
"origin_server_ts": 7,
"depth": 7,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": [
"$00-m-room-guest_access"
],
"auth_events": [
"$00-m-room-create",
"$00-m-room-member-join-alice",
"$00-m-room-power_levels"
]
}
]

View File

@@ -1,232 +0,0 @@
[
{
"event_id": "$00-m-room-create",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.create",
"content": {
"creator": "@alice:example.com",
"room_version": "10"
},
"state_key": "",
"origin_server_ts": 0,
"depth": 0,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": [],
"auth_events": []
},
{
"event_id": "$00-m-room-member-join-alice",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.member",
"content": {
"displayname": "alice",
"membership": "join"
},
"state_key": "@alice:example.com",
"origin_server_ts": 1,
"depth": 1,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$00-m-room-create"],
"auth_events": [
"$00-m-room-create"
]
},
{
"event_id": "$00-m-room-power_levels",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.power_levels",
"content": {
"users": {
"@alice:example.com": 100
}
},
"state_key": "",
"origin_server_ts": 2,
"depth": 2,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$00-m-room-member-join-alice"],
"auth_events": [
"$00-m-room-create",
"$00-m-room-member-join-alice"
]
},
{
"event_id": "$00-m-room-join_rules",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.join_rules",
"content": {
"join_rule": "public"
},
"state_key": "",
"origin_server_ts": 3,
"depth": 3,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$00-m-room-power_levels"],
"auth_events": [
"$00-m-room-create",
"$00-m-room-member-join-alice",
"$00-m-room-power_levels"
]
},
{
"event_id": "$00-m-room-member-join-bob",
"room_id": "!room:example.com",
"sender": "@bob:example.com",
"type": "m.room.member",
"content": {
"displayname": "bob",
"membership": "join"
},
"state_key": "@bob:example.com",
"origin_server_ts": 4,
"depth": 4,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$00-m-room-join_rules"],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$00-m-room-member-join-charlie",
"room_id": "!room:example.com",
"sender": "@charlie:example.com",
"type": "m.room.member",
"content": {
"displayname": "charlie",
"membership": "join"
},
"state_key": "@charlie:example.com",
"origin_server_ts": 5,
"depth": 5,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$00-m-room-member-join-bob"],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$01-m-room-power_levels",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.power_levels",
"content": {
"users": {
"@alice:example.com": 100,
"@bob:example.com": 50
}
},
"state_key": "",
"origin_server_ts": 6,
"depth": 6,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$00-m-room-member-join-charlie"],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-member-join-alice"
]
},
{
"event_id": "$02-m-room-power_levels",
"room_id": "!room:example.com",
"sender": "@bob:example.com",
"type": "m.room.power_levels",
"content": {
"users": {
"@alice:example.com": 100,
"@bob:example.com": 50,
"@charlie:example.com": 50
}
},
"state_key": "",
"origin_server_ts": 7,
"depth": 7,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$01-m-room-power_levels"],
"auth_events": [
"$00-m-room-create",
"$01-m-room-power_levels",
"$00-m-room-member-join-bob"
]
},
{
"event_id": "$00-m-room-member-join-zara",
"room_id": "!room:example.com",
"sender": "@zara:example.com",
"type": "m.room.member",
"content": {
"displayname": "zara",
"membership": "join"
},
"state_key": "@zara:example.com",
"origin_server_ts": 8,
"depth": 8,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$02-m-room-power_levels"],
"auth_events": [
"$00-m-room-create",
"$02-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$00-m-room-member-join-eve",
"room_id": "!room:example.com",
"sender": "@eve:example.com",
"type": "m.room.member",
"content": {
"displayname": "eve",
"membership": "join"
},
"state_key": "@eve:example.com",
"origin_server_ts": 9,
"depth": 9,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$02-m-room-power_levels"],
"auth_events": [
"$00-m-room-create",
"$02-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$01-m-room-member-change-display-name-eve",
"room_id": "!room:example.com",
"sender": "@eve:example.com",
"type": "m.room.member",
"content": {
"displayname": "eve++",
"membership": "join"
},
"state_key": "@eve:example.com",
"origin_server_ts": 9,
"depth": 9,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$00-m-room-member-join-eve"],
"auth_events": [
"$00-m-room-create",
"$02-m-room-power_levels",
"$00-m-room-member-join-eve",
"$00-m-room-join_rules"
]
}
]

View File

@@ -1,206 +0,0 @@
[
{
"event_id": "$00-m-room-create",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.create",
"content": {
"creator": "@alice:example.com",
"room_version": "10"
},
"state_key": "",
"origin_server_ts": 0,
"depth": 0,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": [],
"auth_events": []
},
{
"event_id": "$00-m-room-member-join-alice",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.member",
"content": {
"displayname": "alice",
"membership": "join"
},
"state_key": "@alice:example.com",
"origin_server_ts": 1,
"depth": 1,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$00-m-room-create"],
"auth_events": [
"$00-m-room-create"
]
},
{
"event_id": "$00-m-room-power_levels",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.power_levels",
"content": {
"users": {
"@alice:example.com": 100
}
},
"state_key": "",
"origin_server_ts": 2,
"depth": 2,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$00-m-room-member-join-alice"],
"auth_events": [
"$00-m-room-create",
"$00-m-room-member-join-alice"
]
},
{
"event_id": "$00-m-room-join_rules",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.join_rules",
"content": {
"join_rule": "public"
},
"state_key": "",
"origin_server_ts": 3,
"depth": 3,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$00-m-room-power_levels"],
"auth_events": [
"$00-m-room-create",
"$00-m-room-member-join-alice",
"$00-m-room-power_levels"
]
},
{
"event_id": "$00-m-room-member-join-bob",
"room_id": "!room:example.com",
"sender": "@bob:example.com",
"type": "m.room.member",
"content": {
"displayname": "bob",
"membership": "join"
},
"state_key": "@bob:example.com",
"origin_server_ts": 4,
"depth": 4,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$00-m-room-join_rules"],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$00-m-room-member-join-charlie",
"room_id": "!room:example.com",
"sender": "@charlie:example.com",
"type": "m.room.member",
"content": {
"displayname": "charlie",
"membership": "join"
},
"state_key": "@charlie:example.com",
"origin_server_ts": 5,
"depth": 5,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$00-m-room-member-join-bob"],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-join_rules"
]
},
{
"event_id": "$01-m-room-join_rules",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.join_rules",
"content": {
"join_rule": "invite"
},
"state_key": "",
"origin_server_ts": 6,
"depth": 6,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$00-m-room-member-join-charlie"],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-member-join-alice"
]
},
{
"event_id": "$01-m-room-member-leave-alice",
"room_id": "!room:example.com",
"sender": "@alice:example.com",
"type": "m.room.member",
"content": {
"displayname": "alice",
"membership": "leave"
},
"state_key": "@alice:example.com",
"origin_server_ts": 7,
"depth": 7,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$01-m-room-join_rules"],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-member-join-alice"
]
},
{
"event_id": "$01-m-room-member-change-display-name-bob",
"room_id": "!room:example.com",
"sender": "@bob:example.com",
"type": "m.room.member",
"content": {
"displayname": "bob++",
"membership": "join"
},
"state_key": "@bob:example.com",
"origin_server_ts": 8,
"depth": 8,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$01-m-room-member-leave-alice"],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-member-join-bob",
"$01-m-room-join_rules"
]
},
{
"event_id": "$01-m-room-member-change-display-name-charlie",
"room_id": "!room:example.com",
"sender": "@charlie:example.com",
"type": "m.room.member",
"content": {
"displayname": "charlie++",
"membership": "join"
},
"state_key": "@charlie:example.com",
"origin_server_ts": 9,
"depth": 9,
"hashes": {"sha256": "aaa"},
"signatures": {},
"prev_events": ["$01-m-room-member-leave-alice"],
"auth_events": [
"$00-m-room-create",
"$00-m-room-power_levels",
"$00-m-room-member-join-charlie",
"$01-m-room-join_rules"
]
}
]

View File

@@ -1,3 +0,0 @@
//! Integration tests entrypoint.
mod resolve;

View File

@@ -1,700 +0,0 @@
//! State resolution integration tests.
#![cfg(test)]
use std::{
cmp::Ordering,
collections::{BTreeSet, HashMap},
error::Error,
fs,
path::Path,
};
use ruma::{
OwnedEventId, RoomVersionId,
events::{StateEventType, TimelineEventType},
room_version_rules::{AuthorizationRules, RoomVersionRules, StateResolutionV2Rules},
};
use serde::{Deserialize, Serialize};
use serde_json::{
Error as JsonError, Value as JsonValue, from_str as from_json_str,
to_string_pretty as to_json_string_pretty, to_value as to_json_value,
};
use similar::{Algorithm, udiff::unified_diff};
use tracing_subscriber::EnvFilter;
use tuwunel_core::{
Result, err,
matrix::{
Event, Pdu, StateKey, StateMap,
state_res::{AuthSet, resolve},
},
utils::stream::IterStream,
};
/// Create a new snapshot test.
///
/// # Arguments
///
/// * The test function's name.
/// * A list of JSON files relative to `tests/state_res/fixtures` to load PDUs
/// to resolve from.
macro_rules! snapshot_test {
($name:ident, $paths:expr $(,)?) => {
#[tokio::test]
async fn $name() {
let crate::resolve::Snapshots {
resolved_state,
} = crate::resolve::test_resolve(&$paths).await;
insta::with_settings!({
description => "Resolved state",
omit_expression => true,
snapshot_suffix => "resolved_state",
}, {
insta::assert_json_snapshot!(&resolved_state);
});
}
};
}
/// Create a new snapshot test, attempting to resolve multiple contrived states.
///
/// # Arguments
///
/// * The test function's name.
/// * A list of JSON files relative to `tests/state_res/fixtures` to load PDUs
/// to resolve from.
/// * A list of JSON files relative to `tests/state_res/fixtures` to load event
/// IDs forming contrived states to resolve.
macro_rules! snapshot_test_contrived_states {
($name:ident, $pdus_path:expr, $state_set_paths:expr $(,)?) => {
#[tokio::test]
async fn $name() {
let crate::resolve::Snapshots {
resolved_state,
} = crate::resolve::test_contrived_states(&$pdus_path, &$state_set_paths).await;
insta::with_settings!({
description => "Resolved state",
omit_expression => true,
snapshot_suffix => "resolved_state",
}, {
insta::assert_json_snapshot!(&resolved_state);
});
}
};
}
// This module must be defined lexically after the `snapshot_test` macro.
mod snapshot_tests;
/// Extract `.content.room_version` from a PDU.
#[derive(Deserialize)]
struct ExtractRoomVersion {
room_version: RoomVersionId,
}
/// Type describing a resolved state event.
#[derive(Serialize)]
struct ResolvedStateEvent {
kind: StateEventType,
state_key: StateKey,
event_id: OwnedEventId,
// Ignored in `PartialEq` and `Ord` because we don't want to consider it while sorting.
content: JsonValue,
}
impl PartialEq for ResolvedStateEvent {
fn eq(&self, other: &Self) -> bool {
self.kind == other.kind
&& self.state_key == other.state_key
&& self.event_id == other.event_id
}
}
impl Eq for ResolvedStateEvent {}
impl Ord for ResolvedStateEvent {
fn cmp(&self, other: &Self) -> Ordering {
Ordering::Equal
.then(self.kind.cmp(&other.kind))
.then(self.state_key.cmp(&other.state_key))
.then(self.event_id.cmp(&other.event_id))
}
}
impl PartialOrd for ResolvedStateEvent {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
/// Information to be captured in snapshot assertions
struct Snapshots {
/// The resolved state of the room.
resolved_state: BTreeSet<ResolvedStateEvent>,
}
fn snapshot_test_prelude(
paths: &[&str],
) -> (Vec<Vec<Pdu>>, RoomVersionRules, AuthorizationRules, StateResolutionV2Rules) {
// Run `cargo test -- --show-output` to view traces, set `RUST_LOG` to control
// filtering.
let subscriber = tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_test_writer()
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE)
.finish();
tracing::subscriber::set_global_default(subscriber).ok();
let fixtures_path = Path::new("tests/state_res/fixtures");
let pdu_batches = paths
.iter()
.map(|x| {
from_json_str(
&fs::read_to_string(fixtures_path.join(x))
.expect("should be able to read JSON file of PDUs"),
)
.expect("should be able to deserialize JSON file of PDUs")
})
.collect::<Vec<Vec<Pdu>>>();
let room_version_id = {
let first_pdu = pdu_batches
.first()
.expect("there should be at least one file of PDUs")
.first()
.expect("there should be at least one PDU in the first file");
assert_eq!(
first_pdu.kind,
TimelineEventType::RoomCreate,
"the first PDU in the first file should be an m.room.create event",
);
from_json_str::<ExtractRoomVersion>(first_pdu.content.get())
.expect("the m.room.create PDU's content should be valid")
.room_version
};
let rules = room_version_id
.rules()
.expect("room version should be supported");
let auth_rules = rules.clone().authorization;
let state_res_rules = rules
.state_res
.v2_rules()
.copied()
.expect("resolve only supports state resolution version 2");
(pdu_batches, rules, auth_rules, state_res_rules)
}
/// Reshape the data a bit to make the diff and snapshots easier to compare.
fn reshape(
pdus_by_id: &HashMap<OwnedEventId, Pdu>,
x: StateMap<OwnedEventId>,
) -> Result<BTreeSet<ResolvedStateEvent>, JsonError> {
x.into_iter()
.map(|((kind, state_key), event_id)| {
Ok(ResolvedStateEvent {
kind,
state_key,
content: to_json_value(pdus_by_id[&event_id].content())?,
event_id,
})
})
.collect()
}
/// Test a list of JSON files containing a list of PDUs and return the results.
///
/// State resolution is run both atomically for all PDUs and in batches of PDUs
/// by file.
async fn test_resolve(paths: &[&str]) -> Snapshots {
let (pdu_batches, rules, auth_rules, state_res_rules) = snapshot_test_prelude(paths);
// Resolve PDUs iteratively, using the ordering of `prev_events`.
let iteratively_resolved_state = resolve_iteratively(
&rules,
&auth_rules,
&state_res_rules,
pdu_batches.iter().flat_map(|x| x.iter()),
)
.await
.expect("iterative state resolution should succeed");
// Resolve PDUs in batches by file
let mut pdus_by_id = HashMap::new();
let mut batched_resolved_state = None;
for pdus in &pdu_batches {
batched_resolved_state = Some(
resolve_batch(
&rules,
&auth_rules,
&state_res_rules,
pdus,
&mut pdus_by_id,
&mut batched_resolved_state,
)
.await
.expect("batched state resolution step should succeed"),
);
}
let batched_resolved_state =
batched_resolved_state.expect("batched state resolution should have run at least once");
// Resolve all PDUs in a single step
let atomic_resolved_state = resolve_batch(
&rules,
&auth_rules,
&state_res_rules,
pdu_batches.iter().flat_map(|x| x.iter()),
&mut HashMap::new(),
&mut None,
)
.await
.expect("atomic state resolution should succeed");
let iteratively_resolved_state = reshape(&pdus_by_id, iteratively_resolved_state)
.expect("should be able to reshape iteratively resolved state");
let batched_resolved_state = reshape(&pdus_by_id, batched_resolved_state)
.expect("should be able to reshape batched resolved state");
let atomic_resolved_state = reshape(&pdus_by_id, atomic_resolved_state)
.expect("should be able to reshape atomic resolved state");
let assert_states_match = |first_resolved_state: &BTreeSet<ResolvedStateEvent>,
second_resolved_state: &BTreeSet<ResolvedStateEvent>,
first_name: &str,
second_name: &str| {
if first_resolved_state != second_resolved_state {
let diff = unified_diff(
Algorithm::default(),
&to_json_string_pretty(first_resolved_state)
.expect("should be able to serialize first resolved state"),
&to_json_string_pretty(second_resolved_state)
.expect("should be able to serialize second resolved state"),
3,
Some((first_name, second_name)),
);
panic!(
"{first_name} and {second_name} results should match; but they differ:\n{diff}"
);
}
};
assert_states_match(
&iteratively_resolved_state,
&batched_resolved_state,
"iterative",
"batched",
);
assert_states_match(&batched_resolved_state, &atomic_resolved_state, "batched", "atomic");
Snapshots {
resolved_state: iteratively_resolved_state,
}
}
/// Test a list of JSON files containing a list of PDUs and a list of JSON files
/// containing the event IDs that form a contrived state and return the results.
#[tracing::instrument(parent = None, name = "test", skip_all)]
async fn test_contrived_states(pdus_paths: &[&str], state_sets_paths: &[&str]) -> Snapshots {
let (pdu_batches, rules, _auth_rules, _state_res_rules) = snapshot_test_prelude(pdus_paths);
let pdus = pdu_batches
.into_iter()
.flat_map(IntoIterator::into_iter)
.collect::<Vec<_>>();
let pdus_by_id: HashMap<OwnedEventId, Pdu> = pdus
.clone()
.into_iter()
.map(|pdu| (pdu.event_id().to_owned(), pdu.clone()))
.collect();
let fixtures_path = Path::new("tests/state_res/fixtures");
let state_sets = state_sets_paths
.iter()
.map(|x| {
from_json_str::<Vec<OwnedEventId>>(
&fs::read_to_string(fixtures_path.join(x))
.expect("should be able to read JSON file of PDUs"),
)
.expect("should be able to deserialize JSON file of PDUs")
.into_iter()
.map(|event_id| {
pdus_by_id
.get(&event_id)
.map(|pdu| {
(
(
pdu.event_type().to_string().into(),
pdu.state_key
.clone()
.expect("All PDUs must be state events"),
),
event_id,
)
})
.expect("Event IDs in JSON file must be in PDUs JSON")
})
.collect()
})
.collect::<Vec<StateMap<OwnedEventId>>>();
let mut auth_chain_sets = Vec::new();
for state_map in &state_sets {
let mut auth_chain = AuthSet::new();
for event_id in state_map.values() {
let pdu = pdus_by_id
.get(event_id)
.expect("We already confirmed all state set event ids have pdus");
auth_chain.extend(
auth_events_dfs(&pdus_by_id, pdu).expect("Auth events DFS should not fail"),
);
}
auth_chain_sets.push(auth_chain);
}
let exists = async |x| pdus_by_id.contains_key(&x);
let fetch = async |x| {
pdus_by_id
.get(&x)
.cloned()
.ok_or_else(|| err!(Request(NotFound("event not found"))))
};
let resolved_state = resolve(
&rules,
state_sets.into_iter().stream(),
auth_chain_sets.into_iter().stream(),
&fetch,
&exists,
false,
)
.await
.expect("atomic state resolution should succeed");
Snapshots {
resolved_state: reshape(&pdus_by_id, resolved_state)
.expect("should be able to reshape atomic resolved state"),
}
}
/// Perform state resolution on a batch of PDUs.
///
/// This function can be used to resolve the state of a room in a single call if
/// all PDUs are provided at once, or across multiple calls if given PDUs in
/// batches in a loop. The latter form simulates the case commonly experienced
/// by homeservers during normal operation.
///
/// # Arguments
///
/// * `rules`: The rules of the room version.
/// * `pdus`: An iterator of [`Pdu`]s to resolve, either alone or against the
/// `prev_state`.
/// * `pdus_by_id`: A map of [`OwnedEventId`]s to the [`Pdu`] with that ID.
/// * Should be empty for the first call.
/// * Should not be mutated outside of this function.
/// * `prev_state`: The state returned by a previous call to this function, if
/// any.
/// * Should be `None` for the first call.
/// * Should not be mutated outside of this function.
async fn resolve_batch<'a, I, II>(
rules: &'a RoomVersionRules,
_auth_rules: &'a AuthorizationRules,
_state_res_rules: &'a StateResolutionV2Rules,
pdus: II,
pdus_by_id: &'a mut HashMap<OwnedEventId, Pdu>,
prev_state: &'a mut Option<StateMap<OwnedEventId>>,
) -> Result<StateMap<OwnedEventId>, Box<dyn Error>>
where
I: Iterator<Item = &'a Pdu> + Send + 'a,
II: IntoIterator<IntoIter = I> + Clone + Send + 'a,
Pdu: Send + Sync + 'a,
&'a Pdu: Send + 'a,
{
let mut state_sets = prev_state
.take()
.map(|x| vec![x])
.unwrap_or_default();
for pdu in pdus.clone() {
// Insert each state event into its own StateMap because we don't know any valid
// groupings.
let mut state_map = StateMap::new();
state_map.insert(
(
pdu.event_type().to_string().into(),
pdu.state_key()
.ok_or("all PDUs should be state events")?
.into(),
),
pdu.event_id().to_owned(),
);
state_sets.push(state_map);
}
pdus_by_id.extend(
pdus.clone()
.into_iter()
.map(|pdu| (pdu.event_id().to_owned(), pdu.to_owned())),
);
let mut auth_chain_sets = Vec::new();
for pdu in pdus {
auth_chain_sets.push(auth_events_dfs(&*pdus_by_id, pdu)?);
}
let fetch = async |x| {
pdus_by_id
.get(&x)
.cloned()
.ok_or_else(|| err!(Request(NotFound("event not found"))))
};
let exists = async |x| pdus_by_id.contains_key(&x);
resolve(
rules,
state_sets.into_iter().stream(),
auth_chain_sets.into_iter().stream(),
&fetch,
&exists,
false,
)
.await
.map_err(Into::into)
}
/// Perform state resolution on a batch of PDUs iteratively, one-by-one.
///
/// This function walks the `prev_events` of each PDU forward, resolving each
/// pdu against the state(s) of it's `prev_events`, to emulate what would happen
/// in a regular room a server is participating in.
///
/// # Arguments
///
/// * `auth_rules`: The authorization rules of the room version.
/// * `state_res_rules`: The state resolution rules of the room version.
/// * `pdus`: An iterator of [`Pdu`]s to resolve, with the following
/// assumptions:
/// * `prev_events` of each PDU points to another provided state event.
///
/// # Returns
///
/// The state resolved by resolving all the leaves (PDUs which don't have any
/// other PDUs pointing to it via `prev_events`).
async fn resolve_iteratively<'a, I, II>(
rules: &'a RoomVersionRules,
_auth_rules: &'a AuthorizationRules,
_state_res_rules: &'a StateResolutionV2Rules,
pdus: II,
) -> Result<StateMap<OwnedEventId>, Box<dyn Error>>
where
I: Iterator<Item = &'a Pdu>,
II: IntoIterator<IntoIter = I> + Clone,
{
let mut forward_prev_events_graph: HashMap<OwnedEventId, Vec<_>> = HashMap::new();
let mut stack = Vec::new();
for pdu in pdus.clone() {
let mut has_prev_events = false;
for prev_event in pdu.prev_events() {
forward_prev_events_graph
.entry(prev_event.into())
.or_default()
.push(pdu.event_id().into());
has_prev_events = true;
}
if pdu.event_type() == &TimelineEventType::RoomCreate && !has_prev_events {
stack.push(pdu.event_id().to_owned());
}
}
let pdus_by_id: HashMap<OwnedEventId, Pdu> = pdus
.clone()
.into_iter()
.map(|pdu| (pdu.event_id().to_owned(), pdu.to_owned()))
.collect();
let exists = async |x| pdus_by_id.contains_key(&x);
let fetch = async |x| {
pdus_by_id
.get(&x)
.cloned()
.ok_or_else(|| err!(Request(NotFound("event not found"))))
};
let mut state_at_events: HashMap<OwnedEventId, StateMap<OwnedEventId>> = HashMap::new();
let mut leaves = Vec::new();
'outer: while let Some(event_id) = stack.pop() {
let mut states_before_event = Vec::new();
let mut auth_chain_sets = Vec::new();
let current_pdu = pdus_by_id
.get(&event_id)
.expect("every pdu should be available");
for prev_event in current_pdu.prev_events() {
let Some(state_at_event) = state_at_events.get(prev_event) else {
// State for a prev event is not known, we will come back to this event on a
// later iteration.
continue 'outer;
};
for pdu in state_at_event.values().map(|event_id| {
pdus_by_id
.get(event_id)
.expect("every pdu should be available")
}) {
auth_chain_sets.push(auth_events_dfs(&pdus_by_id, pdu)?);
}
states_before_event.push(state_at_event.clone());
}
if states_before_event.is_empty() {
// initial event, nothing to resolve
state_at_events.insert(
event_id.clone(),
StateMap::from_iter([(
(
current_pdu.event_type().to_string().into(),
current_pdu
.state_key()
.expect("all pdus are state events")
.into(),
),
event_id.clone(),
)]),
);
} else {
let state_before_event = resolve(
rules,
states_before_event.clone().into_iter().stream(),
auth_chain_sets.clone().into_iter().stream(),
&fetch,
&exists,
false,
)
.await?;
let mut proposed_state_at_event = state_before_event.clone();
proposed_state_at_event.insert(
(
current_pdu.event_type().to_string().into(),
current_pdu
.state_key()
.expect("all pdus are state events")
.into(),
),
event_id.clone(),
);
auth_chain_sets.push(auth_events_dfs(&pdus_by_id, current_pdu)?);
let state_at_event = resolve(
rules,
[state_before_event, proposed_state_at_event]
.into_iter()
.stream(),
auth_chain_sets.into_iter().stream(),
&fetch,
&exists,
false,
)
.await?;
state_at_events.insert(event_id.clone(), state_at_event);
}
if let Some(prev_events) = forward_prev_events_graph.get(&event_id) {
stack.extend(prev_events.iter().cloned());
} else {
// pdu is a leaf: no `prev_events` point to it.
leaves.push(event_id);
}
}
assert!(
state_at_events.len() == pdus_by_id.len(),
"Not all events have a state calculated! This is likely due to an event having a \
`prev_events` which points to a non-existent PDU."
);
let mut leaf_states = Vec::new();
let mut auth_chain_sets = Vec::new();
for leaf in leaves {
let state_at_event = state_at_events
.get(&leaf)
.expect("states at all events are known");
for pdu in state_at_event.values().map(|event_id| {
pdus_by_id
.get(event_id)
.expect("every pdu should be available")
}) {
auth_chain_sets.push(auth_events_dfs(&pdus_by_id, pdu)?);
}
leaf_states.push(state_at_event.clone());
}
resolve(
rules,
leaf_states.into_iter().stream(),
auth_chain_sets.into_iter().stream(),
&fetch,
&exists,
false,
)
.await
.map_err(Into::into)
}
/// Depth-first search for the `auth_events` of the given PDU.
///
/// # Errors
///
/// Fails if `pdus` does not contain a PDU that appears in the recursive
/// `auth_events` of `pdu`.
fn auth_events_dfs(
pdus_by_id: &HashMap<OwnedEventId, Pdu>,
pdu: &Pdu,
) -> Result<AuthSet<OwnedEventId>, Box<dyn Error>> {
let mut out = AuthSet::new();
let mut stack = pdu
.auth_events()
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
while let Some(event_id) = stack.pop() {
if out.contains(&event_id) {
continue;
}
out.insert(event_id.clone());
stack.extend(
pdus_by_id
.get(&event_id)
.ok_or_else(|| format!("missing required PDU: {event_id}"))?
.auth_events()
.map(ToOwned::to_owned),
);
}
Ok(out)
}

View File

@@ -1,56 +0,0 @@
//! Snapshot tests.
// Test the minimal set of events required to create a room with the
// "private_chat" preset.
snapshot_test!(minimal_private_chat, ["bootstrap-private-chat.json"]);
// Start with a private room, then transition its join rules to restricted, then
// to public. The events in the second file are tied topologically, so they must
// have the tiebreaking algorithm applied. The ordering should be decided by
// the `origin_server_ts` fields of these events, not the `event_id` fields. The
// power levels of these events are equivalent, so they don't really matter.
snapshot_test!(origin_server_ts_tiebreak, [
"bootstrap-private-chat.json",
"origin-server-ts-tiebreak.json"
],);
// Test that state res v2.0 is implemented starting from the unconflicted set,
// and NOT the empty set, leading to there being no join rules state.
//
// This example comes directly from the "Problem A" section of MSC4297.
snapshot_test_contrived_states!(
msc4297_problem_a_state_res_v2_0,
["MSC4297-problem-A/pdus-v11.json"],
["MSC4297-problem-A/state-bob.json", "MSC4297-problem-A/state-charlie.json"]
);
// Test that state res v2.1 is implemented starting from the empty set, and NOT
// the unconflicted set.
//
// This example comes directly from the "Problem A" section of MSC4297.
snapshot_test_contrived_states!(
msc4297_problem_a_state_res_v2_1,
["MSC4297-problem-A/pdus-hydra.json"],
["MSC4297-problem-A/state-bob.json", "MSC4297-problem-A/state-charlie.json"]
);
// Test that state res v2.0 does NOT consider the conflicted state subgraph as
// part of the full conflicted state set, leading to the state resetting to the
// first power levels event.
//
// This example comes directly from the "Problem B" section of MSC4297.
snapshot_test_contrived_states!(
msc4297_problem_b_state_res_v2_0,
["MSC4297-problem-B/pdus-v11.json"],
["MSC4297-problem-B/state-eve.json", "MSC4297-problem-B/state-zara.json"]
);
// Test that state res v2.1 considers the conflicted state subgraph as part of
// the full conflicted state set.
//
// This example comes directly from the "Problem B" section of MSC4297.
snapshot_test_contrived_states!(
msc4297_problem_b_state_res_v2_1,
["MSC4297-problem-B/pdus-hydra.json"],
["MSC4297-problem-B/state-eve.json", "MSC4297-problem-B/state-zara.json"]
);

View File

@@ -1,58 +0,0 @@
---
source: crates/ruma-state-res/tests/it/resolve/snapshot_tests.rs
description: Resolved state
---
[
{
"kind": "m.room.create",
"state_key": "",
"event_id": "$00-m-room-create",
"content": {
"creator": "@alice:example.com",
"room_version": "10"
}
},
{
"kind": "m.room.guest_access",
"state_key": "",
"event_id": "$00-m-room-guest_access",
"content": {
"guest_access": "can_join"
}
},
{
"kind": "m.room.history_visibility",
"state_key": "",
"event_id": "$00-m-room-history_visibility",
"content": {
"history_visibility": "shared"
}
},
{
"kind": "m.room.join_rules",
"state_key": "",
"event_id": "$00-m-room-join_rules",
"content": {
"join_rule": "invite"
}
},
{
"kind": "m.room.member",
"state_key": "@alice:example.com",
"event_id": "$00-m-room-member-join-alice",
"content": {
"displayname": "alice",
"membership": "join"
}
},
{
"kind": "m.room.power_levels",
"state_key": "",
"event_id": "$00-m-room-power_levels",
"content": {
"users": {
"@alice:example.com": 100
}
}
}
]

View File

@@ -1,51 +0,0 @@
---
source: crates/ruma-state-res/tests/it/resolve/snapshot_tests.rs
description: Resolved state
---
[
{
"kind": "m.room.create",
"state_key": "",
"event_id": "$00-m-room-create",
"content": {
"room_version": "11"
}
},
{
"kind": "m.room.member",
"state_key": "@alice:example.com",
"event_id": "$01-m-room-member-leave-alice",
"content": {
"displayname": "alice",
"membership": "leave"
}
},
{
"kind": "m.room.member",
"state_key": "@bob:example.com",
"event_id": "$01-m-room-member-change-display-name-bob",
"content": {
"displayname": "bob++",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@charlie:example.com",
"event_id": "$01-m-room-member-change-display-name-charlie",
"content": {
"displayname": "charlie++",
"membership": "join"
}
},
{
"kind": "m.room.power_levels",
"state_key": "",
"event_id": "$00-m-room-power_levels",
"content": {
"users": {
"@alice:example.com": 100
}
}
}
]

View File

@@ -1,55 +0,0 @@
---
source: crates/ruma-state-res/tests/it/resolve/snapshot_tests.rs
description: Resolved state
---
[
{
"kind": "m.room.create",
"state_key": "",
"event_id": "$00-m-room-create",
"content": {
"room_version": "12"
}
},
{
"kind": "m.room.join_rules",
"state_key": "",
"event_id": "$01-m-room-join_rules",
"content": {
"join_rule": "invite"
}
},
{
"kind": "m.room.member",
"state_key": "@alice:example.com",
"event_id": "$01-m-room-member-leave-alice",
"content": {
"displayname": "alice",
"membership": "leave"
}
},
{
"kind": "m.room.member",
"state_key": "@bob:example.com",
"event_id": "$01-m-room-member-change-display-name-bob",
"content": {
"displayname": "bob++",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@charlie:example.com",
"event_id": "$01-m-room-member-change-display-name-charlie",
"content": {
"displayname": "charlie++",
"membership": "join"
}
},
{
"kind": "m.room.power_levels",
"state_key": "",
"event_id": "$00-m-room-power_levels",
"content": {}
}
]

View File

@@ -1,77 +0,0 @@
---
source: crates/ruma-state-res/tests/it/resolve/snapshot_tests.rs
description: Resolved state
---
[
{
"kind": "m.room.create",
"state_key": "",
"event_id": "$00-m-room-create",
"content": {
"room_version": "11"
}
},
{
"kind": "m.room.join_rules",
"state_key": "",
"event_id": "$00-m-room-join_rules",
"content": {
"join_rule": "public"
}
},
{
"kind": "m.room.member",
"state_key": "@alice:example.com",
"event_id": "$00-m-room-member-join-alice",
"content": {
"displayname": "alice",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@bob:example.com",
"event_id": "$00-m-room-member-join-bob",
"content": {
"displayname": "bob",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@charlie:example.com",
"event_id": "$00-m-room-member-join-charlie",
"content": {
"displayname": "charlie",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@eve:example.com",
"event_id": "$01-m-room-member-change-display-name-eve",
"content": {
"displayname": "eve++",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@zara:example.com",
"event_id": "$00-m-room-member-join-zara",
"content": {
"displayname": "zara",
"membership": "join"
}
},
{
"kind": "m.room.power_levels",
"state_key": "",
"event_id": "$00-m-room-power_levels",
"content": {
"users": {
"@alice:example.com": 100
}
}
}
]

View File

@@ -1,78 +0,0 @@
---
source: crates/ruma-state-res/tests/it/resolve/snapshot_tests.rs
description: Resolved state
---
[
{
"kind": "m.room.create",
"state_key": "",
"event_id": "$00-m-room-create",
"content": {
"room_version": "12"
}
},
{
"kind": "m.room.join_rules",
"state_key": "",
"event_id": "$00-m-room-join_rules",
"content": {
"join_rule": "public"
}
},
{
"kind": "m.room.member",
"state_key": "@alice:example.com",
"event_id": "$00-m-room-member-join-alice",
"content": {
"displayname": "alice",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@bob:example.com",
"event_id": "$00-m-room-member-join-bob",
"content": {
"displayname": "bob",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@charlie:example.com",
"event_id": "$00-m-room-member-join-charlie",
"content": {
"displayname": "charlie",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@eve:example.com",
"event_id": "$01-m-room-member-change-display-name-eve",
"content": {
"displayname": "eve++",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@zara:example.com",
"event_id": "$00-m-room-member-join-zara",
"content": {
"displayname": "zara",
"membership": "join"
}
},
{
"kind": "m.room.power_levels",
"state_key": "",
"event_id": "$02-m-room-power_levels",
"content": {
"users": {
"@bob:example.com": 50,
"@charlie:example.com": 50
}
}
}
]

View File

@@ -1,58 +0,0 @@
---
source: crates/ruma-state-res/tests/it/resolve/snapshot_tests.rs
description: Resolved state
---
[
{
"kind": "m.room.create",
"state_key": "",
"event_id": "$00-m-room-create",
"content": {
"creator": "@alice:example.com",
"room_version": "10"
}
},
{
"kind": "m.room.guest_access",
"state_key": "",
"event_id": "$00-m-room-guest_access",
"content": {
"guest_access": "can_join"
}
},
{
"kind": "m.room.history_visibility",
"state_key": "",
"event_id": "$00-m-room-history_visibility",
"content": {
"history_visibility": "shared"
}
},
{
"kind": "m.room.join_rules",
"state_key": "",
"event_id": "$01-m-room-join_rules",
"content": {
"join_rule": "public"
}
},
{
"kind": "m.room.member",
"state_key": "@alice:example.com",
"event_id": "$00-m-room-member-join-alice",
"content": {
"displayname": "alice",
"membership": "join"
}
},
{
"kind": "m.room.power_levels",
"state_key": "",
"event_id": "$00-m-room-power_levels",
"content": {
"users": {
"@alice:example.com": 100
}
}
}
]

View File

@@ -1,58 +0,0 @@
---
source: src/core/tests/state_res/resolve/snapshot_tests.rs
description: Resolved state
---
[
{
"kind": "m.room.create",
"state_key": "",
"event_id": "$00-m-room-create",
"content": {
"creator": "@alice:example.com",
"room_version": "10"
}
},
{
"kind": "m.room.guest_access",
"state_key": "",
"event_id": "$00-m-room-guest_access",
"content": {
"guest_access": "can_join"
}
},
{
"kind": "m.room.history_visibility",
"state_key": "",
"event_id": "$00-m-room-history_visibility",
"content": {
"history_visibility": "shared"
}
},
{
"kind": "m.room.join_rules",
"state_key": "",
"event_id": "$00-m-room-join_rules",
"content": {
"join_rule": "invite"
}
},
{
"kind": "m.room.member",
"state_key": "@alice:example.com",
"event_id": "$00-m-room-member-join-alice",
"content": {
"displayname": "alice",
"membership": "join"
}
},
{
"kind": "m.room.power_levels",
"state_key": "",
"event_id": "$00-m-room-power_levels",
"content": {
"users": {
"@alice:example.com": 100
}
}
}
]

View File

@@ -1,51 +0,0 @@
---
source: src/core/tests/state_res/resolve/snapshot_tests.rs
description: Resolved state
---
[
{
"kind": "m.room.create",
"state_key": "",
"event_id": "$00-m-room-create",
"content": {
"room_version": "11"
}
},
{
"kind": "m.room.member",
"state_key": "@alice:example.com",
"event_id": "$01-m-room-member-leave-alice",
"content": {
"displayname": "alice",
"membership": "leave"
}
},
{
"kind": "m.room.member",
"state_key": "@bob:example.com",
"event_id": "$01-m-room-member-change-display-name-bob",
"content": {
"displayname": "bob++",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@charlie:example.com",
"event_id": "$01-m-room-member-change-display-name-charlie",
"content": {
"displayname": "charlie++",
"membership": "join"
}
},
{
"kind": "m.room.power_levels",
"state_key": "",
"event_id": "$00-m-room-power_levels",
"content": {
"users": {
"@alice:example.com": 100
}
}
}
]

View File

@@ -1,55 +0,0 @@
---
source: src/core/tests/state_res/resolve/snapshot_tests.rs
description: Resolved state
---
[
{
"kind": "m.room.create",
"state_key": "",
"event_id": "$00-m-room-create",
"content": {
"room_version": "12"
}
},
{
"kind": "m.room.join_rules",
"state_key": "",
"event_id": "$01-m-room-join_rules",
"content": {
"join_rule": "invite"
}
},
{
"kind": "m.room.member",
"state_key": "@alice:example.com",
"event_id": "$01-m-room-member-leave-alice",
"content": {
"displayname": "alice",
"membership": "leave"
}
},
{
"kind": "m.room.member",
"state_key": "@bob:example.com",
"event_id": "$01-m-room-member-change-display-name-bob",
"content": {
"displayname": "bob++",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@charlie:example.com",
"event_id": "$01-m-room-member-change-display-name-charlie",
"content": {
"displayname": "charlie++",
"membership": "join"
}
},
{
"kind": "m.room.power_levels",
"state_key": "",
"event_id": "$00-m-room-power_levels",
"content": {}
}
]

View File

@@ -1,77 +0,0 @@
---
source: src/core/tests/state_res/resolve/snapshot_tests.rs
description: Resolved state
---
[
{
"kind": "m.room.create",
"state_key": "",
"event_id": "$00-m-room-create",
"content": {
"room_version": "11"
}
},
{
"kind": "m.room.join_rules",
"state_key": "",
"event_id": "$00-m-room-join_rules",
"content": {
"join_rule": "public"
}
},
{
"kind": "m.room.member",
"state_key": "@alice:example.com",
"event_id": "$00-m-room-member-join-alice",
"content": {
"displayname": "alice",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@bob:example.com",
"event_id": "$00-m-room-member-join-bob",
"content": {
"displayname": "bob",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@charlie:example.com",
"event_id": "$00-m-room-member-join-charlie",
"content": {
"displayname": "charlie",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@eve:example.com",
"event_id": "$01-m-room-member-change-display-name-eve",
"content": {
"displayname": "eve++",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@zara:example.com",
"event_id": "$00-m-room-member-join-zara",
"content": {
"displayname": "zara",
"membership": "join"
}
},
{
"kind": "m.room.power_levels",
"state_key": "",
"event_id": "$00-m-room-power_levels",
"content": {
"users": {
"@alice:example.com": 100
}
}
}
]

View File

@@ -1,78 +0,0 @@
---
source: src/core/tests/state_res/resolve/snapshot_tests.rs
description: Resolved state
---
[
{
"kind": "m.room.create",
"state_key": "",
"event_id": "$00-m-room-create",
"content": {
"room_version": "12"
}
},
{
"kind": "m.room.join_rules",
"state_key": "",
"event_id": "$00-m-room-join_rules",
"content": {
"join_rule": "public"
}
},
{
"kind": "m.room.member",
"state_key": "@alice:example.com",
"event_id": "$00-m-room-member-join-alice",
"content": {
"displayname": "alice",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@bob:example.com",
"event_id": "$00-m-room-member-join-bob",
"content": {
"displayname": "bob",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@charlie:example.com",
"event_id": "$00-m-room-member-join-charlie",
"content": {
"displayname": "charlie",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@eve:example.com",
"event_id": "$01-m-room-member-change-display-name-eve",
"content": {
"displayname": "eve++",
"membership": "join"
}
},
{
"kind": "m.room.member",
"state_key": "@zara:example.com",
"event_id": "$00-m-room-member-join-zara",
"content": {
"displayname": "zara",
"membership": "join"
}
},
{
"kind": "m.room.power_levels",
"state_key": "",
"event_id": "$02-m-room-power_levels",
"content": {
"users": {
"@bob:example.com": 50,
"@charlie:example.com": 50
}
}
}
]

View File

@@ -1,58 +0,0 @@
---
source: src/core/tests/state_res/resolve/snapshot_tests.rs
description: Resolved state
---
[
{
"kind": "m.room.create",
"state_key": "",
"event_id": "$00-m-room-create",
"content": {
"creator": "@alice:example.com",
"room_version": "10"
}
},
{
"kind": "m.room.guest_access",
"state_key": "",
"event_id": "$00-m-room-guest_access",
"content": {
"guest_access": "can_join"
}
},
{
"kind": "m.room.history_visibility",
"state_key": "",
"event_id": "$00-m-room-history_visibility",
"content": {
"history_visibility": "shared"
}
},
{
"kind": "m.room.join_rules",
"state_key": "",
"event_id": "$01-m-room-join_rules",
"content": {
"join_rule": "public"
}
},
{
"kind": "m.room.member",
"state_key": "@alice:example.com",
"event_id": "$00-m-room-member-join-alice",
"content": {
"displayname": "alice",
"membership": "join"
}
},
{
"kind": "m.room.power_levels",
"state_key": "",
"event_id": "$00-m-room-power_levels",
"content": {
"users": {
"@alice:example.com": 100
}
}
}
]