State-reset and security mitigations.

Upgrade Ruma to present.

The following are intentionally benign for activation in a later commit:

- Hydra backports not default.
- Room version 12 not default.
- Room version 12 not listed as stable.

Do not enable them manually or you can brick your database.

Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
Jason Volk
2025-06-29 03:33:29 +00:00
parent 2c6dd78502
commit 628597c318
134 changed files with 14961 additions and 4935 deletions

2
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[env]
RUMA_UNSTABLE_EXHAUSTIVE_TYPES = "true"

91
Cargo.lock generated
View File

@@ -720,6 +720,18 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "console"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"windows-sys 0.59.0",
]
[[package]]
name = "const-oid"
version = "0.9.6"
@@ -1143,6 +1155,12 @@ dependencies = [
"serde",
]
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "enum-as-inner"
version = "0.6.1"
@@ -1989,6 +2007,18 @@ version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]]
name = "insta"
version = "1.43.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371"
dependencies = [
"console",
"once_cell",
"serde",
"similar",
]
[[package]]
name = "interpolate_name"
version = "0.2.4"
@@ -3439,8 +3469,8 @@ dependencies = [
[[package]]
name = "ruma"
version = "0.10.1"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.12.6"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"assign",
"js_int",
@@ -3451,7 +3481,6 @@ dependencies = [
"ruma-events",
"ruma-federation-api",
"ruma-identifiers-validation",
"ruma-identity-service-api",
"ruma-push-gateway-api",
"ruma-signatures",
"web-time",
@@ -3459,8 +3488,8 @@ dependencies = [
[[package]]
name = "ruma-appservice-api"
version = "0.10.0"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.12.2"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"js_int",
"ruma-common",
@@ -3471,8 +3500,8 @@ dependencies = [
[[package]]
name = "ruma-client-api"
version = "0.18.0"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.20.4"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"as_variant",
"assign",
@@ -3494,8 +3523,8 @@ dependencies = [
[[package]]
name = "ruma-common"
version = "0.13.0"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.15.4"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"as_variant",
"base64",
@@ -3522,12 +3551,13 @@ dependencies = [
"uuid",
"web-time",
"wildmatch",
"zeroize",
]
[[package]]
name = "ruma-events"
version = "0.28.1"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.30.5"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"as_variant",
"indexmap",
@@ -3547,12 +3577,13 @@ dependencies = [
"url",
"web-time",
"wildmatch",
"zeroize",
]
[[package]]
name = "ruma-federation-api"
version = "0.9.0"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.11.2"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"bytes",
"headers",
@@ -3573,27 +3604,17 @@ dependencies = [
[[package]]
name = "ruma-identifiers-validation"
version = "0.9.5"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.10.1"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"js_int",
"thiserror 2.0.12",
]
[[package]]
name = "ruma-identity-service-api"
version = "0.9.0"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
dependencies = [
"js_int",
"ruma-common",
"serde",
]
[[package]]
name = "ruma-macros"
version = "0.13.0"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.15.2"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"cfg-if",
"proc-macro-crate",
@@ -3607,8 +3628,8 @@ dependencies = [
[[package]]
name = "ruma-push-gateway-api"
version = "0.9.0"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.11.0"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"js_int",
"ruma-common",
@@ -3619,8 +3640,8 @@ dependencies = [
[[package]]
name = "ruma-signatures"
version = "0.15.0"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.17.1"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"base64",
"ed25519-dalek",
@@ -4206,6 +4227,12 @@ dependencies = [
"quote",
]
[[package]]
name = "similar"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]]
name = "simple_asn1"
version = "0.6.3"
@@ -4977,6 +5004,7 @@ dependencies = [
"hardened_malloc-rs",
"http",
"http-body-util",
"insta",
"ipaddress",
"itertools 0.14.0",
"jsonwebtoken",
@@ -4996,6 +5024,7 @@ dependencies = [
"serde_json",
"serde_regex",
"serde_yaml",
"similar",
"smallstr",
"smallvec",
"thiserror 2.0.12",

View File

@@ -219,6 +219,12 @@ features = [
"webp",
]
[workspace.dependencies.insta]
version = "1.43.1"
features = [
"json",
]
[workspace.dependencies.ipaddress]
version = "0.1"
@@ -311,20 +317,18 @@ default-features = false
[workspace.dependencies.ruma]
git = "https://github.com/matrix-construct/ruma"
rev = "0155c2b33233bec9dece79d5134a9574b347f4c1"
rev = "3d3acddfcf96891f1203ca3c36d8f41932ede50f"
features = [
"compat",
"__compat",
"rand",
"appservice-api-c",
"client-api",
"federation-api",
"markdown",
"push-gateway-api-c",
"unstable-exhaustive-types",
"ring-compat",
"compat-upload-signatures",
"identifiers-validation",
"unstable-unspecified",
"unstable-msc2448",
"unstable-msc2666",
"unstable-msc2867",
@@ -332,10 +336,8 @@ features = [
"unstable-msc3026",
"unstable-msc3061",
"unstable-msc3245",
"unstable-msc3266",
"unstable-msc3381", # polls
"unstable-msc3489", # beacon / live location
"unstable-msc3575",
"unstable-msc3930", # polls push rules
"unstable-msc4075",
"unstable-msc4095",
@@ -343,9 +345,9 @@ features = [
"unstable-msc4125",
"unstable-msc4186",
"unstable-msc4203", # sending to-device events to appservices
"unstable-msc4210", # remove legacy mentions
"unstable-msc4311",
"unstable-extensible-events",
"unstable-pdu",
"unstable-hydra",
]
[workspace.dependencies.rustls]
@@ -425,6 +427,9 @@ default-features = false
version = "0.10"
default-features = false
[workspace.dependencies.similar]
version = "2.7.0"
[workspace.dependencies.smallstr]
version = "0.3"
features = [

View File

@@ -87,9 +87,12 @@ pub(super) async fn parse_pdu(&self) -> Result {
}
let string = self.body[1..self.body.len().saturating_sub(1)].join("\n");
let rules = RoomVersionId::V6
.rules()
.expect("rules for V6 rooms");
match serde_json::from_str(&string) {
| Err(e) => return Err!("Invalid json in command body: {e}"),
| Ok(value) => match ruma::signatures::reference_hash(&value, &RoomVersionId::V6) {
| Ok(value) => match ruma::signatures::reference_hash(&value, &rules) {
| Err(e) => return Err!("Could not parse PDU JSON: {e:?}"),
| Ok(hash) => {
let event_id = OwnedEventId::parse(format!("${hash}"));
@@ -252,7 +255,6 @@ pub(super) async fn get_remote_pdu(
.sending
.send_federation_request(&server, ruma::api::federation::event::get_event::v1::Request {
event_id: event_id.clone(),
include_unredacted_content: None,
})
.await
{

View File

@@ -195,6 +195,7 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
self.services
.admin
.make_user_admin(&user_id)
.boxed()
.await?;
warn!("Granting {user_id} admin privileges as the first user");
}
@@ -725,32 +726,38 @@ pub(super) async fn force_demote(&self, user_id: String, room_id: OwnedRoomOrAli
.lock(&room_id)
.await;
let room_power_levels: Option<RoomPowerLevelsEventContent> = self
let room_power_levels: Option<RoomPowerLevels> = self
.services
.rooms
.state_accessor
.room_state_get_content(&room_id, &StateEventType::RoomPowerLevels, "")
.get_power_levels(&room_id)
.await
.ok();
let user_can_demote_self = room_power_levels
let user_can_change_self = room_power_levels
.as_ref()
.is_some_and(|power_levels_content| {
RoomPowerLevels::from(power_levels_content.clone())
.user_can_change_user_power_level(&user_id, &user_id)
}) || self
.services
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
.await
.is_ok_and(|event| event.sender() == user_id);
.is_some_and(|power_levels| {
power_levels.user_can_change_user_power_level(&user_id, &user_id)
});
let user_can_demote_self = user_can_change_self
|| self
.services
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
.await
.is_ok_and(|event| event.sender() == user_id);
if !user_can_demote_self {
return Err!("User is not allowed to modify their own power levels in the room.",);
return Err!("User is not allowed to modify their own power levels in the room.");
}
let mut power_levels_content = room_power_levels.unwrap_or_default();
let mut power_levels_content: RoomPowerLevelsEventContent = room_power_levels
.map(TryInto::try_into)
.transpose()?
.unwrap_or_default();
power_levels_content.users.remove(&user_id);
let event_id = self
@@ -783,6 +790,7 @@ pub(super) async fn make_user_admin(&self, user_id: String) -> Result {
self.services
.admin
.make_user_admin(&user_id)
.boxed()
.await?;
self.write_str(&format!("{user_id} has been granted admin privileges.",))

View File

@@ -11,10 +11,7 @@ use ruma::{
},
uiaa::{AuthFlow, AuthType, UiaaInfo},
},
events::{
StateEventType,
room::power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
events::{StateEventType, room::power_levels::RoomPowerLevelsEventContent},
};
use tuwunel_core::{
Err, Error, Result, err, info,
@@ -60,10 +57,7 @@ pub(crate) async fn change_password_route(
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
..Default::default()
};
match &body.auth {
@@ -189,10 +183,7 @@ pub(crate) async fn deactivate_route(
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
..Default::default()
};
match &body.auth {
@@ -331,21 +322,18 @@ pub async fn full_user_deactivate(
let room_power_levels = services
.rooms
.state_accessor
.room_state_get_content::<RoomPowerLevelsEventContent>(
room_id,
&StateEventType::RoomPowerLevels,
"",
)
.get_power_levels(room_id)
.await
.ok();
let user_can_demote_self =
room_power_levels
.as_ref()
.is_some_and(|power_levels_content| {
RoomPowerLevels::from(power_levels_content.clone())
.user_can_change_user_power_level(user_id, user_id)
}) || services
let user_can_change_self = room_power_levels
.as_ref()
.is_some_and(|power_levels| {
power_levels.user_can_change_user_power_level(user_id, user_id)
});
let user_can_demote_self = user_can_change_self
|| services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, "")
@@ -353,7 +341,11 @@ pub async fn full_user_deactivate(
.is_ok_and(|event| event.sender() == user_id);
if user_can_demote_self {
let mut power_levels_content = room_power_levels.unwrap_or_default();
let mut power_levels_content: RoomPowerLevelsEventContent = room_power_levels
.map(TryInto::try_into)
.transpose()?
.unwrap_or_default();
power_levels_content.users.remove(user_id);
// ignore errors so deactivation doesn't fail

View File

@@ -3,9 +3,12 @@ use std::collections::BTreeMap;
use axum::extract::State;
use ruma::{
RoomVersionId,
api::client::discovery::get_capabilities::{
self, Capabilities, GetLoginTokenCapability, RoomVersionStability,
RoomVersionsCapability, ThirdPartyIdChangesCapability,
api::client::discovery::{
get_capabilities,
get_capabilities::v3::{
Capabilities, GetLoginTokenCapability, RoomVersionStability, RoomVersionsCapability,
ThirdPartyIdChangesCapability,
},
},
};
use serde_json::json;

View File

@@ -145,10 +145,7 @@ pub(crate) async fn delete_device_route(
// UIAA
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
..Default::default()
};
match &body.auth {
@@ -224,10 +221,7 @@ pub(crate) async fn delete_devices_route(
// UIAA
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
..Default::default()
};
match &body.auth {

View File

@@ -16,18 +16,15 @@ use ruma::{
},
federation,
},
directory::{Filter, PublicRoomJoinRule, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
directory::{Filter, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
events::{
StateEventType,
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
room::join_rules::{JoinRule, RoomJoinRulesEventContent},
},
uint,
};
use tuwunel_core::{
Err, Result, err, info,
Err, Result, err, info, is_true,
matrix::Event,
utils::{
TryFutureExtExt,
@@ -51,7 +48,7 @@ pub(crate) async fn get_public_rooms_filtered_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_public_rooms_filtered::v3::Request>,
) -> Result<get_public_rooms_filtered::v3::Response> {
check_banned(&services, body.server.as_deref())?;
check_server_banned(&services, body.server.as_deref())?;
let response = get_public_rooms_filtered_helper(
&services,
@@ -80,7 +77,7 @@ pub(crate) async fn get_public_rooms_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_public_rooms::v3::Request>,
) -> Result<get_public_rooms::v3::Response> {
check_banned(&services, body.server.as_deref())?;
check_server_banned(&services, body.server.as_deref())?;
let response = get_public_rooms_filtered_helper(
&services,
@@ -393,15 +390,11 @@ async fn user_can_publish_room(
match services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomPowerLevels, "")
.get_power_levels(room_id)
.await
{
| Ok(event) => serde_json::from_str(event.content().get())
.map_err(|_| err!(Database("Invalid event content for m.room.power_levels")))
.map(|content: RoomPowerLevelsEventContent| {
RoomPowerLevels::from(content)
.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)
}),
| Ok(power_levels) =>
Ok(power_levels.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)),
| _ => {
match services
.rooms
@@ -453,7 +446,7 @@ async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> Public
.state_accessor
.room_state_get_content(&room_id, &StateEventType::RoomJoinRules, "")
.map_ok(|c: RoomJoinRulesEventContent| match c.join_rule {
| JoinRule::Public => PublicRoomJoinRule::Public,
| JoinRule::Public => "public".into(),
| JoinRule::Knock => "knock".into(),
| JoinRule::KnockRestricted(_) => "knock_restricted".into(),
| _ => "invite".into(),
@@ -497,24 +490,25 @@ async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> Public
}
}
fn check_banned(services: &Services, server: Option<&ServerName>) -> Result {
fn check_server_banned(services: &Services, server: Option<&ServerName>) -> Result {
let Some(server) = server else {
return Ok(());
};
let forbidden_remote_directory = services
.config
.forbidden_remote_room_directory_server_names
.is_match(server.host());
let conditions = [
services
.config
.forbidden_remote_room_directory_server_names
.is_match(server.host()),
services
.config
.forbidden_remote_server_names
.is_match(server.host()),
];
let forbidden_remote_server = services
.config
.forbidden_remote_server_names
.is_match(server.host());
if forbidden_remote_directory || forbidden_remote_server {
Err!(Request(Forbidden("Server is banned on this homeserver.")))
} else {
Ok(())
if conditions.iter().any(is_true!()) {
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
}
Ok(())
}

View File

@@ -3,7 +3,8 @@ use std::collections::{BTreeMap, HashMap, HashSet};
use axum::extract::State;
use futures::{StreamExt, stream::FuturesUnordered};
use ruma::{
OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
CanonicalJsonObject, CanonicalJsonValue, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId,
UserId,
api::{
client::{
error::ErrorKind,
@@ -162,10 +163,7 @@ pub(crate) async fn upload_signing_keys_route(
// UIAA
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
..Default::default()
};
match check_for_new_keys(
@@ -599,18 +597,19 @@ fn add_unsigned_device_display_name(
include_display_names: bool,
) -> Result {
if let Some(display_name) = metadata.display_name {
let mut object = keys.deserialize_as::<serde_json::Map<String, serde_json::Value>>()?;
let mut object = keys.deserialize_as_unchecked::<CanonicalJsonObject>()?;
let unsigned = object
.entry("unsigned")
.or_insert_with(|| json!({}));
if let serde_json::Value::Object(unsigned_object) = unsigned {
.entry("unsigned".into())
.or_insert_with(CanonicalJsonValue::default);
if let CanonicalJsonValue::Object(unsigned_object) = unsigned {
if include_display_names {
unsigned_object.insert("device_display_name".to_owned(), display_name.into());
} else {
unsigned_object.insert(
"device_display_name".to_owned(),
Some(metadata.device_id.as_str().to_owned()).into(),
CanonicalJsonValue::String(metadata.device_id.as_str().to_owned()),
);
}
}

View File

@@ -7,8 +7,9 @@ use ruma::{
events::room::member::{MembershipState, RoomMemberEventContent},
};
use tuwunel_core::{
Err, Result, debug_error, err, info,
Err, Result, err,
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
warn,
};
use tuwunel_service::Services;
@@ -27,8 +28,8 @@ pub(crate) async fn invite_user_route(
let sender_user = body.sender_user();
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
debug_error!(
"User {sender_user} is not an admin and attempted to send an invite to room {}",
warn!(
"{sender_user} is not an admin and attempted to send an invite to {}",
&body.room_id
);
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
@@ -104,10 +105,7 @@ pub(crate) async fn invite_helper(
is_direct: bool,
) -> Result {
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
info!(
"User {sender_user} is not an admin and attempted to send an invite to room \
{room_id}"
);
warn!("{sender_user} is not an admin and attempted to send an invite to {room_id}");
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
}
@@ -156,7 +154,10 @@ pub(crate) async fn invite_helper(
.sending
.convert_to_outgoing_federation_event(pdu_json.clone())
.await,
invite_room_state,
invite_room_state: invite_room_state
.into_iter()
.map(Into::into)
.collect(),
via: services
.rooms
.state_cache

View File

@@ -25,10 +25,9 @@ use ruma::{
use tuwunel_core::{
Err, Result, debug, debug_info, debug_warn, err, error, info,
matrix::{
StateKey,
event::{gen_event_id, gen_event_id_canonical_json},
pdu::{PduBuilder, PduEvent},
state_res,
room_version, state_res,
},
result::FlatOk,
trace,
@@ -551,7 +550,13 @@ async fn join_room_by_id_helper_remote(
})
.ready_filter_map(Result::ok)
.fold(HashMap::new(), async |mut state, (event_id, value)| {
let pdu = match PduEvent::from_id_val(&event_id, value.clone()) {
let pdu = if value["type"] == "m.room.create" {
PduEvent::from_rid_val(room_id, &event_id, value.clone())
} else {
PduEvent::from_id_val(&event_id, value.clone())
};
let pdu = match pdu {
| Ok(pdu) => pdu,
| Err(e) => {
debug_warn!("Invalid PDU in send_join response: {e:?}: {value:#?}");
@@ -604,36 +609,26 @@ async fn join_room_by_id_helper_remote(
drop(cork);
debug!("Running send_join auth check");
let fetch_state = &state;
let state_fetch = |k: StateEventType, s: StateKey| async move {
let shortstatekey = services
.rooms
.short
.get_shortstatekey(&k, &s)
.await
.ok()?;
let event_id = fetch_state.get(&shortstatekey)?;
services
.rooms
.timeline
.get_pdu(event_id)
.await
.ok()
};
let auth_check = state_res::event_auth::auth_check(
&state_res::RoomVersion::new(&room_version_id)?,
state_res::auth_check(
&room_version::rules(&room_version_id)?,
&parsed_join_pdu,
None, // TODO: third party invite
|k, s| state_fetch(k.clone(), s.into()),
)
.await
.map_err(|e| err!(Request(Forbidden(warn!("Auth check failed: {e:?}")))))?;
&async |event_id| services.rooms.timeline.get_pdu(&event_id).await,
&async |event_type, state_key| {
let shortstatekey = services
.rooms
.short
.get_shortstatekey(&event_type, state_key.as_str())
.await?;
if !auth_check {
return Err!(Request(Forbidden("Auth check failed")));
}
let event_id = state.get(&shortstatekey).ok_or_else(|| {
err!(Request(NotFound("Missing fetch_state {shortstatekey:?}")))
})?;
services.rooms.timeline.get_pdu(event_id).await
},
)
.boxed()
.await?;
info!("Compressing state from send_join");
let compressed: CompressedState = services

View File

@@ -8,7 +8,10 @@ use ruma::{
RoomVersionId, UserId,
api::{
client::knock::knock_room,
federation::{self},
federation::{
membership::RawStrippedState,
{self},
},
},
canonical_json::to_canonical_value,
events::{
@@ -17,7 +20,7 @@ use ruma::{
},
};
use tuwunel_core::{
Err, Result, debug, debug_info, debug_warn, err, info,
Err, Result, debug, debug_info, debug_warn, err, extract_variant, info,
matrix::{
event::{Event, gen_event_id},
pdu::{PduBuilder, PduEvent},
@@ -346,7 +349,7 @@ async fn knock_room_helper_local(
let knock_event = knock_event_stub;
info!("Asking {remote_server} for send_knock in room {room_id}");
let send_knock_request = federation::knock::send_knock::v1::Request {
let send_knock_request = federation::membership::create_knock_event::v1::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
pdu: services
@@ -384,7 +387,13 @@ async fn knock_room_helper_local(
.get_content::<RoomMemberEventContent>()
.expect("we just created this"),
sender_user,
Some(send_knock_response.knock_room_state),
Some(
send_knock_response
.knock_room_state
.into_iter()
.filter_map(|s| extract_variant!(s, RawStrippedState::Stripped))
.collect(),
),
None,
false,
)
@@ -477,7 +486,7 @@ async fn knock_room_helper_remote(
let knock_event = knock_event_stub;
info!("Asking {remote_server} for send_knock in room {room_id}");
let send_knock_request = federation::knock::send_knock::v1::Request {
let send_knock_request = federation::membership::create_knock_event::v1::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
pdu: services
@@ -507,7 +516,14 @@ async fn knock_room_helper_remote(
let state = send_knock_response
.knock_room_state
.iter()
.map(|event| serde_json::from_str::<CanonicalJsonObject>(event.clone().into_json().get()))
.map(|event| {
serde_json::from_str::<CanonicalJsonObject>(
extract_variant!(event.clone(), RawStrippedState::Stripped)
.expect("Raw<AnyStrippedStateEvent>")
.json()
.get(),
)
})
.filter_map(Result::ok);
let mut state_map: HashMap<u64, OwnedEventId> = HashMap::new();
@@ -594,7 +610,13 @@ async fn knock_room_helper_remote(
.get_content::<RoomMemberEventContent>()
.expect("we just created this"),
sender_user,
Some(send_knock_response.knock_room_state),
Some(
send_knock_response
.knock_room_state
.into_iter()
.filter_map(|s| extract_variant!(s, RawStrippedState::Stripped))
.collect(),
),
None,
false,
)
@@ -628,7 +650,7 @@ async fn make_knock_request(
sender_user: &UserId,
room_id: &RoomId,
servers: &[OwnedServerName],
) -> Result<(federation::knock::create_knock_event_template::v1::Response, OwnedServerName)> {
) -> Result<(federation::membership::prepare_knock_event::v1::Response, OwnedServerName)> {
let mut make_knock_response_and_server =
Err!(BadServerResponse("No server available to assist in knocking."));
@@ -645,7 +667,7 @@ async fn make_knock_request(
.sending
.send_federation_request(
remote_server,
federation::knock::create_knock_event_template::v1::Request {
federation::membership::prepare_knock_event::v1::Request {
room_id: room_id.to_owned(),
user_id: sender_user.to_owned(),
ver: services

View File

@@ -70,15 +70,16 @@ pub(crate) async fn banned_room_check(
if let Some(room_id) = room_id {
if services.rooms.metadata.is_banned(room_id).await
|| services
.config
.forbidden_remote_server_names
.is_match(
room_id
.server_name()
.expect("legacy room mxid")
.host(),
) {
|| (room_id.server_name().is_some()
&& services
.config
.forbidden_remote_server_names
.is_match(
room_id
.server_name()
.expect("legacy room mxid")
.host(),
)) {
warn!(
"User {user_id} who is not an admin attempted to send an invite for or \
attempted to join a banned room or banned room server name: {room_id}"

View File

@@ -60,7 +60,7 @@ pub(super) use message::*;
pub(super) use openid::*;
pub(super) use presence::*;
pub(super) use profile::*;
pub use profile::{update_all_rooms, update_avatar_url, update_displayname};
pub use profile::{update_avatar_url, update_displayname};
pub(super) use push::*;
pub(super) use read_marker::*;
pub(super) use redact::*;

View File

@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
use axum::extract::State;
use futures::{
StreamExt, TryStreamExt,
FutureExt, StreamExt, TryStreamExt,
future::{join, join3, join4},
};
use ruma::{
@@ -368,7 +368,9 @@ pub async fn update_displayname(
.collect()
.await;
update_all_rooms(services, all_joined_rooms, user_id).await;
update_all_rooms(services, all_joined_rooms, user_id)
.boxed()
.await;
}
pub async fn update_avatar_url(
@@ -421,10 +423,12 @@ pub async fn update_avatar_url(
.collect()
.await;
update_all_rooms(services, all_joined_rooms, user_id).await;
update_all_rooms(services, all_joined_rooms, user_id)
.boxed()
.await;
}
pub async fn update_all_rooms(
async fn update_all_rooms(
services: &Services,
all_joined_rooms: Vec<(PduBuilder, &OwnedRoomId)>,
user_id: &UserId,

View File

@@ -316,7 +316,7 @@ pub(crate) async fn register_route(
stages: vec![AuthType::RegistrationToken],
}],
completed: Vec::new(),
params: Box::default(),
params: Default::default(),
session: None,
auth_error: None,
};
@@ -326,7 +326,7 @@ pub(crate) async fn register_route(
uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
completed: Vec::new(),
params: Box::default(),
params: Default::default(),
session: None,
auth_error: None,
};
@@ -523,7 +523,11 @@ pub(crate) async fn register_route(
.await
.is_ok_and(is_equal_to!(1))
{
services.admin.make_user_admin(&user_id).await?;
services
.admin
.make_user_admin(&user_id)
.boxed()
.await?;
warn!("Granting {user_id} admin privileges as the first user");
}

View File

@@ -15,6 +15,8 @@ use tuwunel_service::Services;
use crate::Ruma;
const REASON_MAX_LEN: usize = 750;
/// # `POST /_matrix/client/v3/rooms/{roomId}/report`
///
/// Reports an abusive room to homeserver admins
@@ -29,18 +31,13 @@ pub(crate) async fn report_room_route(
info!(
"Received room report by user {sender_user} for room {} with reason: \"{}\"",
body.room_id,
body.reason.as_deref().unwrap_or("")
body.room_id, body.reason,
);
if body
.reason
.as_ref()
.is_some_and(|s| s.len() > 750)
{
return Err!(Request(
InvalidParam("Reason too long, should be 750 characters or fewer",)
));
if body.reason.len().gt(&REASON_MAX_LEN) {
return Err!(Request(InvalidParam(
"Reason too long, should be {REASON_MAX_LEN} characters or fewer"
)));
}
delay_response().await;
@@ -64,7 +61,7 @@ pub(crate) async fn report_room_route(
"@room Room report received from {} -\n\nRoom ID: {}\n\nReport Reason: {}",
sender_user.to_owned(),
body.room_id,
body.reason.as_deref().unwrap_or("")
body.reason,
)))
.await
.ok();

View File

@@ -1,10 +1,13 @@
use std::collections::BTreeMap;
use axum::extract::State;
use futures::FutureExt;
use futures::{FutureExt, future::OptionFuture};
use ruma::{
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId,
api::client::room::{self, create_room},
api::client::room::{
self, create_room,
create_room::v3::{CreationContent, RoomPreset},
},
events::{
TimelineEventType,
room::{
@@ -16,19 +19,21 @@ use ruma::{
member::{MembershipState, RoomMemberEventContent},
name::RoomNameEventContent,
power_levels::RoomPowerLevelsEventContent,
topic::RoomTopicEventContent,
topic::{RoomTopicEventContent, TopicContentBlock},
},
},
int,
room_version_rules::{RoomIdFormatVersion, RoomVersionRules},
serde::{JsonObject, Raw},
};
use serde_json::{json, value::to_raw_value};
use tuwunel_core::{
Err, Result, debug_info, debug_warn, err, info,
matrix::{StateKey, pdu::PduBuilder},
matrix::{StateKey, pdu::PduBuilder, room_version},
utils::BoolExt,
warn,
};
use tuwunel_service::{Services, appservice::RegistrationInfo};
use tuwunel_service::{Services, appservice::RegistrationInfo, rooms::state::RoomMutexGuard};
use crate::{Ruma, client::invite_helper};
@@ -53,164 +58,61 @@ pub(crate) async fn create_room_route(
State(services): State<crate::State>,
body: Ruma<create_room::v3::Request>,
) -> Result<create_room::v3::Response> {
use create_room::v3::RoomPreset;
can_create_room_check(&services, &body).await?;
can_publish_directory_check(&services, &body).await?;
let sender_user = body.sender_user();
// Figure out preset. We need it for preset specific events
let preset = body
.preset
.clone()
.unwrap_or(match &body.visibility {
| room::Visibility::Public => RoomPreset::PublicChat,
| _ => RoomPreset::PrivateChat, // Room visibility should not be custom
});
if !services.globals.allow_room_creation()
&& body.appservice_info.is_none()
&& !services.users.is_admin(sender_user).await
{
return Err!(Request(Forbidden("Room creation has been disabled.",)));
}
let alias: OptionFuture<_> = body
.room_alias_name
.as_ref()
.map(|alias| room_alias_check(&services, alias, body.appservice_info.as_ref()))
.into();
let room_id: OwnedRoomId = match &body.room_id {
| Some(custom_room_id) => custom_room_id_check(&services, custom_room_id)?,
| _ => RoomId::new(&services.server.name),
};
// check if room ID doesn't already exist instead of erroring on auth check
if services
.rooms
.short
.get_shortroomid(&room_id)
.await
.is_ok()
{
return Err!(Request(RoomInUse("Room with that custom room ID already exists",)));
}
if body.visibility == room::Visibility::Public
&& services
.server
.config
.lockdown_public_room_directory
&& !services.users.is_admin(sender_user).await
&& body.appservice_info.is_none()
{
warn!(
"Non-admin user {sender_user} tried to publish {room_id} to the room directory \
while \"lockdown_public_room_directory\" is enabled"
);
if services.server.config.admin_room_notices {
// Determine room version
let (room_version, version_rules) = body
.room_version
.as_ref()
.map_or(Ok(&services.server.config.default_room_version), |version| {
services
.admin
.notice(&format!(
"Non-admin user {sender_user} tried to publish {room_id} to the room \
directory while \"lockdown_public_room_directory\" is enabled"
))
.await;
}
return Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")));
}
let _short_id = services
.rooms
.short
.get_or_create_shortroomid(&room_id)
.await;
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
let alias: Option<OwnedRoomAliasId> = match body.room_alias_name.as_ref() {
| Some(alias) =>
Some(room_alias_check(&services, alias, body.appservice_info.as_ref()).await?),
| _ => None,
};
let room_version = match body.room_version.clone() {
| Some(room_version) =>
if services
.server
.supported_room_version(&room_version)
{
room_version
} else {
return Err!(Request(UnsupportedRoomVersion(
"This server does not support that room version."
)));
},
| None => services
.server
.config
.default_room_version
.clone(),
};
.supported_room_version(version)
.then_ok_or_else(version, || {
err!(Request(UnsupportedRoomVersion(
"This server does not support room version {version:?}"
)))
})
})
.and_then(|version| Ok((version, room_version::rules(version)?)))?;
let create_content = match &body.creation_content {
| Some(content) => {
use RoomVersionId::*;
let mut content = content
.deserialize_as::<CanonicalJsonObject>()
.map_err(|e| {
err!(Request(BadJson(error!(
"Failed to deserialise content as canonical JSON: {e}"
))))
})?;
match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
content.insert(
"creator".into(),
json!(&sender_user).try_into().map_err(|e| {
err!(Request(BadJson(debug_error!("Invalid creation content: {e}"))))
})?,
);
},
| _ => {
// V11+ removed the "creator" key
},
}
content.insert(
"room_version".into(),
json!(room_version.as_str())
.try_into()
.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
);
content
},
| None => {
use RoomVersionId::*;
let content = match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
RoomCreateEventContent::new_v1(sender_user.to_owned()),
| _ => RoomCreateEventContent::new_v11(),
};
let mut content =
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())
.unwrap();
content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
content
},
};
// Error on existing alias before committing to creation.
let alias = alias.await.transpose()?;
// Increment and hold the counter; the room will sync atomically to clients
// which is preferable.
let next_count = services.globals.next_count();
// 1. The room create event
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_content)?,
state_key: Some(StateKey::new()),
..Default::default()
},
sender_user,
&room_id,
&state_lock,
)
.boxed()
.await?;
// 1. Create the create event.
let (room_id, state_lock) = match version_rules.room_id_format {
| RoomIdFormatVersion::V1 =>
create_create_event_legacy(&services, &body, room_version, &version_rules).await?,
| RoomIdFormatVersion::V2 =>
create_create_event(&services, &body, &preset, room_version, &version_rules)
.await
.map_err(|e| {
err!(Request(InvalidParam("Error while creating m.room.create event: {e}")))
})?,
};
// 2. Let the room creator join
let sender_user = body.sender_user();
services
.rooms
.timeline
@@ -230,17 +132,14 @@ pub(crate) async fn create_room_route(
.await?;
// 3. Power levels
// Figure out preset. We need it for preset specific events
let preset = body
.preset
.clone()
.unwrap_or(match &body.visibility {
| room::Visibility::Public => RoomPreset::PublicChat,
| _ => RoomPreset::PrivateChat, // Room visibility should not be custom
});
let mut users = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]);
let mut users = if !version_rules
.authorization
.explicitly_privilege_room_creators
{
BTreeMap::from_iter([(sender_user.to_owned(), int!(100))])
} else {
BTreeMap::new()
};
if preset == RoomPreset::TrustedPrivateChat {
for invite in &body.invite {
@@ -260,11 +159,17 @@ pub(crate) async fn create_room_route(
continue;
}
users.insert(invite.clone(), int!(100));
if !version_rules
.authorization
.additional_room_creators
{
users.insert(invite.clone(), int!(100));
}
}
}
let power_levels_content = default_power_levels_content(
&version_rules,
body.power_level_content_override.as_ref(),
&body.visibility,
users,
@@ -365,7 +270,7 @@ pub(crate) async fn create_room_route(
// 6. Events listed in initial_state
for event in &body.initial_state {
let mut pdu_builder = event
.deserialize_as::<PduBuilder>()
.deserialize_as_unchecked::<PduBuilder>()
.map_err(|e| {
err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}"))))
})?;
@@ -421,7 +326,10 @@ pub(crate) async fn create_room_route(
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomTopicEventContent { topic: topic.clone() }),
PduBuilder::state(String::new(), &RoomTopicEventContent {
topic: topic.clone(),
topic_block: TopicContentBlock::default(),
}),
sender_user,
&room_id,
&state_lock,
@@ -488,25 +396,226 @@ pub(crate) async fn create_room_route(
Ok(create_room::v3::Response::new(room_id))
}
async fn create_create_event(
services: &Services,
body: &Ruma<create_room::v3::Request>,
preset: &RoomPreset,
room_version: &RoomVersionId,
version_rules: &RoomVersionRules,
) -> Result<(OwnedRoomId, RoomMutexGuard)> {
let _sender_user = body.sender_user();
let mut create_content = match &body.creation_content {
| Some(content) => {
let mut content = content
.deserialize_as_unchecked::<CanonicalJsonObject>()
.map_err(|e| {
err!(Request(BadJson(error!(
"Failed to deserialise content as canonical JSON: {e}"
))))
})?;
content.insert(
"room_version".into(),
json!(room_version.as_str())
.try_into()
.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
);
content
},
| None => {
let content = RoomCreateEventContent::new_v11();
let mut content =
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
content
},
};
if version_rules
.authorization
.additional_room_creators
{
let mut additional_creators = body
.creation_content
.as_ref()
.and_then(|c| {
c.deserialize_as_unchecked::<CreationContent>()
.ok()
})
.unwrap_or_default()
.additional_creators;
if *preset == RoomPreset::TrustedPrivateChat {
additional_creators.extend(body.invite.clone());
}
additional_creators.sort();
additional_creators.dedup();
if !additional_creators.is_empty() {
create_content
.insert("additional_creators".into(), json!(additional_creators).try_into()?);
}
}
// 1. The room create event, using a placeholder room_id
let room_id = ruma::room_id!("!thiswillbereplaced").to_owned();
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
let create_event_id = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_content)?,
state_key: Some(StateKey::new()),
..Default::default()
},
body.sender_user(),
&room_id,
&state_lock,
)
.boxed()
.await?;
drop(state_lock);
// The real room_id is now the event_id.
let room_id = OwnedRoomId::from_parts('!', create_event_id.localpart(), None)?;
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
Ok((room_id, state_lock))
}
async fn create_create_event_legacy(
services: &Services,
body: &Ruma<create_room::v3::Request>,
room_version: &RoomVersionId,
_version_rules: &RoomVersionRules,
) -> Result<(OwnedRoomId, RoomMutexGuard)> {
let room_id: OwnedRoomId = match &body.room_id {
| None => RoomId::new_v1(&services.server.name),
| Some(custom_id) => custom_room_id_check(services, custom_id).await?,
};
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
let _short_id = services
.rooms
.short
.get_or_create_shortroomid(&room_id)
.await;
let create_content = match &body.creation_content {
| Some(content) => {
use RoomVersionId::*;
let mut content = content
.deserialize_as_unchecked::<CanonicalJsonObject>()
.map_err(|e| {
err!(Request(BadJson(error!(
"Failed to deserialise content as canonical JSON: {e}"
))))
})?;
match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
content.insert(
"creator".into(),
json!(body.sender_user())
.try_into()
.map_err(|e| {
err!(Request(BadJson(debug_error!(
"Invalid creation content: {e}"
))))
})?,
);
},
| _ => {
// V11+ removed the "creator" key
},
}
content.insert(
"room_version".into(),
json!(room_version.as_str())
.try_into()
.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
);
content
},
| None => {
use RoomVersionId::*;
let content = match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
RoomCreateEventContent::new_v1(body.sender_user().to_owned()),
| _ => RoomCreateEventContent::new_v11(),
};
let mut content =
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
content
},
};
// 1. The room create event
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_content)?,
state_key: Some(StateKey::new()),
..Default::default()
},
body.sender_user(),
&room_id,
&state_lock,
)
.boxed()
.await?;
Ok((room_id, state_lock))
}
/// creates the power_levels_content for the PDU builder
fn default_power_levels_content(
version_rules: &RoomVersionRules,
power_level_content_override: Option<&Raw<RoomPowerLevelsEventContent>>,
visibility: &room::Visibility,
users: BTreeMap<OwnedUserId, Int>,
) -> Result<serde_json::Value> {
use serde_json::to_value;
let mut power_levels_content =
to_value(RoomPowerLevelsEventContent { users, ..Default::default() })?;
let mut power_levels_content = RoomPowerLevelsEventContent::new(&version_rules.authorization);
power_levels_content.users = users;
let mut power_levels_content = to_value(power_levels_content)?;
// secure proper defaults of sensitive/dangerous permissions that moderators
// (power level 50) should not have easy access to
power_levels_content["events"]["m.room.power_levels"] = to_value(100)?;
power_levels_content["events"]["m.room.server_acl"] = to_value(100)?;
power_levels_content["events"]["m.room.tombstone"] = to_value(100)?;
power_levels_content["events"]["m.room.encryption"] = to_value(100)?;
power_levels_content["events"]["m.room.history_visibility"] = to_value(100)?;
if version_rules
.authorization
.explicitly_privilege_room_creators
{
power_levels_content["events"]["m.room.tombstone"] = to_value(150)?;
} else {
power_levels_content["events"]["m.room.tombstone"] = to_value(100)?;
}
// always allow users to respond (not post new) to polls. this is primarily
// useful in read-only announcement rooms that post a public poll.
power_levels_content["events"]["org.matrix.msc3381.poll.response"] = to_value(0)?;
@@ -599,7 +708,7 @@ async fn room_alias_check(
}
/// if a room is being created with a custom room ID, run our checks against it
fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<OwnedRoomId> {
async fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<OwnedRoomId> {
// apply forbidden room alias checks to custom room IDs too
if services
.globals
@@ -623,8 +732,65 @@ fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<Own
let server_name = services.globals.server_name();
let full_room_id = format!("!{custom_room_id}:{server_name}");
OwnedRoomId::parse(full_room_id)
.map_err(Into::into)
let room_id = OwnedRoomId::parse(full_room_id)
.inspect(|full_room_id| debug_info!(?full_room_id, "Full custom room ID"))
.inspect_err(|e| warn!(?e, ?custom_room_id, "Failed to create room with custom room ID",))
.inspect_err(|e| {
warn!(?e, ?custom_room_id, "Failed to create room with custom room ID");
})?;
// check if room ID doesn't already exist instead of erroring on auth check
if services
.rooms
.short
.get_shortroomid(&room_id)
.await
.is_ok()
{
return Err!(Request(RoomInUse("Room with that custom room ID already exists",)));
}
Ok(room_id)
}
async fn can_publish_directory_check(
services: &Services,
body: &Ruma<create_room::v3::Request>,
) -> Result {
if !services
.server
.config
.lockdown_public_room_directory
|| body.appservice_info.is_some()
|| body.visibility != room::Visibility::Public
|| services.users.is_admin(body.sender_user()).await
{
return Ok(());
}
let msg = format!(
"Non-admin user {} tried to publish new to the directory while \
lockdown_public_room_directory is enabled",
body.sender_user(),
);
warn!("{msg}");
if services.server.config.admin_room_notices {
services.admin.notice(&msg).await;
}
Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")))
}
async fn can_create_room_check(
services: &Services,
body: &Ruma<create_room::v3::Request>,
) -> Result {
if !services.globals.allow_room_creation()
&& body.appservice_info.is_none()
&& !services.users.is_admin(body.sender_user()).await
{
return Err!(Request(Forbidden("Room creation has been disabled.",)));
}
Ok(())
}

View File

@@ -12,7 +12,7 @@ use ruma::{
federation::space::{SpaceHierarchyParentSummary, get_hierarchy},
},
events::room::member::MembershipState,
space::SpaceRoomJoinRule::{self, *},
room::{JoinRuleSummary, RoomSummary},
};
use tuwunel_core::{
Err, Result, debug_warn, trace,
@@ -34,8 +34,8 @@ use crate::{Ruma, RumaResponse};
pub(crate) async fn get_room_summary_legacy(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_summary::msc3266::Request>,
) -> Result<RumaResponse<get_summary::msc3266::Response>> {
body: Ruma<get_summary::v1::Request>,
) -> Result<RumaResponse<get_summary::v1::Response>> {
get_room_summary(State(services), InsecureClientIp(client), body)
.boxed()
.await
@@ -51,8 +51,8 @@ pub(crate) async fn get_room_summary_legacy(
pub(crate) async fn get_room_summary(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_summary::msc3266::Request>,
) -> Result<get_summary::msc3266::Response> {
body: Ruma<get_summary::v1::Request>,
) -> Result<get_summary::v1::Response> {
let (room_id, servers) = services
.rooms
.alias
@@ -73,7 +73,7 @@ async fn room_summary_response(
room_id: &RoomId,
servers: &[OwnedServerName],
sender_user: Option<&UserId>,
) -> Result<get_summary::msc3266::Response> {
) -> Result<get_summary::v1::Response> {
if services
.rooms
.state_cache
@@ -85,23 +85,12 @@ async fn room_summary_response(
.await;
}
let room =
remote_room_summary_hierarchy_response(services, room_id, servers, sender_user).await?;
let summary = remote_room_summary_hierarchy_response(services, room_id, servers, sender_user)
.await?
.summary;
Ok(get_summary::msc3266::Response {
room_id: room_id.to_owned(),
canonical_alias: room.canonical_alias,
avatar_url: room.avatar_url,
guest_can_join: room.guest_can_join,
name: room.name,
num_joined_members: room.num_joined_members,
topic: room.topic,
world_readable: room.world_readable,
join_rule: room.join_rule,
room_type: room.room_type,
room_version: room.room_version,
encryption: room.encryption,
allowed_room_ids: room.allowed_room_ids,
Ok(get_summary::v1::Response {
summary,
membership: sender_user
.is_some()
.then_some(MembershipState::Leave),
@@ -112,7 +101,7 @@ async fn local_room_summary_response(
services: &Services,
room_id: &RoomId,
sender_user: Option<&UserId>,
) -> Result<get_summary::msc3266::Response> {
) -> Result<get_summary::v1::Response> {
trace!(?sender_user, "Sending local room summary response for {room_id:?}");
let join_rule = services
.rooms
@@ -139,7 +128,7 @@ async fn local_room_summary_response(
&join_rule.clone().into(),
guest_can_join,
world_readable,
join_rule.allowed_rooms(),
join_rule.allowed_room_ids(),
sender_user,
)
.await?;
@@ -224,24 +213,22 @@ async fn local_room_summary_response(
membership,
);
Ok(get_summary::msc3266::Response {
room_id: room_id.to_owned(),
canonical_alias,
avatar_url,
guest_can_join,
name,
num_joined_members: num_joined_members.try_into().unwrap_or_default(),
topic,
world_readable,
room_type,
room_version,
encryption,
Ok(get_summary::v1::Response {
summary: RoomSummary {
room_id: room_id.to_owned(),
canonical_alias,
avatar_url,
guest_can_join,
name,
num_joined_members: num_joined_members.try_into().unwrap_or_default(),
topic,
world_readable,
room_type,
room_version,
encryption,
join_rule: join_rule.into(),
},
membership,
allowed_room_ids: join_rule
.allowed_rooms()
.map(Into::into)
.collect(),
join_rule: join_rule.into(),
})
}
@@ -277,10 +264,11 @@ async fn remote_room_summary_hierarchy_response(
while let Some(Ok(response)) = requests.next().await {
trace!("{response:?}");
let room = response.room.clone();
if room.room_id != room_id {
let summary = &room.summary;
if summary.room_id != room_id {
debug_warn!(
"Room ID {} returned does not belong to the requested room ID {}",
room.room_id,
summary.room_id,
room_id
);
continue;
@@ -289,10 +277,10 @@ async fn remote_room_summary_hierarchy_response(
return user_can_see_summary(
services,
room_id,
&room.join_rule,
room.guest_can_join,
room.world_readable,
room.allowed_room_ids.iter().map(AsRef::as_ref),
&summary.join_rule,
summary.guest_can_join,
summary.world_readable,
summary.join_rule.allowed_room_ids(),
sender_user,
)
.await
@@ -308,7 +296,7 @@ async fn remote_room_summary_hierarchy_response(
async fn user_can_see_summary<'a, I>(
services: &Services,
room_id: &RoomId,
join_rule: &SpaceRoomJoinRule,
join_rule: &JoinRuleSummary,
guest_can_join: bool,
world_readable: bool,
allowed_room_ids: I,
@@ -317,17 +305,23 @@ async fn user_can_see_summary<'a, I>(
where
I: Iterator<Item = &'a RoomId> + Send,
{
let is_public_room = matches!(join_rule, Public | Knock | KnockRestricted);
let is_public_room = matches!(
join_rule,
JoinRuleSummary::Public | JoinRuleSummary::Knock | JoinRuleSummary::KnockRestricted(_)
);
match sender_user {
| Some(sender_user) => {
let user_can_see_state_events = services
.rooms
.state_accessor
.user_can_see_state_events(sender_user, room_id);
let is_guest = services
.users
.is_deactivated(sender_user)
.unwrap_or(false);
let user_in_allowed_restricted_room = allowed_room_ids.stream().any(|room| {
services
.rooms

View File

@@ -4,7 +4,7 @@ use axum::extract::State;
use futures::StreamExt;
use ruma::{
CanonicalJsonObject, RoomId, RoomVersionId,
api::client::{error::ErrorKind, room::upgrade_room},
api::client::room::upgrade_room,
events::{
StateEventType, TimelineEventType,
room::{
@@ -14,11 +14,12 @@ use ruma::{
},
},
int,
room_version_rules::RoomIdFormatVersion,
};
use serde_json::{json, value::to_raw_value};
use tuwunel_core::{
Err, Error, Result, err,
matrix::{Event, StateKey, pdu::PduBuilder},
Err, Result, err,
matrix::{Event, StateKey, pdu::PduBuilder, room_version},
};
use crate::Ruma;
@@ -61,14 +62,23 @@ pub(crate) async fn upgrade_room_route(
.server
.supported_room_version(&body.new_version)
{
return Err(Error::BadRequest(
ErrorKind::UnsupportedRoomVersion,
return Err!(Request(UnsupportedRoomVersion(
"This server does not support that room version.",
));
)));
}
if matches!(body.new_version, RoomVersionId::V12) {
return Err!(Request(UnsupportedRoomVersion(
"Upgrading to version 12 is still under development.",
)));
}
let room_version_rules = room_version::rules(&body.new_version)?;
let room_id_format = &room_version_rules.room_id_format;
assert!(*room_id_format == RoomIdFormatVersion::V1, "TODO");
// Create a replacement room
let replacement_room = RoomId::new(services.globals.server_name());
let replacement_room = RoomId::new_v1(services.globals.server_name());
let _short_id = services
.rooms

View File

@@ -1,3 +1,4 @@
use futures::FutureExt;
use ruma::{OwnedUserId, UserId};
use tuwunel_core::{Err, Result, debug};
use tuwunel_service::Services;
@@ -63,6 +64,7 @@ pub(super) async fn ldap_login(
services
.admin
.make_user_admin(lowercased_user_id)
.boxed()
.await?;
} else if !is_ldap_admin && is_tuwunel_admin {
services

View File

@@ -56,10 +56,7 @@ pub(crate) async fn login_token_route(
let mut uiaainfo = uiaa::UiaaInfo {
flows: vec![password_flow],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
..Default::default()
};
match &body.auth {

View File

@@ -2,7 +2,7 @@ use axum::extract::State;
use futures::{FutureExt, TryFutureExt, TryStreamExt};
use ruma::{
OwnedEventId, RoomId, UserId,
api::client::state::{get_state_events, get_state_events_for_key, send_state_event},
api::client::state::{get_state_event_for_key, get_state_events, send_state_event},
events::{
AnyStateEventContent, StateEventType,
room::{
@@ -107,8 +107,8 @@ pub(crate) async fn get_state_events_route(
/// readable
pub(crate) async fn get_state_events_for_key_route(
State(services): State<crate::State>,
body: Ruma<get_state_events_for_key::v3::Request>,
) -> Result<get_state_events_for_key::v3::Response> {
body: Ruma<get_state_event_for_key::v3::Request>,
) -> Result<get_state_event_for_key::v3::Response> {
let sender_user = body.sender_user();
if !services
@@ -140,7 +140,7 @@ pub(crate) async fn get_state_events_for_key_route(
.as_ref()
.is_some_and(|f| f.to_lowercase().eq("event"));
Ok(get_state_events_for_key::v3::Response {
Ok(get_state_event_for_key::v3::Response {
content: event_format.or(|| event.get_content_as_value()),
event: event_format.then(|| {
json!({
@@ -167,8 +167,8 @@ pub(crate) async fn get_state_events_for_key_route(
/// readable
pub(crate) async fn get_state_events_for_empty_key_route(
State(services): State<crate::State>,
body: Ruma<get_state_events_for_key::v3::Request>,
) -> Result<RumaResponse<get_state_events_for_key::v3::Response>> {
body: Ruma<get_state_event_for_key::v3::Request>,
) -> Result<RumaResponse<get_state_event_for_key::v3::Response>> {
get_state_events_for_key_route(State(services), body)
.await
.map(RumaResponse)
@@ -222,7 +222,7 @@ async fn allowed_to_send_state_event(
| StateEventType::RoomServerAcl => {
// prevents common ACL paw-guns as ACL management is difficult and prone to
// irreversible mistakes
match json.deserialize_as::<RoomServerAclEventContent>() {
match json.deserialize_as_unchecked::<RoomServerAclEventContent>() {
| Ok(acl_content) => {
if acl_content.allow_is_empty() {
return Err!(Request(BadJson(debug_warn!(
@@ -282,7 +282,7 @@ async fn allowed_to_send_state_event(
// admin room is a sensitive room, it should not ever be made public
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
if admin_room_id == room_id {
match json.deserialize_as::<RoomJoinRulesEventContent>() {
match json.deserialize_as_unchecked::<RoomJoinRulesEventContent>() {
| Ok(join_rule) =>
if join_rule.join_rule == JoinRule::Public {
return Err!(Request(Forbidden(
@@ -301,7 +301,7 @@ async fn allowed_to_send_state_event(
| StateEventType::RoomHistoryVisibility => {
// admin room is a sensitive room, it should not ever be made world readable
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
match json.deserialize_as::<RoomHistoryVisibilityEventContent>() {
match json.deserialize_as_unchecked::<RoomHistoryVisibilityEventContent>() {
| Ok(visibility_content) => {
if admin_room_id == room_id
&& visibility_content.history_visibility
@@ -322,7 +322,7 @@ async fn allowed_to_send_state_event(
}
},
| StateEventType::RoomCanonicalAlias => {
match json.deserialize_as::<RoomCanonicalAliasEventContent>() {
match json.deserialize_as_unchecked::<RoomCanonicalAliasEventContent>() {
| Ok(canonical_alias_content) => {
let mut aliases = canonical_alias_content.alt_aliases.clone();
@@ -354,52 +354,53 @@ async fn allowed_to_send_state_event(
},
}
},
| StateEventType::RoomMember => match json.deserialize_as::<RoomMemberEventContent>() {
| Ok(membership_content) => {
let Ok(_state_key) = UserId::parse(state_key) else {
return Err!(Request(BadJson(
"Membership event has invalid or non-existent state key"
)));
};
if let Some(authorising_user) =
membership_content.join_authorized_via_users_server
{
if membership_content.membership != MembershipState::Join {
| StateEventType::RoomMember =>
match json.deserialize_as_unchecked::<RoomMemberEventContent>() {
| Ok(membership_content) => {
let Ok(_state_key) = UserId::parse(state_key) else {
return Err!(Request(BadJson(
"join_authorised_via_users_server is only for member joins"
"Membership event has invalid or non-existent state key"
)));
}
};
if !services.globals.user_is_local(&authorising_user) {
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} does not belong to this \
homeserver"
)));
}
if let Some(authorising_user) =
membership_content.join_authorized_via_users_server
{
if membership_content.membership != MembershipState::Join {
return Err!(Request(BadJson(
"join_authorised_via_users_server is only for member joins"
)));
}
services
.rooms
.state_cache
.is_joined(&authorising_user, room_id)
.map(is_false!())
.map(BoolExt::into_result)
.map_err(|()| {
err!(Request(InvalidParam(
"Authorising user {authorising_user} is not in the room. They \
cannot authorise the join."
)))
})
.await?;
}
if !services.globals.user_is_local(&authorising_user) {
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} does not belong to this \
homeserver"
)));
}
services
.rooms
.state_cache
.is_joined(&authorising_user, room_id)
.map(is_false!())
.map(BoolExt::into_result)
.map_err(|()| {
err!(Request(InvalidParam(
"Authorising user {authorising_user} is not in the room. \
They cannot authorise the join."
)))
})
.await?;
}
},
| Err(e) => {
return Err!(Request(BadJson(
"Membership content must have a valid JSON body with at least a valid \
membership state: {e}"
)));
},
},
| Err(e) => {
return Err!(Request(BadJson(
"Membership content must have a valid JSON body with at least a valid \
membership state: {e}"
)));
},
},
| _ => (),
}

View File

@@ -14,11 +14,11 @@ use ruma::{
api::client::{
filter::FilterDefinition,
sync::sync_events::{
self, DeviceLists, UnreadNotificationsCount,
self, DeviceLists, StrippedState, UnreadNotificationsCount,
v3::{
Ephemeral, Filter, GlobalAccountData, InviteState, InvitedRoom, JoinedRoom,
KnockState, KnockedRoom, LeftRoom, Presence, RoomAccountData, RoomSummary, Rooms,
State as RoomState, Timeline, ToDevice,
State as RoomState, StateEvents, Timeline, ToDevice,
},
},
uiaa::UiaaResponse,
@@ -295,7 +295,12 @@ async fn build_sync_events(
}
let invited_room = InvitedRoom {
invite_state: InviteState { events: invite_state },
invite_state: InviteState {
events: invite_state
.into_iter()
.map(Raw::cast::<StrippedState>)
.collect(),
},
};
invited_rooms.insert(room_id, invited_room);
@@ -320,7 +325,12 @@ async fn build_sync_events(
}
let knocked_room = KnockedRoom {
knock_state: KnockState { events: knock_state },
knock_state: KnockState {
events: knock_state
.into_iter()
.map(Raw::cast::<StrippedState>)
.collect(),
},
};
knocked_rooms.insert(room_id, knocked_room);
@@ -540,7 +550,7 @@ async fn handle_left_room(
prev_batch: Some(next_batch.to_string()),
events: Vec::new(),
},
state: RoomState { events: vec![event.into_format()] },
state: RoomState::Before(StateEvents { events: vec![event.into_format()] }),
}));
}
@@ -635,7 +645,7 @@ async fn handle_left_room(
prev_batch: Some(next_batch.to_string()),
events: Vec::new(), // and so we dont need to set this to empty vec
},
state: RoomState { events: left_state_events },
state: RoomState::Before(StateEvents { events: left_state_events }),
}))
}
@@ -1042,7 +1052,7 @@ async fn load_joined_room(
let joined_room = JoinedRoom {
account_data: RoomAccountData { events: account_data_events },
ephemeral: Ephemeral { events: edus },
state: RoomState { events: state_events },
state: RoomState::Before(StateEvents { events: state_events }),
summary: RoomSummary {
joined_member_count: joined_member_count.map(ruma_from_u64),
invited_member_count: invited_member_count.map(ruma_from_u64),

View File

@@ -13,7 +13,10 @@ use futures::{
};
use ruma::{
DeviceId, OwnedEventId, OwnedRoomId, RoomId, UInt, UserId,
api::client::sync::sync_events::{self, DeviceLists, UnreadNotificationsCount},
api::client::sync::sync_events::{
self, DeviceLists, StrippedState, UnreadNotificationsCount,
v5::request::ExtensionRoomConfig,
},
directory::RoomTypeFilter,
events::{
AnyRawAccountDataEvent, AnySyncEphemeralRoomEvent, StateEventType, TimelineEventType,
@@ -647,7 +650,11 @@ where
name: room_name.or(hero_name),
initial: Some(roomsince == &0),
is_dm: None,
invite_state,
invite_state: invite_state.map(|s| {
s.into_iter()
.map(Raw::cast::<StrippedState>)
.collect()
}),
unread_notifications: UnreadNotificationsCount {
highlight_count: Some(
services
@@ -727,7 +734,10 @@ async fn collect_account_data(
.await;
if let Some(rooms) = &body.extensions.account_data.rooms {
for room in rooms {
for room in rooms
.iter()
.filter_map(|erc| extract_variant!(erc, ExtensionRoomConfig::Room))
{
account_data.rooms.insert(
room.clone(),
services

View File

@@ -146,7 +146,7 @@ pub(crate) async fn set_profile_key_route(
)));
}
let Some(profile_key_value) = body.kv_pair.get(&body.key_name) else {
let Some(profile_key_value) = body.kv_pair.get(&body.key) else {
return Err!(Request(BadJson(
"The key does not match the URL field key, or JSON body is empty (use DELETE)"
)));
@@ -164,7 +164,7 @@ pub(crate) async fn set_profile_key_route(
return Err!(Request(BadJson("Key names cannot be longer than 128 bytes")));
}
if body.key_name == "displayname" {
if body.key == "displayname" {
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
@@ -180,7 +180,7 @@ pub(crate) async fn set_profile_key_route(
&all_joined_rooms,
)
.await;
} else if body.key_name == "avatar_url" {
} else if body.key == "avatar_url" {
let mxc = ruma::OwnedMxcUri::from(profile_key_value.to_string());
let all_joined_rooms: Vec<OwnedRoomId> = services
@@ -193,11 +193,9 @@ pub(crate) async fn set_profile_key_route(
update_avatar_url(&services, &body.user_id, Some(mxc), None, &all_joined_rooms).await;
} else {
services.users.set_profile_key(
&body.user_id,
&body.key_name,
Some(profile_key_value.clone()),
);
services
.users
.set_profile_key(&body.user_id, &body.key, Some(profile_key_value.clone()));
}
if services.config.allow_local_presence {
@@ -233,7 +231,7 @@ pub(crate) async fn delete_profile_key_route(
)));
}
if body.key_name == "displayname" {
if body.key == "displayname" {
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
@@ -243,7 +241,7 @@ pub(crate) async fn delete_profile_key_route(
.await;
update_displayname(&services, &body.user_id, None, &all_joined_rooms).await;
} else if body.key_name == "avatar_url" {
} else if body.key == "avatar_url" {
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
@@ -256,7 +254,7 @@ pub(crate) async fn delete_profile_key_route(
} else {
services
.users
.set_profile_key(&body.user_id, &body.key_name, None);
.set_profile_key(&body.user_id, &body.key, None);
}
if services.config.allow_local_presence {
@@ -379,14 +377,12 @@ pub(crate) async fn get_profile_key_route(
.users
.set_timezone(&body.user_id, response.tz.clone());
match response.custom_profile_fields.get(&body.key_name) {
match response.custom_profile_fields.get(&body.key) {
| Some(value) => {
profile_key_value.insert(body.key_name.clone(), value.clone());
services.users.set_profile_key(
&body.user_id,
&body.key_name,
Some(value.clone()),
);
profile_key_value.insert(body.key.clone(), value.clone());
services
.users
.set_profile_key(&body.user_id, &body.key, Some(value.clone()));
},
| _ => {
return Err!(Request(NotFound("The requested profile key does not exist.")));
@@ -409,11 +405,11 @@ pub(crate) async fn get_profile_key_route(
match services
.users
.profile_key(&body.user_id, &body.key_name)
.profile_key(&body.user_id, &body.key)
.await
{
| Ok(value) => {
profile_key_value.insert(body.key_name.clone(), value);
profile_key_value.insert(body.key.clone(), value);
},
| _ => {
return Err!(Request(NotFound("The requested profile key does not exist.")));

View File

@@ -1,12 +1,9 @@
use axum::{Json, extract::State, response::IntoResponse};
use ruma::api::client::{
discovery::{
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
discover_support::{self, Contact},
},
error::ErrorKind,
use ruma::api::client::discovery::{
discover_homeserver::{self, HomeserverInfo},
discover_support::{self, Contact},
};
use tuwunel_core::{Error, Result};
use tuwunel_core::{Err, Result};
use crate::Ruma;
@@ -19,13 +16,12 @@ pub(crate) async fn well_known_client(
) -> Result<discover_homeserver::Response> {
let client_url = match services.server.config.well_known.client.as_ref() {
| Some(url) => url.to_string(),
| None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
| None => return Err!(Request(NotFound("Not found."))),
};
Ok(discover_homeserver::Response {
homeserver: HomeserverInfo { base_url: client_url.clone() },
homeserver: HomeserverInfo { base_url: client_url },
identity_server: None,
sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }),
tile_server: None,
})
}
@@ -54,7 +50,7 @@ pub(crate) async fn well_known_support(
// support page or role must be either defined for this to be valid
if support_page.is_none() && role.is_none() {
return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
return Err!(Request(NotFound("Not found.")));
}
let email_address = services
@@ -63,6 +59,7 @@ pub(crate) async fn well_known_support(
.well_known
.support_email
.clone();
let matrix_id = services
.server
.config
@@ -72,7 +69,7 @@ pub(crate) async fn well_known_support(
// if a role is specified, an email address or matrix id is required
if role.is_some() && (email_address.is_none() && matrix_id.is_none()) {
return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
return Err!(Request(NotFound("Not found.")));
}
// TODO: support defining multiple contacts in the config
@@ -86,7 +83,7 @@ pub(crate) async fn well_known_support(
// support page or role+contacts must be either defined for this to be valid
if contacts.is_empty() && support_page.is_none() {
return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
return Err!(Request(NotFound("Not found.")));
}
Ok(discover_support::Response { contacts, support_page })
@@ -103,7 +100,7 @@ pub(crate) async fn syncv3_client_server_json(
| Some(url) => url.to_string(),
| None => match services.server.config.well_known.server.as_ref() {
| Some(url) => url.to_string(),
| None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
| None => return Err!(Request(NotFound("Not found."))),
},
};

View File

@@ -1,4 +1,4 @@
#![type_length_limit = "65536"] //TODO: reduce me
#![type_length_limit = "262144"] //TODO: REDUCE ME
#![allow(clippy::toplevel_ref_arg)]
pub mod client;

View File

@@ -69,7 +69,10 @@ pub(super) async fn auth(
json_body: Option<&CanonicalJsonValue>,
metadata: &Metadata,
) -> Result<Auth> {
use AuthScheme::{AccessToken, AccessTokenOptional, AppserviceToken, ServerSignatures};
use AuthScheme::{
AccessToken, AccessTokenOptional, AppserviceToken, AppserviceTokenOptional,
ServerSignatures,
};
use Error::BadRequest;
use ErrorKind::UnknownToken;
use Token::{Appservice, Expired, Invalid, User};
@@ -129,7 +132,7 @@ pub(super) async fn auth(
| (AccessToken, Appservice(info)) => Ok(auth_appservice(services, request, info).await?),
| (AccessToken, Token::None) => match metadata {
| (AccessToken | AppserviceToken, Token::None) => match metadata {
| &get_turn_server_info::v3::Request::METADATA
if services.server.config.turn_allow_guests =>
Ok(Auth::default()),
@@ -137,22 +140,25 @@ pub(super) async fn auth(
| _ => Err!(Request(MissingToken("Missing access token."))),
},
| (AccessToken | AccessTokenOptional | AuthScheme::None, User(user)) => Ok(Auth {
| (
AccessToken | AccessTokenOptional | AppserviceTokenOptional | AuthScheme::None,
User(user),
) => Ok(Auth {
sender_user: Some(user.0),
sender_device: Some(user.1),
_expires_at: user.2,
..Auth::default()
}),
//TODO: add AppserviceTokenOptional
| (AccessTokenOptional | AppserviceToken | AuthScheme::None, Appservice(info)) =>
Ok(Auth {
appservice_info: Some(*info),
..Auth::default()
}),
| (
AccessTokenOptional | AppserviceTokenOptional | AppserviceToken | AuthScheme::None,
Appservice(info),
) => Ok(Auth {
appservice_info: Some(*info),
..Auth::default()
}),
//TODO: add AppserviceTokenOptional
| (AccessTokenOptional | AppserviceToken | AuthScheme::None, Token::None) =>
| (AccessTokenOptional | AppserviceTokenOptional | AuthScheme::None, Token::None) =>
Ok(Auth::default()),
}
}
@@ -306,7 +312,7 @@ async fn auth_server(
let keys: PubKeys = [(x_matrix.key.to_string(), key.key)].into();
let keys: PubKeyMap = [(origin.as_str().into(), keys)].into();
if let Err(e) = ruma::signatures::verify_json(&keys, authorization) {
if let Err(e) = ruma::signatures::verify_json(&keys, &authorization) {
debug_error!("Failed to verify federation request from {origin}: {e}");
if request.parts.uri.to_string().contains('@') {
warn!(

View File

@@ -64,17 +64,18 @@ pub(crate) async fn get_hierarchy_route(
})
.unzip()
.map(|(children, inaccessible_children): (Vec<_>, Vec<_>)| {
(
children
.into_iter()
.flatten()
.map(Into::into)
.collect(),
inaccessible_children
.into_iter()
.flatten()
.collect(),
)
let children = children
.into_iter()
.flatten()
.map(|parent| parent.summary)
.collect();
let inaccessible_children = inaccessible_children
.into_iter()
.flatten()
.collect();
(children, inaccessible_children)
})
.await;

View File

@@ -3,12 +3,15 @@ use axum_client_ip::InsecureClientIp;
use base64::{Engine as _, engine::general_purpose};
use ruma::{
CanonicalJsonValue, OwnedUserId, UserId,
api::{client::error::ErrorKind, federation::membership::create_invite},
api::{
client::error::ErrorKind,
federation::membership::{RawStrippedState, create_invite},
},
events::room::member::{MembershipState, RoomMemberEventContent},
serde::JsonObject,
};
use tuwunel_core::{
Err, Error, Result, err,
Err, Error, Result, err, extract_variant,
matrix::{Event, PduEvent, event::gen_event_id},
utils,
utils::hash::sha256,
@@ -119,7 +122,12 @@ pub(crate) async fn create_invite_route(
return Err!(Request(Forbidden("This server does not allow room invites.")));
}
let mut invite_state = body.invite_room_state.clone();
let mut invite_state: Vec<_> = body
.invite_room_state
.clone()
.into_iter()
.filter_map(|s| extract_variant!(s, RawStrippedState::Stripped))
.collect();
let mut event: JsonObject = serde_json::from_str(body.event.get())
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;

View File

@@ -2,7 +2,7 @@ use RoomVersionId::*;
use axum::extract::State;
use ruma::{
RoomVersionId,
api::{client::error::ErrorKind, federation::knock::create_knock_event_template},
api::{client::error::ErrorKind, federation::membership::prepare_knock_event},
events::room::member::{MembershipState, RoomMemberEventContent},
};
use serde_json::value::to_raw_value;
@@ -15,8 +15,8 @@ use crate::Ruma;
/// Creates a knock template.
pub(crate) async fn create_knock_event_template_route(
State(services): State<crate::State>,
body: Ruma<create_knock_event_template::v1::Request>,
) -> Result<create_knock_event_template::v1::Response> {
body: Ruma<prepare_knock_event::v1::Request>,
) -> Result<prepare_knock_event::v1::Response> {
if !services
.rooms
.metadata
@@ -124,7 +124,7 @@ pub(crate) async fn create_knock_event_template_route(
// room v3 and above removed the "event_id" field from remote PDU format
super::maybe_strip_event_id(&mut pdu_json, &room_version_id)?;
Ok(create_knock_event_template::v1::Response {
Ok(prepare_knock_event::v1::Response {
room_version: room_version_id,
event: to_raw_value(&pdu_json).expect("CanonicalJson can be serialized to JSON"),
})

View File

@@ -354,6 +354,7 @@ pub(crate) async fn create_join_event_v2_route(
create_join_event(&services, body.origin(), &body.room_id, &body.pdu)
.boxed()
.await?;
let room_state = create_join_event::v2::RoomState {
members_omitted: false,
auth_chain,

View File

@@ -3,7 +3,7 @@ use futures::FutureExt;
use ruma::{
OwnedServerName, OwnedUserId,
RoomVersionId::*,
api::federation::knock::send_knock,
api::federation::membership::create_knock_event,
events::{
StateEventType,
room::member::{MembershipState, RoomMemberEventContent},
@@ -23,8 +23,8 @@ use crate::Ruma;
/// Submits a signed knock event.
pub(crate) async fn create_knock_event_v1_route(
State(services): State<crate::State>,
body: Ruma<send_knock::v1::Request>,
) -> Result<send_knock::v1::Response> {
body: Ruma<create_knock_event::v1::Request>,
) -> Result<create_knock_event::v1::Response> {
if services
.config
.forbidden_remote_server_names
@@ -189,7 +189,14 @@ pub(crate) async fn create_knock_event_v1_route(
.send_pdu_room(&body.room_id, &pdu_id)
.await?;
let knock_room_state = services.rooms.state.summary_stripped(&pdu).await;
Ok(send_knock::v1::Response { knock_room_state })
Ok(create_knock_event::v1::Response {
knock_room_state: services
.rooms
.state
.summary_stripped(&pdu)
.await
.into_iter()
.map(Into::into)
.collect(),
})
}

View File

@@ -119,7 +119,9 @@ hardened_malloc-rs.workspace = true
hardened_malloc-rs.optional = true
[dev-dependencies]
insta.workspace = true
maplit.workspace = true
similar.workspace = true
[lints]
workspace = true

View File

@@ -665,20 +665,33 @@ pub struct Config {
pub allow_room_creation: bool,
/// Set to false to disable users from joining or creating room versions
/// that aren't officially supported by tuwunel.
/// that aren't officially supported by tuwunel. Unstable room versions may
/// have flawed specifications or our implementation may be non-conforming.
/// Correct operation may not be guaranteed, but incorrect operation may be
/// tolerable and unnoticed.
///
/// tuwunel officially supports room versions 6 - 11.
/// tuwunel officially supports room versions 6+. tuwunel has slightly
/// experimental (though works fine in practice) support for versions 3 - 5.
///
/// tuwunel has slightly experimental (though works fine in practice)
/// support for versions 3 - 5.
/// default: true
#[serde(default = "true_fn")]
pub allow_unstable_room_versions: bool,
/// Set to true to enable experimental room versions.
///
/// Unlike unstable room versions these versions are either under
/// development, protype spec-changes, or somehow present a serious risk to
/// the server's operation or database corruption. This is for developer use
/// only.
#[serde(default)]
pub allow_experimental_room_versions: bool,
/// Default room version tuwunel will create rooms with.
///
/// Per spec, room version 11 is the default.
///
/// default: 11
/// The default is prescribed by the spec, but may be selected by developer
/// recommendation. To prevent stale documentation we no longer list it
/// here. It is only advised to override this if you know what you are
/// doing, and by doing so, updates with new versions are precluded.
#[serde(default = "default_default_room_version")]
pub default_room_version: RoomVersionId,
@@ -1885,6 +1898,30 @@ pub struct Config {
#[serde(default)]
pub allow_invalid_tls_certificates: bool,
/// Backport state-reset security fixes to all room versions.
///
/// This option applies the State Resolution 2.1 mitigation developed during
/// project Hydra for room version 12 to all prior State Resolution 2.0 room
/// versions (all room versions supported by this server). These mitigations
/// increase resilience to state-resets without any new definition of
/// correctness; therefor it is safe to set this to true for existing rooms.
///
/// Furthermore, state-reset attacks are not consistent as they result in
/// rooms without any single consensus, therefor it is unnecessary to set
/// this to false to match other servers which set this to false or simply
/// lack support; even if replicating the post-reset state suffered by other
/// servers is somehow desired.
///
/// This option exists for developer and debug use, and as a failsafe in
/// lieu of hardcoding it.
///
/// This currently defaults to false as a matter of development until
/// real-world testing can shake out any implementation issues rather than
/// jeopardize existing rooms, but otherwise will default to true at the
/// next point release or patch.
#[serde(default)]
pub hydra_backports: bool,
// external structure; separate section
#[serde(default)]
pub blurhashing: BlurhashConfig,

View File

@@ -118,6 +118,8 @@ pub enum Error {
Mxc(#[from] ruma::MxcUriError),
#[error(transparent)]
Mxid(#[from] ruma::IdParseError),
#[error(transparent)]
PowerLevels(#[from] ruma::events::room::power_levels::PowerLevelsError),
#[error("from {0}: {1}")]
Redaction(ruma::OwnedServerName, ruma::canonical_json::RedactionError),
#[error("{0}: {1}")]
@@ -126,8 +128,6 @@ pub enum Error {
Ruma(#[from] ruma::api::client::error::Error),
#[error(transparent)]
Signatures(#[from] ruma::signatures::Error),
#[error(transparent)]
StateRes(#[from] crate::state_res::Error),
#[error("uiaa")]
Uiaa(ruma::api::client::uiaa::UiaaInfo),

View File

@@ -41,10 +41,7 @@ impl From<Error> for UiaaResponse {
message: error.message(),
};
Self::MatrixError(ruma::api::client::error::Error {
status_code: error.status_code(),
body,
})
Self::MatrixError(ruma::api::client::error::Error::new(error.status_code(), body))
}
}

View File

@@ -2,10 +2,19 @@
use std::iter::once;
use ruma::{RoomVersionId, api::client::discovery::get_capabilities::RoomVersionStability};
use ruma::{RoomVersionId, api::client::discovery::get_capabilities::v3::RoomVersionStability};
use crate::{at, is_equal_to};
/// Partially supported non-compliant room versions
pub const UNSTABLE_ROOM_VERSIONS: &[RoomVersionId] = &[
RoomVersionId::V1,
RoomVersionId::V2,
RoomVersionId::V3,
RoomVersionId::V4,
RoomVersionId::V5,
];
/// Supported and stable room versions
pub const STABLE_ROOM_VERSIONS: &[RoomVersionId] = &[
RoomVersionId::V6,
@@ -16,9 +25,8 @@ pub const STABLE_ROOM_VERSIONS: &[RoomVersionId] = &[
RoomVersionId::V11,
];
/// Experimental, partially supported room versions
pub const UNSTABLE_ROOM_VERSIONS: &[RoomVersionId] =
&[RoomVersionId::V2, RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5];
/// Experimental and prototype room versions under development.
pub const EXPERIMENTAL_ROOM_VERSIONS: &[RoomVersionId] = &[RoomVersionId::V12];
type RoomVersion = (RoomVersionId, RoomVersionStability);
@@ -31,8 +39,15 @@ impl crate::Server {
#[inline]
pub fn supported_room_versions(&self) -> impl Iterator<Item = RoomVersionId> + '_ {
let experimental_room_versions = EXPERIMENTAL_ROOM_VERSIONS
.iter()
.cloned()
.zip(once(RoomVersionStability::Unstable).cycle())
.filter(|_| self.config.allow_experimental_room_versions);
Self::available_room_versions()
.filter(|(_, stability)| self.supported_stability(stability))
.chain(experimental_room_versions)
.map(at!(0))
}

View File

@@ -4,6 +4,7 @@ mod format;
mod id;
mod redact;
mod relation;
pub mod state_key;
mod type_ext;
mod unsigned;
@@ -16,12 +17,23 @@ use ruma::{
use serde::Deserialize;
use serde_json::{Value as JsonValue, value::RawValue as RawJsonValue};
pub use self::{filter::Matches, id::*, relation::RelationTypeEqual, type_ext::TypeExt};
use super::{pdu::Pdu, state_key::StateKey};
pub use self::{
filter::Matches,
id::*,
relation::RelationTypeEqual,
state_key::{StateKey, TypeStateKey},
type_ext::TypeExt,
};
use super::pdu::Pdu;
use crate::{Result, utils};
/// Abstraction of a PDU so users can have their own PDU types.
pub trait Event: Clone + Debug + Send + Sync {
#[inline]
fn is_type_and_state_key(&self, kind: &TimelineEventType, state_key: &str) -> bool {
self.kind() == kind && self.state_key() == Some(state_key)
}
/// Serialize into a Ruma JSON format, consuming.
#[inline]
fn into_format<T>(self) -> T
@@ -152,6 +164,11 @@ pub trait Event: Clone + Debug + Send + Sync {
/// All the authenticating events for this event.
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_;
/// All the authenticating events for this event.
fn auth_events_into(
self,
) -> impl IntoIterator<IntoIter = impl Iterator<Item = OwnedEventId>> + Send;
/// The event's content.
fn content(&self) -> &RawJsonValue;
@@ -167,6 +184,9 @@ pub trait Event: Clone + Debug + Send + Sync {
/// If this event is a redaction event this is the event it redacts.
fn redacts(&self) -> Option<&EventId>;
/// see: https://spec.matrix.org/v1.14/rooms/v11/#rejected-events
fn rejected(&self) -> bool;
/// The `RoomId` of this event.
fn room_id(&self) -> &RoomId;

View File

@@ -1,8 +1,8 @@
use ruma::{
events::{
AnyMessageLikeEvent, AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent,
AnySyncTimelineEvent, AnyTimelineEvent, StateEvent, room::member::RoomMemberEventContent,
space::child::HierarchySpaceChildEvent,
AnyMessageLikeEvent, AnyStateEvent, AnyStrippedStateEvent, AnySyncMessageLikeEvent,
AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent, StateEvent,
room::member::RoomMemberEventContent, space::child::HierarchySpaceChildEvent,
},
serde::Raw,
};
@@ -106,6 +106,36 @@ impl<'a, E: Event> From<Ref<'a, E>> for Raw<AnyMessageLikeEvent> {
}
}
impl<E: Event> From<Owned<E>> for Raw<AnySyncMessageLikeEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<AnySyncMessageLikeEvent> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let (redacts, content) = redact::copy(event);
let mut json = json!({
"content": content,
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"sender": event.sender(),
"type": event.kind(),
});
if let Some(redacts) = &redacts {
json["redacts"] = json!(redacts);
}
if let Some(state_key) = event.state_key() {
json["state_key"] = json!(state_key);
}
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}
impl<E: Event> From<Owned<E>> for Raw<AnyStateEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}

View File

@@ -1,7 +1,7 @@
use ruma::{CanonicalJsonObject, OwnedEventId, RoomVersionId};
use serde_json::value::RawValue as RawJsonValue;
use crate::{Result, err};
use crate::{Result, err, matrix::room_version};
/// Generates a correct eventId for the incoming pdu.
///
@@ -24,8 +24,8 @@ pub fn gen_event_id(
value: &CanonicalJsonObject,
room_version_id: &RoomVersionId,
) -> Result<OwnedEventId> {
let reference_hash = ruma::signatures::reference_hash(value, room_version_id)?;
let event_id: OwnedEventId = format!("${reference_hash}").try_into()?;
let room_version_rules = room_version::rules(room_version_id)?;
let reference_hash = ruma::signatures::reference_hash(value, &room_version_rules)?;
Ok(event_id)
OwnedEventId::from_parts('$', &reference_hash, None).map_err(Into::into)
}

View File

@@ -0,0 +1,17 @@
use std::cmp::Ordering;
use ruma::events::StateEventType;
use smallstr::SmallString;
pub type TypeStateKey = (StateEventType, StateKey);
pub type StateKey = SmallString<[u8; INLINE_SIZE]>;
const INLINE_SIZE: usize = 48;
#[inline]
#[must_use]
pub fn cmp(a: &TypeStateKey, b: &TypeStateKey) -> Ordering { a.0.cmp(&b.0).then(a.1.cmp(&b.1)) }
#[inline]
#[must_use]
pub fn rcmp(a: &TypeStateKey, b: &TypeStateKey) -> Ordering { b.0.cmp(&a.0).then(b.1.cmp(&a.1)) }

View File

@@ -21,12 +21,12 @@ impl TypeExt for &StateEventType {
impl TypeExt for TimelineEventType {
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
(self.into(), state_key.into())
(self.to_cow_str().into(), state_key.into())
}
}
impl TypeExt for &TimelineEventType {
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
(self.clone().into(), state_key.into())
(self.to_cow_str().into(), state_key.into())
}
}

View File

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

View File

@@ -67,8 +67,25 @@ pub struct Pdu {
// BTreeMap<Box<ServerName>, BTreeMap<ServerSigningKeyId, String>>
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signatures: Option<Box<RawJsonValue>>,
//TODO: https://spec.matrix.org/v1.14/rooms/v11/#rejected-events
#[cfg(test)]
#[serde(default, skip_serializing)]
pub rejected: bool,
}
/// The [maximum size allowed] for a PDU.
/// [maximum size allowed]: https://spec.matrix.org/latest/client-server-api/#size-limits
pub const MAX_PDU_BYTES: usize = 65_535;
/// The [maximum length allowed] for the `prev_events` array of a PDU.
/// [maximum length allowed]: https://spec.matrix.org/latest/rooms/v1/#event-format
pub const MAX_PREV_EVENTS: usize = 20;
/// The [maximum length allowed] for the `auth_events` array of a PDU.
/// [maximum length allowed]: https://spec.matrix.org/latest/rooms/v1/#event-format
pub const MAX_AUTH_EVENTS: usize = 10;
impl Pdu {
pub fn from_id_val(event_id: &EventId, mut json: CanonicalJsonObject) -> Result<Self> {
let event_id = CanonicalJsonValue::String(event_id.into());
@@ -77,6 +94,20 @@ impl Pdu {
.and_then(serde_json::from_value)
.map_err(Into::into)
}
pub fn from_rid_val(
room_id: &RoomId,
event_id: &EventId,
mut json: CanonicalJsonObject,
) -> Result<Self> {
let event_id = CanonicalJsonValue::String(event_id.into());
let room_id = CanonicalJsonValue::String(room_id.into());
json.insert("event_id".into(), event_id);
json.insert("room_id".into(), room_id);
serde_json::to_value(json)
.and_then(serde_json::from_value)
.map_err(Into::into)
}
}
impl Event for Pdu
@@ -88,6 +119,13 @@ where
self.auth_events.iter().map(AsRef::as_ref)
}
#[inline]
fn auth_events_into(
self,
) -> impl IntoIterator<IntoIter = impl Iterator<Item = OwnedEventId>> + Send {
self.auth_events.into_iter()
}
#[inline]
fn content(&self) -> &RawJsonValue { &self.content }
@@ -107,6 +145,14 @@ where
#[inline]
fn redacts(&self) -> Option<&EventId> { self.redacts.as_deref() }
#[cfg(test)]
#[inline]
fn rejected(&self) -> bool { self.rejected }
#[cfg(not(test))]
#[inline]
fn rejected(&self) -> bool { false }
#[inline]
fn room_id(&self) -> &RoomId { &self.room_id }
@@ -144,6 +190,13 @@ where
self.auth_events.iter().map(AsRef::as_ref)
}
#[inline]
fn auth_events_into(
self,
) -> impl IntoIterator<IntoIter = impl Iterator<Item = OwnedEventId>> + Send {
self.auth_events.iter().map(ToOwned::to_owned)
}
#[inline]
fn content(&self) -> &RawJsonValue { &self.content }
@@ -163,6 +216,14 @@ where
#[inline]
fn redacts(&self) -> Option<&EventId> { self.redacts.as_deref() }
#[cfg(test)]
#[inline]
fn rejected(&self) -> bool { self.rejected }
#[cfg(not(test))]
#[inline]
fn rejected(&self) -> bool { false }
#[inline]
fn room_id(&self) -> &RoomId { &self.room_id }

View File

@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedEventId,
events::{EventContent, MessageLikeEventType, StateEventType, TimelineEventType},
events::{MessageLikeEventContent, StateEventContent, TimelineEventType},
};
use serde::Deserialize;
use serde_json::value::{RawValue as RawJsonValue, to_raw_value};
@@ -33,7 +33,7 @@ type Unsigned = BTreeMap<String, serde_json::Value>;
impl Builder {
pub fn state<S, T>(state_key: S, content: &T) -> Self
where
T: EventContent<EventType = StateEventType>,
T: StateEventContent,
S: Into<StateKey>,
{
Self {
@@ -47,7 +47,7 @@ impl Builder {
pub fn timeline<T>(content: &T) -> Self
where
T: EventContent<EventType = MessageLikeEventType>,
T: MessageLikeEventContent,
{
Self {
event_type: content.event_type().into(),

View File

@@ -10,7 +10,13 @@ pub fn redact(&mut self, room_version_id: &RoomVersionId, reason: JsonValue) ->
let mut content = serde_json::from_str(self.content.get())
.map_err(|e| err!(Request(BadJson("Failed to deserialize content into type: {e}"))))?;
redact_content_in_place(&mut content, room_version_id, self.kind.to_string())
let room_version_rules = room_version_id.rules().ok_or_else(|| {
err!(Request(UnsupportedRoomVersion(
"Cannot redact event for unknown room version {room_version_id:?}."
)))
})?;
redact_content_in_place(&mut content, &room_version_rules.redaction, self.kind.to_string())
.map_err(|e| Error::Redaction(self.sender.server_name().to_owned(), e))?;
let reason = serde_json::to_value(reason).expect("Failed to preserialize reason");

View File

@@ -0,0 +1,23 @@
use ruma::{RoomVersionId, events::room::create::RoomCreateEventContent};
pub use ruma::{RoomVersionId as RoomVersion, room_version_rules::RoomVersionRules};
use crate::{Result, err, matrix::Event};
pub fn rules(room_version_id: &RoomVersionId) -> Result<RoomVersionRules> {
room_version_id.rules().ok_or_else(|| {
err!(Request(UnsupportedRoomVersion(
"Unknown or unsupported room version {room_version_id:?}.",
)))
})
}
pub fn from_create_event<Pdu: Event>(create_event: &Pdu) -> Result<RoomVersionId> {
let content: RoomCreateEventContent = create_event.get_content()?;
Ok(from_create_content(&content).clone())
}
#[inline]
#[must_use]
pub fn from_create_content(content: &RoomCreateEventContent) -> &RoomVersionId {
&content.room_version
}

View File

@@ -1,5 +0,0 @@
use smallstr::SmallString;
pub type StateKey = SmallString<[u8; INLINE_SIZE]>;
const INLINE_SIZE: usize = 48;

View File

@@ -1,18 +1,18 @@
#![cfg_attr(not(tuwunel_bench), allow(unused_imports, dead_code))]
#[cfg(tuwunel_bench)]
extern crate test;
use std::{
borrow::Borrow,
collections::{HashMap, HashSet},
collections::HashMap,
sync::atomic::{AtomicU64, Ordering::SeqCst},
};
use futures::{future, future::ready};
use maplit::{btreemap, hashmap, hashset};
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, Signatures, UserId,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, UserId,
events::{
StateEventType, TimelineEventType,
TimelineEventType,
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
@@ -25,9 +25,11 @@ use serde_json::{
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
};
use super::{AuthSet, StateMap, test_utils::not_found};
use crate::{
matrix::{Event, Pdu, pdu::EventHash},
state_res::{self as state_res, Error, Result, StateMap},
Result,
matrix::{Event, EventHash, PduEvent, event::TypeExt},
utils::stream::IterStream,
};
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
@@ -35,6 +37,12 @@ static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
#[cfg(tuwunel_bench)]
#[cfg_attr(tuwunel_bench, bench)]
fn lexico_topo_sort(c: &mut test::Bencher) {
use maplit::{hashmap, hashset};
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
let graph = hashmap! {
event_id("l") => hashset![event_id("o")],
event_id("m") => hashset![event_id("n"), event_id("o")],
@@ -43,9 +51,12 @@ fn lexico_topo_sort(c: &mut test::Bencher) {
event_id("p") => hashset![event_id("o")],
};
c.iter(|| {
let _ = state_res::lexicographical_topological_sort(&graph, &|_| {
future::ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0))))
c.iter(move || {
rt.block_on(async {
_ = super::topological_sort(&graph, &async |_id| {
Ok((int!(0).into(), MilliSecondsSinceUnixEpoch(uint!(0))))
})
.await;
});
});
}
@@ -53,43 +64,56 @@ fn lexico_topo_sort(c: &mut test::Bencher) {
#[cfg(tuwunel_bench)]
#[cfg_attr(tuwunel_bench, bench)]
fn resolution_shallow_auth_chain(c: &mut test::Bencher) {
let mut store = TestStore(hashmap! {});
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
let mut store = TestStore(maplit::hashmap! {});
// build up the DAG
let (state_at_bob, state_at_charlie, _) = store.set_up();
c.iter(|| async {
let ev_map = store.0.clone();
let state_sets = [&state_at_bob, &state_at_charlie];
let fetch = |id: OwnedEventId| ready(ev_map.get(&id).map(ToOwned::to_owned));
let exists = |id: OwnedEventId| ready(ev_map.get(&id).is_some());
let auth_chain_sets: Vec<HashSet<_>> = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect();
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 _ = match state_res::resolve(
&RoomVersionId::V6,
state_sets.into_iter(),
&auth_chain_sets,
&fetch,
&exists,
let func = async || {
if let Err(e) = super::resolve(
&rules,
state_sets.clone().into_iter().stream(),
auth_chains.clone().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}"),
};
panic!("{e}")
}
};
c.iter(move || {
rt.block_on(async {
func().await;
});
});
}
#[cfg(tuwunel_bench)]
#[cfg_attr(tuwunel_bench, bench)]
fn resolve_deeper_event_set(c: &mut test::Bencher) {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
let mut inner = INITIAL_EVENTS();
let ban = BAN_STATE_SET();
@@ -97,66 +121,73 @@ fn resolve_deeper_event_set(c: &mut test::Bencher) {
let store = TestStore(inner.clone());
let state_set_a = [
inner.get(&event_id("CREATE")).unwrap(),
inner.get(&event_id("IJR")).unwrap(),
inner.get(&event_id("IMA")).unwrap(),
inner.get(&event_id("IMB")).unwrap(),
inner.get(&event_id("IMC")).unwrap(),
inner.get(&event_id("MB")).unwrap(),
inner.get(&event_id("PA")).unwrap(),
&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().clone().into(), ev.state_key().unwrap().into()),
ev.event_type()
.with_state_key(ev.state_key().unwrap()),
ev.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
let state_set_b = [
inner.get(&event_id("CREATE")).unwrap(),
inner.get(&event_id("IJR")).unwrap(),
inner.get(&event_id("IMA")).unwrap(),
inner.get(&event_id("IMB")).unwrap(),
inner.get(&event_id("IMC")).unwrap(),
inner.get(&event_id("IME")).unwrap(),
inner.get(&event_id("PA")).unwrap(),
&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().clone().into(), ev.state_key().unwrap().into()),
ev.event_type()
.with_state_key(ev.state_key().unwrap()),
ev.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
c.iter(|| async {
let state_sets = [&state_set_a, &state_set_b];
let auth_chain_sets: Vec<HashSet<_>> = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect();
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 fetch = |id: OwnedEventId| ready(inner.get(&id).map(ToOwned::to_owned));
let exists = |id: OwnedEventId| ready(inner.get(&id).is_some());
let _ = match state_res::resolve(
&RoomVersionId::V6,
state_sets.into_iter(),
&auth_chain_sets,
&fetch,
&exists,
let func = async || {
if let Err(e) = super::resolve(
&rules,
state_sets.clone().into_iter().stream(),
auth_chains.clone().into_iter().stream(),
&async |id| inner.get(&id).cloned().ok_or_else(not_found),
&async |id| inner.contains_key(&id),
false,
)
.await
{
| Ok(state) => state,
| Err(_) => panic!("resolution failed during benchmarking"),
};
panic!("{e}")
}
};
c.iter(move || {
rt.block_on(async {
func().await;
});
});
}
@@ -168,12 +199,12 @@ fn resolve_deeper_event_set(c: &mut test::Bencher) {
struct TestStore<E: Event>(HashMap<OwnedEventId, E>);
#[allow(unused)]
impl<E: Event + Clone> TestStore<E> {
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_else(|| Error::NotFound(format!("{} not found", event_id)))
.ok_or_else(not_found)
}
/// Returns the events that correspond to the `event_ids` sorted in the same
@@ -191,13 +222,12 @@ impl<E: Event + Clone> TestStore<E> {
&self,
room_id: &RoomId,
event_ids: Vec<OwnedEventId>,
) -> Result<HashSet<OwnedEventId>> {
let mut result = HashSet::new();
) -> Result<AuthSet<OwnedEventId>> {
let mut result = AuthSet::new();
let mut stack = event_ids;
// DFS for auth event chain
while !stack.is_empty() {
let ev_id = stack.pop().unwrap();
while let Some(ev_id) = stack.pop() {
if result.contains(&ev_id) {
continue;
}
@@ -226,7 +256,8 @@ impl<E: Event + Clone> TestStore<E> {
let chain = self
.auth_event_ids(room_id, ids)?
.into_iter()
.collect::<HashSet<_>>();
.collect::<AuthSet<_>>();
auth_chain_sets.push(chain);
}
@@ -234,7 +265,7 @@ impl<E: Event + Clone> TestStore<E> {
let common = auth_chain_sets
.iter()
.skip(1)
.fold(first, |a, b| a.intersection(b).cloned().collect::<HashSet<_>>());
.fold(first, |a, b| a.intersection(b).cloned().collect::<AuthSet<_>>());
Ok(auth_chain_sets
.into_iter()
@@ -247,7 +278,7 @@ impl<E: Event + Clone> TestStore<E> {
}
}
impl TestStore<Pdu> {
impl TestStore<PduEvent> {
#[allow(clippy::type_complexity)]
fn set_up(
&mut self,
@@ -261,8 +292,9 @@ impl TestStore<Pdu> {
&[],
&[],
);
let cre = create_event.event_id().to_owned();
self.0.insert(cre.clone(), create_event.clone());
let cre = create_event.event_id();
self.0
.insert(cre.to_owned(), create_event.clone());
let alice_mem = to_pdu_event(
"IMA",
@@ -270,8 +302,8 @@ impl TestStore<Pdu> {
TimelineEventType::RoomMember,
Some(alice().to_string().as_str()),
member_content_join(),
&[cre.clone()],
&[cre.clone()],
&[cre.to_owned()],
&[cre.to_owned()],
);
self.0
.insert(alice_mem.event_id().to_owned(), alice_mem.clone());
@@ -282,7 +314,7 @@ impl TestStore<Pdu> {
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
&[cre.clone(), alice_mem.event_id().to_owned()],
&[cre.to_owned(), alice_mem.event_id().to_owned()],
&[alice_mem.event_id().to_owned()],
);
self.0
@@ -296,7 +328,7 @@ impl TestStore<Pdu> {
TimelineEventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_join(),
&[cre.clone(), join_rules.event_id().to_owned()],
&[cre.to_owned(), join_rules.event_id().to_owned()],
&[join_rules.event_id().to_owned()],
);
self.0
@@ -308,7 +340,7 @@ impl TestStore<Pdu> {
TimelineEventType::RoomMember,
Some(charlie().to_string().as_str()),
member_content_join(),
&[cre, join_rules.event_id().to_owned()],
&[cre.to_owned(), join_rules.event_id().to_owned()],
&[join_rules.event_id().to_owned()],
);
self.0
@@ -316,30 +348,33 @@ impl TestStore<Pdu> {
let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem]
.iter()
.map(|ev| {
.map(|e| {
(
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
ev.event_id().to_owned(),
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(|ev| {
.map(|e| {
(
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
ev.event_id().to_owned(),
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(|ev| {
.map(|e| {
(
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
ev.event_id().to_owned(),
e.event_type()
.with_state_key(e.state_key().unwrap()),
e.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
@@ -352,7 +387,7 @@ fn event_id(id: &str) -> OwnedEventId {
if id.contains('$') {
return id.try_into().unwrap();
}
format!("${}:foo", id).try_into().unwrap()
format!("${id}:foo").try_into().unwrap()
}
fn alice() -> &'static UserId { user_id!("@alice:foo") }
@@ -381,7 +416,7 @@ fn to_pdu_event<S>(
content: Box<RawJsonValue>,
auth_events: &[S],
prev_events: &[S],
) -> Pdu
) -> PduEvent
where
S: AsRef<str>,
{
@@ -391,28 +426,31 @@ where
let id = if id.contains('$') {
id.to_owned()
} else {
format!("${}:foo", id)
format!("${id}:foo")
};
let auth_events = auth_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
let prev_events = prev_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
Pdu {
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,
origin: None,
redacts: None,
unsigned: None,
auth_events,
@@ -420,12 +458,14 @@ where
depth: uint!(0),
hashes: EventHash::default(),
signatures: None,
#[cfg(test)]
rejected: false,
}
}
// all graphs start with these input events
#[allow(non_snake_case)]
fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
fn INITIAL_EVENTS() -> HashMap<OwnedEventId, PduEvent> {
vec![
to_pdu_event::<&EventId>(
"CREATE",
@@ -507,7 +547,7 @@ fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
// all graphs start with these input events
#[allow(non_snake_case)]
fn BAN_STATE_SET() -> HashMap<OwnedEventId, Pdu> {
fn BAN_STATE_SET() -> HashMap<OwnedEventId, PduEvent> {
vec![
to_pdu_event(
"PA",

View File

@@ -1,23 +0,0 @@
use serde_json::Error as JsonError;
use thiserror::Error;
/// Represents the various errors that arise when resolving state.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
/// A deserialization error.
#[error(transparent)]
SerdeJson(#[from] JsonError),
/// The given option or version is unsupported.
#[error("Unsupported room version: {0}")]
Unsupported(String),
/// The given event was not found.
#[error("Not found error: {0}")]
NotFound(String),
/// Invalid fields in the given PDU.
#[error("Invalid PDU: {0}")]
InvalidPdu(String),
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,461 @@
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, ID_MAX_BYTES, RoomId, int,
room_version_rules::EventFormatRules,
};
use serde_json::to_string as to_json_string;
use crate::{
Err, Result, err,
matrix::pdu::{MAX_AUTH_EVENTS, MAX_PDU_BYTES, MAX_PREV_EVENTS},
};
/// Check that the given canonicalized PDU respects the event format of the room
/// version and the [size limits] from the Matrix specification.
///
/// This is part of the [checks performed on receipt of a PDU].
///
/// This checks the following and enforces their size limits:
///
/// * Full PDU
/// * `sender`
/// * `room_id`
/// * `type`
/// * `event_id`
/// * `state_key`
/// * `prev_events`
/// * `auth_events`
/// * `depth`
///
/// Returns an `Err(_)` if the JSON is malformed or if the PDU doesn't pass the
/// checks.
///
/// [size limits]: https://spec.matrix.org/latest/client-server-api/#size-limits
/// [checks performed on receipt of a PDU]: https://spec.matrix.org/latest/server-server-api/#checks-performed-on-receipt-of-a-pdu
pub fn check_pdu_format(pdu: &CanonicalJsonObject, rules: &EventFormatRules) -> Result {
// Check the PDU size, it must occur on the full PDU with signatures.
let json = to_json_string(&pdu)
.map_err(|e| err!(Request(BadJson("Failed to serialize canonical JSON: {e}"))))?;
if json.len() > MAX_PDU_BYTES {
return Err!(Request(InvalidParam(
"PDU is larger than maximum of {MAX_PDU_BYTES} bytes"
)));
}
// Check the presence, type and length of the `type` field.
let event_type = extract_required_string_field(pdu, "type")?;
// Check the presence, type and length of the `sender` field.
extract_required_string_field(pdu, "sender")?;
// Check the presence, type and length of the `room_id` field.
let room_id = (event_type != "m.room.create" || rules.require_room_create_room_id)
.then(|| extract_required_string_field(pdu, "room_id"))
.transpose()?;
// Check the presence, type and length of the `event_id` field.
if rules.require_event_id {
extract_required_string_field(pdu, "event_id")?;
}
// Check the type and length of the `state_key` field.
extract_optional_string_field(pdu, "state_key")?;
// Check the presence, type and length of the `prev_events` field.
extract_required_array_field(pdu, "prev_events", MAX_PREV_EVENTS)?;
// Check the presence, type and length of the `auth_events` field.
let auth_events = extract_required_array_field(pdu, "auth_events", MAX_AUTH_EVENTS)?;
if !rules.allow_room_create_in_auth_events {
// The only case where the room ID should be missing is for m.room.create which
// shouldn't have any auth_events.
if let Some(room_id) = room_id {
let room_create_event_reference_hash = <&RoomId>::try_from(room_id.as_str())
.map_err(|e| err!("invalid `room_id` field in PDU: {e}"))?
.strip_sigil();
for event_id in auth_events {
let CanonicalJsonValue::String(event_id) = event_id else {
return Err!(Request(InvalidParam(
"unexpected format of array item in `auth_events` field in PDU: \
expected string, got {event_id:?}"
)));
};
let reference_hash =
event_id
.strip_prefix('$')
.ok_or(err!(Request(InvalidParam(
"unexpected format of array item in `auth_events` field in PDU: \
string not beginning with the `$` sigil"
))))?;
if reference_hash == room_create_event_reference_hash {
return Err!(Request(InvalidParam(
"invalid `auth_events` field in PDU: cannot contain the `m.room.create` \
event ID"
)));
}
}
}
}
// Check the presence, type and value of the `depth` field.
match pdu.get("depth") {
| Some(CanonicalJsonValue::Integer(value)) =>
if *value < int!(0) {
return Err!(Request(InvalidParam(
"invalid `depth` field in PDU: cannot be a negative integer"
)));
},
| Some(value) => {
return Err!(Request(InvalidParam(
"unexpected format of `depth` field in PDU: expected integer, got {value:?}"
)));
},
| None => return Err!(Request(InvalidParam("missing `depth` field in PDU"))),
}
Ok(())
}
/// Extract the optional string field with the given name from the given
/// canonical JSON object.
///
/// Returns `Ok(Some(value))` if the field is present and a valid string,
/// `Ok(None)` if the field is missing and `Err(_)` if the field is not a string
/// or its length is bigger than [`ID_MAX_BYTES`].
fn extract_optional_string_field<'a>(
object: &'a CanonicalJsonObject,
field: &'a str,
) -> Result<Option<&'a String>> {
match object.get(field) {
| Some(CanonicalJsonValue::String(value)) =>
if value.len() > ID_MAX_BYTES {
Err!(Request(InvalidParam(
"invalid `{field}` field in PDU: string length is larger than maximum of \
{ID_MAX_BYTES} bytes"
)))
} else {
Ok(Some(value))
},
| Some(value) => Err!(Request(InvalidParam(
"unexpected format of `{field}` field in PDU: expected string, got {value:?}"
))),
| None => Ok(None),
}
}
/// Extract the required string field with the given name from the given
/// canonical JSON object.
///
/// Returns `Ok(value)` if the field is present and a valid string and `Err(_)`
/// if the field is missing, not a string or its length is bigger than
/// [`ID_MAX_BYTES`].
fn extract_required_string_field<'a>(
object: &'a CanonicalJsonObject,
field: &'a str,
) -> Result<&'a String> {
extract_optional_string_field(object, field)?
.ok_or_else(|| err!(Request(InvalidParam("missing `{field}` field in PDU"))))
}
/// Extract the required array field with the given name from the given
/// canonical JSON object.
///
/// Returns `Ok(value)` if the field is present and a valid array or `Err(_)` if
/// the field is missing, not an array or its length is bigger than the given
/// value.
fn extract_required_array_field<'a>(
object: &'a CanonicalJsonObject,
field: &'a str,
max_len: usize,
) -> Result<&'a [CanonicalJsonValue]> {
match object.get(field) {
| Some(CanonicalJsonValue::Array(value)) =>
if value.len() > max_len {
Err!(Request(InvalidParam(
"invalid `{field}` field in PDU: array length is larger than maximum of \
{max_len}"
)))
} else {
Ok(value)
},
| Some(value) => Err!(Request(InvalidParam(
"unexpected format of `{field}` field in PDU: expected array, got {value:?}"
))),
| None => Err!(Request(InvalidParam("missing `{field}` field in PDU"))),
}
}
#[cfg(test)]
mod tests {
use std::iter::repeat_n;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, int, room_version_rules::EventFormatRules,
};
use serde_json::{from_value as from_json_value, json};
use super::check_pdu_format;
/// Construct a PDU valid for the event format of room v1.
fn pdu_v1() -> CanonicalJsonObject {
let pdu = json!({
"auth_events": [
[
"$af232176:example.org",
{ "sha256": "abase64encodedsha256hashshouldbe43byteslong" },
],
],
"content": {
"key": "value",
},
"depth": 12,
"event_id": "$a4ecee13e2accdadf56c1025:example.com",
"hashes": {
"sha256": "thishashcoversallfieldsincasethisisredacted"
},
"origin_server_ts": 1_838_188_000,
"prev_events": [
[
"$af232176:example.org",
{ "sha256": "abase64encodedsha256hashshouldbe43byteslong" }
],
],
"room_id": "!UcYsUzyxTGDxLBEvLy:example.org",
"sender": "@alice:example.com",
"signatures": {
"example.com": {
"ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus",
},
},
"type": "m.room.message",
"unsigned": {
"age": 4612,
},
});
from_json_value(pdu).unwrap()
}
/// Construct a PDU valid for the event format of room v3.
fn pdu_v3() -> CanonicalJsonObject {
let pdu = json!({
"auth_events": [
"$base64encodedeventid",
"$adifferenteventid",
],
"content": {
"key": "value",
},
"depth": 12,
"hashes": {
"sha256": "thishashcoversallfieldsincasethisisredacted",
},
"origin_server_ts": 1_838_188_000,
"prev_events": [
"$base64encodedeventid",
"$adifferenteventid",
],
"redacts": "$some/old+event",
"room_id": "!UcYsUzyxTGDxLBEvLy:example.org",
"sender": "@alice:example.com",
"signatures": {
"example.com": {
"ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus",
},
},
"type": "m.room.message",
"unsigned": {
"age": 4612,
}
});
from_json_value(pdu).unwrap()
}
/// Construct an `m.room.create` PDU valid for the event format of
/// `org.matrix.hydra.11`.
fn room_create_hydra() -> CanonicalJsonObject {
let pdu = json!({
"auth_events": [],
"content": {
"room_version": "org.matrix.hydra.11",
},
"depth": 1,
"hashes": {
"sha256": "thishashcoversallfieldsincasethisisredacted",
},
"origin_server_ts": 1_838_188_000,
"prev_events": [],
"sender": "@alice:example.com",
"signatures": {
"example.com": {
"ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus",
},
},
"type": "m.room.create",
"unsigned": {
"age": 4612,
}
});
from_json_value(pdu).unwrap()
}
/// Construct a PDU valid for the event format of `org.matrix.hydra.11`.
fn pdu_hydra() -> CanonicalJsonObject {
let pdu = json!({
"auth_events": [
"$base64encodedeventid",
"$adifferenteventid",
],
"content": {
"key": "value",
},
"depth": 12,
"hashes": {
"sha256": "thishashcoversallfieldsincasethisisredacted",
},
"origin_server_ts": 1_838_188_000,
"prev_events": [
"$base64encodedeventid",
],
"room_id": "!roomcreatereferencehash",
"sender": "@alice:example.com",
"signatures": {
"example.com": {
"ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus",
},
},
"type": "m.room.message",
"unsigned": {
"age": 4612,
}
});
from_json_value(pdu).unwrap()
}
#[test]
fn check_pdu_format_valid_v1() {
check_pdu_format(&pdu_v1(), &EventFormatRules::V1).unwrap();
}
#[test]
fn check_pdu_format_valid_v3() {
check_pdu_format(&pdu_v3(), &EventFormatRules::V3).unwrap();
}
#[test]
fn check_pdu_format_pdu_too_big() {
// Add a lot of data in the content to reach MAX_PDU_SIZE.
let mut pdu = pdu_v3();
let content = pdu
.get_mut("content")
.unwrap()
.as_object_mut()
.unwrap();
let long_string = repeat_n('a', 66_000).collect::<String>();
content.insert("big_data".to_owned(), long_string.into());
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
#[test]
fn check_pdu_format_fields_missing() {
for field in
&["event_id", "sender", "room_id", "type", "prev_events", "auth_events", "depth"]
{
let mut pdu = pdu_v1();
pdu.remove(*field).unwrap();
check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err();
}
}
#[test]
fn check_pdu_format_strings_too_big() {
for field in &["event_id", "sender", "room_id", "type", "state_key"] {
let mut pdu = pdu_v1();
let value = repeat_n('a', 300).collect::<String>();
pdu.insert((*field).to_owned(), value.into());
check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err();
}
}
#[test]
fn check_pdu_format_strings_wrong_format() {
for field in &["event_id", "sender", "room_id", "type", "state_key"] {
let mut pdu = pdu_v1();
pdu.insert((*field).to_owned(), true.into());
check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err();
}
}
#[test]
fn check_pdu_format_arrays_too_big() {
for field in &["prev_events", "auth_events"] {
let mut pdu = pdu_v3();
let value: Vec<_> =
repeat_n(CanonicalJsonValue::from("$eventid".to_owned()), 30).collect();
pdu.insert((*field).to_owned(), value.into());
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
}
#[test]
fn check_pdu_format_arrays_wrong_format() {
for field in &["prev_events", "auth_events"] {
let mut pdu = pdu_v3();
pdu.insert((*field).to_owned(), true.into());
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
}
#[test]
fn check_pdu_format_negative_depth() {
let mut pdu = pdu_v3();
pdu.insert("depth".to_owned(), int!(-1).into())
.unwrap();
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
#[test]
fn check_pdu_format_depth_wrong_format() {
let mut pdu = pdu_v3();
pdu.insert("depth".to_owned(), true.into());
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
#[test]
fn check_pdu_format_valid_room_create_hydra() {
let pdu = room_create_hydra();
check_pdu_format(&pdu, &EventFormatRules::V12).unwrap();
}
#[test]
fn check_pdu_format_valid_hydra() {
let pdu = pdu_hydra();
check_pdu_format(&pdu, &EventFormatRules::V12).unwrap();
}
#[test]
fn check_pdu_format_hydra_with_room_create() {
let mut pdu = pdu_hydra();
pdu.get_mut("auth_events")
.unwrap()
.as_array_mut()
.unwrap()
.push("$roomcreatereferencehash".to_owned().into());
check_pdu_format(&pdu, &EventFormatRules::V12).unwrap_err();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,74 @@
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)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,265 +0,0 @@
use std::collections::BTreeMap;
use ruma::{
Int, OwnedUserId, UserId,
events::{TimelineEventType, room::power_levels::RoomPowerLevelsEventContent},
power_levels::{NotificationPowerLevels, default_power_level},
serde::{
deserialize_v1_powerlevel, vec_deserialize_int_powerlevel_values,
vec_deserialize_v1_powerlevel_values,
},
};
use serde::Deserialize;
use serde_json::{Error, from_str as from_json_str};
use super::{Result, RoomVersion};
use crate::error;
#[derive(Deserialize)]
struct IntRoomPowerLevelsEventContent {
#[serde(default = "default_power_level")]
ban: Int,
#[serde(default)]
events: BTreeMap<TimelineEventType, Int>,
#[serde(default)]
events_default: Int,
#[serde(default)]
invite: Int,
#[serde(default = "default_power_level")]
kick: Int,
#[serde(default = "default_power_level")]
redact: Int,
#[serde(default = "default_power_level")]
state_default: Int,
#[serde(default)]
users: BTreeMap<OwnedUserId, Int>,
#[serde(default)]
users_default: Int,
#[serde(default)]
notifications: IntNotificationPowerLevels,
}
impl From<IntRoomPowerLevelsEventContent> for RoomPowerLevelsEventContent {
fn from(int_pl: IntRoomPowerLevelsEventContent) -> Self {
let IntRoomPowerLevelsEventContent {
ban,
events,
events_default,
invite,
kick,
redact,
state_default,
users,
users_default,
notifications,
} = int_pl;
let mut pl = Self::new();
pl.ban = ban;
pl.events = events;
pl.events_default = events_default;
pl.invite = invite;
pl.kick = kick;
pl.redact = redact;
pl.state_default = state_default;
pl.users = users;
pl.users_default = users_default;
pl.notifications = notifications.into();
pl
}
}
#[derive(Deserialize)]
struct IntNotificationPowerLevels {
#[serde(default = "default_power_level")]
room: Int,
}
impl Default for IntNotificationPowerLevels {
fn default() -> Self { Self { room: default_power_level() } }
}
impl From<IntNotificationPowerLevels> for NotificationPowerLevels {
fn from(int_notif: IntNotificationPowerLevels) -> Self {
let mut notif = Self::new();
notif.room = int_notif.room;
notif
}
}
#[inline]
pub(crate) fn deserialize_power_levels(
content: &str,
room_version: &RoomVersion,
) -> Option<RoomPowerLevelsEventContent> {
if room_version.integer_power_levels {
deserialize_integer_power_levels(content)
} else {
deserialize_legacy_power_levels(content)
}
}
fn deserialize_integer_power_levels(content: &str) -> Option<RoomPowerLevelsEventContent> {
match from_json_str::<IntRoomPowerLevelsEventContent>(content) {
| Ok(content) => Some(content.into()),
| Err(_) => {
error!("m.room.power_levels event is not valid with integer values");
None
},
}
}
fn deserialize_legacy_power_levels(content: &str) -> Option<RoomPowerLevelsEventContent> {
match from_json_str(content) {
| Ok(content) => Some(content),
| Err(_) => {
error!(
"m.room.power_levels event is not valid with integer or string integer values"
);
None
},
}
}
#[derive(Deserialize)]
pub(crate) struct PowerLevelsContentFields {
#[serde(
default,
deserialize_with = "vec_deserialize_v1_powerlevel_values"
)]
pub(crate) users: Vec<(OwnedUserId, Int)>,
#[serde(default, deserialize_with = "deserialize_v1_powerlevel")]
pub(crate) users_default: Int,
}
impl PowerLevelsContentFields {
pub(crate) fn get_user_power(&self, user_id: &UserId) -> Option<&Int> {
let comparator = |item: &(OwnedUserId, Int)| {
let item: &UserId = &item.0;
item.cmp(user_id)
};
self.users
.binary_search_by(comparator)
.ok()
.and_then(|idx| self.users.get(idx).map(|item| &item.1))
}
}
#[derive(Deserialize)]
struct IntPowerLevelsContentFields {
#[serde(
default,
deserialize_with = "vec_deserialize_int_powerlevel_values"
)]
users: Vec<(OwnedUserId, Int)>,
#[serde(default)]
users_default: Int,
}
impl From<IntPowerLevelsContentFields> for PowerLevelsContentFields {
fn from(pl: IntPowerLevelsContentFields) -> Self {
let IntPowerLevelsContentFields { users, users_default } = pl;
Self { users, users_default }
}
}
#[inline]
pub(crate) fn deserialize_power_levels_content_fields(
content: &str,
room_version: &RoomVersion,
) -> Result<PowerLevelsContentFields, Error> {
if room_version.integer_power_levels {
deserialize_integer_power_levels_content_fields(content)
} else {
deserialize_legacy_power_levels_content_fields(content)
}
}
fn deserialize_integer_power_levels_content_fields(
content: &str,
) -> Result<PowerLevelsContentFields, Error> {
from_json_str::<IntPowerLevelsContentFields>(content).map(Into::into)
}
fn deserialize_legacy_power_levels_content_fields(
content: &str,
) -> Result<PowerLevelsContentFields, Error> {
from_json_str(content)
}
#[derive(Deserialize)]
pub(crate) struct PowerLevelsContentInvite {
#[serde(default, deserialize_with = "deserialize_v1_powerlevel")]
pub(crate) invite: Int,
}
#[derive(Deserialize)]
struct IntPowerLevelsContentInvite {
#[serde(default)]
invite: Int,
}
impl From<IntPowerLevelsContentInvite> for PowerLevelsContentInvite {
fn from(pl: IntPowerLevelsContentInvite) -> Self {
let IntPowerLevelsContentInvite { invite } = pl;
Self { invite }
}
}
pub(crate) fn deserialize_power_levels_content_invite(
content: &str,
room_version: &RoomVersion,
) -> Result<PowerLevelsContentInvite, Error> {
if room_version.integer_power_levels {
from_json_str::<IntPowerLevelsContentInvite>(content).map(Into::into)
} else {
from_json_str(content)
}
}
#[derive(Deserialize)]
pub(crate) struct PowerLevelsContentRedact {
#[serde(
default = "default_power_level",
deserialize_with = "deserialize_v1_powerlevel"
)]
pub(crate) redact: Int,
}
#[derive(Deserialize)]
pub(crate) struct IntPowerLevelsContentRedact {
#[serde(default = "default_power_level")]
redact: Int,
}
impl From<IntPowerLevelsContentRedact> for PowerLevelsContentRedact {
fn from(pl: IntPowerLevelsContentRedact) -> Self {
let IntPowerLevelsContentRedact { redact } = pl;
Self { redact }
}
}
pub(crate) fn deserialize_power_levels_content_redact(
content: &str,
room_version: &RoomVersion,
) -> Result<PowerLevelsContentRedact, Error> {
if room_version.integer_power_levels {
from_json_str::<IntPowerLevelsContentRedact>(content).map(Into::into)
} else {
from_json_str(content)
}
}

View File

@@ -0,0 +1,213 @@
#[cfg(test)]
mod tests;
mod auth_difference;
mod conflicted_subgraph;
mod iterative_auth_check;
mod mainline_sort;
mod power_sort;
mod split_conflicted;
mod topological_sort;
use std::collections::{BTreeMap, BTreeSet};
use futures::{FutureExt, Stream, StreamExt, TryFutureExt, future::OptionFuture};
use ruma::{OwnedEventId, events::StateEventType, room_version_rules::RoomVersionRules};
pub use self::topological_sort::topological_sort;
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},
utils::stream::{BroadbandExt, IterStream},
};
/// ConflictMap of OwnedEventId specifically.
pub type ConflictMap = StateMap<ConflictVec>;
/// 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>;
/// List of conflicting event_ids
type ConflictVec = Vec<OwnedEventId>;
/// 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<'a, 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_state, unconflicted = unconflicted_state.len(), "unresolved state");
debug!(?conflicted_states, conflicted = conflicted_states.len(), "unresolved states");
if conflicted_states.is_empty() {
return Ok(unconflicted_state.into_iter().collect());
}
let consider_conflicted_subgraph = rules
.state_res
.v2_rules()
.is_some_and(|rules| rules.consider_conflicted_state_subgraph)
|| backport_css;
// Since `org.matrix.hydra.11`, fetch the conflicted state subgraph.
let conflicted_subgraph: OptionFuture<_> = consider_conflicted_subgraph
.then(|| conflicted_states.clone().into_values().flatten())
.map(async |ids| conflicted_subgraph_dfs(ids.stream(), fetch))
.into();
let conflicted_subgraph = conflicted_subgraph
.await
.into_iter()
.stream()
.flatten();
// 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: AuthSet<_> = auth_difference(auth_sets)
.chain(conflicted_states.into_values().flatten().stream())
.broad_filter_map(async |id| exists(id.clone()).await.then_some(id))
.chain(conflicted_subgraph)
.collect::<AuthSet<_>>()
.inspect(|set| debug!(count = set.len(), "full conflicted set"))
.inspect(|set| debug!(?set, "full conflicted set"))
.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_events: Vec<_> = power_sort(rules, &full_conflicted_set, fetch)
.inspect_ok(|list| debug!(count = list.len(), "sorted power events"))
.inspect_ok(|list| debug!(?list, "sorted power events"))
.await?;
let sorted_power_events_set: AuthSet<_> = sorted_power_events.iter().collect();
let sorted_power_events = sorted_power_events
.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_events, initial_state, fetch)
.inspect_ok(|map| debug!(count = map.len(), "partially resolved power state"))
.inspect_ok(|map| debug!(?map, "partially resolved power state"))
.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| !sorted_power_events_set.contains(id))
.collect();
debug!(count = remaining_events.len(), "remaining events");
debug!(list = ?remaining_events, "remaining events");
let have_remaining_events = !remaining_events.is_empty();
let remaining_events = remaining_events
.iter()
.stream()
.map(AsRef::as_ref);
// 3. Take all remaining events that werent picked in step 1 and order them by
// the mainline ordering based on the power level in the partially resolved
// state obtained in step 2.
let sorted_remaining_events: OptionFuture<_> = have_remaining_events
.then(move || mainline_sort(power_event.cloned(), remaining_events, fetch))
.into();
let sorted_remaining_events = sorted_remaining_events
.await
.unwrap_or(Ok(Vec::new()))?;
debug!(count = sorted_remaining_events.len(), "sorted remaining events");
debug!(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)
.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");
debug!(?resolved_state, "resolved state");
Ok(resolved_state)
}

View File

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

View File

@@ -0,0 +1,124 @@
use std::{
collections::HashSet as Set,
mem::take,
sync::{Arc, Mutex},
};
use futures::{Future, FutureExt, Stream, StreamExt};
use ruma::OwnedEventId;
use crate::{
Result,
matrix::Event,
utils::stream::{IterStream, automatic_width},
};
#[derive(Default)]
struct Global {
subgraph: Mutex<Set<OwnedEventId>>,
seen: Mutex<Set<OwnedEventId>>,
}
#[derive(Default)]
struct Local {
path: Vec<OwnedEventId>,
stack: Vec<Vec<OwnedEventId>>,
}
pub(super) fn conflicted_subgraph_dfs<ConflictedEventIds, Fetch, Fut, Pdu>(
conflicted_event_ids: ConflictedEventIds,
fetch: &Fetch,
) -> impl Stream<Item = OwnedEventId> + Send
where
ConflictedEventIds: Stream<Item = OwnedEventId> + Send,
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
conflicted_event_ids
.collect::<Set<_>>()
.map(|ids| (Arc::new(Global::default()), ids))
.then(async |(state, conflicted_event_ids)| {
conflicted_event_ids
.iter()
.stream()
.map(ToOwned::to_owned)
.map(|event_id| (state.clone(), event_id))
.for_each_concurrent(automatic_width(), async |(state, event_id)| {
subgraph_descent(state, event_id, &conflicted_event_ids, fetch)
.await
.expect("only mutex errors expected");
})
.await;
let mut state = state.subgraph.lock().expect("locked");
take(&mut *state)
})
.map(Set::into_iter)
.map(IterStream::stream)
.flatten_stream()
}
async fn subgraph_descent<Fetch, Fut, Pdu>(
state: Arc<Global>,
conflicted_event_id: OwnedEventId,
conflicted_event_ids: &Set<OwnedEventId>,
fetch: &Fetch,
) -> Result<Arc<Global>>
where
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let Global { subgraph, seen } = &*state;
let mut local = Local {
path: vec![conflicted_event_id.clone()],
stack: vec![vec![conflicted_event_id]],
};
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(state)
}
fn pop(local: &mut Local) -> Option<OwnedEventId> {
let Local { path, stack } = local;
while stack.last().is_some_and(Vec::is_empty) {
stack.pop();
path.pop();
}
stack
.last_mut()
.and_then(Vec::pop)
.inspect(|event_id| path.push(event_id.clone()))
}

View File

@@ -0,0 +1,203 @@
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},
trace,
utils::stream::{IterStream, ReadyExt, TryReadyExt, TryWidebandExt},
};
/// Perform the iterative auth checks to the given list of events.
///
/// Definition in the specification:
///
/// The iterative auth checks algorithm takes as input an initial room state and
/// a sorted list of state events, and constructs a new room state by iterating
/// through the event list and applying the state event to the room state if the
/// state event is allowed by the authorization rules. If the state event is not
/// allowed by the authorization rules, then the event is ignored. If a
/// (event_type, state_key) key that is required for checking the authorization
/// rules is not present in the state, then the appropriate state event from the
/// events auth_events is used if the auth event is not rejected.
///
/// ## Arguments
///
/// * `rules` - The authorization rules for the current room version.
/// * `events` - The sorted state events to apply to the `partial_state`.
/// * `state` - The current state that was partially resolved for the room.
/// * `fetch_event` - Function to fetch an event in the room given its event ID.
///
/// ## Returns
///
/// Returns the partially resolved state, or an `Err(_)` if one of the state
/// events in the room has an unexpected format.
#[tracing::instrument(
name = "iterative_auth",
level = "debug",
skip_all,
fields(
states = ?state.len(),
)
)]
pub(super) async fn iterative_auth_check<'b, SortedPowerEvents, Fetch, Fut, Pdu>(
rules: &RoomVersionRules,
events: SortedPowerEvents,
state: StateMap<OwnedEventId>,
fetch: &Fetch,
) -> Result<StateMap<OwnedEventId>>
where
SortedPowerEvents: Stream<Item = &'b EventId> + Send,
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
events
.map(Ok)
.wide_and_then(async |event_id| {
let event = fetch(event_id.to_owned()).await?;
let state_key: StateKey = event
.state_key()
.ok_or_else(|| err!(Request(InvalidParam("Missing state_key"))))?
.into();
Ok((event_id, state_key, event))
})
.try_fold(state, |state, (event_id, state_key, event)| {
auth_check(rules, state, event_id, state_key, event, fetch)
})
.await
}
#[tracing::instrument(
name = "check",
level = "debug",
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: Vec<_> = auth_events
.chain(auth_types_events)
.try_collect()
.map_ok(|mut vec: Vec<_>| {
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 failed auth check: {e}"))
.is_ok()
{
let key = event.event_type().with_state_key(state_key);
state.insert(key, event_id.to_owned());
}
Ok(state)
}

View File

@@ -0,0 +1,171 @@
use std::collections::HashMap;
use futures::{Stream, StreamExt, TryStreamExt, pin_mut};
use ruma::{EventId, OwnedEventId, events::TimelineEventType};
use crate::{
Result,
matrix::Event,
trace,
utils::stream::{IterStream, TryReadyExt, WidebandExt},
};
/// Perform mainline ordering of the given events.
///
/// Definition in the spec:
/// Given mainline positions calculated from P, the mainline ordering based on P
/// of a set of events is the ordering, from smallest to largest, using the
/// following comparison relation on events: for events x and y, x < y if
///
/// 1. the mainline position of x is greater than the mainline position of y
/// (i.e. the auth chain of x is based on an earlier event in the mainline
/// than y); or
/// 2. the mainline positions of the events are the same, but xs
/// origin_server_ts is less than ys origin_server_ts; or
/// 3. the mainline positions of the events are the same and the events have the
/// same origin_server_ts, but xs event_id is less than ys event_id.
///
/// ## Arguments
///
/// * `events` - The list of event IDs to sort.
/// * `power_level` - The power level event in the current state.
/// * `fetch_event` - Function to fetch an event in the room given its event ID.
///
/// ## Returns
///
/// Returns the sorted list of event IDs, or an `Err(_)` if one the event in the
/// room has an unexpected format.
#[tracing::instrument(
level = "debug",
skip_all,
fields(
?power_level,
)
)]
pub(super) async fn mainline_sort<'a, RemainingEvents, Fetch, Fut, Pdu>(
mut power_level: 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 mut mainline = vec![];
while let Some(power_level_event_id) = power_level {
let power_level_event = fetch(power_level_event_id).await?;
mainline.push(power_level_event.event_id().to_owned());
power_level = get_power_levels_auth_event(&power_level_event, fetch)
.await?
.map(|event| event.event_id().to_owned());
}
let mainline_map: HashMap<_, _> = mainline
.iter()
.rev()
.enumerate()
.map(|(idx, event_id)| (event_id.clone(), idx))
.collect();
let order_map: HashMap<_, _> = events
.wide_filter_map(async |event_id| {
let event = fetch(event_id.to_owned()).await.ok()?;
let position = mainline_position(&event, &mainline_map, fetch)
.await
.ok()?;
let event_id = event.event_id().to_owned();
let origin_server_ts = event.origin_server_ts();
Some((event_id, (position, origin_server_ts)))
})
.collect()
.await;
let mut sorted_event_ids: Vec<_> = order_map.keys().cloned().collect();
sorted_event_ids.sort_by(|a, b| {
let (a_pos, a_ots) = &order_map[a];
let (b_pos, b_ots) = &order_map[b];
a_pos
.cmp(b_pos)
.then(a_ots.cmp(b_ots))
.then(a.cmp(b))
});
Ok(sorted_event_ids)
}
/// 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(
level = "trace",
skip_all,
fields(
event = ?event.event_id(),
mainline = mainline_map.len(),
)
)]
async fn mainline_position<Fetch, Fut, Pdu>(
event: &Pdu,
mainline_map: &HashMap<OwnedEventId, usize>,
fetch: &Fetch,
) -> Result<usize>
where
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>>,
Pdu: Event,
{
let mut current_event = Some(event.clone());
while let Some(event) = current_event {
trace!(event_id = ?event.event_id(), "mainline");
// If the current event is in the mainline map, return its position.
if let Some(position) = mainline_map.get(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)
}
#[allow(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>>,
Pdu: Event,
{
let power_level_event = event
.auth_events()
.try_stream()
.map_ok(ToOwned::to_owned)
.and_then(|auth_event_id| fetch(auth_event_id))
.ready_try_skip_while(|auth_event| {
Ok(!auth_event.is_type_and_state_key(&TimelineEventType::RoomPowerLevels, ""))
});
pin_mut!(power_level_event);
power_level_event.try_next().await
}

View File

@@ -0,0 +1,273 @@
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,
},
AuthSet, topological_sort,
};
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(
full_conflicted = full_conflicted_set.len(),
)
)]
pub(super) async fn power_sort<Fetch, Fut, Pdu>(
rules: &RoomVersionRules,
full_conflicted_set: &AuthSet<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(
level = "trace",
skip_all,
fields(
?event_id,
graph = graph.len(),
)
)]
async fn add_event_auth_chain<Fetch, Fut, Pdu>(
mut graph: HashMap<OwnedEventId, HashSet<OwnedEventId>>,
full_conflicted_set: &AuthSet<OwnedEventId>,
event_id: &EventId,
fetch: &Fetch,
) -> HashMap<OwnedEventId, HashSet<OwnedEventId>>
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();
let auth_events = event
.as_ref()
.map(Event::auth_events)
.into_iter()
.flatten();
for auth_event_id in auth_events {
// If the auth event ID is in the full conflicted set…
if !full_conflicted_set.contains(auth_event_id) {
continue;
}
// If the auth event ID is not in the graph, we need to check its auth events
// later.
if !graph.contains_key(auth_event_id) {
state.push(auth_event_id.to_owned());
}
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.
graph
.get_mut(event_id)
.expect("event_id must be added to graph")
.insert(auth_event_id.to_owned());
}
}
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(
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 {
if rules
.authorization
.room_create_event_id_as_room_id
{
let create_id = event.room_id().as_event_id()?;
let fetched = fetch(create_id).await?;
room_create_event = Some(RoomCreateEvent::new(fetched));
}
}
for auth_event_id in event
.as_ref()
.map(Event::auth_events)
.into_iter()
.flatten()
{
if let Ok(auth_event) = fetch(auth_event_id.to_owned()).await {
if auth_event.is_type_and_state_key(&TimelineEventType::RoomPowerLevels, "") {
room_power_levels_event = Some(RoomPowerLevelsEvent::new(auth_event));
} else if !rules
.authorization
.room_create_event_id_as_room_id
&& auth_event.is_type_and_state_key(&TimelineEventType::RoomCreate, "")
{
room_create_event = Some(RoomCreateEvent::new(auth_event));
}
if room_power_levels_event.is_some() && room_create_event.is_some() {
break;
}
}
}
let auth_rules = &rules.authorization;
let creators = room_create_event
.as_ref()
.and_then(|event| event.creators(auth_rules).ok());
if let Some((event, creators)) = event.ok().zip(creators) {
room_power_levels_event.user_power_level(event.sender(), creators, auth_rules)
} else {
room_power_levels_event
.get_as_int_or_default(RoomPowerLevelsIntField::UsersDefault, auth_rules)
.map(Into::into)
}
}
/// Whether the given event ID belongs to a power event.
///
/// See the docs of `is_power_event()` for the definition of a power event.
#[tracing::instrument(
name = "is_power_event",
level = "trace",
skip_all,
fields(
?event_id,
)
)]
async fn is_power_event_id<Fetch, Fut, Pdu>(event_id: &EventId, fetch: &Fetch) -> bool
where
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
match fetch(event_id.to_owned()).await {
| Ok(state) => is_power_event(&state),
| _ => false,
}
}

View File

@@ -0,0 +1,70 @@
use std::{collections::HashMap, hash::Hash};
use futures::{Stream, StreamExt};
use super::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.
pub(super) async fn split_conflicted_state<'a, Maps, Id>(
state_maps: Maps,
) -> (StateMap<Id>, StateMap<Vec<Id>>)
where
Maps: Stream<Item = StateMap<Id>>,
Id: Clone + Eq + Hash + Ord + Send + Sync + 'a,
{
let state_maps: Vec<_> = state_maps.collect().await;
let mut state_set_count = 0_usize;
let mut occurrences = HashMap::<_, HashMap<_, usize>>::new();
let state_maps = state_maps.iter().inspect(|_state| {
state_set_count = validated!(state_set_count + 1);
});
for (k, v) in state_maps.into_iter().flat_map(|s| s.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 = StateMap::<Vec<Id>>::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 {
conflicted_state_set
.entry((k.0.clone(), k.1.clone()))
.or_default()
.push(id.clone());
}
}
}
(unconflicted_state_map, conflicted_state_set)
}

View File

@@ -0,0 +1,840 @@
use std::collections::HashMap;
use futures::StreamExt;
use maplit::{hashmap, hashset};
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::{
AuthSet, 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: AuthSet<OwnedEventId> = AuthSet::new();
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 = &[
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]
async fn test_reverse_topological_power_sort() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let graph = hashmap! {
event_id("l") => hashset![event_id("o")],
event_id("m") => hashset![event_id("n"), event_id("o")],
event_id("n") => hashset![event_id("o")],
event_id("o") => hashset![], // "o" has zero outgoing edges but 4 incoming edges
event_id("p") => hashset![event_id("o")],
};
let res = 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;
}
#[allow(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()
}
#[allow(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),* $(,)?) => {{
#[allow(unused_mut)]
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" => vec![0],
StateEventType::RoomMember => "@b:hs1" => vec![1],
StateEventType::RoomMember => "@c:hs1" => vec![2],
],);
}
#[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" => vec![0, 1, 2],
],);
}
#[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" => vec![1],
StateEventType::RoomMember => "@c:hs1" => vec![2],
],);
}

View File

@@ -0,0 +1,163 @@
use std::{
cmp::{Ordering, Reverse},
collections::{BinaryHeap, HashMap, HashSet},
};
use futures::TryStreamExt;
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, events::room::power_levels::UserPowerLevel,
};
use crate::{Result, utils::stream::IterStream};
#[derive(PartialEq, Eq)]
struct TieBreaker<'a> {
power_level: UserPowerLevel,
origin_server_ts: MilliSecondsSinceUnixEpoch,
event_id: &'a EventId,
}
type PduInfo = (UserPowerLevel, MilliSecondsSinceUnixEpoch);
// 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.
///
/// 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
/// auth events. The reverse topological power ordering is ordered from earliest
/// event to latest. For comparing two topological orderings to determine which
/// is the lexicographically smallest, the following comparison relation on
/// events is used: for events x and y, x < y if
///
/// 1. xs sender has greater power level than ys sender, when looking at their
/// respective auth_events; or
/// 2. the senders have the same power level, but xs origin_server_ts is less
/// than ys origin_server_ts; or
/// 3. the senders have the same power level and the events have the same
/// origin_server_ts, but xs event_id is less than ys event_id.
///
/// The reverse topological power ordering can be found by sorting the events
/// using Kahns algorithm for topological sorting, and at each step selecting,
/// among all the candidate vertices, the smallest vertex using the above
/// comparison relation.
///
/// ## Arguments
///
/// * `graph` - The graph to sort. A map of event ID to its auth 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(),
)
)]
pub async fn topological_sort<Query, Fut>(
graph: &HashMap<OwnedEventId, HashSet<OwnedEventId>>,
query: &Query,
) -> Result<Vec<OwnedEventId>>
where
Query: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<PduInfo>> + Send,
{
// We consider that the DAG is directed from most recent events to oldest
// events, so an event is an incoming edge to its auth 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 auth events.
let init = (Vec::new(), HashMap::<OwnedEventId, HashSet<OwnedEventId>>::new());
// 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() {
let (power_level, origin_server_ts) = query(event_id.clone()).await?;
// `Reverse` because `BinaryHeap` sorts largest -> smallest and we need
// smallest -> largest.
zero_outdeg.push(Reverse(TieBreaker {
power_level,
origin_server_ts,
event_id,
}));
}
incoming_edges.entry(event_id.into()).or_default();
for auth_event_id in outgoing_edges {
incoming_edges
.entry(auth_event_id.into())
.or_default()
.insert(event_id.into());
}
Ok((zero_outdeg, incoming_edges))
},
)
.await?;
// Map of event to the list of events in its auth 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![];
// 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)
.expect("event_id in heap should also be in incoming_edges")
{
let outgoing_edges = outgoing_edges_map
.get_mut(parent_id)
.expect("outgoing_edges should contain all event_ids");
outgoing_edges.remove(item.event_id);
if !outgoing_edges.is_empty() {
continue;
}
// Push on the heap once all the outgoing edges have been removed.
let (power_level, origin_server_ts) = query(parent_id.clone()).await?;
heap.push(Reverse(TieBreaker {
power_level,
origin_server_ts,
event_id: parent_id.as_ref(),
}));
}
sorted.push(item.event_id.into());
}
Ok(sorted)
}

View File

@@ -1,150 +0,0 @@
use ruma::RoomVersionId;
use super::{Error, Result};
#[derive(Debug)]
#[allow(clippy::exhaustive_enums)]
pub enum RoomDisposition {
/// A room version that has a stable specification.
Stable,
/// A room version that is not yet fully specified.
Unstable,
}
#[derive(Debug)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub enum EventFormatVersion {
/// $id:server event id format
V1,
/// MSC1659-style $hash event id format: introduced for room v3
V2,
/// MSC1884-style $hash format: introduced for room v4
V3,
}
#[derive(Debug)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub enum StateResolutionVersion {
/// State resolution for rooms at version 1.
V1,
/// State resolution for room at version 2 or later.
V2,
}
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[allow(clippy::struct_excessive_bools)]
pub struct RoomVersion {
/// The stability of this room.
pub disposition: RoomDisposition,
/// The format of the EventId.
pub event_format: EventFormatVersion,
/// Which state resolution algorithm is used.
pub state_res: StateResolutionVersion,
// FIXME: not sure what this one means?
pub enforce_key_validity: bool,
/// `m.room.aliases` had special auth rules and redaction rules
/// before room version 6.
///
/// before MSC2261/MSC2432,
pub special_case_aliases_auth: bool,
/// Strictly enforce canonical json, do not allow:
/// * Integers outside the range of [-2 ^ 53 + 1, 2 ^ 53 - 1]
/// * Floats
/// * NaN, Infinity, -Infinity
pub strict_canonicaljson: bool,
/// Verify notifications key while checking m.room.power_levels.
///
/// bool: MSC2209: Check 'notifications'
pub limit_notifications_power_levels: bool,
/// Extra rules when verifying redaction events.
pub extra_redaction_checks: bool,
/// Allow knocking in event authentication.
///
/// See [room v7 specification](https://spec.matrix.org/latest/rooms/v7/) for more information.
pub allow_knocking: bool,
/// Adds support for the restricted join rule.
///
/// See: [MSC3289](https://github.com/matrix-org/matrix-spec-proposals/pull/3289) for more information.
pub restricted_join_rules: bool,
/// Adds support for the knock_restricted join rule.
///
/// See: [MSC3787](https://github.com/matrix-org/matrix-spec-proposals/pull/3787) for more information.
pub knock_restricted_join_rule: bool,
/// Enforces integer power levels.
///
/// See: [MSC3667](https://github.com/matrix-org/matrix-spec-proposals/pull/3667) for more information.
pub integer_power_levels: bool,
/// Determine the room creator using the `m.room.create` event's `sender`,
/// instead of the event content's `creator` field.
///
/// See: [MSC2175](https://github.com/matrix-org/matrix-spec-proposals/pull/2175) for more information.
pub use_room_create_sender: bool,
}
impl RoomVersion {
pub const V1: Self = Self {
disposition: RoomDisposition::Stable,
event_format: EventFormatVersion::V1,
state_res: StateResolutionVersion::V1,
enforce_key_validity: false,
special_case_aliases_auth: true,
strict_canonicaljson: false,
limit_notifications_power_levels: false,
extra_redaction_checks: true,
allow_knocking: false,
restricted_join_rules: false,
knock_restricted_join_rule: false,
integer_power_levels: false,
use_room_create_sender: false,
};
pub const V10: Self = Self {
knock_restricted_join_rule: true,
integer_power_levels: true,
..Self::V9
};
pub const V11: Self = Self {
use_room_create_sender: true,
..Self::V10
};
pub const V2: Self = Self {
state_res: StateResolutionVersion::V2,
..Self::V1
};
pub const V3: Self = Self {
event_format: EventFormatVersion::V2,
extra_redaction_checks: false,
..Self::V2
};
pub const V4: Self = Self {
event_format: EventFormatVersion::V3,
..Self::V3
};
pub const V5: Self = Self { enforce_key_validity: true, ..Self::V4 };
pub const V6: Self = Self {
special_case_aliases_auth: false,
strict_canonicaljson: true,
limit_notifications_power_levels: true,
..Self::V5
};
pub const V7: Self = Self { allow_knocking: true, ..Self::V6 };
pub const V8: Self = Self { restricted_join_rules: true, ..Self::V7 };
pub const V9: Self = Self::V8;
pub fn new(version: &RoomVersionId) -> Result<Self> {
Ok(match version {
| RoomVersionId::V1 => Self::V1,
| RoomVersionId::V2 => Self::V2,
| RoomVersionId::V3 => Self::V3,
| RoomVersionId::V4 => Self::V4,
| RoomVersionId::V5 => Self::V5,
| RoomVersionId::V6 => Self::V6,
| RoomVersionId::V7 => Self::V7,
| RoomVersionId::V8 => Self::V8,
| RoomVersionId::V9 => Self::V9,
| RoomVersionId::V10 => Self::V10,
| RoomVersionId::V11 => Self::V11,
| ver => return Err(Error::Unsupported(format!("found version `{ver}`"))),
})
}
}

View File

@@ -1,37 +1,53 @@
use std::{
borrow::Borrow,
collections::{BTreeMap, HashMap, HashSet},
sync::atomic::{AtomicU64, Ordering::SeqCst},
collections::{HashMap, HashSet},
pin::Pin,
slice,
sync::{
Arc,
atomic::{AtomicU64, Ordering::SeqCst},
},
};
use futures::future::ready;
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, ServerSignatures,
UserId, event_id,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId, event_id,
events::{
TimelineEventType,
StateEventType, TimelineEventType,
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
},
},
int, room_id, uint, user_id,
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::auth_types_for_event;
use super::{AuthSet, StateMap, auth_types_for_event, events::RoomCreateEvent};
use crate::{
Result, info,
matrix::{Event, EventTypeExt, Pdu, StateMap, pdu::EventHash},
Error, Result, err, info,
matrix::{Event, EventHash, EventTypeExt, PduEvent, StateKey},
utils::stream::IterStream,
};
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
pub(crate) async fn do_check(
events: &[Pdu],
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>,
) {
@@ -79,35 +95,32 @@ pub(crate) async fn do_check(
}
}
// event_id -> Pdu
let mut event_map: HashMap<OwnedEventId, Pdu> = HashMap::new();
// 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::lexicographical_topological_sort(&graph, &|_id| async {
Ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0))))
for node in super::topological_sort(&graph, &async |_id| {
Ok((int!(0).into(), MilliSecondsSinceUnixEpoch(uint!(0))))
})
.await
.unwrap()
{
let fake_event = fake_event_map.get(&node).unwrap();
let fake_event = &fake_event_map[&node];
let event_id = fake_event.event_id().to_owned();
let prev_events = graph.get(&node).unwrap();
let prev_events = &graph[&node];
let state_before: StateMap<OwnedEventId> = if prev_events.is_empty() {
HashMap::new()
StateMap::new()
} else if prev_events.len() == 1 {
state_at_event
.get(prev_events.iter().next().unwrap())
.unwrap()
.clone()
state_at_event[prev_events.iter().next().unwrap()].clone()
} else {
let state_sets = prev_events
.iter()
.filter_map(|k| state_at_event.get(k))
.filter_map(|k| state_at_event.get(k).cloned())
.collect::<Vec<_>>();
info!(
@@ -121,21 +134,27 @@ pub(crate) async fn do_check(
.collect::<Vec<_>>()
);
let auth_chain_sets: Vec<_> = state_sets
let auth_chain_sets = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect();
.collect::<Vec<_>>();
let event_map = &event_map;
let fetch = |id: OwnedEventId| ready(event_map.get(&id).cloned());
let exists = |id: OwnedEventId| ready(event_map.get(&id).is_some());
let resolved =
super::resolve(&RoomVersionId::V6, state_sets, &auth_chain_sets, &fetch, &exists)
.await;
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,
@@ -147,13 +166,15 @@ pub(crate) async fn do_check(
let ty = fake_event.event_type();
let key = fake_event.state_key().unwrap();
state_after.insert(ty.with_state_key(key), event_id.to_owned());
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();
@@ -183,7 +204,7 @@ pub(crate) async fn do_check(
store.0.insert(ev_id.to_owned(), event.clone());
state_at_event.insert(node, state_after);
event_map.insert(event_id.to_owned(), store.0.get(ev_id).unwrap().clone());
event_map.insert(event_id.clone(), store.0[ev_id].clone());
}
let mut expected_state = StateMap::new();
@@ -228,24 +249,23 @@ pub(crate) async fn do_check(
}
#[allow(clippy::exhaustive_structs)]
pub(crate) struct TestStore<E: Event>(pub(crate) HashMap<OwnedEventId, E>);
pub(super) struct TestStore(pub(super) HashMap<OwnedEventId, PduEvent>);
impl<E: Event + Clone> TestStore<E> {
pub(crate) fn get_event(&self, _: &RoomId, event_id: &EventId) -> Result<E> {
impl TestStore {
pub(super) fn get_event(&self, _: &RoomId, event_id: &EventId) -> Result<PduEvent> {
self.0
.get(event_id)
.cloned()
.ok_or_else(|| super::Error::NotFound(format!("{event_id} not found")))
.map_err(Into::into)
.ok_or_else(|| event_not_found(event_id))
}
/// Returns a Vec of the related auth events to the given `event`.
pub(crate) fn auth_event_ids(
pub(super) fn auth_event_ids(
&self,
room_id: &RoomId,
event_ids: Vec<OwnedEventId>,
) -> Result<HashSet<OwnedEventId>> {
let mut result = HashSet::new();
) -> Result<AuthSet<OwnedEventId>> {
let mut result = AuthSet::new();
let mut stack = event_ids;
// DFS for auth event chain
@@ -267,8 +287,8 @@ impl<E: Event + Clone> TestStore<E> {
// A StateStore implementation for testing
#[allow(clippy::type_complexity)]
impl TestStore<Pdu> {
pub(crate) fn set_up(
impl TestStore {
pub(super) fn set_up(
&mut self,
) -> (StateMap<OwnedEventId>, StateMap<OwnedEventId>, StateMap<OwnedEventId>) {
let create_event = to_pdu_event::<&EventId>(
@@ -289,8 +309,8 @@ impl TestStore<Pdu> {
TimelineEventType::RoomMember,
Some(alice().as_str()),
member_content_join(),
&[cre.clone()],
&[cre.clone()],
slice::from_ref(&cre),
slice::from_ref(&cre),
);
self.0
.insert(alice_mem.event_id().to_owned(), alice_mem.clone());
@@ -370,7 +390,7 @@ impl TestStore<Pdu> {
}
}
pub(crate) fn event_id(id: &str) -> OwnedEventId {
pub(super) fn event_id(id: &str) -> OwnedEventId {
if id.contains('$') {
return id.try_into().unwrap();
}
@@ -378,33 +398,35 @@ pub(crate) fn event_id(id: &str) -> OwnedEventId {
format!("${id}:foo").try_into().unwrap()
}
pub(crate) fn alice() -> &'static UserId { user_id!("@alice:foo") }
pub(super) fn alice() -> &'static UserId { user_id!("@alice:foo") }
pub(crate) fn bob() -> &'static UserId { user_id!("@bob:foo") }
pub(super) fn bob() -> &'static UserId { user_id!("@bob:foo") }
pub(crate) fn charlie() -> &'static UserId { user_id!("@charlie:foo") }
pub(super) fn charlie() -> &'static UserId { user_id!("@charlie:foo") }
pub(crate) fn ella() -> &'static UserId { user_id!("@ella:foo") }
pub(super) fn ella() -> &'static UserId { user_id!("@ella:foo") }
pub(crate) fn zara() -> &'static UserId { user_id!("@zara:foo") }
pub(super) fn zara() -> &'static UserId { user_id!("@zara:foo") }
pub(crate) fn room_id() -> &'static RoomId { room_id!("!test:foo") }
pub(super) fn room_id() -> &'static RoomId { room_id!("!test:foo") }
pub(crate) fn member_content_ban() -> Box<RawJsonValue> {
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(crate) fn member_content_join() -> Box<RawJsonValue> {
pub(super) fn member_content_join() -> Box<RawJsonValue> {
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap()
}
pub(crate) fn to_init_pdu_event(
pub(super) fn to_init_pdu_event(
id: &str,
sender: &UserId,
ev_type: TimelineEventType,
state_key: Option<&str>,
content: Box<RawJsonValue>,
) -> Pdu {
) -> PduEvent {
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
let id = if id.contains('$') {
id.to_owned()
@@ -412,15 +434,16 @@ pub(crate) fn to_init_pdu_event(
format!("${id}:foo")
};
Pdu {
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,
origin: None,
redacts: None,
unsigned: None,
auth_events: vec![],
@@ -428,10 +451,11 @@ pub(crate) fn to_init_pdu_event(
depth: uint!(0),
hashes: EventHash::default(),
signatures: None,
rejected: false,
}
}
pub(crate) fn to_pdu_event<S>(
pub(super) fn to_pdu_event<S>(
id: &str,
sender: &UserId,
ev_type: TimelineEventType,
@@ -439,7 +463,7 @@ pub(crate) fn to_pdu_event<S>(
content: Box<RawJsonValue>,
auth_events: &[S],
prev_events: &[S],
) -> Pdu
) -> PduEvent
where
S: AsRef<str>,
{
@@ -460,15 +484,16 @@ where
.map(event_id)
.collect::<Vec<_>>();
Pdu {
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,
origin: None,
redacts: None,
unsigned: None,
auth_events,
@@ -476,12 +501,153 @@ where
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 auth_events = auth_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
let prev_events = prev_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
let state_key = state_key.map(ToOwned::to_owned);
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::<Vec<_>>();
let prev_events = prev_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
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: vec![],
prev_events: vec![],
depth: uint!(0),
hashes: EventHash::default(),
signatures: None,
rejected: false,
}
}
// all graphs start with these input events
#[allow(non_snake_case)]
pub(crate) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
pub(super) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, PduEvent> {
vec![
to_pdu_event::<&EventId>(
"CREATE",
@@ -561,9 +727,88 @@ pub(crate) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
.collect()
}
/// Batch of initial events to use for incoming events from room version
/// `org.matrix.hydra.11` onwards.
#[allow(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
#[allow(non_snake_case)]
pub(crate) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap<OwnedEventId, Pdu> {
pub(super) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap<OwnedEventId, PduEvent> {
vec![to_pdu_event::<&EventId>(
"CREATE",
alice(),
@@ -579,9 +824,99 @@ pub(crate) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap<OwnedEventId, Pdu> {
}
#[allow(non_snake_case)]
pub(crate) fn INITIAL_EDGES() -> Vec<OwnedEventId> {
pub(super) fn INITIAL_EDGES() -> Vec<OwnedEventId> {
vec!["START", "IMC", "IMB", "IJR", "IPOWER", "IMA", "CREATE"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>()
}
pub(super) fn init_subscriber() -> tracing::dispatcher::DefaultGuard {
tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
)
}
/// Wrapper around a state map.
pub(super) struct TestStateMap(HashMap<StateEventType, HashMap<String, PduEvent>>);
impl TestStateMap {
/// Construct a `TestStateMap` from the given event map.
pub(super) fn new(events: &HashMap<OwnedEventId, PduEvent>) -> Arc<Self> {
let mut state_map: HashMap<StateEventType, HashMap<String, PduEvent>> = HashMap::new();
for event in events.values() {
let event_type = StateEventType::from(event.event_type().to_string());
state_map
.entry(event_type)
.or_default()
.insert(event.state_key().unwrap().to_owned(), event.clone());
}
Arc::new(Self(state_map))
}
/// Get the event with the given event type and state key.
pub(super) fn get(
self: &Arc<Self>,
event_type: &StateEventType,
state_key: &str,
) -> Result<PduEvent> {
self.0
.get(event_type)
.ok_or_else(|| state_not_found(event_type, state_key))?
.get(state_key)
.cloned()
.ok_or_else(|| state_not_found(event_type, state_key))
}
/// A function to get a state event from this map.
pub(super) fn fetch_state_fn(
self: &Arc<Self>,
) -> impl Fn(StateEventType, StateKey) -> Pin<Box<dyn Future<Output = Result<PduEvent>> + Send>>
{
move |event_type: StateEventType, state_key: StateKey| {
let s = self.clone();
Box::pin(async move { s.get(&event_type, state_key.as_str()) })
}
}
/// The `m.room.create` event contained in this map.
///
/// Panics if there is no `m.room.create` event in this map.
pub(super) fn room_create_event(self: &Arc<Self>) -> RoomCreateEvent<PduEvent> {
RoomCreateEvent::new(self.get(&StateEventType::RoomCreate, "").unwrap())
}
}
/// Create an `m.room.third_party_invite` event with the given sender.
pub(super) fn room_third_party_invite(sender: &UserId) -> PduEvent {
let content = json!({
"display_name": "o...@g...",
"key_validity_url": "https://identity.local/_matrix/identity/v2/pubkey/isvalid",
"public_key": "Gb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE",
"public_keys": [
{
"key_validity_url": "https://identity.local/_matrix/identity/v2/pubkey/isvalid",
"public_key": "Gb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE"
},
{
"key_validity_url": "https://identity.local/_matrix/identity/v2/pubkey/ephemeral/isvalid",
"public_key": "Kxdvv7lo0O6JVI7yimFgmYPfpLGnctcpYjuypP5zx/c"
}
]
});
to_pdu_event(
"THIRDPARTY",
sender,
TimelineEventType::RoomThirdPartyInvite,
Some("somerandomtoken"),
to_raw_json_value(&content).unwrap(),
&["CREATE", "IJR", "IPOWER"],
&["IPOWER"],
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,700 @@
//! 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/it/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/it/fixtures` to load PDUs to
/// resolve from.
/// * A list of JSON files relative to `tests/it/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/it/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/it/fixtures");
let state_sets = state_sets_paths
.iter()
.map(|x| {
from_json_str::<Vec<OwnedEventId>>(
&fs::read_to_string(fixtures_path.join(x))
.expect("should be able to read JSON file of PDUs"),
)
.expect("should be able to deserialize JSON file of PDUs")
.into_iter()
.map(|event_id| {
pdus_by_id
.get(&event_id)
.map(|pdu| {
(
(
pdu.event_type().to_string().into(),
pdu.state_key
.clone()
.expect("All PDUs must be state events"),
),
event_id,
)
})
.expect("Event IDs in JSON file must be in PDUs JSON")
})
.collect()
})
.collect::<Vec<StateMap<OwnedEventId>>>();
let mut auth_chain_sets = Vec::new();
for state_map in &state_sets {
let mut auth_chain = AuthSet::new();
for event_id in state_map.values() {
let pdu = pdus_by_id
.get(event_id)
.expect("We already confirmed all state set event ids have pdus");
auth_chain.extend(
auth_events_dfs(&pdus_by_id, pdu).expect("Auth events DFS should not fail"),
);
}
auth_chain_sets.push(auth_chain);
}
let exists = async |x| pdus_by_id.contains_key(&x);
let fetch = async |x| {
pdus_by_id
.get(&x)
.cloned()
.ok_or_else(|| err!(Request(NotFound("event not found"))))
};
let resolved_state = resolve(
&rules,
state_sets.into_iter().stream(),
auth_chain_sets.into_iter().stream(),
&fetch,
&exists,
false,
)
.await
.expect("atomic state resolution should succeed");
Snapshots {
resolved_state: reshape(&pdus_by_id, resolved_state)
.expect("should be able to reshape atomic resolved state"),
}
}
/// Perform state resolution on a batch of PDUs.
///
/// This function can be used to resolve the state of a room in a single call if
/// all PDUs are provided at once, or across multiple calls if given PDUs in
/// batches in a loop. The latter form simulates the case commonly experienced
/// by homeservers during normal operation.
///
/// # Arguments
///
/// * `rules`: The rules of the room version.
/// * `pdus`: An iterator of [`Pdu`]s to resolve, either alone or against the
/// `prev_state`.
/// * `pdus_by_id`: A map of [`OwnedEventId`]s to the [`Pdu`] with that ID.
/// * Should be empty for the first call.
/// * Should not be mutated outside of this function.
/// * `prev_state`: The state returned by a previous call to this function, if
/// any.
/// * Should be `None` for the first call.
/// * Should not be mutated outside of this function.
async fn resolve_batch<'a, I, II>(
rules: &'a RoomVersionRules,
_auth_rules: &'a AuthorizationRules,
_state_res_rules: &'a StateResolutionV2Rules,
pdus: II,
pdus_by_id: &'a mut HashMap<OwnedEventId, Pdu>,
prev_state: &'a mut Option<StateMap<OwnedEventId>>,
) -> Result<StateMap<OwnedEventId>, Box<dyn Error>>
where
I: Iterator<Item = &'a Pdu> + Send + 'a,
II: IntoIterator<IntoIter = I> + Clone + Send + 'a,
Pdu: Send + Sync + 'a,
&'a Pdu: Send + 'a,
{
let mut state_sets = prev_state
.take()
.map(|x| vec![x])
.unwrap_or_default();
for pdu in pdus.clone() {
// Insert each state event into its own StateMap because we don't know any valid
// groupings.
let mut state_map = StateMap::new();
state_map.insert(
(
pdu.event_type().to_string().into(),
pdu.state_key()
.ok_or("all PDUs should be state events")?
.into(),
),
pdu.event_id().to_owned(),
);
state_sets.push(state_map);
}
pdus_by_id.extend(
pdus.clone()
.into_iter()
.map(|pdu| (pdu.event_id().to_owned(), pdu.to_owned())),
);
let mut auth_chain_sets = Vec::new();
for pdu in pdus {
auth_chain_sets.push(auth_events_dfs(&*pdus_by_id, pdu)?);
}
let fetch = async |x| {
pdus_by_id
.get(&x)
.cloned()
.ok_or_else(|| err!(Request(NotFound("event not found"))))
};
let exists = async |x| pdus_by_id.contains_key(&x);
resolve(
rules,
state_sets.into_iter().stream(),
auth_chain_sets.into_iter().stream(),
&fetch,
&exists,
false,
)
.await
.map_err(Into::into)
}
/// Perform state resolution on a batch of PDUs iteratively, one-by-one.
///
/// This function walks the `prev_events` of each PDU forward, resolving each
/// pdu against the state(s) of it's `prev_events`, to emulate what would happen
/// in a regular room a server is participating in.
///
/// # Arguments
///
/// * `auth_rules`: The authorization rules of the room version.
/// * `state_res_rules`: The state resolution rules of the room version.
/// * `pdus`: An iterator of [`Pdu`]s to resolve, with the following
/// assumptions:
/// * `prev_events` of each PDU points to another provided state event.
///
/// # Returns
///
/// The state resolved by resolving all the leaves (PDUs which don't have any
/// other PDUs pointing to it via `prev_events`).
async fn resolve_iteratively<'a, I, II>(
rules: &'a RoomVersionRules,
_auth_rules: &'a AuthorizationRules,
_state_res_rules: &'a StateResolutionV2Rules,
pdus: II,
) -> Result<StateMap<OwnedEventId>, Box<dyn Error>>
where
I: Iterator<Item = &'a Pdu>,
II: IntoIterator<IntoIter = I> + Clone,
{
let mut forward_prev_events_graph: HashMap<OwnedEventId, Vec<_>> = HashMap::new();
let mut stack = Vec::new();
for pdu in pdus.clone() {
let mut has_prev_events = false;
for prev_event in pdu.prev_events() {
forward_prev_events_graph
.entry(prev_event.into())
.or_default()
.push(pdu.event_id().into());
has_prev_events = true;
}
if pdu.event_type() == &TimelineEventType::RoomCreate && !has_prev_events {
stack.push(pdu.event_id().to_owned());
}
}
let pdus_by_id: HashMap<OwnedEventId, Pdu> = pdus
.clone()
.into_iter()
.map(|pdu| (pdu.event_id().to_owned(), pdu.to_owned()))
.collect();
let exists = async |x| pdus_by_id.contains_key(&x);
let fetch = async |x| {
pdus_by_id
.get(&x)
.cloned()
.ok_or_else(|| err!(Request(NotFound("event not found"))))
};
let mut state_at_events: HashMap<OwnedEventId, StateMap<OwnedEventId>> = HashMap::new();
let mut leaves = Vec::new();
'outer: while let Some(event_id) = stack.pop() {
let mut states_before_event = Vec::new();
let mut auth_chain_sets = Vec::new();
let current_pdu = pdus_by_id
.get(&event_id)
.expect("every pdu should be available");
for prev_event in current_pdu.prev_events() {
let Some(state_at_event) = state_at_events.get(prev_event) else {
// State for a prev event is not known, we will come back to this event on a
// later iteration.
continue 'outer;
};
for pdu in state_at_event.values().map(|event_id| {
pdus_by_id
.get(event_id)
.expect("every pdu should be available")
}) {
auth_chain_sets.push(auth_events_dfs(&pdus_by_id, pdu)?);
}
states_before_event.push(state_at_event.clone());
}
if states_before_event.is_empty() {
// initial event, nothing to resolve
state_at_events.insert(
event_id.clone(),
StateMap::from_iter([(
(
current_pdu.event_type().to_string().into(),
current_pdu
.state_key()
.expect("all pdus are state events")
.into(),
),
event_id.clone(),
)]),
);
} else {
let state_before_event = resolve(
rules,
states_before_event.clone().into_iter().stream(),
auth_chain_sets.clone().into_iter().stream(),
&fetch,
&exists,
false,
)
.await?;
let mut proposed_state_at_event = state_before_event.clone();
proposed_state_at_event.insert(
(
current_pdu.event_type().to_string().into(),
current_pdu
.state_key()
.expect("all pdus are state events")
.into(),
),
event_id.clone(),
);
auth_chain_sets.push(auth_events_dfs(&pdus_by_id, current_pdu)?);
let state_at_event = resolve(
rules,
[state_before_event, proposed_state_at_event]
.into_iter()
.stream(),
auth_chain_sets.into_iter().stream(),
&fetch,
&exists,
false,
)
.await?;
state_at_events.insert(event_id.clone(), state_at_event);
}
if let Some(prev_events) = forward_prev_events_graph.get(&event_id) {
stack.extend(prev_events.iter().cloned());
} else {
// pdu is a leaf: no `prev_events` point to it.
leaves.push(event_id);
}
}
assert!(
state_at_events.len() == pdus_by_id.len(),
"Not all events have a state calculated! This is likely due to an event having a \
`prev_events` which points to a non-existent PDU."
);
let mut leaf_states = Vec::new();
let mut auth_chain_sets = Vec::new();
for leaf in leaves {
let state_at_event = state_at_events
.get(&leaf)
.expect("states at all events are known");
for pdu in state_at_event.values().map(|event_id| {
pdus_by_id
.get(event_id)
.expect("every pdu should be available")
}) {
auth_chain_sets.push(auth_events_dfs(&pdus_by_id, pdu)?);
}
leaf_states.push(state_at_event.clone());
}
resolve(
rules,
leaf_states.into_iter().stream(),
auth_chain_sets.into_iter().stream(),
&fetch,
&exists,
false,
)
.await
.map_err(Into::into)
}
/// Depth-first search for the `auth_events` of the given PDU.
///
/// # Errors
///
/// Fails if `pdus` does not contain a PDU that appears in the recursive
/// `auth_events` of `pdu`.
fn auth_events_dfs(
pdus_by_id: &HashMap<OwnedEventId, Pdu>,
pdu: &Pdu,
) -> Result<AuthSet<OwnedEventId>, Box<dyn Error>> {
let mut out = AuthSet::new();
let mut stack = pdu
.auth_events()
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
while let Some(event_id) = stack.pop() {
if out.contains(&event_id) {
continue;
}
out.insert(event_id.clone());
stack.extend(
pdus_by_id
.get(&event_id)
.ok_or_else(|| format!("missing required PDU: {event_id}"))?
.auth_events()
.map(ToOwned::to_owned),
);
}
Ok(out)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,78 @@
---
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
}
}
}
]

Some files were not shown because too many files have changed in this diff Show More