Move state_res from tuwunel_core to tuwunel_service.
Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
@@ -116,7 +116,3 @@ similar.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[bench]]
|
||||
name = "state_res"
|
||||
harness = false
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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::*,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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 don’t 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 sender’s 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 sender’s 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 sender’s 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 sender’s own entry:
|
||||
// - Since v1, if the current value is greater than or equal to the sender’s
|
||||
// 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 sender’s 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")
|
||||
}
|
||||
@@ -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 sender’s 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 target’s 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(())
|
||||
}
|
||||
@@ -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 sender’s 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 user’s 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 sender’s 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 user’s
|
||||
// current membership state is invite or join.
|
||||
// Since v7, if the sender matches state_key, allow if and only if that user’s
|
||||
// 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 sender’s 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 user’s current membership state is ban, and the
|
||||
// sender’s 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 sender’s power level is greater than or equal to the kick
|
||||
// level, and the target user’s power level is less than the sender’s 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 sender’s 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 sender’s power level is greater than or equal to the ban level, and
|
||||
// the target user’s power level is less than the sender’s 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 sender’s 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
@@ -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 someone’s 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,
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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>);
|
||||
@@ -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}"
|
||||
)))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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 weren’t 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
|
||||
}
|
||||
@@ -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 doesn’t 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()
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -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
|
||||
/// event’s 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)
|
||||
}
|
||||
@@ -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 x’s
|
||||
/// origin_server_ts is less than y’s origin_server_ts; or
|
||||
/// 3. the mainline positions of the events are the same and the events have the
|
||||
/// same origin_server_ts, but x’s event_id is less than y’s 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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(),
|
||||
],);
|
||||
}
|
||||
@@ -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"],
|
||||
)
|
||||
}
|
||||
@@ -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. x’s sender has greater power level than y’s sender, when looking at their
|
||||
//! respective referenced events; or
|
||||
//! 2. the senders have the same power level, but x’s origin_server_ts is less
|
||||
//! than y’s origin_server_ts; or
|
||||
//! 3. the senders have the same power level and the events have the same
|
||||
//! origin_server_ts, but x’s event_id is less than y’s event_id.
|
||||
//!
|
||||
//! The reverse topological power ordering can be found by sorting the events
|
||||
//! using Kahn’s 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)
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
]
|
||||
@@ -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"
|
||||
]
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
]
|
||||
@@ -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"
|
||||
]
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,3 +0,0 @@
|
||||
//! Integration tests entrypoint.
|
||||
|
||||
mod resolve;
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"]
|
||||
);
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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": {}
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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": {}
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user