Working ban_vs_power_level test, add travis.yml, logging
This commit is contained in:
parent
2f443cf41a
commit
5842ddf36e
7
.travis.yml
Normal file
7
.travis.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
language: rust
|
||||||
|
sudo: false
|
||||||
|
rust:
|
||||||
|
- stable
|
||||||
|
- nightly
|
||||||
|
script:
|
||||||
|
- cargo test --all
|
@ -13,10 +13,12 @@ serde = { version = "1.0.114", features = ["derive"] }
|
|||||||
serde_json = "1.0.56"
|
serde_json = "1.0.56"
|
||||||
tracing = "0.1.16"
|
tracing = "0.1.16"
|
||||||
maplit = "1.0.2"
|
maplit = "1.0.2"
|
||||||
|
tracing-subscriber = "0.2.8"
|
||||||
|
|
||||||
[dependencies.ruma]
|
[dependencies.ruma]
|
||||||
# git = "https://github.com/ruma/ruma"
|
git = "https://github.com/DevinR528/ruma"
|
||||||
path = "../__forks__/ruma/ruma"
|
branch = "pdu-deserialize"
|
||||||
|
# path = "../__forks__/ruma/ruma"
|
||||||
features = ["client-api", "federation-api", "appservice-api"]
|
features = ["client-api", "federation-api", "appservice-api"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
@ -65,22 +65,26 @@ pub fn auth_check(
|
|||||||
event: &StateEvent,
|
event: &StateEvent,
|
||||||
auth_events: StateMap<StateEvent>,
|
auth_events: StateMap<StateEvent>,
|
||||||
) -> Option<bool> {
|
) -> Option<bool> {
|
||||||
tracing::debug!("auth_check begingin");
|
tracing::info!("auth_check beginning");
|
||||||
|
|
||||||
|
// don't let power from other rooms be used
|
||||||
for auth_event in auth_events.values() {
|
for auth_event in auth_events.values() {
|
||||||
if auth_event.room_id() != event.room_id() {
|
if auth_event.room_id() != event.room_id() {
|
||||||
return Some(false);
|
return Some(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO sig_check is false when called by `iterative_auth_check`
|
// TODO do_sig_check, do_size_check is false when called by `iterative_auth_check`
|
||||||
|
|
||||||
// Implementation of https://matrix.org/docs/spec/rooms/v1#authorization-rules
|
// Implementation of https://matrix.org/docs/spec/rooms/v1#authorization-rules
|
||||||
//
|
//
|
||||||
// 1. If type is m.room.create:
|
// 1. If type is m.room.create:
|
||||||
if event.kind() == EventType::RoomCreate {
|
if event.kind() == EventType::RoomCreate {
|
||||||
|
tracing::info!("start m.room.create check");
|
||||||
|
|
||||||
// domain of room_id must match domain of sender.
|
// domain of room_id must match domain of sender.
|
||||||
if event.room_id().map(|id| id.server_name()) != Some(event.sender().server_name()) {
|
if event.room_id().map(|id| id.server_name()) != Some(event.sender().server_name()) {
|
||||||
return Some(false);
|
return Some(false); // creation events room id does not match senders
|
||||||
}
|
}
|
||||||
|
|
||||||
// if content.room_version is present and is not a valid version
|
// if content.room_version is present and is not a valid version
|
||||||
@ -97,7 +101,7 @@ pub fn auth_check(
|
|||||||
return Some(false);
|
return Some(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::debug!("m.room.create event was allowed");
|
tracing::info!("m.room.create event was allowed");
|
||||||
return Some(true);
|
return Some(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,18 +110,25 @@ pub fn auth_check(
|
|||||||
.get(&(EventType::RoomCreate, "".into()))
|
.get(&(EventType::RoomCreate, "".into()))
|
||||||
.is_none()
|
.is_none()
|
||||||
{
|
{
|
||||||
|
tracing::warn!("no m.room.create event in auth chain");
|
||||||
|
|
||||||
return Some(false);
|
return Some(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for m.federate
|
// check for m.federate
|
||||||
if event.room_id().map(|id| id.server_name()) != Some(event.sender().server_name()) {
|
if event.room_id().map(|id| id.server_name()) != Some(event.sender().server_name()) {
|
||||||
|
tracing::info!("checking federation");
|
||||||
|
|
||||||
if !can_federate(&auth_events) {
|
if !can_federate(&auth_events) {
|
||||||
|
tracing::warn!("federation not allowed");
|
||||||
|
|
||||||
return Some(false);
|
return Some(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. if type is m.room.aliases
|
// 4. if type is m.room.aliases
|
||||||
if event.kind() == EventType::RoomAliases {
|
if event.kind() == EventType::RoomAliases {
|
||||||
|
tracing::info!("starting m.room.aliases check");
|
||||||
// TODO && room_version "special case aliases auth" ??
|
// TODO && room_version "special case aliases auth" ??
|
||||||
if event.state_key().is_none() {
|
if event.state_key().is_none() {
|
||||||
return Some(false); // must have state_key
|
return Some(false); // must have state_key
|
||||||
@ -130,18 +141,29 @@ pub fn auth_check(
|
|||||||
return Some(false);
|
return Some(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::debug!("m.room.aliases event was allowed");
|
tracing::info!("m.room.aliases event was allowed");
|
||||||
return Some(true);
|
return Some(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.kind() == EventType::RoomMember {
|
if event.kind() == EventType::RoomMember {
|
||||||
|
tracing::info!("starting m.room.member check");
|
||||||
|
|
||||||
if is_membership_change_allowed(event, &auth_events)? {
|
if is_membership_change_allowed(event, &auth_events)? {
|
||||||
tracing::debug!("m.room.member event was allowed");
|
tracing::info!("m.room.member membership change was allowed");
|
||||||
return Some(true);
|
return Some(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tracing::info!("m.room.member event was allowed");
|
||||||
|
return Some(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !check_event_sender_in_room(event, &auth_events)? {
|
if let Some(in_room) = check_event_sender_in_room(event, &auth_events) {
|
||||||
|
if !in_room {
|
||||||
|
tracing::warn!("sender not in room");
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::warn!("sender not in room");
|
||||||
return Some(false);
|
return Some(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,6 +175,7 @@ pub fn auth_check(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !can_send_event(event, &auth_events)? {
|
if !can_send_event(event, &auth_events)? {
|
||||||
|
tracing::warn!("user cannot send event");
|
||||||
return Some(false);
|
return Some(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,6 +183,7 @@ pub fn auth_check(
|
|||||||
if !check_power_levels(room_version, event, &auth_events)? {
|
if !check_power_levels(room_version, event, &auth_events)? {
|
||||||
return Some(false);
|
return Some(false);
|
||||||
}
|
}
|
||||||
|
tracing::info!("power levels event allowed");
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.kind() == EventType::RoomRedaction {
|
if event.kind() == EventType::RoomRedaction {
|
||||||
@ -168,7 +192,7 @@ pub fn auth_check(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::debug!("allowing event passed all checks");
|
tracing::info!("allowing event passed all checks");
|
||||||
Some(true)
|
Some(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,7 +218,8 @@ fn is_membership_change_allowed(
|
|||||||
) -> Option<bool> {
|
) -> Option<bool> {
|
||||||
let content = event
|
let content = event
|
||||||
.deserialize_content::<room::member::MemberEventContent>()
|
.deserialize_content::<room::member::MemberEventContent>()
|
||||||
.ok()?;
|
.ok()
|
||||||
|
.unwrap();
|
||||||
let membership = content.membership;
|
let membership = content.membership;
|
||||||
|
|
||||||
// check if this is the room creator joining
|
// check if this is the room creator joining
|
||||||
@ -210,17 +235,14 @@ fn is_membership_change_allowed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let target_user_id = UserId::try_from(event.state_key()?).ok()?;
|
let target_user_id = UserId::try_from(event.state_key().unwrap()).ok().unwrap();
|
||||||
// if the server_names are different and federation is NOT allowed
|
// if the server_names are different and federation is NOT allowed
|
||||||
if event.room_id()?.server_name() != target_user_id.server_name() {
|
if event.room_id().unwrap().server_name() != target_user_id.server_name() {
|
||||||
if !can_federate(auth_events) {
|
if !can_federate(auth_events) {
|
||||||
return Some(false);
|
return Some(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO according to
|
|
||||||
// https://github.com/matrix-org/synapse/blob/f2af3e4fc550e7e93be1b0f425c3e9c484b96293/synapse/events/__init__.py#L240
|
|
||||||
// sender is the `user_id`?
|
|
||||||
let key = (EventType::RoomMember, event.sender().to_string());
|
let key = (EventType::RoomMember, event.sender().to_string());
|
||||||
let caller = auth_events.get(&key);
|
let caller = auth_events.get(&key);
|
||||||
|
|
||||||
@ -246,7 +268,7 @@ fn is_membership_change_allowed(
|
|||||||
let user_level = get_user_power_level(event.sender(), auth_events);
|
let user_level = get_user_power_level(event.sender(), auth_events);
|
||||||
let target_level = get_user_power_level(event.sender(), auth_events);
|
let target_level = get_user_power_level(event.sender(), auth_events);
|
||||||
|
|
||||||
// synapse has a not "what to do for default here ##"
|
// synapse has a not "what to do for default here 50"
|
||||||
let ban_level = get_named_level(auth_events, "ban", 50);
|
let ban_level = get_named_level(auth_events, "ban", 50);
|
||||||
|
|
||||||
// TODO clean this up
|
// TODO clean this up
|
||||||
@ -327,12 +349,14 @@ fn is_membership_change_allowed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Is the event's sender in the room that they sent the event to.
|
/// Is the event's sender in the room that they sent the event to.
|
||||||
|
///
|
||||||
|
/// A return value of None is not a failure
|
||||||
fn check_event_sender_in_room(
|
fn check_event_sender_in_room(
|
||||||
event: &StateEvent,
|
event: &StateEvent,
|
||||||
auth_events: &StateMap<StateEvent>,
|
auth_events: &StateMap<StateEvent>,
|
||||||
) -> Option<bool> {
|
) -> Option<bool> {
|
||||||
let mem = auth_events.get(&(EventType::RoomMember, event.sender().to_string()))?;
|
let mem = auth_events.get(&(EventType::RoomMember, event.sender().to_string()))?;
|
||||||
// TODO this is check_membership
|
// TODO this is check_membership a helper fn in synapse but it does this
|
||||||
Some(
|
Some(
|
||||||
mem.deserialize_content::<room::member::MemberEventContent>()
|
mem.deserialize_content::<room::member::MemberEventContent>()
|
||||||
.ok()?
|
.ok()?
|
||||||
|
121
src/lib.rs
121
src/lib.rs
@ -67,7 +67,7 @@ impl StateResolution {
|
|||||||
store: &dyn StateStore,
|
store: &dyn StateStore,
|
||||||
// TODO actual error handling (`thiserror`??)
|
// TODO actual error handling (`thiserror`??)
|
||||||
) -> Result<ResolutionResult, String> {
|
) -> Result<ResolutionResult, String> {
|
||||||
tracing::debug!("State resolution starting");
|
tracing::info!("State resolution starting");
|
||||||
|
|
||||||
let mut event_map = if let Some(ev_map) = event_map {
|
let mut event_map = if let Some(ev_map) = event_map {
|
||||||
ev_map
|
ev_map
|
||||||
@ -81,7 +81,7 @@ impl StateResolution {
|
|||||||
return Ok(ResolutionResult::Resolved(clean));
|
return Ok(ResolutionResult::Resolved(clean));
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::debug!("computing {} conflicting events", conflicting.len());
|
tracing::info!("computing {} conflicting events", conflicting.len());
|
||||||
|
|
||||||
// the set of auth events that are not common across server forks
|
// the set of auth events that are not common across server forks
|
||||||
let mut auth_diff = self.get_auth_chain_diff(room_id, &state_sets, &event_map, store)?;
|
let mut auth_diff = self.get_auth_chain_diff(room_id, &state_sets, &event_map, store)?;
|
||||||
@ -94,7 +94,7 @@ impl StateResolution {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
println!(
|
tracing::debug!(
|
||||||
"FULL CONF {:?}",
|
"FULL CONF {:?}",
|
||||||
all_conflicted
|
all_conflicted
|
||||||
.iter()
|
.iter()
|
||||||
@ -102,7 +102,7 @@ impl StateResolution {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
);
|
);
|
||||||
|
|
||||||
tracing::debug!("full conflicted set is {} events", all_conflicted.len());
|
tracing::info!("full conflicted set is {} events", all_conflicted.len());
|
||||||
|
|
||||||
// gather missing events for the event_map
|
// gather missing events for the event_map
|
||||||
let events = store
|
let events = store
|
||||||
@ -147,7 +147,8 @@ impl StateResolution {
|
|||||||
.filter(|id| is_power_event(id, store))
|
.filter(|id| is_power_event(id, store))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
println!(
|
|
||||||
|
tracing::debug!(
|
||||||
"POWER {:?}",
|
"POWER {:?}",
|
||||||
power_events
|
power_events
|
||||||
.iter()
|
.iter()
|
||||||
@ -164,7 +165,7 @@ impl StateResolution {
|
|||||||
&all_conflicted,
|
&all_conflicted,
|
||||||
);
|
);
|
||||||
|
|
||||||
println!(
|
tracing::debug!(
|
||||||
"SRTD {:?}",
|
"SRTD {:?}",
|
||||||
sorted_power_levels
|
sorted_power_levels
|
||||||
.iter()
|
.iter()
|
||||||
@ -172,7 +173,7 @@ impl StateResolution {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
);
|
);
|
||||||
|
|
||||||
// sequentially auth check each event.
|
// sequentially auth check each power level event event.
|
||||||
let resolved = self.iterative_auth_check(
|
let resolved = self.iterative_auth_check(
|
||||||
room_id,
|
room_id,
|
||||||
room_version,
|
room_version,
|
||||||
@ -182,7 +183,13 @@ impl StateResolution {
|
|||||||
store,
|
store,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
println!("AUTHED {:?}", resolved.iter().collect::<Vec<_>>());
|
tracing::debug!(
|
||||||
|
"AUTHED {:?}",
|
||||||
|
resolved
|
||||||
|
.iter()
|
||||||
|
.map(|(key, id)| (key, id.to_string()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
|
||||||
// At this point the power_events have been resolved we now have to
|
// At this point the power_events have been resolved we now have to
|
||||||
// sort the remaining events using the mainline of the resolved power level.
|
// sort the remaining events using the mainline of the resolved power level.
|
||||||
@ -196,7 +203,7 @@ impl StateResolution {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
println!(
|
tracing::debug!(
|
||||||
"LEFT {:?}",
|
"LEFT {:?}",
|
||||||
events_to_resolve
|
events_to_resolve
|
||||||
.iter()
|
.iter()
|
||||||
@ -206,14 +213,14 @@ impl StateResolution {
|
|||||||
|
|
||||||
let power_event = resolved.get(&(EventType::RoomPowerLevels, "".into()));
|
let power_event = resolved.get(&(EventType::RoomPowerLevels, "".into()));
|
||||||
|
|
||||||
println!("PL {:?}", power_event);
|
tracing::debug!("PL {:?}", power_event);
|
||||||
|
|
||||||
let sorted_left_events =
|
let sorted_left_events =
|
||||||
self.mainline_sort(room_id, &events_to_resolve, power_event, &event_map, store);
|
self.mainline_sort(room_id, &events_to_resolve, power_event, &event_map, store);
|
||||||
|
|
||||||
println!(
|
tracing::debug!(
|
||||||
"SORTED LEFT {:?}",
|
"SORTED LEFT {:?}",
|
||||||
events_to_resolve
|
sorted_left_events
|
||||||
.iter()
|
.iter()
|
||||||
.map(ToString::to_string)
|
.map(ToString::to_string)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
@ -244,6 +251,11 @@ impl StateResolution {
|
|||||||
) -> (StateMap<EventId>, StateMap<Vec<EventId>>) {
|
) -> (StateMap<EventId>, StateMap<Vec<EventId>>) {
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"seperating {} sets of events into conflicted/unconflicted",
|
||||||
|
state_sets.len()
|
||||||
|
);
|
||||||
|
|
||||||
let mut unconflicted_state = StateMap::new();
|
let mut unconflicted_state = StateMap::new();
|
||||||
let mut conflicted_state = StateMap::new();
|
let mut conflicted_state = StateMap::new();
|
||||||
|
|
||||||
@ -275,17 +287,8 @@ impl StateResolution {
|
|||||||
) -> Result<Vec<EventId>, String> {
|
) -> Result<Vec<EventId>, String> {
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
println!(
|
|
||||||
"{:?}",
|
|
||||||
state_sets
|
|
||||||
.iter()
|
|
||||||
.flat_map(|map| map.values())
|
|
||||||
.map(ToString::to_string)
|
|
||||||
.dedup()
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
);
|
|
||||||
|
|
||||||
tracing::debug!("calculating auth chain difference");
|
tracing::debug!("calculating auth chain difference");
|
||||||
|
|
||||||
store.auth_chain_diff(
|
store.auth_chain_diff(
|
||||||
room_id,
|
room_id,
|
||||||
state_sets
|
state_sets
|
||||||
@ -321,7 +324,7 @@ impl StateResolution {
|
|||||||
let mut event_to_pl = BTreeMap::new();
|
let mut event_to_pl = BTreeMap::new();
|
||||||
for (idx, event_id) in graph.keys().enumerate() {
|
for (idx, event_id) in graph.keys().enumerate() {
|
||||||
let pl = self.get_power_level_for_sender(room_id, &event_id, event_map, store);
|
let pl = self.get_power_level_for_sender(room_id, &event_id, event_map, store);
|
||||||
println!("{}", pl);
|
tracing::debug!("{} power level {}", event_id.to_string(), pl);
|
||||||
|
|
||||||
event_to_pl.insert(event_id.clone(), pl);
|
event_to_pl.insert(event_id.clone(), pl);
|
||||||
|
|
||||||
@ -333,7 +336,7 @@ impl StateResolution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.lexicographical_topological_sort(&mut graph, |event_id| {
|
self.lexicographical_topological_sort(&mut graph, |event_id| {
|
||||||
println!("{:?}", event_map.get(event_id).unwrap().origin_server_ts());
|
// tracing::debug!("{:?}", event_map.get(event_id).unwrap().origin_server_ts());
|
||||||
let ev = event_map.get(event_id).unwrap();
|
let ev = event_map.get(event_id).unwrap();
|
||||||
let pl = event_to_pl.get(event_id).unwrap();
|
let pl = event_to_pl.get(event_id).unwrap();
|
||||||
// This return value is the key used for sorting events,
|
// This return value is the key used for sorting events,
|
||||||
@ -354,6 +357,7 @@ impl StateResolution {
|
|||||||
where
|
where
|
||||||
F: Fn(&EventId) -> (i64, SystemTime, Option<EventId>),
|
F: Fn(&EventId) -> (i64, SystemTime, Option<EventId>),
|
||||||
{
|
{
|
||||||
|
tracing::info!("starting lexicographical topological sort");
|
||||||
// NOTE: an event that has no incoming edges happened most recently,
|
// NOTE: an event that has no incoming edges happened most recently,
|
||||||
// and an event that has no outgoing edges happened least recently.
|
// and an event that has no outgoing edges happened least recently.
|
||||||
|
|
||||||
@ -409,7 +413,7 @@ impl StateResolution {
|
|||||||
sorted.push(node.clone());
|
sorted.push(node.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// println!(
|
// tracing::debug!(
|
||||||
// "{:#?}",
|
// "{:#?}",
|
||||||
// sorted.iter().map(ToString::to_string).collect::<Vec<_>>()
|
// sorted.iter().map(ToString::to_string).collect::<Vec<_>>()
|
||||||
// );
|
// );
|
||||||
@ -423,7 +427,11 @@ impl StateResolution {
|
|||||||
_event_map: &EventMap<StateEvent>, // TODO use event_map over store ??
|
_event_map: &EventMap<StateEvent>, // TODO use event_map over store ??
|
||||||
store: &dyn StateStore,
|
store: &dyn StateStore,
|
||||||
) -> i64 {
|
) -> i64 {
|
||||||
|
tracing::info!("fetch event senders ({}) power level", event_id.to_string());
|
||||||
|
|
||||||
let mut pl = None;
|
let mut pl = None;
|
||||||
|
// TODO store.auth_event_ids returns "self" with the event ids is this ok
|
||||||
|
// event.auth_event_ids does not include its own event id ?
|
||||||
for aid in store.auth_event_ids(room_id, &[event_id.clone()]).unwrap() {
|
for aid in store.auth_event_ids(room_id, &[event_id.clone()]).unwrap() {
|
||||||
if let Ok(aev) = store.get_event(&aid) {
|
if let Ok(aev) = store.get_event(&aid) {
|
||||||
if aev.is_type_and_key(EventType::RoomPowerLevels, "") {
|
if aev.is_type_and_key(EventType::RoomPowerLevels, "") {
|
||||||
@ -467,25 +475,26 @@ impl StateResolution {
|
|||||||
|
|
||||||
fn iterative_auth_check(
|
fn iterative_auth_check(
|
||||||
&mut self,
|
&mut self,
|
||||||
room_id: &RoomId,
|
_room_id: &RoomId,
|
||||||
room_version: &RoomVersionId,
|
room_version: &RoomVersionId,
|
||||||
power_events: &[EventId],
|
power_events: &[EventId],
|
||||||
unconflicted_state: &StateMap<EventId>,
|
unconflicted_state: &StateMap<EventId>,
|
||||||
_event_map: &EventMap<StateEvent>, // TODO use event_map over store ??
|
_event_map: &EventMap<StateEvent>, // TODO use event_map over store ??
|
||||||
store: &dyn StateStore,
|
store: &dyn StateStore,
|
||||||
) -> Result<StateMap<EventId>, String> {
|
) -> Result<StateMap<EventId>, String> {
|
||||||
tracing::debug!("starting iter auth check");
|
tracing::info!("starting iterative auth check");
|
||||||
|
|
||||||
let resolved_state = unconflicted_state.clone();
|
let mut resolved_state = unconflicted_state.clone();
|
||||||
|
|
||||||
for (idx, event_id) in power_events.iter().enumerate() {
|
for (idx, event_id) in power_events.iter().enumerate() {
|
||||||
let event = store.get_event(event_id).unwrap();
|
let event = store.get_event(event_id).unwrap();
|
||||||
|
|
||||||
let mut auth_events = BTreeMap::new();
|
let mut auth_events = BTreeMap::new();
|
||||||
for aid in store.auth_event_ids(room_id, &[event_id.clone()]).unwrap() {
|
for aid in event.auth_event_ids() {
|
||||||
if let Ok(ev) = store.get_event(&aid) {
|
if let Ok(ev) = store.get_event(&aid) {
|
||||||
// TODO is None the same as "" for state_key, pretty sure it is NOT
|
// TODO what to do when no state_key is found ??
|
||||||
auth_events.insert((ev.kind(), ev.state_key().unwrap_or_default()), ev);
|
// TODO check "rejected_reason", I'm guessing this is redacted_because for ruma ??
|
||||||
|
auth_events.insert((ev.kind(), ev.state_key().unwrap()), ev);
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("auth event id for {} is missing {}", aid, event_id);
|
tracing::warn!("auth event id for {} is missing {}", aid, event_id);
|
||||||
}
|
}
|
||||||
@ -502,9 +511,14 @@ impl StateResolution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !event_auth::auth_check(room_version, &event, auth_events)
|
if !event_auth::auth_check(room_version, &event, auth_events)
|
||||||
.ok_or("Auth check failed due to deserialization most likely".to_string())?
|
.ok_or("Auth check failed due to deserialization most likely".to_string())
|
||||||
|
.unwrap()
|
||||||
{
|
{
|
||||||
// TODO synapse passes here on AuthError ??
|
// TODO synapse passes here on AuthError ??
|
||||||
|
tracing::warn!("event {} failed the authentication", event_id.to_string());
|
||||||
|
} else {
|
||||||
|
// add event to resolved state map
|
||||||
|
resolved_state.insert((event.kind(), event.state_key().unwrap()), event_id.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// We yield occasionally when we're working with large data sets to
|
// We yield occasionally when we're working with large data sets to
|
||||||
@ -527,6 +541,10 @@ impl StateResolution {
|
|||||||
store: &dyn StateStore,
|
store: &dyn StateStore,
|
||||||
) -> Vec<EventId> {
|
) -> Vec<EventId> {
|
||||||
tracing::debug!("mainline sort of remaining events");
|
tracing::debug!("mainline sort of remaining events");
|
||||||
|
// tracing::debug!(
|
||||||
|
// "{:?}",
|
||||||
|
// to_sort.iter().map(ToString::to_string).collect::<Vec<_>>()
|
||||||
|
// );
|
||||||
// There can be no EventId's to sort, bail.
|
// There can be no EventId's to sort, bail.
|
||||||
if to_sort.is_empty() {
|
if to_sort.is_empty() {
|
||||||
return vec![];
|
return vec![];
|
||||||
@ -565,12 +583,7 @@ impl StateResolution {
|
|||||||
|
|
||||||
let mut order_map = BTreeMap::new();
|
let mut order_map = BTreeMap::new();
|
||||||
for (idx, ev_id) in to_sort.iter().enumerate() {
|
for (idx, ev_id) in to_sort.iter().enumerate() {
|
||||||
let depth = self.get_mainline_depth(
|
let depth = self.get_mainline_depth(store.get_event(ev_id).ok(), &mainline_map, store);
|
||||||
room_id,
|
|
||||||
event_map.get(ev_id).cloned(),
|
|
||||||
&mainline_map,
|
|
||||||
store,
|
|
||||||
);
|
|
||||||
order_map.insert(
|
order_map.insert(
|
||||||
ev_id,
|
ev_id,
|
||||||
(
|
(
|
||||||
@ -596,36 +609,30 @@ impl StateResolution {
|
|||||||
|
|
||||||
fn get_mainline_depth(
|
fn get_mainline_depth(
|
||||||
&mut self,
|
&mut self,
|
||||||
room_id: &RoomId,
|
|
||||||
mut event: Option<StateEvent>,
|
mut event: Option<StateEvent>,
|
||||||
mainline_map: &EventMap<usize>,
|
mainline_map: &EventMap<usize>,
|
||||||
store: &dyn StateStore,
|
store: &dyn StateStore,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
let mut count = 0;
|
|
||||||
while let Some(sort_ev) = event {
|
while let Some(sort_ev) = event {
|
||||||
|
tracing::debug!(
|
||||||
|
"mainline EVENT ID {}",
|
||||||
|
sort_ev.event_id().unwrap().to_string()
|
||||||
|
);
|
||||||
if let Some(id) = sort_ev.event_id() {
|
if let Some(id) = sort_ev.event_id() {
|
||||||
if let Some(depth) = mainline_map.get(id) {
|
if let Some(depth) = mainline_map.get(id) {
|
||||||
return *depth;
|
return *depth;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let auth_events = if let Some(id) = sort_ev.event_id() {
|
let auth_events = sort_ev.auth_event_ids();
|
||||||
store.auth_event_ids(room_id, &[id.clone()]).unwrap()
|
tracing::debug!(
|
||||||
} else {
|
"mainline AUTH EV {:?}",
|
||||||
vec![]
|
auth_events
|
||||||
};
|
.iter()
|
||||||
if count < 15 {
|
.map(ToString::to_string)
|
||||||
println!(
|
.collect::<Vec<_>>()
|
||||||
"{:?}",
|
);
|
||||||
auth_events
|
|
||||||
.iter()
|
|
||||||
.map(ToString::to_string)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
panic!("{}", sort_ev.event_id().unwrap().to_string())
|
|
||||||
}
|
|
||||||
count += 1;
|
|
||||||
event = None;
|
event = None;
|
||||||
|
|
||||||
for aid in auth_events {
|
for aid in auth_events {
|
||||||
|
@ -1 +0,0 @@
|
|||||||
|
|
@ -21,6 +21,7 @@ use ruma::{
|
|||||||
};
|
};
|
||||||
use serde_json::{from_value as from_json_value, json, Value as JsonValue};
|
use serde_json::{from_value as from_json_value, json, Value as JsonValue};
|
||||||
use state_res::{ResolutionResult, StateEvent, StateMap, StateResolution, StateStore};
|
use state_res::{ResolutionResult, StateEvent, StateMap, StateResolution, StateStore};
|
||||||
|
use tracing_subscriber as tracer;
|
||||||
|
|
||||||
static mut SERVER_TIMESTAMP: i32 = 0;
|
static mut SERVER_TIMESTAMP: i32 = 0;
|
||||||
|
|
||||||
@ -66,15 +67,18 @@ fn member_content_join() -> JsonValue {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_pdu_event(
|
fn to_pdu_event<S>(
|
||||||
id: &str,
|
id: &str,
|
||||||
sender: UserId,
|
sender: UserId,
|
||||||
ev_type: EventType,
|
ev_type: EventType,
|
||||||
state_key: Option<&str>,
|
state_key: Option<&str>,
|
||||||
content: JsonValue,
|
content: JsonValue,
|
||||||
auth_events: &[EventId],
|
auth_events: &[S],
|
||||||
prev_events: &[EventId],
|
prev_events: &[S],
|
||||||
) -> StateEvent {
|
) -> StateEvent
|
||||||
|
where
|
||||||
|
S: AsRef<str>,
|
||||||
|
{
|
||||||
let ts = unsafe {
|
let ts = unsafe {
|
||||||
let ts = SERVER_TIMESTAMP;
|
let ts = SERVER_TIMESTAMP;
|
||||||
// increment the "origin_server_ts" value
|
// increment the "origin_server_ts" value
|
||||||
@ -86,6 +90,36 @@ fn to_pdu_event(
|
|||||||
} else {
|
} else {
|
||||||
format!("${}:foo", id)
|
format!("${}:foo", id)
|
||||||
};
|
};
|
||||||
|
let auth_events = auth_events
|
||||||
|
.iter()
|
||||||
|
.map(AsRef::as_ref)
|
||||||
|
.map(|s| {
|
||||||
|
EventId::try_from(
|
||||||
|
if s.contains("$") {
|
||||||
|
s.to_owned()
|
||||||
|
} else {
|
||||||
|
format!("${}:foo", s)
|
||||||
|
}
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.unwrap();
|
||||||
|
let prev_events = prev_events
|
||||||
|
.iter()
|
||||||
|
.map(AsRef::as_ref)
|
||||||
|
.map(|s| {
|
||||||
|
EventId::try_from(
|
||||||
|
if s.contains("$") {
|
||||||
|
s.to_owned()
|
||||||
|
} else {
|
||||||
|
format!("${}:foo", s)
|
||||||
|
}
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let json = if let Some(state_key) = state_key {
|
let json = if let Some(state_key) = state_key {
|
||||||
json!({
|
json!({
|
||||||
@ -249,102 +283,14 @@ fn INITIAL_EDGES() -> Vec<EventId> {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TestStore(RefCell<BTreeMap<EventId, StateEvent>>);
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
impl StateStore for TestStore {
|
|
||||||
fn get_events(&self, events: &[EventId]) -> Result<Vec<StateEvent>, String> {
|
|
||||||
Ok(self
|
|
||||||
.0
|
|
||||||
.borrow()
|
|
||||||
.iter()
|
|
||||||
.filter(|e| events.contains(e.0))
|
|
||||||
.map(|(_, s)| s)
|
|
||||||
.cloned()
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_event(&self, event_id: &EventId) -> Result<StateEvent, String> {
|
|
||||||
self.0
|
|
||||||
.borrow()
|
|
||||||
.get(event_id)
|
|
||||||
.cloned()
|
|
||||||
.ok_or(format!("{} not found", event_id.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn auth_event_ids(
|
|
||||||
&self,
|
|
||||||
room_id: &RoomId,
|
|
||||||
event_ids: &[EventId],
|
|
||||||
) -> Result<Vec<EventId>, String> {
|
|
||||||
let mut result = vec![];
|
|
||||||
let mut stack = event_ids.to_vec();
|
|
||||||
|
|
||||||
while !stack.is_empty() {
|
|
||||||
let ev_id = stack.pop().unwrap();
|
|
||||||
if result.contains(&ev_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(ev_id.clone());
|
|
||||||
|
|
||||||
let event = self.get_event(&ev_id).unwrap();
|
|
||||||
for aid in event.auth_event_ids() {
|
|
||||||
stack.push(aid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn auth_chain_diff(
|
|
||||||
&self,
|
|
||||||
room_id: &RoomId,
|
|
||||||
event_ids: Vec<Vec<EventId>>,
|
|
||||||
) -> Result<Vec<EventId>, String> {
|
|
||||||
use itertools::Itertools;
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"EVENTS FOR AUTH {:?}",
|
|
||||||
event_ids
|
|
||||||
.iter()
|
|
||||||
.map(|v| v.iter().map(ToString::to_string).collect::<Vec<_>>())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut chains = vec![];
|
|
||||||
for ids in event_ids {
|
|
||||||
let chain = self
|
|
||||||
.auth_event_ids(room_id, &ids)?
|
|
||||||
.into_iter()
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
chains.push(chain);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(chain) = chains.first() {
|
|
||||||
let rest = chains.iter().skip(1).flatten().cloned().collect();
|
|
||||||
let common = chain.intersection(&rest).collect::<Vec<_>>();
|
|
||||||
println!(
|
|
||||||
"COMMON {:?}",
|
|
||||||
common.iter().map(ToString::to_string).collect::<Vec<_>>()
|
|
||||||
);
|
|
||||||
Ok(chains
|
|
||||||
.iter()
|
|
||||||
.flatten()
|
|
||||||
.filter(|id| !common.contains(&id))
|
|
||||||
.cloned()
|
|
||||||
.collect::<BTreeSet<_>>()
|
|
||||||
.into_iter()
|
|
||||||
.collect())
|
|
||||||
} else {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_check(events: &[StateEvent], edges: Vec<Vec<EventId>>, expected_state_ids: Vec<EventId>) {
|
fn do_check(events: &[StateEvent], edges: Vec<Vec<EventId>>, expected_state_ids: Vec<EventId>) {
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
// to activate logging use `RUST_LOG=debug cargo t`
|
||||||
|
tracer::fmt()
|
||||||
|
.with_env_filter(tracer::EnvFilter::from_default_env())
|
||||||
|
.init();
|
||||||
|
|
||||||
let mut resolver = StateResolution::default();
|
let mut resolver = StateResolution::default();
|
||||||
// TODO what do we fill this with, everything ??
|
// TODO what do we fill this with, everything ??
|
||||||
let store = TestStore(RefCell::new(
|
let store = TestStore(RefCell::new(
|
||||||
@ -363,7 +309,6 @@ fn do_check(events: &[StateEvent], edges: Vec<Vec<EventId>>, expected_state_ids:
|
|||||||
// create the DB of events that led up to this point
|
// create the DB of events that led up to this point
|
||||||
// TODO maybe clean up some of these clones it is just tests but...
|
// TODO maybe clean up some of these clones it is just tests but...
|
||||||
for ev in INITIAL_EVENTS().values().chain(events) {
|
for ev in INITIAL_EVENTS().values().chain(events) {
|
||||||
println!("{:?}", ev.event_id().unwrap().to_string());
|
|
||||||
graph.insert(ev.event_id().unwrap().clone(), vec![]);
|
graph.insert(ev.event_id().unwrap().clone(), vec![]);
|
||||||
fake_event_map.insert(ev.event_id().unwrap().clone(), ev.clone());
|
fake_event_map.insert(ev.event_id().unwrap().clone(), ev.clone());
|
||||||
}
|
}
|
||||||
@ -393,7 +338,6 @@ fn do_check(events: &[StateEvent], edges: Vec<Vec<EventId>>, expected_state_ids:
|
|||||||
// TODO is this `key_fn` return correct ??
|
// TODO is this `key_fn` return correct ??
|
||||||
.lexicographical_topological_sort(&graph, |id| (0, UNIX_EPOCH, Some(id.clone())))
|
.lexicographical_topological_sort(&graph, |id| (0, UNIX_EPOCH, Some(id.clone())))
|
||||||
{
|
{
|
||||||
println!("{}", node.to_string());
|
|
||||||
let fake_event = fake_event_map.get(&node).unwrap();
|
let fake_event = fake_event_map.get(&node).unwrap();
|
||||||
let event_id = fake_event.event_id().unwrap();
|
let event_id = fake_event.event_id().unwrap();
|
||||||
|
|
||||||
@ -411,7 +355,7 @@ fn do_check(events: &[StateEvent], edges: Vec<Vec<EventId>>, expected_state_ids:
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// println!(
|
// println!(
|
||||||
// "resolving {:#?}",
|
// "RESOLVING {:?}",
|
||||||
// state_sets
|
// state_sets
|
||||||
// .iter()
|
// .iter()
|
||||||
// .map(|map| map
|
// .map(|map| map
|
||||||
@ -430,7 +374,17 @@ fn do_check(events: &[StateEvent], edges: Vec<Vec<EventId>>, expected_state_ids:
|
|||||||
);
|
);
|
||||||
match resolved {
|
match resolved {
|
||||||
Ok(ResolutionResult::Resolved(state)) => state,
|
Ok(ResolutionResult::Resolved(state)) => state,
|
||||||
_ => panic!("resolution for {} failed", node),
|
Ok(ResolutionResult::Conflicted(state)) => panic!(
|
||||||
|
"conflicted: {:?}",
|
||||||
|
state
|
||||||
|
.iter()
|
||||||
|
.map(|map| map
|
||||||
|
.iter()
|
||||||
|
.map(|(key, id)| (key, id.to_string()))
|
||||||
|
.collect::<Vec<_>>())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
),
|
||||||
|
Err(e) => panic!("resolution for {} failed: {}", node, e),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -443,10 +397,10 @@ fn do_check(events: &[StateEvent], edges: Vec<Vec<EventId>>, expected_state_ids:
|
|||||||
}
|
}
|
||||||
|
|
||||||
let auth_types = state_res::auth_types_for_event(fake_event);
|
let auth_types = state_res::auth_types_for_event(fake_event);
|
||||||
println!(
|
// println!(
|
||||||
"AUTH TYPES {:?}",
|
// "AUTH TYPES {:?}",
|
||||||
auth_types.iter().map(|(t, id)| (t, id)).collect::<Vec<_>>()
|
// auth_types.iter().map(|(t, id)| (t, id)).collect::<Vec<_>>()
|
||||||
);
|
// );
|
||||||
|
|
||||||
let mut auth_events = vec![];
|
let mut auth_events = vec![];
|
||||||
for key in auth_types {
|
for key in auth_types {
|
||||||
@ -500,14 +454,7 @@ fn do_check(events: &[StateEvent], edges: Vec<Vec<EventId>>, expected_state_ids:
|
|||||||
.get(&EventId::try_from("$END:foo").unwrap())
|
.get(&EventId::try_from("$END:foo").unwrap())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(k, v)| {
|
.filter(|(k, v)| expected_state.contains_key(k) || start_state.get(k) != Some(*v))
|
||||||
println!(
|
|
||||||
"{:?} == {:?}",
|
|
||||||
start_state.get(k).map(ToString::to_string),
|
|
||||||
Some(v.to_string())
|
|
||||||
);
|
|
||||||
expected_state.contains_key(k) || start_state.get(k) != Some(*v)
|
|
||||||
})
|
|
||||||
.map(|(k, v)| (k.clone(), v.clone()))
|
.map(|(k, v)| (k.clone(), v.clone()))
|
||||||
.collect::<StateMap<EventId>>();
|
.collect::<StateMap<EventId>>();
|
||||||
|
|
||||||
@ -639,3 +586,100 @@ fn test_lexicographical_sort() {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A StateStore implementation for testing
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
/// The test state store.
|
||||||
|
pub struct TestStore(RefCell<BTreeMap<EventId, StateEvent>>);
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
impl StateStore for TestStore {
|
||||||
|
fn get_events(&self, events: &[EventId]) -> Result<Vec<StateEvent>, String> {
|
||||||
|
Ok(self
|
||||||
|
.0
|
||||||
|
.borrow()
|
||||||
|
.iter()
|
||||||
|
.filter(|e| events.contains(e.0))
|
||||||
|
.map(|(_, s)| s)
|
||||||
|
.cloned()
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_event(&self, event_id: &EventId) -> Result<StateEvent, String> {
|
||||||
|
self.0
|
||||||
|
.borrow()
|
||||||
|
.get(event_id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or(format!("{} not found", event_id.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auth_event_ids(
|
||||||
|
&self,
|
||||||
|
room_id: &RoomId,
|
||||||
|
event_ids: &[EventId],
|
||||||
|
) -> Result<Vec<EventId>, String> {
|
||||||
|
let mut result = vec![];
|
||||||
|
let mut stack = event_ids.to_vec();
|
||||||
|
|
||||||
|
// DFS for auth event chain
|
||||||
|
while !stack.is_empty() {
|
||||||
|
let ev_id = stack.pop().unwrap();
|
||||||
|
if result.contains(&ev_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(ev_id.clone());
|
||||||
|
|
||||||
|
let event = self.get_event(&ev_id).unwrap();
|
||||||
|
stack.extend(event.auth_event_ids());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auth_chain_diff(
|
||||||
|
&self,
|
||||||
|
room_id: &RoomId,
|
||||||
|
event_ids: Vec<Vec<EventId>>,
|
||||||
|
) -> Result<Vec<EventId>, String> {
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
// println!(
|
||||||
|
// "EVENTS FOR AUTH {:?}",
|
||||||
|
// event_ids
|
||||||
|
// .iter()
|
||||||
|
// .map(|v| v.iter().map(ToString::to_string).collect::<Vec<_>>())
|
||||||
|
// .collect::<Vec<_>>()
|
||||||
|
// );
|
||||||
|
|
||||||
|
let mut chains = vec![];
|
||||||
|
for ids in event_ids {
|
||||||
|
let chain = self
|
||||||
|
.auth_event_ids(room_id, &ids)?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
chains.push(chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(chain) = chains.first() {
|
||||||
|
let rest = chains.iter().skip(1).flatten().cloned().collect();
|
||||||
|
let common = chain.intersection(&rest).collect::<Vec<_>>();
|
||||||
|
// println!(
|
||||||
|
// "COMMON {:?}",
|
||||||
|
// common.iter().map(ToString::to_string).collect::<Vec<_>>()
|
||||||
|
// );
|
||||||
|
Ok(chains
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.filter(|id| !common.contains(&id))
|
||||||
|
.cloned()
|
||||||
|
.collect::<BTreeSet<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.collect())
|
||||||
|
} else {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user