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:
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[env]
|
||||||
|
RUMA_UNSTABLE_EXHAUSTIVE_TYPES = "true"
|
||||||
91
Cargo.lock
generated
91
Cargo.lock
generated
@@ -720,6 +720,18 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"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]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
@@ -1143,6 +1155,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encode_unicode"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enum-as-inner"
|
name = "enum-as-inner"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@@ -1989,6 +2007,18 @@ version = "0.1.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
|
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]]
|
[[package]]
|
||||||
name = "interpolate_name"
|
name = "interpolate_name"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -3439,8 +3469,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma"
|
name = "ruma"
|
||||||
version = "0.10.1"
|
version = "0.12.6"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assign",
|
"assign",
|
||||||
"js_int",
|
"js_int",
|
||||||
@@ -3451,7 +3481,6 @@ dependencies = [
|
|||||||
"ruma-events",
|
"ruma-events",
|
||||||
"ruma-federation-api",
|
"ruma-federation-api",
|
||||||
"ruma-identifiers-validation",
|
"ruma-identifiers-validation",
|
||||||
"ruma-identity-service-api",
|
|
||||||
"ruma-push-gateway-api",
|
"ruma-push-gateway-api",
|
||||||
"ruma-signatures",
|
"ruma-signatures",
|
||||||
"web-time",
|
"web-time",
|
||||||
@@ -3459,8 +3488,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-appservice-api"
|
name = "ruma-appservice-api"
|
||||||
version = "0.10.0"
|
version = "0.12.2"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
@@ -3471,8 +3500,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-client-api"
|
name = "ruma-client-api"
|
||||||
version = "0.18.0"
|
version = "0.20.4"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"assign",
|
"assign",
|
||||||
@@ -3494,8 +3523,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-common"
|
name = "ruma-common"
|
||||||
version = "0.13.0"
|
version = "0.15.4"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -3522,12 +3551,13 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
"web-time",
|
"web-time",
|
||||||
"wildmatch",
|
"wildmatch",
|
||||||
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-events"
|
name = "ruma-events"
|
||||||
version = "0.28.1"
|
version = "0.30.5"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
@@ -3547,12 +3577,13 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
"web-time",
|
"web-time",
|
||||||
"wildmatch",
|
"wildmatch",
|
||||||
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-federation-api"
|
name = "ruma-federation-api"
|
||||||
version = "0.9.0"
|
version = "0.11.2"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"headers",
|
"headers",
|
||||||
@@ -3573,27 +3604,17 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-identifiers-validation"
|
name = "ruma-identifiers-validation"
|
||||||
version = "0.9.5"
|
version = "0.10.1"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"thiserror 2.0.12",
|
"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]]
|
[[package]]
|
||||||
name = "ruma-macros"
|
name = "ruma-macros"
|
||||||
version = "0.13.0"
|
version = "0.15.2"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
@@ -3607,8 +3628,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-push-gateway-api"
|
name = "ruma-push-gateway-api"
|
||||||
version = "0.9.0"
|
version = "0.11.0"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
@@ -3619,8 +3640,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-signatures"
|
name = "ruma-signatures"
|
||||||
version = "0.15.0"
|
version = "0.17.1"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
@@ -4206,6 +4227,12 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "similar"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simple_asn1"
|
name = "simple_asn1"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
@@ -4977,6 +5004,7 @@ dependencies = [
|
|||||||
"hardened_malloc-rs",
|
"hardened_malloc-rs",
|
||||||
"http",
|
"http",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
|
"insta",
|
||||||
"ipaddress",
|
"ipaddress",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
@@ -4996,6 +5024,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_regex",
|
"serde_regex",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
|
"similar",
|
||||||
"smallstr",
|
"smallstr",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
|
|||||||
21
Cargo.toml
21
Cargo.toml
@@ -219,6 +219,12 @@ features = [
|
|||||||
"webp",
|
"webp",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[workspace.dependencies.insta]
|
||||||
|
version = "1.43.1"
|
||||||
|
features = [
|
||||||
|
"json",
|
||||||
|
]
|
||||||
|
|
||||||
[workspace.dependencies.ipaddress]
|
[workspace.dependencies.ipaddress]
|
||||||
version = "0.1"
|
version = "0.1"
|
||||||
|
|
||||||
@@ -311,20 +317,18 @@ default-features = false
|
|||||||
|
|
||||||
[workspace.dependencies.ruma]
|
[workspace.dependencies.ruma]
|
||||||
git = "https://github.com/matrix-construct/ruma"
|
git = "https://github.com/matrix-construct/ruma"
|
||||||
rev = "0155c2b33233bec9dece79d5134a9574b347f4c1"
|
rev = "3d3acddfcf96891f1203ca3c36d8f41932ede50f"
|
||||||
features = [
|
features = [
|
||||||
"compat",
|
"__compat",
|
||||||
"rand",
|
"rand",
|
||||||
"appservice-api-c",
|
"appservice-api-c",
|
||||||
"client-api",
|
"client-api",
|
||||||
"federation-api",
|
"federation-api",
|
||||||
"markdown",
|
"markdown",
|
||||||
"push-gateway-api-c",
|
"push-gateway-api-c",
|
||||||
"unstable-exhaustive-types",
|
|
||||||
"ring-compat",
|
"ring-compat",
|
||||||
"compat-upload-signatures",
|
"compat-upload-signatures",
|
||||||
"identifiers-validation",
|
"identifiers-validation",
|
||||||
"unstable-unspecified",
|
|
||||||
"unstable-msc2448",
|
"unstable-msc2448",
|
||||||
"unstable-msc2666",
|
"unstable-msc2666",
|
||||||
"unstable-msc2867",
|
"unstable-msc2867",
|
||||||
@@ -332,10 +336,8 @@ features = [
|
|||||||
"unstable-msc3026",
|
"unstable-msc3026",
|
||||||
"unstable-msc3061",
|
"unstable-msc3061",
|
||||||
"unstable-msc3245",
|
"unstable-msc3245",
|
||||||
"unstable-msc3266",
|
|
||||||
"unstable-msc3381", # polls
|
"unstable-msc3381", # polls
|
||||||
"unstable-msc3489", # beacon / live location
|
"unstable-msc3489", # beacon / live location
|
||||||
"unstable-msc3575",
|
|
||||||
"unstable-msc3930", # polls push rules
|
"unstable-msc3930", # polls push rules
|
||||||
"unstable-msc4075",
|
"unstable-msc4075",
|
||||||
"unstable-msc4095",
|
"unstable-msc4095",
|
||||||
@@ -343,9 +345,9 @@ features = [
|
|||||||
"unstable-msc4125",
|
"unstable-msc4125",
|
||||||
"unstable-msc4186",
|
"unstable-msc4186",
|
||||||
"unstable-msc4203", # sending to-device events to appservices
|
"unstable-msc4203", # sending to-device events to appservices
|
||||||
"unstable-msc4210", # remove legacy mentions
|
"unstable-msc4311",
|
||||||
"unstable-extensible-events",
|
"unstable-extensible-events",
|
||||||
"unstable-pdu",
|
"unstable-hydra",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies.rustls]
|
[workspace.dependencies.rustls]
|
||||||
@@ -425,6 +427,9 @@ default-features = false
|
|||||||
version = "0.10"
|
version = "0.10"
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
|
[workspace.dependencies.similar]
|
||||||
|
version = "2.7.0"
|
||||||
|
|
||||||
[workspace.dependencies.smallstr]
|
[workspace.dependencies.smallstr]
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
features = [
|
features = [
|
||||||
|
|||||||
@@ -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 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) {
|
match serde_json::from_str(&string) {
|
||||||
| Err(e) => return Err!("Invalid json in command body: {e}"),
|
| 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:?}"),
|
| Err(e) => return Err!("Could not parse PDU JSON: {e:?}"),
|
||||||
| Ok(hash) => {
|
| Ok(hash) => {
|
||||||
let event_id = OwnedEventId::parse(format!("${hash}"));
|
let event_id = OwnedEventId::parse(format!("${hash}"));
|
||||||
@@ -252,7 +255,6 @@ pub(super) async fn get_remote_pdu(
|
|||||||
.sending
|
.sending
|
||||||
.send_federation_request(&server, ruma::api::federation::event::get_event::v1::Request {
|
.send_federation_request(&server, ruma::api::federation::event::get_event::v1::Request {
|
||||||
event_id: event_id.clone(),
|
event_id: event_id.clone(),
|
||||||
include_unredacted_content: None,
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
|
|||||||
self.services
|
self.services
|
||||||
.admin
|
.admin
|
||||||
.make_user_admin(&user_id)
|
.make_user_admin(&user_id)
|
||||||
|
.boxed()
|
||||||
.await?;
|
.await?;
|
||||||
warn!("Granting {user_id} admin privileges as the first user");
|
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)
|
.lock(&room_id)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let room_power_levels: Option<RoomPowerLevelsEventContent> = self
|
let room_power_levels: Option<RoomPowerLevels> = self
|
||||||
.services
|
.services
|
||||||
.rooms
|
.rooms
|
||||||
.state_accessor
|
.state_accessor
|
||||||
.room_state_get_content(&room_id, &StateEventType::RoomPowerLevels, "")
|
.get_power_levels(&room_id)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
let user_can_demote_self = room_power_levels
|
let user_can_change_self = room_power_levels
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|power_levels_content| {
|
.is_some_and(|power_levels| {
|
||||||
RoomPowerLevels::from(power_levels_content.clone())
|
power_levels.user_can_change_user_power_level(&user_id, &user_id)
|
||||||
.user_can_change_user_power_level(&user_id, &user_id)
|
});
|
||||||
}) || self
|
|
||||||
.services
|
let user_can_demote_self = user_can_change_self
|
||||||
.rooms
|
|| self
|
||||||
.state_accessor
|
.services
|
||||||
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
|
.rooms
|
||||||
.await
|
.state_accessor
|
||||||
.is_ok_and(|event| event.sender() == user_id);
|
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
|
||||||
|
.await
|
||||||
|
.is_ok_and(|event| event.sender() == user_id);
|
||||||
|
|
||||||
if !user_can_demote_self {
|
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);
|
power_levels_content.users.remove(&user_id);
|
||||||
|
|
||||||
let event_id = self
|
let event_id = self
|
||||||
@@ -783,6 +790,7 @@ pub(super) async fn make_user_admin(&self, user_id: String) -> Result {
|
|||||||
self.services
|
self.services
|
||||||
.admin
|
.admin
|
||||||
.make_user_admin(&user_id)
|
.make_user_admin(&user_id)
|
||||||
|
.boxed()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.write_str(&format!("{user_id} has been granted admin privileges.",))
|
self.write_str(&format!("{user_id} has been granted admin privileges.",))
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||||
},
|
},
|
||||||
events::{
|
events::{StateEventType, room::power_levels::RoomPowerLevelsEventContent},
|
||||||
StateEventType,
|
|
||||||
room::power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use tuwunel_core::{
|
use tuwunel_core::{
|
||||||
Err, Error, Result, err, info,
|
Err, Error, Result, err, info,
|
||||||
@@ -60,10 +57,7 @@ pub(crate) async fn change_password_route(
|
|||||||
|
|
||||||
let mut uiaainfo = UiaaInfo {
|
let mut uiaainfo = UiaaInfo {
|
||||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||||
completed: Vec::new(),
|
..Default::default()
|
||||||
params: Box::default(),
|
|
||||||
session: None,
|
|
||||||
auth_error: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match &body.auth {
|
match &body.auth {
|
||||||
@@ -189,10 +183,7 @@ pub(crate) async fn deactivate_route(
|
|||||||
|
|
||||||
let mut uiaainfo = UiaaInfo {
|
let mut uiaainfo = UiaaInfo {
|
||||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||||
completed: Vec::new(),
|
..Default::default()
|
||||||
params: Box::default(),
|
|
||||||
session: None,
|
|
||||||
auth_error: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match &body.auth {
|
match &body.auth {
|
||||||
@@ -331,21 +322,18 @@ pub async fn full_user_deactivate(
|
|||||||
let room_power_levels = services
|
let room_power_levels = services
|
||||||
.rooms
|
.rooms
|
||||||
.state_accessor
|
.state_accessor
|
||||||
.room_state_get_content::<RoomPowerLevelsEventContent>(
|
.get_power_levels(room_id)
|
||||||
room_id,
|
|
||||||
&StateEventType::RoomPowerLevels,
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
let user_can_demote_self =
|
let user_can_change_self = room_power_levels
|
||||||
room_power_levels
|
.as_ref()
|
||||||
.as_ref()
|
.is_some_and(|power_levels| {
|
||||||
.is_some_and(|power_levels_content| {
|
power_levels.user_can_change_user_power_level(user_id, user_id)
|
||||||
RoomPowerLevels::from(power_levels_content.clone())
|
});
|
||||||
.user_can_change_user_power_level(user_id, user_id)
|
|
||||||
}) || services
|
let user_can_demote_self = user_can_change_self
|
||||||
|
|| services
|
||||||
.rooms
|
.rooms
|
||||||
.state_accessor
|
.state_accessor
|
||||||
.room_state_get(room_id, &StateEventType::RoomCreate, "")
|
.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);
|
.is_ok_and(|event| event.sender() == user_id);
|
||||||
|
|
||||||
if user_can_demote_self {
|
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);
|
power_levels_content.users.remove(user_id);
|
||||||
|
|
||||||
// ignore errors so deactivation doesn't fail
|
// ignore errors so deactivation doesn't fail
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ use std::collections::BTreeMap;
|
|||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
RoomVersionId,
|
RoomVersionId,
|
||||||
api::client::discovery::get_capabilities::{
|
api::client::discovery::{
|
||||||
self, Capabilities, GetLoginTokenCapability, RoomVersionStability,
|
get_capabilities,
|
||||||
RoomVersionsCapability, ThirdPartyIdChangesCapability,
|
get_capabilities::v3::{
|
||||||
|
Capabilities, GetLoginTokenCapability, RoomVersionStability, RoomVersionsCapability,
|
||||||
|
ThirdPartyIdChangesCapability,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|||||||
@@ -145,10 +145,7 @@ pub(crate) async fn delete_device_route(
|
|||||||
// UIAA
|
// UIAA
|
||||||
let mut uiaainfo = UiaaInfo {
|
let mut uiaainfo = UiaaInfo {
|
||||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||||
completed: Vec::new(),
|
..Default::default()
|
||||||
params: Box::default(),
|
|
||||||
session: None,
|
|
||||||
auth_error: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match &body.auth {
|
match &body.auth {
|
||||||
@@ -224,10 +221,7 @@ pub(crate) async fn delete_devices_route(
|
|||||||
// UIAA
|
// UIAA
|
||||||
let mut uiaainfo = UiaaInfo {
|
let mut uiaainfo = UiaaInfo {
|
||||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||||
completed: Vec::new(),
|
..Default::default()
|
||||||
params: Box::default(),
|
|
||||||
session: None,
|
|
||||||
auth_error: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match &body.auth {
|
match &body.auth {
|
||||||
|
|||||||
@@ -16,18 +16,15 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
federation,
|
federation,
|
||||||
},
|
},
|
||||||
directory::{Filter, PublicRoomJoinRule, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
|
directory::{Filter, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
|
||||||
events::{
|
events::{
|
||||||
StateEventType,
|
StateEventType,
|
||||||
room::{
|
room::join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
|
||||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
uint,
|
uint,
|
||||||
};
|
};
|
||||||
use tuwunel_core::{
|
use tuwunel_core::{
|
||||||
Err, Result, err, info,
|
Err, Result, err, info, is_true,
|
||||||
matrix::Event,
|
matrix::Event,
|
||||||
utils::{
|
utils::{
|
||||||
TryFutureExtExt,
|
TryFutureExtExt,
|
||||||
@@ -51,7 +48,7 @@ pub(crate) async fn get_public_rooms_filtered_route(
|
|||||||
InsecureClientIp(client): InsecureClientIp,
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
body: Ruma<get_public_rooms_filtered::v3::Request>,
|
body: Ruma<get_public_rooms_filtered::v3::Request>,
|
||||||
) -> Result<get_public_rooms_filtered::v3::Response> {
|
) -> 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(
|
let response = get_public_rooms_filtered_helper(
|
||||||
&services,
|
&services,
|
||||||
@@ -80,7 +77,7 @@ pub(crate) async fn get_public_rooms_route(
|
|||||||
InsecureClientIp(client): InsecureClientIp,
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
body: Ruma<get_public_rooms::v3::Request>,
|
body: Ruma<get_public_rooms::v3::Request>,
|
||||||
) -> Result<get_public_rooms::v3::Response> {
|
) -> 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(
|
let response = get_public_rooms_filtered_helper(
|
||||||
&services,
|
&services,
|
||||||
@@ -393,15 +390,11 @@ async fn user_can_publish_room(
|
|||||||
match services
|
match services
|
||||||
.rooms
|
.rooms
|
||||||
.state_accessor
|
.state_accessor
|
||||||
.room_state_get(room_id, &StateEventType::RoomPowerLevels, "")
|
.get_power_levels(room_id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
| Ok(event) => serde_json::from_str(event.content().get())
|
| Ok(power_levels) =>
|
||||||
.map_err(|_| err!(Database("Invalid event content for m.room.power_levels")))
|
Ok(power_levels.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)),
|
||||||
.map(|content: RoomPowerLevelsEventContent| {
|
|
||||||
RoomPowerLevels::from(content)
|
|
||||||
.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)
|
|
||||||
}),
|
|
||||||
| _ => {
|
| _ => {
|
||||||
match services
|
match services
|
||||||
.rooms
|
.rooms
|
||||||
@@ -453,7 +446,7 @@ async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> Public
|
|||||||
.state_accessor
|
.state_accessor
|
||||||
.room_state_get_content(&room_id, &StateEventType::RoomJoinRules, "")
|
.room_state_get_content(&room_id, &StateEventType::RoomJoinRules, "")
|
||||||
.map_ok(|c: RoomJoinRulesEventContent| match c.join_rule {
|
.map_ok(|c: RoomJoinRulesEventContent| match c.join_rule {
|
||||||
| JoinRule::Public => PublicRoomJoinRule::Public,
|
| JoinRule::Public => "public".into(),
|
||||||
| JoinRule::Knock => "knock".into(),
|
| JoinRule::Knock => "knock".into(),
|
||||||
| JoinRule::KnockRestricted(_) => "knock_restricted".into(),
|
| JoinRule::KnockRestricted(_) => "knock_restricted".into(),
|
||||||
| _ => "invite".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 {
|
let Some(server) = server else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let forbidden_remote_directory = services
|
let conditions = [
|
||||||
.config
|
services
|
||||||
.forbidden_remote_room_directory_server_names
|
.config
|
||||||
.is_match(server.host());
|
.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
|
if conditions.iter().any(is_true!()) {
|
||||||
.config
|
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
||||||
.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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use std::collections::{BTreeMap, HashMap, HashSet};
|
|||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use futures::{StreamExt, stream::FuturesUnordered};
|
use futures::{StreamExt, stream::FuturesUnordered};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
|
CanonicalJsonObject, CanonicalJsonValue, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId,
|
||||||
|
UserId,
|
||||||
api::{
|
api::{
|
||||||
client::{
|
client::{
|
||||||
error::ErrorKind,
|
error::ErrorKind,
|
||||||
@@ -162,10 +163,7 @@ pub(crate) async fn upload_signing_keys_route(
|
|||||||
// UIAA
|
// UIAA
|
||||||
let mut uiaainfo = UiaaInfo {
|
let mut uiaainfo = UiaaInfo {
|
||||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||||
completed: Vec::new(),
|
..Default::default()
|
||||||
params: Box::default(),
|
|
||||||
session: None,
|
|
||||||
auth_error: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match check_for_new_keys(
|
match check_for_new_keys(
|
||||||
@@ -599,18 +597,19 @@ fn add_unsigned_device_display_name(
|
|||||||
include_display_names: bool,
|
include_display_names: bool,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
if let Some(display_name) = metadata.display_name {
|
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
|
let unsigned = object
|
||||||
.entry("unsigned")
|
.entry("unsigned".into())
|
||||||
.or_insert_with(|| json!({}));
|
.or_insert_with(CanonicalJsonValue::default);
|
||||||
if let serde_json::Value::Object(unsigned_object) = unsigned {
|
|
||||||
|
if let CanonicalJsonValue::Object(unsigned_object) = unsigned {
|
||||||
if include_display_names {
|
if include_display_names {
|
||||||
unsigned_object.insert("device_display_name".to_owned(), display_name.into());
|
unsigned_object.insert("device_display_name".to_owned(), display_name.into());
|
||||||
} else {
|
} else {
|
||||||
unsigned_object.insert(
|
unsigned_object.insert(
|
||||||
"device_display_name".to_owned(),
|
"device_display_name".to_owned(),
|
||||||
Some(metadata.device_id.as_str().to_owned()).into(),
|
CanonicalJsonValue::String(metadata.device_id.as_str().to_owned()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ use ruma::{
|
|||||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||||
};
|
};
|
||||||
use tuwunel_core::{
|
use tuwunel_core::{
|
||||||
Err, Result, debug_error, err, info,
|
Err, Result, err,
|
||||||
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
|
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
|
||||||
|
warn,
|
||||||
};
|
};
|
||||||
use tuwunel_service::Services;
|
use tuwunel_service::Services;
|
||||||
|
|
||||||
@@ -27,8 +28,8 @@ pub(crate) async fn invite_user_route(
|
|||||||
let sender_user = body.sender_user();
|
let sender_user = body.sender_user();
|
||||||
|
|
||||||
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
|
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
|
||||||
debug_error!(
|
warn!(
|
||||||
"User {sender_user} is not an admin and attempted to send an invite to room {}",
|
"{sender_user} is not an admin and attempted to send an invite to {}",
|
||||||
&body.room_id
|
&body.room_id
|
||||||
);
|
);
|
||||||
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
|
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
|
||||||
@@ -104,10 +105,7 @@ pub(crate) async fn invite_helper(
|
|||||||
is_direct: bool,
|
is_direct: bool,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
|
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
|
||||||
info!(
|
warn!("{sender_user} is not an admin and attempted to send an invite to {room_id}");
|
||||||
"User {sender_user} is not an admin and attempted to send an invite to room \
|
|
||||||
{room_id}"
|
|
||||||
);
|
|
||||||
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
|
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +154,10 @@ pub(crate) async fn invite_helper(
|
|||||||
.sending
|
.sending
|
||||||
.convert_to_outgoing_federation_event(pdu_json.clone())
|
.convert_to_outgoing_federation_event(pdu_json.clone())
|
||||||
.await,
|
.await,
|
||||||
invite_room_state,
|
invite_room_state: invite_room_state
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect(),
|
||||||
via: services
|
via: services
|
||||||
.rooms
|
.rooms
|
||||||
.state_cache
|
.state_cache
|
||||||
|
|||||||
@@ -25,10 +25,9 @@ use ruma::{
|
|||||||
use tuwunel_core::{
|
use tuwunel_core::{
|
||||||
Err, Result, debug, debug_info, debug_warn, err, error, info,
|
Err, Result, debug, debug_info, debug_warn, err, error, info,
|
||||||
matrix::{
|
matrix::{
|
||||||
StateKey,
|
|
||||||
event::{gen_event_id, gen_event_id_canonical_json},
|
event::{gen_event_id, gen_event_id_canonical_json},
|
||||||
pdu::{PduBuilder, PduEvent},
|
pdu::{PduBuilder, PduEvent},
|
||||||
state_res,
|
room_version, state_res,
|
||||||
},
|
},
|
||||||
result::FlatOk,
|
result::FlatOk,
|
||||||
trace,
|
trace,
|
||||||
@@ -551,7 +550,13 @@ async fn join_room_by_id_helper_remote(
|
|||||||
})
|
})
|
||||||
.ready_filter_map(Result::ok)
|
.ready_filter_map(Result::ok)
|
||||||
.fold(HashMap::new(), async |mut state, (event_id, value)| {
|
.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,
|
| Ok(pdu) => pdu,
|
||||||
| Err(e) => {
|
| Err(e) => {
|
||||||
debug_warn!("Invalid PDU in send_join response: {e:?}: {value:#?}");
|
debug_warn!("Invalid PDU in send_join response: {e:?}: {value:#?}");
|
||||||
@@ -604,36 +609,26 @@ async fn join_room_by_id_helper_remote(
|
|||||||
drop(cork);
|
drop(cork);
|
||||||
|
|
||||||
debug!("Running send_join auth check");
|
debug!("Running send_join auth check");
|
||||||
let fetch_state = &state;
|
state_res::auth_check(
|
||||||
let state_fetch = |k: StateEventType, s: StateKey| async move {
|
&room_version::rules(&room_version_id)?,
|
||||||
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)?,
|
|
||||||
&parsed_join_pdu,
|
&parsed_join_pdu,
|
||||||
None, // TODO: third party invite
|
&async |event_id| services.rooms.timeline.get_pdu(&event_id).await,
|
||||||
|k, s| state_fetch(k.clone(), s.into()),
|
&async |event_type, state_key| {
|
||||||
)
|
let shortstatekey = services
|
||||||
.await
|
.rooms
|
||||||
.map_err(|e| err!(Request(Forbidden(warn!("Auth check failed: {e:?}")))))?;
|
.short
|
||||||
|
.get_shortstatekey(&event_type, state_key.as_str())
|
||||||
|
.await?;
|
||||||
|
|
||||||
if !auth_check {
|
let event_id = state.get(&shortstatekey).ok_or_else(|| {
|
||||||
return Err!(Request(Forbidden("Auth check failed")));
|
err!(Request(NotFound("Missing fetch_state {shortstatekey:?}")))
|
||||||
}
|
})?;
|
||||||
|
|
||||||
|
services.rooms.timeline.get_pdu(event_id).await
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
.await?;
|
||||||
|
|
||||||
info!("Compressing state from send_join");
|
info!("Compressing state from send_join");
|
||||||
let compressed: CompressedState = services
|
let compressed: CompressedState = services
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ use ruma::{
|
|||||||
RoomVersionId, UserId,
|
RoomVersionId, UserId,
|
||||||
api::{
|
api::{
|
||||||
client::knock::knock_room,
|
client::knock::knock_room,
|
||||||
federation::{self},
|
federation::{
|
||||||
|
membership::RawStrippedState,
|
||||||
|
{self},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
canonical_json::to_canonical_value,
|
canonical_json::to_canonical_value,
|
||||||
events::{
|
events::{
|
||||||
@@ -17,7 +20,7 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use tuwunel_core::{
|
use tuwunel_core::{
|
||||||
Err, Result, debug, debug_info, debug_warn, err, info,
|
Err, Result, debug, debug_info, debug_warn, err, extract_variant, info,
|
||||||
matrix::{
|
matrix::{
|
||||||
event::{Event, gen_event_id},
|
event::{Event, gen_event_id},
|
||||||
pdu::{PduBuilder, PduEvent},
|
pdu::{PduBuilder, PduEvent},
|
||||||
@@ -346,7 +349,7 @@ async fn knock_room_helper_local(
|
|||||||
let knock_event = knock_event_stub;
|
let knock_event = knock_event_stub;
|
||||||
|
|
||||||
info!("Asking {remote_server} for send_knock in room {room_id}");
|
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(),
|
room_id: room_id.to_owned(),
|
||||||
event_id: event_id.clone(),
|
event_id: event_id.clone(),
|
||||||
pdu: services
|
pdu: services
|
||||||
@@ -384,7 +387,13 @@ async fn knock_room_helper_local(
|
|||||||
.get_content::<RoomMemberEventContent>()
|
.get_content::<RoomMemberEventContent>()
|
||||||
.expect("we just created this"),
|
.expect("we just created this"),
|
||||||
sender_user,
|
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,
|
None,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -477,7 +486,7 @@ async fn knock_room_helper_remote(
|
|||||||
let knock_event = knock_event_stub;
|
let knock_event = knock_event_stub;
|
||||||
|
|
||||||
info!("Asking {remote_server} for send_knock in room {room_id}");
|
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(),
|
room_id: room_id.to_owned(),
|
||||||
event_id: event_id.clone(),
|
event_id: event_id.clone(),
|
||||||
pdu: services
|
pdu: services
|
||||||
@@ -507,7 +516,14 @@ async fn knock_room_helper_remote(
|
|||||||
let state = send_knock_response
|
let state = send_knock_response
|
||||||
.knock_room_state
|
.knock_room_state
|
||||||
.iter()
|
.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);
|
.filter_map(Result::ok);
|
||||||
|
|
||||||
let mut state_map: HashMap<u64, OwnedEventId> = HashMap::new();
|
let mut state_map: HashMap<u64, OwnedEventId> = HashMap::new();
|
||||||
@@ -594,7 +610,13 @@ async fn knock_room_helper_remote(
|
|||||||
.get_content::<RoomMemberEventContent>()
|
.get_content::<RoomMemberEventContent>()
|
||||||
.expect("we just created this"),
|
.expect("we just created this"),
|
||||||
sender_user,
|
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,
|
None,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -628,7 +650,7 @@ async fn make_knock_request(
|
|||||||
sender_user: &UserId,
|
sender_user: &UserId,
|
||||||
room_id: &RoomId,
|
room_id: &RoomId,
|
||||||
servers: &[OwnedServerName],
|
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 =
|
let mut make_knock_response_and_server =
|
||||||
Err!(BadServerResponse("No server available to assist in knocking."));
|
Err!(BadServerResponse("No server available to assist in knocking."));
|
||||||
|
|
||||||
@@ -645,7 +667,7 @@ async fn make_knock_request(
|
|||||||
.sending
|
.sending
|
||||||
.send_federation_request(
|
.send_federation_request(
|
||||||
remote_server,
|
remote_server,
|
||||||
federation::knock::create_knock_event_template::v1::Request {
|
federation::membership::prepare_knock_event::v1::Request {
|
||||||
room_id: room_id.to_owned(),
|
room_id: room_id.to_owned(),
|
||||||
user_id: sender_user.to_owned(),
|
user_id: sender_user.to_owned(),
|
||||||
ver: services
|
ver: services
|
||||||
|
|||||||
@@ -70,15 +70,16 @@ pub(crate) async fn banned_room_check(
|
|||||||
|
|
||||||
if let Some(room_id) = room_id {
|
if let Some(room_id) = room_id {
|
||||||
if services.rooms.metadata.is_banned(room_id).await
|
if services.rooms.metadata.is_banned(room_id).await
|
||||||
|| services
|
|| (room_id.server_name().is_some()
|
||||||
.config
|
&& services
|
||||||
.forbidden_remote_server_names
|
.config
|
||||||
.is_match(
|
.forbidden_remote_server_names
|
||||||
room_id
|
.is_match(
|
||||||
.server_name()
|
room_id
|
||||||
.expect("legacy room mxid")
|
.server_name()
|
||||||
.host(),
|
.expect("legacy room mxid")
|
||||||
) {
|
.host(),
|
||||||
|
)) {
|
||||||
warn!(
|
warn!(
|
||||||
"User {user_id} who is not an admin attempted to send an invite for or \
|
"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}"
|
attempted to join a banned room or banned room server name: {room_id}"
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ pub(super) use message::*;
|
|||||||
pub(super) use openid::*;
|
pub(super) use openid::*;
|
||||||
pub(super) use presence::*;
|
pub(super) use presence::*;
|
||||||
pub(super) use profile::*;
|
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 push::*;
|
||||||
pub(super) use read_marker::*;
|
pub(super) use read_marker::*;
|
||||||
pub(super) use redact::*;
|
pub(super) use redact::*;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
|
|||||||
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use futures::{
|
use futures::{
|
||||||
StreamExt, TryStreamExt,
|
FutureExt, StreamExt, TryStreamExt,
|
||||||
future::{join, join3, join4},
|
future::{join, join3, join4},
|
||||||
};
|
};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
@@ -368,7 +368,9 @@ pub async fn update_displayname(
|
|||||||
.collect()
|
.collect()
|
||||||
.await;
|
.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(
|
pub async fn update_avatar_url(
|
||||||
@@ -421,10 +423,12 @@ pub async fn update_avatar_url(
|
|||||||
.collect()
|
.collect()
|
||||||
.await;
|
.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,
|
services: &Services,
|
||||||
all_joined_rooms: Vec<(PduBuilder, &OwnedRoomId)>,
|
all_joined_rooms: Vec<(PduBuilder, &OwnedRoomId)>,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ pub(crate) async fn register_route(
|
|||||||
stages: vec![AuthType::RegistrationToken],
|
stages: vec![AuthType::RegistrationToken],
|
||||||
}],
|
}],
|
||||||
completed: Vec::new(),
|
completed: Vec::new(),
|
||||||
params: Box::default(),
|
params: Default::default(),
|
||||||
session: None,
|
session: None,
|
||||||
auth_error: None,
|
auth_error: None,
|
||||||
};
|
};
|
||||||
@@ -326,7 +326,7 @@ pub(crate) async fn register_route(
|
|||||||
uiaainfo = UiaaInfo {
|
uiaainfo = UiaaInfo {
|
||||||
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
|
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
|
||||||
completed: Vec::new(),
|
completed: Vec::new(),
|
||||||
params: Box::default(),
|
params: Default::default(),
|
||||||
session: None,
|
session: None,
|
||||||
auth_error: None,
|
auth_error: None,
|
||||||
};
|
};
|
||||||
@@ -523,7 +523,11 @@ pub(crate) async fn register_route(
|
|||||||
.await
|
.await
|
||||||
.is_ok_and(is_equal_to!(1))
|
.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");
|
warn!("Granting {user_id} admin privileges as the first user");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ use tuwunel_service::Services;
|
|||||||
|
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
|
|
||||||
|
const REASON_MAX_LEN: usize = 750;
|
||||||
|
|
||||||
/// # `POST /_matrix/client/v3/rooms/{roomId}/report`
|
/// # `POST /_matrix/client/v3/rooms/{roomId}/report`
|
||||||
///
|
///
|
||||||
/// Reports an abusive room to homeserver admins
|
/// Reports an abusive room to homeserver admins
|
||||||
@@ -29,18 +31,13 @@ pub(crate) async fn report_room_route(
|
|||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Received room report by user {sender_user} for room {} with reason: \"{}\"",
|
"Received room report by user {sender_user} for room {} with reason: \"{}\"",
|
||||||
body.room_id,
|
body.room_id, body.reason,
|
||||||
body.reason.as_deref().unwrap_or("")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if body
|
if body.reason.len().gt(&REASON_MAX_LEN) {
|
||||||
.reason
|
return Err!(Request(InvalidParam(
|
||||||
.as_ref()
|
"Reason too long, should be {REASON_MAX_LEN} characters or fewer"
|
||||||
.is_some_and(|s| s.len() > 750)
|
)));
|
||||||
{
|
|
||||||
return Err!(Request(
|
|
||||||
InvalidParam("Reason too long, should be 750 characters or fewer",)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delay_response().await;
|
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: {}",
|
"@room Room report received from {} -\n\nRoom ID: {}\n\nReport Reason: {}",
|
||||||
sender_user.to_owned(),
|
sender_user.to_owned(),
|
||||||
body.room_id,
|
body.room_id,
|
||||||
body.reason.as_deref().unwrap_or("")
|
body.reason,
|
||||||
)))
|
)))
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use futures::FutureExt;
|
use futures::{FutureExt, future::OptionFuture};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId,
|
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::{
|
events::{
|
||||||
TimelineEventType,
|
TimelineEventType,
|
||||||
room::{
|
room::{
|
||||||
@@ -16,19 +19,21 @@ use ruma::{
|
|||||||
member::{MembershipState, RoomMemberEventContent},
|
member::{MembershipState, RoomMemberEventContent},
|
||||||
name::RoomNameEventContent,
|
name::RoomNameEventContent,
|
||||||
power_levels::RoomPowerLevelsEventContent,
|
power_levels::RoomPowerLevelsEventContent,
|
||||||
topic::RoomTopicEventContent,
|
topic::{RoomTopicEventContent, TopicContentBlock},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
int,
|
int,
|
||||||
|
room_version_rules::{RoomIdFormatVersion, RoomVersionRules},
|
||||||
serde::{JsonObject, Raw},
|
serde::{JsonObject, Raw},
|
||||||
};
|
};
|
||||||
use serde_json::{json, value::to_raw_value};
|
use serde_json::{json, value::to_raw_value};
|
||||||
use tuwunel_core::{
|
use tuwunel_core::{
|
||||||
Err, Result, debug_info, debug_warn, err, info,
|
Err, Result, debug_info, debug_warn, err, info,
|
||||||
matrix::{StateKey, pdu::PduBuilder},
|
matrix::{StateKey, pdu::PduBuilder, room_version},
|
||||||
|
utils::BoolExt,
|
||||||
warn,
|
warn,
|
||||||
};
|
};
|
||||||
use tuwunel_service::{Services, appservice::RegistrationInfo};
|
use tuwunel_service::{Services, appservice::RegistrationInfo, rooms::state::RoomMutexGuard};
|
||||||
|
|
||||||
use crate::{Ruma, client::invite_helper};
|
use crate::{Ruma, client::invite_helper};
|
||||||
|
|
||||||
@@ -53,164 +58,61 @@ pub(crate) async fn create_room_route(
|
|||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
body: Ruma<create_room::v3::Request>,
|
body: Ruma<create_room::v3::Request>,
|
||||||
) -> Result<create_room::v3::Response> {
|
) -> 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()
|
let alias: OptionFuture<_> = body
|
||||||
&& body.appservice_info.is_none()
|
.room_alias_name
|
||||||
&& !services.users.is_admin(sender_user).await
|
.as_ref()
|
||||||
{
|
.map(|alias| room_alias_check(&services, alias, body.appservice_info.as_ref()))
|
||||||
return Err!(Request(Forbidden("Room creation has been disabled.",)));
|
.into();
|
||||||
}
|
|
||||||
|
|
||||||
let room_id: OwnedRoomId = match &body.room_id {
|
// Determine room version
|
||||||
| Some(custom_room_id) => custom_room_id_check(&services, custom_room_id)?,
|
let (room_version, version_rules) = body
|
||||||
| _ => RoomId::new(&services.server.name),
|
.room_version
|
||||||
};
|
.as_ref()
|
||||||
|
.map_or(Ok(&services.server.config.default_room_version), |version| {
|
||||||
// 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 {
|
|
||||||
services
|
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
|
.server
|
||||||
.supported_room_version(&room_version)
|
.supported_room_version(version)
|
||||||
{
|
.then_ok_or_else(version, || {
|
||||||
room_version
|
err!(Request(UnsupportedRoomVersion(
|
||||||
} else {
|
"This server does not support room version {version:?}"
|
||||||
return Err!(Request(UnsupportedRoomVersion(
|
)))
|
||||||
"This server does not support that room version."
|
})
|
||||||
)));
|
})
|
||||||
},
|
.and_then(|version| Ok((version, room_version::rules(version)?)))?;
|
||||||
| None => services
|
|
||||||
.server
|
|
||||||
.config
|
|
||||||
.default_room_version
|
|
||||||
.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let create_content = match &body.creation_content {
|
// Error on existing alias before committing to creation.
|
||||||
| Some(content) => {
|
let alias = alias.await.transpose()?;
|
||||||
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
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Increment and hold the counter; the room will sync atomically to clients
|
// Increment and hold the counter; the room will sync atomically to clients
|
||||||
// which is preferable.
|
// which is preferable.
|
||||||
let next_count = services.globals.next_count();
|
let next_count = services.globals.next_count();
|
||||||
|
|
||||||
// 1. The room create event
|
// 1. Create the create event.
|
||||||
services
|
let (room_id, state_lock) = match version_rules.room_id_format {
|
||||||
.rooms
|
| RoomIdFormatVersion::V1 =>
|
||||||
.timeline
|
create_create_event_legacy(&services, &body, room_version, &version_rules).await?,
|
||||||
.build_and_append_pdu(
|
| RoomIdFormatVersion::V2 =>
|
||||||
PduBuilder {
|
create_create_event(&services, &body, &preset, room_version, &version_rules)
|
||||||
event_type: TimelineEventType::RoomCreate,
|
.await
|
||||||
content: to_raw_value(&create_content)?,
|
.map_err(|e| {
|
||||||
state_key: Some(StateKey::new()),
|
err!(Request(InvalidParam("Error while creating m.room.create event: {e}")))
|
||||||
..Default::default()
|
})?,
|
||||||
},
|
};
|
||||||
sender_user,
|
|
||||||
&room_id,
|
|
||||||
&state_lock,
|
|
||||||
)
|
|
||||||
.boxed()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// 2. Let the room creator join
|
// 2. Let the room creator join
|
||||||
|
let sender_user = body.sender_user();
|
||||||
services
|
services
|
||||||
.rooms
|
.rooms
|
||||||
.timeline
|
.timeline
|
||||||
@@ -230,17 +132,14 @@ pub(crate) async fn create_room_route(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// 3. Power levels
|
// 3. Power levels
|
||||||
|
let mut users = if !version_rules
|
||||||
// Figure out preset. We need it for preset specific events
|
.authorization
|
||||||
let preset = body
|
.explicitly_privilege_room_creators
|
||||||
.preset
|
{
|
||||||
.clone()
|
BTreeMap::from_iter([(sender_user.to_owned(), int!(100))])
|
||||||
.unwrap_or(match &body.visibility {
|
} else {
|
||||||
| room::Visibility::Public => RoomPreset::PublicChat,
|
BTreeMap::new()
|
||||||
| _ => RoomPreset::PrivateChat, // Room visibility should not be custom
|
};
|
||||||
});
|
|
||||||
|
|
||||||
let mut users = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]);
|
|
||||||
|
|
||||||
if preset == RoomPreset::TrustedPrivateChat {
|
if preset == RoomPreset::TrustedPrivateChat {
|
||||||
for invite in &body.invite {
|
for invite in &body.invite {
|
||||||
@@ -260,11 +159,17 @@ pub(crate) async fn create_room_route(
|
|||||||
continue;
|
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(
|
let power_levels_content = default_power_levels_content(
|
||||||
|
&version_rules,
|
||||||
body.power_level_content_override.as_ref(),
|
body.power_level_content_override.as_ref(),
|
||||||
&body.visibility,
|
&body.visibility,
|
||||||
users,
|
users,
|
||||||
@@ -365,7 +270,7 @@ pub(crate) async fn create_room_route(
|
|||||||
// 6. Events listed in initial_state
|
// 6. Events listed in initial_state
|
||||||
for event in &body.initial_state {
|
for event in &body.initial_state {
|
||||||
let mut pdu_builder = event
|
let mut pdu_builder = event
|
||||||
.deserialize_as::<PduBuilder>()
|
.deserialize_as_unchecked::<PduBuilder>()
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}"))))
|
err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}"))))
|
||||||
})?;
|
})?;
|
||||||
@@ -421,7 +326,10 @@ pub(crate) async fn create_room_route(
|
|||||||
.rooms
|
.rooms
|
||||||
.timeline
|
.timeline
|
||||||
.build_and_append_pdu(
|
.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,
|
sender_user,
|
||||||
&room_id,
|
&room_id,
|
||||||
&state_lock,
|
&state_lock,
|
||||||
@@ -488,25 +396,226 @@ pub(crate) async fn create_room_route(
|
|||||||
Ok(create_room::v3::Response::new(room_id))
|
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
|
/// creates the power_levels_content for the PDU builder
|
||||||
fn default_power_levels_content(
|
fn default_power_levels_content(
|
||||||
|
version_rules: &RoomVersionRules,
|
||||||
power_level_content_override: Option<&Raw<RoomPowerLevelsEventContent>>,
|
power_level_content_override: Option<&Raw<RoomPowerLevelsEventContent>>,
|
||||||
visibility: &room::Visibility,
|
visibility: &room::Visibility,
|
||||||
users: BTreeMap<OwnedUserId, Int>,
|
users: BTreeMap<OwnedUserId, Int>,
|
||||||
) -> Result<serde_json::Value> {
|
) -> Result<serde_json::Value> {
|
||||||
use serde_json::to_value;
|
use serde_json::to_value;
|
||||||
|
|
||||||
let mut power_levels_content =
|
let mut power_levels_content = RoomPowerLevelsEventContent::new(&version_rules.authorization);
|
||||||
to_value(RoomPowerLevelsEventContent { users, ..Default::default() })?;
|
power_levels_content.users = users;
|
||||||
|
|
||||||
|
let mut power_levels_content = to_value(power_levels_content)?;
|
||||||
|
|
||||||
// secure proper defaults of sensitive/dangerous permissions that moderators
|
// secure proper defaults of sensitive/dangerous permissions that moderators
|
||||||
// (power level 50) should not have easy access to
|
// (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.power_levels"] = to_value(100)?;
|
||||||
power_levels_content["events"]["m.room.server_acl"] = 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.encryption"] = to_value(100)?;
|
||||||
power_levels_content["events"]["m.room.history_visibility"] = 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
|
// always allow users to respond (not post new) to polls. this is primarily
|
||||||
// useful in read-only announcement rooms that post a public poll.
|
// useful in read-only announcement rooms that post a public poll.
|
||||||
power_levels_content["events"]["org.matrix.msc3381.poll.response"] = to_value(0)?;
|
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
|
/// 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
|
// apply forbidden room alias checks to custom room IDs too
|
||||||
if services
|
if services
|
||||||
.globals
|
.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 server_name = services.globals.server_name();
|
||||||
let full_room_id = format!("!{custom_room_id}:{server_name}");
|
let full_room_id = format!("!{custom_room_id}:{server_name}");
|
||||||
|
|
||||||
OwnedRoomId::parse(full_room_id)
|
let room_id = OwnedRoomId::parse(full_room_id)
|
||||||
.map_err(Into::into)
|
|
||||||
.inspect(|full_room_id| debug_info!(?full_room_id, "Full custom 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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use ruma::{
|
|||||||
federation::space::{SpaceHierarchyParentSummary, get_hierarchy},
|
federation::space::{SpaceHierarchyParentSummary, get_hierarchy},
|
||||||
},
|
},
|
||||||
events::room::member::MembershipState,
|
events::room::member::MembershipState,
|
||||||
space::SpaceRoomJoinRule::{self, *},
|
room::{JoinRuleSummary, RoomSummary},
|
||||||
};
|
};
|
||||||
use tuwunel_core::{
|
use tuwunel_core::{
|
||||||
Err, Result, debug_warn, trace,
|
Err, Result, debug_warn, trace,
|
||||||
@@ -34,8 +34,8 @@ use crate::{Ruma, RumaResponse};
|
|||||||
pub(crate) async fn get_room_summary_legacy(
|
pub(crate) async fn get_room_summary_legacy(
|
||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
InsecureClientIp(client): InsecureClientIp,
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
body: Ruma<get_summary::msc3266::Request>,
|
body: Ruma<get_summary::v1::Request>,
|
||||||
) -> Result<RumaResponse<get_summary::msc3266::Response>> {
|
) -> Result<RumaResponse<get_summary::v1::Response>> {
|
||||||
get_room_summary(State(services), InsecureClientIp(client), body)
|
get_room_summary(State(services), InsecureClientIp(client), body)
|
||||||
.boxed()
|
.boxed()
|
||||||
.await
|
.await
|
||||||
@@ -51,8 +51,8 @@ pub(crate) async fn get_room_summary_legacy(
|
|||||||
pub(crate) async fn get_room_summary(
|
pub(crate) async fn get_room_summary(
|
||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
InsecureClientIp(client): InsecureClientIp,
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
body: Ruma<get_summary::msc3266::Request>,
|
body: Ruma<get_summary::v1::Request>,
|
||||||
) -> Result<get_summary::msc3266::Response> {
|
) -> Result<get_summary::v1::Response> {
|
||||||
let (room_id, servers) = services
|
let (room_id, servers) = services
|
||||||
.rooms
|
.rooms
|
||||||
.alias
|
.alias
|
||||||
@@ -73,7 +73,7 @@ async fn room_summary_response(
|
|||||||
room_id: &RoomId,
|
room_id: &RoomId,
|
||||||
servers: &[OwnedServerName],
|
servers: &[OwnedServerName],
|
||||||
sender_user: Option<&UserId>,
|
sender_user: Option<&UserId>,
|
||||||
) -> Result<get_summary::msc3266::Response> {
|
) -> Result<get_summary::v1::Response> {
|
||||||
if services
|
if services
|
||||||
.rooms
|
.rooms
|
||||||
.state_cache
|
.state_cache
|
||||||
@@ -85,23 +85,12 @@ async fn room_summary_response(
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let room =
|
let summary = remote_room_summary_hierarchy_response(services, room_id, servers, sender_user)
|
||||||
remote_room_summary_hierarchy_response(services, room_id, servers, sender_user).await?;
|
.await?
|
||||||
|
.summary;
|
||||||
|
|
||||||
Ok(get_summary::msc3266::Response {
|
Ok(get_summary::v1::Response {
|
||||||
room_id: room_id.to_owned(),
|
summary,
|
||||||
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,
|
|
||||||
membership: sender_user
|
membership: sender_user
|
||||||
.is_some()
|
.is_some()
|
||||||
.then_some(MembershipState::Leave),
|
.then_some(MembershipState::Leave),
|
||||||
@@ -112,7 +101,7 @@ async fn local_room_summary_response(
|
|||||||
services: &Services,
|
services: &Services,
|
||||||
room_id: &RoomId,
|
room_id: &RoomId,
|
||||||
sender_user: Option<&UserId>,
|
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:?}");
|
trace!(?sender_user, "Sending local room summary response for {room_id:?}");
|
||||||
let join_rule = services
|
let join_rule = services
|
||||||
.rooms
|
.rooms
|
||||||
@@ -139,7 +128,7 @@ async fn local_room_summary_response(
|
|||||||
&join_rule.clone().into(),
|
&join_rule.clone().into(),
|
||||||
guest_can_join,
|
guest_can_join,
|
||||||
world_readable,
|
world_readable,
|
||||||
join_rule.allowed_rooms(),
|
join_rule.allowed_room_ids(),
|
||||||
sender_user,
|
sender_user,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -224,24 +213,22 @@ async fn local_room_summary_response(
|
|||||||
membership,
|
membership,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(get_summary::msc3266::Response {
|
Ok(get_summary::v1::Response {
|
||||||
room_id: room_id.to_owned(),
|
summary: RoomSummary {
|
||||||
canonical_alias,
|
room_id: room_id.to_owned(),
|
||||||
avatar_url,
|
canonical_alias,
|
||||||
guest_can_join,
|
avatar_url,
|
||||||
name,
|
guest_can_join,
|
||||||
num_joined_members: num_joined_members.try_into().unwrap_or_default(),
|
name,
|
||||||
topic,
|
num_joined_members: num_joined_members.try_into().unwrap_or_default(),
|
||||||
world_readable,
|
topic,
|
||||||
room_type,
|
world_readable,
|
||||||
room_version,
|
room_type,
|
||||||
encryption,
|
room_version,
|
||||||
|
encryption,
|
||||||
|
join_rule: join_rule.into(),
|
||||||
|
},
|
||||||
membership,
|
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 {
|
while let Some(Ok(response)) = requests.next().await {
|
||||||
trace!("{response:?}");
|
trace!("{response:?}");
|
||||||
let room = response.room.clone();
|
let room = response.room.clone();
|
||||||
if room.room_id != room_id {
|
let summary = &room.summary;
|
||||||
|
if summary.room_id != room_id {
|
||||||
debug_warn!(
|
debug_warn!(
|
||||||
"Room ID {} returned does not belong to the requested room ID {}",
|
"Room ID {} returned does not belong to the requested room ID {}",
|
||||||
room.room_id,
|
summary.room_id,
|
||||||
room_id
|
room_id
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@@ -289,10 +277,10 @@ async fn remote_room_summary_hierarchy_response(
|
|||||||
return user_can_see_summary(
|
return user_can_see_summary(
|
||||||
services,
|
services,
|
||||||
room_id,
|
room_id,
|
||||||
&room.join_rule,
|
&summary.join_rule,
|
||||||
room.guest_can_join,
|
summary.guest_can_join,
|
||||||
room.world_readable,
|
summary.world_readable,
|
||||||
room.allowed_room_ids.iter().map(AsRef::as_ref),
|
summary.join_rule.allowed_room_ids(),
|
||||||
sender_user,
|
sender_user,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -308,7 +296,7 @@ async fn remote_room_summary_hierarchy_response(
|
|||||||
async fn user_can_see_summary<'a, I>(
|
async fn user_can_see_summary<'a, I>(
|
||||||
services: &Services,
|
services: &Services,
|
||||||
room_id: &RoomId,
|
room_id: &RoomId,
|
||||||
join_rule: &SpaceRoomJoinRule,
|
join_rule: &JoinRuleSummary,
|
||||||
guest_can_join: bool,
|
guest_can_join: bool,
|
||||||
world_readable: bool,
|
world_readable: bool,
|
||||||
allowed_room_ids: I,
|
allowed_room_ids: I,
|
||||||
@@ -317,17 +305,23 @@ async fn user_can_see_summary<'a, I>(
|
|||||||
where
|
where
|
||||||
I: Iterator<Item = &'a RoomId> + Send,
|
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 {
|
match sender_user {
|
||||||
| Some(sender_user) => {
|
| Some(sender_user) => {
|
||||||
let user_can_see_state_events = services
|
let user_can_see_state_events = services
|
||||||
.rooms
|
.rooms
|
||||||
.state_accessor
|
.state_accessor
|
||||||
.user_can_see_state_events(sender_user, room_id);
|
.user_can_see_state_events(sender_user, room_id);
|
||||||
|
|
||||||
let is_guest = services
|
let is_guest = services
|
||||||
.users
|
.users
|
||||||
.is_deactivated(sender_user)
|
.is_deactivated(sender_user)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
let user_in_allowed_restricted_room = allowed_room_ids.stream().any(|room| {
|
let user_in_allowed_restricted_room = allowed_room_ids.stream().any(|room| {
|
||||||
services
|
services
|
||||||
.rooms
|
.rooms
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use axum::extract::State;
|
|||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
CanonicalJsonObject, RoomId, RoomVersionId,
|
CanonicalJsonObject, RoomId, RoomVersionId,
|
||||||
api::client::{error::ErrorKind, room::upgrade_room},
|
api::client::room::upgrade_room,
|
||||||
events::{
|
events::{
|
||||||
StateEventType, TimelineEventType,
|
StateEventType, TimelineEventType,
|
||||||
room::{
|
room::{
|
||||||
@@ -14,11 +14,12 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
int,
|
int,
|
||||||
|
room_version_rules::RoomIdFormatVersion,
|
||||||
};
|
};
|
||||||
use serde_json::{json, value::to_raw_value};
|
use serde_json::{json, value::to_raw_value};
|
||||||
use tuwunel_core::{
|
use tuwunel_core::{
|
||||||
Err, Error, Result, err,
|
Err, Result, err,
|
||||||
matrix::{Event, StateKey, pdu::PduBuilder},
|
matrix::{Event, StateKey, pdu::PduBuilder, room_version},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
@@ -61,14 +62,23 @@ pub(crate) async fn upgrade_room_route(
|
|||||||
.server
|
.server
|
||||||
.supported_room_version(&body.new_version)
|
.supported_room_version(&body.new_version)
|
||||||
{
|
{
|
||||||
return Err(Error::BadRequest(
|
return Err!(Request(UnsupportedRoomVersion(
|
||||||
ErrorKind::UnsupportedRoomVersion,
|
|
||||||
"This server does not support that room version.",
|
"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
|
// 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
|
let _short_id = services
|
||||||
.rooms
|
.rooms
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use futures::FutureExt;
|
||||||
use ruma::{OwnedUserId, UserId};
|
use ruma::{OwnedUserId, UserId};
|
||||||
use tuwunel_core::{Err, Result, debug};
|
use tuwunel_core::{Err, Result, debug};
|
||||||
use tuwunel_service::Services;
|
use tuwunel_service::Services;
|
||||||
@@ -63,6 +64,7 @@ pub(super) async fn ldap_login(
|
|||||||
services
|
services
|
||||||
.admin
|
.admin
|
||||||
.make_user_admin(lowercased_user_id)
|
.make_user_admin(lowercased_user_id)
|
||||||
|
.boxed()
|
||||||
.await?;
|
.await?;
|
||||||
} else if !is_ldap_admin && is_tuwunel_admin {
|
} else if !is_ldap_admin && is_tuwunel_admin {
|
||||||
services
|
services
|
||||||
|
|||||||
@@ -56,10 +56,7 @@ pub(crate) async fn login_token_route(
|
|||||||
|
|
||||||
let mut uiaainfo = uiaa::UiaaInfo {
|
let mut uiaainfo = uiaa::UiaaInfo {
|
||||||
flows: vec![password_flow],
|
flows: vec![password_flow],
|
||||||
completed: Vec::new(),
|
..Default::default()
|
||||||
params: Box::default(),
|
|
||||||
session: None,
|
|
||||||
auth_error: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match &body.auth {
|
match &body.auth {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use axum::extract::State;
|
|||||||
use futures::{FutureExt, TryFutureExt, TryStreamExt};
|
use futures::{FutureExt, TryFutureExt, TryStreamExt};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedEventId, RoomId, UserId,
|
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::{
|
events::{
|
||||||
AnyStateEventContent, StateEventType,
|
AnyStateEventContent, StateEventType,
|
||||||
room::{
|
room::{
|
||||||
@@ -107,8 +107,8 @@ pub(crate) async fn get_state_events_route(
|
|||||||
/// readable
|
/// readable
|
||||||
pub(crate) async fn get_state_events_for_key_route(
|
pub(crate) async fn get_state_events_for_key_route(
|
||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
body: Ruma<get_state_events_for_key::v3::Request>,
|
body: Ruma<get_state_event_for_key::v3::Request>,
|
||||||
) -> Result<get_state_events_for_key::v3::Response> {
|
) -> Result<get_state_event_for_key::v3::Response> {
|
||||||
let sender_user = body.sender_user();
|
let sender_user = body.sender_user();
|
||||||
|
|
||||||
if !services
|
if !services
|
||||||
@@ -140,7 +140,7 @@ pub(crate) async fn get_state_events_for_key_route(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|f| f.to_lowercase().eq("event"));
|
.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()),
|
content: event_format.or(|| event.get_content_as_value()),
|
||||||
event: event_format.then(|| {
|
event: event_format.then(|| {
|
||||||
json!({
|
json!({
|
||||||
@@ -167,8 +167,8 @@ pub(crate) async fn get_state_events_for_key_route(
|
|||||||
/// readable
|
/// readable
|
||||||
pub(crate) async fn get_state_events_for_empty_key_route(
|
pub(crate) async fn get_state_events_for_empty_key_route(
|
||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
body: Ruma<get_state_events_for_key::v3::Request>,
|
body: Ruma<get_state_event_for_key::v3::Request>,
|
||||||
) -> Result<RumaResponse<get_state_events_for_key::v3::Response>> {
|
) -> Result<RumaResponse<get_state_event_for_key::v3::Response>> {
|
||||||
get_state_events_for_key_route(State(services), body)
|
get_state_events_for_key_route(State(services), body)
|
||||||
.await
|
.await
|
||||||
.map(RumaResponse)
|
.map(RumaResponse)
|
||||||
@@ -222,7 +222,7 @@ async fn allowed_to_send_state_event(
|
|||||||
| StateEventType::RoomServerAcl => {
|
| StateEventType::RoomServerAcl => {
|
||||||
// prevents common ACL paw-guns as ACL management is difficult and prone to
|
// prevents common ACL paw-guns as ACL management is difficult and prone to
|
||||||
// irreversible mistakes
|
// irreversible mistakes
|
||||||
match json.deserialize_as::<RoomServerAclEventContent>() {
|
match json.deserialize_as_unchecked::<RoomServerAclEventContent>() {
|
||||||
| Ok(acl_content) => {
|
| Ok(acl_content) => {
|
||||||
if acl_content.allow_is_empty() {
|
if acl_content.allow_is_empty() {
|
||||||
return Err!(Request(BadJson(debug_warn!(
|
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
|
// 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 let Ok(admin_room_id) = services.admin.get_admin_room().await {
|
||||||
if admin_room_id == room_id {
|
if admin_room_id == room_id {
|
||||||
match json.deserialize_as::<RoomJoinRulesEventContent>() {
|
match json.deserialize_as_unchecked::<RoomJoinRulesEventContent>() {
|
||||||
| Ok(join_rule) =>
|
| Ok(join_rule) =>
|
||||||
if join_rule.join_rule == JoinRule::Public {
|
if join_rule.join_rule == JoinRule::Public {
|
||||||
return Err!(Request(Forbidden(
|
return Err!(Request(Forbidden(
|
||||||
@@ -301,7 +301,7 @@ async fn allowed_to_send_state_event(
|
|||||||
| StateEventType::RoomHistoryVisibility => {
|
| StateEventType::RoomHistoryVisibility => {
|
||||||
// admin room is a sensitive room, it should not ever be made world readable
|
// 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 {
|
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) => {
|
| Ok(visibility_content) => {
|
||||||
if admin_room_id == room_id
|
if admin_room_id == room_id
|
||||||
&& visibility_content.history_visibility
|
&& visibility_content.history_visibility
|
||||||
@@ -322,7 +322,7 @@ async fn allowed_to_send_state_event(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
| StateEventType::RoomCanonicalAlias => {
|
| StateEventType::RoomCanonicalAlias => {
|
||||||
match json.deserialize_as::<RoomCanonicalAliasEventContent>() {
|
match json.deserialize_as_unchecked::<RoomCanonicalAliasEventContent>() {
|
||||||
| Ok(canonical_alias_content) => {
|
| Ok(canonical_alias_content) => {
|
||||||
let mut aliases = canonical_alias_content.alt_aliases.clone();
|
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>() {
|
| StateEventType::RoomMember =>
|
||||||
| Ok(membership_content) => {
|
match json.deserialize_as_unchecked::<RoomMemberEventContent>() {
|
||||||
let Ok(_state_key) = UserId::parse(state_key) else {
|
| Ok(membership_content) => {
|
||||||
return Err!(Request(BadJson(
|
let Ok(_state_key) = UserId::parse(state_key) else {
|
||||||
"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 {
|
|
||||||
return Err!(Request(BadJson(
|
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) {
|
if let Some(authorising_user) =
|
||||||
return Err!(Request(InvalidParam(
|
membership_content.join_authorized_via_users_server
|
||||||
"Authorising user {authorising_user} does not belong to this \
|
{
|
||||||
homeserver"
|
if membership_content.membership != MembershipState::Join {
|
||||||
)));
|
return Err!(Request(BadJson(
|
||||||
}
|
"join_authorised_via_users_server is only for member joins"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
services
|
if !services.globals.user_is_local(&authorising_user) {
|
||||||
.rooms
|
return Err!(Request(InvalidParam(
|
||||||
.state_cache
|
"Authorising user {authorising_user} does not belong to this \
|
||||||
.is_joined(&authorising_user, room_id)
|
homeserver"
|
||||||
.map(is_false!())
|
)));
|
||||||
.map(BoolExt::into_result)
|
}
|
||||||
.map_err(|()| {
|
|
||||||
err!(Request(InvalidParam(
|
services
|
||||||
"Authorising user {authorising_user} is not in the room. They \
|
.rooms
|
||||||
cannot authorise the join."
|
.state_cache
|
||||||
)))
|
.is_joined(&authorising_user, room_id)
|
||||||
})
|
.map(is_false!())
|
||||||
.await?;
|
.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}"
|
|
||||||
)));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
| _ => (),
|
| _ => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ use ruma::{
|
|||||||
api::client::{
|
api::client::{
|
||||||
filter::FilterDefinition,
|
filter::FilterDefinition,
|
||||||
sync::sync_events::{
|
sync::sync_events::{
|
||||||
self, DeviceLists, UnreadNotificationsCount,
|
self, DeviceLists, StrippedState, UnreadNotificationsCount,
|
||||||
v3::{
|
v3::{
|
||||||
Ephemeral, Filter, GlobalAccountData, InviteState, InvitedRoom, JoinedRoom,
|
Ephemeral, Filter, GlobalAccountData, InviteState, InvitedRoom, JoinedRoom,
|
||||||
KnockState, KnockedRoom, LeftRoom, Presence, RoomAccountData, RoomSummary, Rooms,
|
KnockState, KnockedRoom, LeftRoom, Presence, RoomAccountData, RoomSummary, Rooms,
|
||||||
State as RoomState, Timeline, ToDevice,
|
State as RoomState, StateEvents, Timeline, ToDevice,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
uiaa::UiaaResponse,
|
uiaa::UiaaResponse,
|
||||||
@@ -295,7 +295,12 @@ async fn build_sync_events(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let invited_room = InvitedRoom {
|
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);
|
invited_rooms.insert(room_id, invited_room);
|
||||||
@@ -320,7 +325,12 @@ async fn build_sync_events(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let knocked_room = KnockedRoom {
|
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);
|
knocked_rooms.insert(room_id, knocked_room);
|
||||||
@@ -540,7 +550,7 @@ async fn handle_left_room(
|
|||||||
prev_batch: Some(next_batch.to_string()),
|
prev_batch: Some(next_batch.to_string()),
|
||||||
events: Vec::new(),
|
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()),
|
prev_batch: Some(next_batch.to_string()),
|
||||||
events: Vec::new(), // and so we dont need to set this to empty vec
|
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 {
|
let joined_room = JoinedRoom {
|
||||||
account_data: RoomAccountData { events: account_data_events },
|
account_data: RoomAccountData { events: account_data_events },
|
||||||
ephemeral: Ephemeral { events: edus },
|
ephemeral: Ephemeral { events: edus },
|
||||||
state: RoomState { events: state_events },
|
state: RoomState::Before(StateEvents { events: state_events }),
|
||||||
summary: RoomSummary {
|
summary: RoomSummary {
|
||||||
joined_member_count: joined_member_count.map(ruma_from_u64),
|
joined_member_count: joined_member_count.map(ruma_from_u64),
|
||||||
invited_member_count: invited_member_count.map(ruma_from_u64),
|
invited_member_count: invited_member_count.map(ruma_from_u64),
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ use futures::{
|
|||||||
};
|
};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
DeviceId, OwnedEventId, OwnedRoomId, RoomId, UInt, UserId,
|
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,
|
directory::RoomTypeFilter,
|
||||||
events::{
|
events::{
|
||||||
AnyRawAccountDataEvent, AnySyncEphemeralRoomEvent, StateEventType, TimelineEventType,
|
AnyRawAccountDataEvent, AnySyncEphemeralRoomEvent, StateEventType, TimelineEventType,
|
||||||
@@ -647,7 +650,11 @@ where
|
|||||||
name: room_name.or(hero_name),
|
name: room_name.or(hero_name),
|
||||||
initial: Some(roomsince == &0),
|
initial: Some(roomsince == &0),
|
||||||
is_dm: None,
|
is_dm: None,
|
||||||
invite_state,
|
invite_state: invite_state.map(|s| {
|
||||||
|
s.into_iter()
|
||||||
|
.map(Raw::cast::<StrippedState>)
|
||||||
|
.collect()
|
||||||
|
}),
|
||||||
unread_notifications: UnreadNotificationsCount {
|
unread_notifications: UnreadNotificationsCount {
|
||||||
highlight_count: Some(
|
highlight_count: Some(
|
||||||
services
|
services
|
||||||
@@ -727,7 +734,10 @@ async fn collect_account_data(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Some(rooms) = &body.extensions.account_data.rooms {
|
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(
|
account_data.rooms.insert(
|
||||||
room.clone(),
|
room.clone(),
|
||||||
services
|
services
|
||||||
|
|||||||
@@ -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(
|
return Err!(Request(BadJson(
|
||||||
"The key does not match the URL field key, or JSON body is empty (use DELETE)"
|
"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")));
|
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
|
let all_joined_rooms: Vec<OwnedRoomId> = services
|
||||||
.rooms
|
.rooms
|
||||||
.state_cache
|
.state_cache
|
||||||
@@ -180,7 +180,7 @@ pub(crate) async fn set_profile_key_route(
|
|||||||
&all_joined_rooms,
|
&all_joined_rooms,
|
||||||
)
|
)
|
||||||
.await;
|
.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 mxc = ruma::OwnedMxcUri::from(profile_key_value.to_string());
|
||||||
|
|
||||||
let all_joined_rooms: Vec<OwnedRoomId> = services
|
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;
|
update_avatar_url(&services, &body.user_id, Some(mxc), None, &all_joined_rooms).await;
|
||||||
} else {
|
} else {
|
||||||
services.users.set_profile_key(
|
services
|
||||||
&body.user_id,
|
.users
|
||||||
&body.key_name,
|
.set_profile_key(&body.user_id, &body.key, Some(profile_key_value.clone()));
|
||||||
Some(profile_key_value.clone()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if services.config.allow_local_presence {
|
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
|
let all_joined_rooms: Vec<OwnedRoomId> = services
|
||||||
.rooms
|
.rooms
|
||||||
.state_cache
|
.state_cache
|
||||||
@@ -243,7 +241,7 @@ pub(crate) async fn delete_profile_key_route(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
update_displayname(&services, &body.user_id, None, &all_joined_rooms).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
|
let all_joined_rooms: Vec<OwnedRoomId> = services
|
||||||
.rooms
|
.rooms
|
||||||
.state_cache
|
.state_cache
|
||||||
@@ -256,7 +254,7 @@ pub(crate) async fn delete_profile_key_route(
|
|||||||
} else {
|
} else {
|
||||||
services
|
services
|
||||||
.users
|
.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 {
|
if services.config.allow_local_presence {
|
||||||
@@ -379,14 +377,12 @@ pub(crate) async fn get_profile_key_route(
|
|||||||
.users
|
.users
|
||||||
.set_timezone(&body.user_id, response.tz.clone());
|
.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) => {
|
| Some(value) => {
|
||||||
profile_key_value.insert(body.key_name.clone(), value.clone());
|
profile_key_value.insert(body.key.clone(), value.clone());
|
||||||
services.users.set_profile_key(
|
services
|
||||||
&body.user_id,
|
.users
|
||||||
&body.key_name,
|
.set_profile_key(&body.user_id, &body.key, Some(value.clone()));
|
||||||
Some(value.clone()),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
| _ => {
|
| _ => {
|
||||||
return Err!(Request(NotFound("The requested profile key does not exist.")));
|
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
|
match services
|
||||||
.users
|
.users
|
||||||
.profile_key(&body.user_id, &body.key_name)
|
.profile_key(&body.user_id, &body.key)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
| Ok(value) => {
|
| 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.")));
|
return Err!(Request(NotFound("The requested profile key does not exist.")));
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
use axum::{Json, extract::State, response::IntoResponse};
|
use axum::{Json, extract::State, response::IntoResponse};
|
||||||
use ruma::api::client::{
|
use ruma::api::client::discovery::{
|
||||||
discovery::{
|
discover_homeserver::{self, HomeserverInfo},
|
||||||
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
|
discover_support::{self, Contact},
|
||||||
discover_support::{self, Contact},
|
|
||||||
},
|
|
||||||
error::ErrorKind,
|
|
||||||
};
|
};
|
||||||
use tuwunel_core::{Error, Result};
|
use tuwunel_core::{Err, Result};
|
||||||
|
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
|
|
||||||
@@ -19,13 +16,12 @@ pub(crate) async fn well_known_client(
|
|||||||
) -> Result<discover_homeserver::Response> {
|
) -> Result<discover_homeserver::Response> {
|
||||||
let client_url = match services.server.config.well_known.client.as_ref() {
|
let client_url = match services.server.config.well_known.client.as_ref() {
|
||||||
| Some(url) => url.to_string(),
|
| 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 {
|
Ok(discover_homeserver::Response {
|
||||||
homeserver: HomeserverInfo { base_url: client_url.clone() },
|
homeserver: HomeserverInfo { base_url: client_url },
|
||||||
identity_server: None,
|
identity_server: None,
|
||||||
sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }),
|
|
||||||
tile_server: None,
|
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
|
// support page or role must be either defined for this to be valid
|
||||||
if support_page.is_none() && role.is_none() {
|
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
|
let email_address = services
|
||||||
@@ -63,6 +59,7 @@ pub(crate) async fn well_known_support(
|
|||||||
.well_known
|
.well_known
|
||||||
.support_email
|
.support_email
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
let matrix_id = services
|
let matrix_id = services
|
||||||
.server
|
.server
|
||||||
.config
|
.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 a role is specified, an email address or matrix id is required
|
||||||
if role.is_some() && (email_address.is_none() && matrix_id.is_none()) {
|
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
|
// 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
|
// support page or role+contacts must be either defined for this to be valid
|
||||||
if contacts.is_empty() && support_page.is_none() {
|
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 })
|
Ok(discover_support::Response { contacts, support_page })
|
||||||
@@ -103,7 +100,7 @@ pub(crate) async fn syncv3_client_server_json(
|
|||||||
| Some(url) => url.to_string(),
|
| Some(url) => url.to_string(),
|
||||||
| None => match services.server.config.well_known.server.as_ref() {
|
| None => match services.server.config.well_known.server.as_ref() {
|
||||||
| Some(url) => url.to_string(),
|
| Some(url) => url.to_string(),
|
||||||
| None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
|
| None => return Err!(Request(NotFound("Not found."))),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#![type_length_limit = "65536"] //TODO: reduce me
|
#![type_length_limit = "262144"] //TODO: REDUCE ME
|
||||||
#![allow(clippy::toplevel_ref_arg)]
|
#![allow(clippy::toplevel_ref_arg)]
|
||||||
|
|
||||||
pub mod client;
|
pub mod client;
|
||||||
|
|||||||
@@ -69,7 +69,10 @@ pub(super) async fn auth(
|
|||||||
json_body: Option<&CanonicalJsonValue>,
|
json_body: Option<&CanonicalJsonValue>,
|
||||||
metadata: &Metadata,
|
metadata: &Metadata,
|
||||||
) -> Result<Auth> {
|
) -> Result<Auth> {
|
||||||
use AuthScheme::{AccessToken, AccessTokenOptional, AppserviceToken, ServerSignatures};
|
use AuthScheme::{
|
||||||
|
AccessToken, AccessTokenOptional, AppserviceToken, AppserviceTokenOptional,
|
||||||
|
ServerSignatures,
|
||||||
|
};
|
||||||
use Error::BadRequest;
|
use Error::BadRequest;
|
||||||
use ErrorKind::UnknownToken;
|
use ErrorKind::UnknownToken;
|
||||||
use Token::{Appservice, Expired, Invalid, User};
|
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, 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
|
| &get_turn_server_info::v3::Request::METADATA
|
||||||
if services.server.config.turn_allow_guests =>
|
if services.server.config.turn_allow_guests =>
|
||||||
Ok(Auth::default()),
|
Ok(Auth::default()),
|
||||||
@@ -137,22 +140,25 @@ pub(super) async fn auth(
|
|||||||
| _ => Err!(Request(MissingToken("Missing access token."))),
|
| _ => 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_user: Some(user.0),
|
||||||
sender_device: Some(user.1),
|
sender_device: Some(user.1),
|
||||||
_expires_at: user.2,
|
_expires_at: user.2,
|
||||||
..Auth::default()
|
..Auth::default()
|
||||||
}),
|
}),
|
||||||
|
|
||||||
//TODO: add AppserviceTokenOptional
|
| (
|
||||||
| (AccessTokenOptional | AppserviceToken | AuthScheme::None, Appservice(info)) =>
|
AccessTokenOptional | AppserviceTokenOptional | AppserviceToken | AuthScheme::None,
|
||||||
Ok(Auth {
|
Appservice(info),
|
||||||
appservice_info: Some(*info),
|
) => Ok(Auth {
|
||||||
..Auth::default()
|
appservice_info: Some(*info),
|
||||||
}),
|
..Auth::default()
|
||||||
|
}),
|
||||||
|
|
||||||
//TODO: add AppserviceTokenOptional
|
| (AccessTokenOptional | AppserviceTokenOptional | AuthScheme::None, Token::None) =>
|
||||||
| (AccessTokenOptional | AppserviceToken | AuthScheme::None, Token::None) =>
|
|
||||||
Ok(Auth::default()),
|
Ok(Auth::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,7 +312,7 @@ async fn auth_server(
|
|||||||
|
|
||||||
let keys: PubKeys = [(x_matrix.key.to_string(), key.key)].into();
|
let keys: PubKeys = [(x_matrix.key.to_string(), key.key)].into();
|
||||||
let keys: PubKeyMap = [(origin.as_str().into(), keys)].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}");
|
debug_error!("Failed to verify federation request from {origin}: {e}");
|
||||||
if request.parts.uri.to_string().contains('@') {
|
if request.parts.uri.to_string().contains('@') {
|
||||||
warn!(
|
warn!(
|
||||||
|
|||||||
@@ -64,17 +64,18 @@ pub(crate) async fn get_hierarchy_route(
|
|||||||
})
|
})
|
||||||
.unzip()
|
.unzip()
|
||||||
.map(|(children, inaccessible_children): (Vec<_>, Vec<_>)| {
|
.map(|(children, inaccessible_children): (Vec<_>, Vec<_>)| {
|
||||||
(
|
let children = children
|
||||||
children
|
.into_iter()
|
||||||
.into_iter()
|
.flatten()
|
||||||
.flatten()
|
.map(|parent| parent.summary)
|
||||||
.map(Into::into)
|
.collect();
|
||||||
.collect(),
|
|
||||||
inaccessible_children
|
let inaccessible_children = inaccessible_children
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.collect(),
|
.collect();
|
||||||
)
|
|
||||||
|
(children, inaccessible_children)
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ use axum_client_ip::InsecureClientIp;
|
|||||||
use base64::{Engine as _, engine::general_purpose};
|
use base64::{Engine as _, engine::general_purpose};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
CanonicalJsonValue, OwnedUserId, UserId,
|
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},
|
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||||
serde::JsonObject,
|
serde::JsonObject,
|
||||||
};
|
};
|
||||||
use tuwunel_core::{
|
use tuwunel_core::{
|
||||||
Err, Error, Result, err,
|
Err, Error, Result, err, extract_variant,
|
||||||
matrix::{Event, PduEvent, event::gen_event_id},
|
matrix::{Event, PduEvent, event::gen_event_id},
|
||||||
utils,
|
utils,
|
||||||
utils::hash::sha256,
|
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.")));
|
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())
|
let mut event: JsonObject = serde_json::from_str(body.event.get())
|
||||||
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
|
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use RoomVersionId::*;
|
|||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
RoomVersionId,
|
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},
|
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||||
};
|
};
|
||||||
use serde_json::value::to_raw_value;
|
use serde_json::value::to_raw_value;
|
||||||
@@ -15,8 +15,8 @@ use crate::Ruma;
|
|||||||
/// Creates a knock template.
|
/// Creates a knock template.
|
||||||
pub(crate) async fn create_knock_event_template_route(
|
pub(crate) async fn create_knock_event_template_route(
|
||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
body: Ruma<create_knock_event_template::v1::Request>,
|
body: Ruma<prepare_knock_event::v1::Request>,
|
||||||
) -> Result<create_knock_event_template::v1::Response> {
|
) -> Result<prepare_knock_event::v1::Response> {
|
||||||
if !services
|
if !services
|
||||||
.rooms
|
.rooms
|
||||||
.metadata
|
.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
|
// room v3 and above removed the "event_id" field from remote PDU format
|
||||||
super::maybe_strip_event_id(&mut pdu_json, &room_version_id)?;
|
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,
|
room_version: room_version_id,
|
||||||
event: to_raw_value(&pdu_json).expect("CanonicalJson can be serialized to JSON"),
|
event: to_raw_value(&pdu_json).expect("CanonicalJson can be serialized to JSON"),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -354,6 +354,7 @@ pub(crate) async fn create_join_event_v2_route(
|
|||||||
create_join_event(&services, body.origin(), &body.room_id, &body.pdu)
|
create_join_event(&services, body.origin(), &body.room_id, &body.pdu)
|
||||||
.boxed()
|
.boxed()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let room_state = create_join_event::v2::RoomState {
|
let room_state = create_join_event::v2::RoomState {
|
||||||
members_omitted: false,
|
members_omitted: false,
|
||||||
auth_chain,
|
auth_chain,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use futures::FutureExt;
|
|||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedServerName, OwnedUserId,
|
OwnedServerName, OwnedUserId,
|
||||||
RoomVersionId::*,
|
RoomVersionId::*,
|
||||||
api::federation::knock::send_knock,
|
api::federation::membership::create_knock_event,
|
||||||
events::{
|
events::{
|
||||||
StateEventType,
|
StateEventType,
|
||||||
room::member::{MembershipState, RoomMemberEventContent},
|
room::member::{MembershipState, RoomMemberEventContent},
|
||||||
@@ -23,8 +23,8 @@ use crate::Ruma;
|
|||||||
/// Submits a signed knock event.
|
/// Submits a signed knock event.
|
||||||
pub(crate) async fn create_knock_event_v1_route(
|
pub(crate) async fn create_knock_event_v1_route(
|
||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
body: Ruma<send_knock::v1::Request>,
|
body: Ruma<create_knock_event::v1::Request>,
|
||||||
) -> Result<send_knock::v1::Response> {
|
) -> Result<create_knock_event::v1::Response> {
|
||||||
if services
|
if services
|
||||||
.config
|
.config
|
||||||
.forbidden_remote_server_names
|
.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)
|
.send_pdu_room(&body.room_id, &pdu_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let knock_room_state = services.rooms.state.summary_stripped(&pdu).await;
|
Ok(create_knock_event::v1::Response {
|
||||||
|
knock_room_state: services
|
||||||
Ok(send_knock::v1::Response { knock_room_state })
|
.rooms
|
||||||
|
.state
|
||||||
|
.summary_stripped(&pdu)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,9 @@ hardened_malloc-rs.workspace = true
|
|||||||
hardened_malloc-rs.optional = true
|
hardened_malloc-rs.optional = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
insta.workspace = true
|
||||||
maplit.workspace = true
|
maplit.workspace = true
|
||||||
|
similar.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -665,20 +665,33 @@ pub struct Config {
|
|||||||
pub allow_room_creation: bool,
|
pub allow_room_creation: bool,
|
||||||
|
|
||||||
/// Set to false to disable users from joining or creating room versions
|
/// 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)
|
/// default: true
|
||||||
/// support for versions 3 - 5.
|
|
||||||
#[serde(default = "true_fn")]
|
#[serde(default = "true_fn")]
|
||||||
pub allow_unstable_room_versions: bool,
|
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.
|
/// Default room version tuwunel will create rooms with.
|
||||||
///
|
///
|
||||||
/// Per spec, room version 11 is the default.
|
/// The default is prescribed by the spec, but may be selected by developer
|
||||||
///
|
/// recommendation. To prevent stale documentation we no longer list it
|
||||||
/// default: 11
|
/// 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")]
|
#[serde(default = "default_default_room_version")]
|
||||||
pub default_room_version: RoomVersionId,
|
pub default_room_version: RoomVersionId,
|
||||||
|
|
||||||
@@ -1885,6 +1898,30 @@ pub struct Config {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub allow_invalid_tls_certificates: bool,
|
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
|
// external structure; separate section
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub blurhashing: BlurhashConfig,
|
pub blurhashing: BlurhashConfig,
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ pub enum Error {
|
|||||||
Mxc(#[from] ruma::MxcUriError),
|
Mxc(#[from] ruma::MxcUriError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Mxid(#[from] ruma::IdParseError),
|
Mxid(#[from] ruma::IdParseError),
|
||||||
|
#[error(transparent)]
|
||||||
|
PowerLevels(#[from] ruma::events::room::power_levels::PowerLevelsError),
|
||||||
#[error("from {0}: {1}")]
|
#[error("from {0}: {1}")]
|
||||||
Redaction(ruma::OwnedServerName, ruma::canonical_json::RedactionError),
|
Redaction(ruma::OwnedServerName, ruma::canonical_json::RedactionError),
|
||||||
#[error("{0}: {1}")]
|
#[error("{0}: {1}")]
|
||||||
@@ -126,8 +128,6 @@ pub enum Error {
|
|||||||
Ruma(#[from] ruma::api::client::error::Error),
|
Ruma(#[from] ruma::api::client::error::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Signatures(#[from] ruma::signatures::Error),
|
Signatures(#[from] ruma::signatures::Error),
|
||||||
#[error(transparent)]
|
|
||||||
StateRes(#[from] crate::state_res::Error),
|
|
||||||
#[error("uiaa")]
|
#[error("uiaa")]
|
||||||
Uiaa(ruma::api::client::uiaa::UiaaInfo),
|
Uiaa(ruma::api::client::uiaa::UiaaInfo),
|
||||||
|
|
||||||
|
|||||||
@@ -41,10 +41,7 @@ impl From<Error> for UiaaResponse {
|
|||||||
message: error.message(),
|
message: error.message(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Self::MatrixError(ruma::api::client::error::Error {
|
Self::MatrixError(ruma::api::client::error::Error::new(error.status_code(), body))
|
||||||
status_code: error.status_code(),
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,19 @@
|
|||||||
|
|
||||||
use std::iter::once;
|
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};
|
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
|
/// Supported and stable room versions
|
||||||
pub const STABLE_ROOM_VERSIONS: &[RoomVersionId] = &[
|
pub const STABLE_ROOM_VERSIONS: &[RoomVersionId] = &[
|
||||||
RoomVersionId::V6,
|
RoomVersionId::V6,
|
||||||
@@ -16,9 +25,8 @@ pub const STABLE_ROOM_VERSIONS: &[RoomVersionId] = &[
|
|||||||
RoomVersionId::V11,
|
RoomVersionId::V11,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Experimental, partially supported room versions
|
/// Experimental and prototype room versions under development.
|
||||||
pub const UNSTABLE_ROOM_VERSIONS: &[RoomVersionId] =
|
pub const EXPERIMENTAL_ROOM_VERSIONS: &[RoomVersionId] = &[RoomVersionId::V12];
|
||||||
&[RoomVersionId::V2, RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5];
|
|
||||||
|
|
||||||
type RoomVersion = (RoomVersionId, RoomVersionStability);
|
type RoomVersion = (RoomVersionId, RoomVersionStability);
|
||||||
|
|
||||||
@@ -31,8 +39,15 @@ impl crate::Server {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn supported_room_versions(&self) -> impl Iterator<Item = RoomVersionId> + '_ {
|
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()
|
Self::available_room_versions()
|
||||||
.filter(|(_, stability)| self.supported_stability(stability))
|
.filter(|(_, stability)| self.supported_stability(stability))
|
||||||
|
.chain(experimental_room_versions)
|
||||||
.map(at!(0))
|
.map(at!(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ mod format;
|
|||||||
mod id;
|
mod id;
|
||||||
mod redact;
|
mod redact;
|
||||||
mod relation;
|
mod relation;
|
||||||
|
pub mod state_key;
|
||||||
mod type_ext;
|
mod type_ext;
|
||||||
mod unsigned;
|
mod unsigned;
|
||||||
|
|
||||||
@@ -16,12 +17,23 @@ use ruma::{
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{Value as JsonValue, value::RawValue as RawJsonValue};
|
use serde_json::{Value as JsonValue, value::RawValue as RawJsonValue};
|
||||||
|
|
||||||
pub use self::{filter::Matches, id::*, relation::RelationTypeEqual, type_ext::TypeExt};
|
pub use self::{
|
||||||
use super::{pdu::Pdu, state_key::StateKey};
|
filter::Matches,
|
||||||
|
id::*,
|
||||||
|
relation::RelationTypeEqual,
|
||||||
|
state_key::{StateKey, TypeStateKey},
|
||||||
|
type_ext::TypeExt,
|
||||||
|
};
|
||||||
|
use super::pdu::Pdu;
|
||||||
use crate::{Result, utils};
|
use crate::{Result, utils};
|
||||||
|
|
||||||
/// Abstraction of a PDU so users can have their own PDU types.
|
/// Abstraction of a PDU so users can have their own PDU types.
|
||||||
pub trait Event: Clone + Debug + Send + Sync {
|
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.
|
/// Serialize into a Ruma JSON format, consuming.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn into_format<T>(self) -> T
|
fn into_format<T>(self) -> T
|
||||||
@@ -152,6 +164,11 @@ pub trait Event: Clone + Debug + Send + Sync {
|
|||||||
/// All the authenticating events for this event.
|
/// All the authenticating events for this event.
|
||||||
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_;
|
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.
|
/// The event's content.
|
||||||
fn content(&self) -> &RawJsonValue;
|
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.
|
/// If this event is a redaction event this is the event it redacts.
|
||||||
fn redacts(&self) -> Option<&EventId>;
|
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.
|
/// The `RoomId` of this event.
|
||||||
fn room_id(&self) -> &RoomId;
|
fn room_id(&self) -> &RoomId;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use ruma::{
|
use ruma::{
|
||||||
events::{
|
events::{
|
||||||
AnyMessageLikeEvent, AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent,
|
AnyMessageLikeEvent, AnyStateEvent, AnyStrippedStateEvent, AnySyncMessageLikeEvent,
|
||||||
AnySyncTimelineEvent, AnyTimelineEvent, StateEvent, room::member::RoomMemberEventContent,
|
AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent, StateEvent,
|
||||||
space::child::HierarchySpaceChildEvent,
|
room::member::RoomMemberEventContent, space::child::HierarchySpaceChildEvent,
|
||||||
},
|
},
|
||||||
serde::Raw,
|
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> {
|
impl<E: Event> From<Owned<E>> for Raw<AnyStateEvent> {
|
||||||
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
|
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use ruma::{CanonicalJsonObject, OwnedEventId, RoomVersionId};
|
use ruma::{CanonicalJsonObject, OwnedEventId, RoomVersionId};
|
||||||
use serde_json::value::RawValue as RawJsonValue;
|
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.
|
/// Generates a correct eventId for the incoming pdu.
|
||||||
///
|
///
|
||||||
@@ -24,8 +24,8 @@ pub fn gen_event_id(
|
|||||||
value: &CanonicalJsonObject,
|
value: &CanonicalJsonObject,
|
||||||
room_version_id: &RoomVersionId,
|
room_version_id: &RoomVersionId,
|
||||||
) -> Result<OwnedEventId> {
|
) -> Result<OwnedEventId> {
|
||||||
let reference_hash = ruma::signatures::reference_hash(value, room_version_id)?;
|
let room_version_rules = room_version::rules(room_version_id)?;
|
||||||
let event_id: OwnedEventId = format!("${reference_hash}").try_into()?;
|
let reference_hash = ruma::signatures::reference_hash(value, &room_version_rules)?;
|
||||||
|
|
||||||
Ok(event_id)
|
OwnedEventId::from_parts('$', &reference_hash, None).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/core/matrix/event/state_key.rs
Normal file
17
src/core/matrix/event/state_key.rs
Normal 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)) }
|
||||||
@@ -21,12 +21,12 @@ impl TypeExt for &StateEventType {
|
|||||||
|
|
||||||
impl TypeExt for TimelineEventType {
|
impl TypeExt for TimelineEventType {
|
||||||
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
|
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 {
|
impl TypeExt for &TimelineEventType {
|
||||||
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod pdu;
|
pub mod pdu;
|
||||||
pub mod state_key;
|
pub mod room_version;
|
||||||
pub mod state_res;
|
pub mod state_res;
|
||||||
|
|
||||||
pub use event::{Event, TypeExt as EventTypeExt};
|
pub use event::{Event, StateKey, TypeExt as EventTypeExt, TypeStateKey, state_key};
|
||||||
pub use pdu::{Pdu, PduBuilder, PduCount, PduEvent, PduId, RawPduId, ShortId};
|
pub use pdu::{EventHash, Pdu, PduBuilder, PduCount, PduEvent, PduId, RawPduId, ShortId};
|
||||||
pub use state_key::StateKey;
|
pub use room_version::{RoomVersion, RoomVersionRules};
|
||||||
pub use state_res::{RoomVersion, StateMap, TypeStateKey};
|
pub use state_res::{StateMap, events};
|
||||||
|
|||||||
@@ -67,8 +67,25 @@ pub struct Pdu {
|
|||||||
// BTreeMap<Box<ServerName>, BTreeMap<ServerSigningKeyId, String>>
|
// BTreeMap<Box<ServerName>, BTreeMap<ServerSigningKeyId, String>>
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub signatures: Option<Box<RawJsonValue>>,
|
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 {
|
impl Pdu {
|
||||||
pub fn from_id_val(event_id: &EventId, mut json: CanonicalJsonObject) -> Result<Self> {
|
pub fn from_id_val(event_id: &EventId, mut json: CanonicalJsonObject) -> Result<Self> {
|
||||||
let event_id = CanonicalJsonValue::String(event_id.into());
|
let event_id = CanonicalJsonValue::String(event_id.into());
|
||||||
@@ -77,6 +94,20 @@ impl Pdu {
|
|||||||
.and_then(serde_json::from_value)
|
.and_then(serde_json::from_value)
|
||||||
.map_err(Into::into)
|
.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
|
impl Event for Pdu
|
||||||
@@ -88,6 +119,13 @@ where
|
|||||||
self.auth_events.iter().map(AsRef::as_ref)
|
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]
|
#[inline]
|
||||||
fn content(&self) -> &RawJsonValue { &self.content }
|
fn content(&self) -> &RawJsonValue { &self.content }
|
||||||
|
|
||||||
@@ -107,6 +145,14 @@ where
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn redacts(&self) -> Option<&EventId> { self.redacts.as_deref() }
|
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]
|
#[inline]
|
||||||
fn room_id(&self) -> &RoomId { &self.room_id }
|
fn room_id(&self) -> &RoomId { &self.room_id }
|
||||||
|
|
||||||
@@ -144,6 +190,13 @@ where
|
|||||||
self.auth_events.iter().map(AsRef::as_ref)
|
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]
|
#[inline]
|
||||||
fn content(&self) -> &RawJsonValue { &self.content }
|
fn content(&self) -> &RawJsonValue { &self.content }
|
||||||
|
|
||||||
@@ -163,6 +216,14 @@ where
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn redacts(&self) -> Option<&EventId> { self.redacts.as_deref() }
|
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]
|
#[inline]
|
||||||
fn room_id(&self) -> &RoomId { &self.room_id }
|
fn room_id(&self) -> &RoomId { &self.room_id }
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
|
|||||||
|
|
||||||
use ruma::{
|
use ruma::{
|
||||||
MilliSecondsSinceUnixEpoch, OwnedEventId,
|
MilliSecondsSinceUnixEpoch, OwnedEventId,
|
||||||
events::{EventContent, MessageLikeEventType, StateEventType, TimelineEventType},
|
events::{MessageLikeEventContent, StateEventContent, TimelineEventType},
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::value::{RawValue as RawJsonValue, to_raw_value};
|
use serde_json::value::{RawValue as RawJsonValue, to_raw_value};
|
||||||
@@ -33,7 +33,7 @@ type Unsigned = BTreeMap<String, serde_json::Value>;
|
|||||||
impl Builder {
|
impl Builder {
|
||||||
pub fn state<S, T>(state_key: S, content: &T) -> Self
|
pub fn state<S, T>(state_key: S, content: &T) -> Self
|
||||||
where
|
where
|
||||||
T: EventContent<EventType = StateEventType>,
|
T: StateEventContent,
|
||||||
S: Into<StateKey>,
|
S: Into<StateKey>,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
@@ -47,7 +47,7 @@ impl Builder {
|
|||||||
|
|
||||||
pub fn timeline<T>(content: &T) -> Self
|
pub fn timeline<T>(content: &T) -> Self
|
||||||
where
|
where
|
||||||
T: EventContent<EventType = MessageLikeEventType>,
|
T: MessageLikeEventContent,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
event_type: content.event_type().into(),
|
event_type: content.event_type().into(),
|
||||||
|
|||||||
@@ -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())
|
let mut content = serde_json::from_str(self.content.get())
|
||||||
.map_err(|e| err!(Request(BadJson("Failed to deserialize content into type: {e}"))))?;
|
.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))?;
|
.map_err(|e| Error::Redaction(self.sender.server_name().to_owned(), e))?;
|
||||||
|
|
||||||
let reason = serde_json::to_value(reason).expect("Failed to preserialize reason");
|
let reason = serde_json::to_value(reason).expect("Failed to preserialize reason");
|
||||||
|
|||||||
23
src/core/matrix/room_version.rs
Normal file
23
src/core/matrix/room_version.rs
Normal 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
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
use smallstr::SmallString;
|
|
||||||
|
|
||||||
pub type StateKey = SmallString<[u8; INLINE_SIZE]>;
|
|
||||||
|
|
||||||
const INLINE_SIZE: usize = 48;
|
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
|
#![cfg_attr(not(tuwunel_bench), allow(unused_imports, dead_code))]
|
||||||
|
|
||||||
#[cfg(tuwunel_bench)]
|
#[cfg(tuwunel_bench)]
|
||||||
extern crate test;
|
extern crate test;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Borrow,
|
borrow::Borrow,
|
||||||
collections::{HashMap, HashSet},
|
collections::HashMap,
|
||||||
sync::atomic::{AtomicU64, Ordering::SeqCst},
|
sync::atomic::{AtomicU64, Ordering::SeqCst},
|
||||||
};
|
};
|
||||||
|
|
||||||
use futures::{future, future::ready};
|
|
||||||
use maplit::{btreemap, hashmap, hashset};
|
|
||||||
use ruma::{
|
use ruma::{
|
||||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, Signatures, UserId,
|
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, UserId,
|
||||||
events::{
|
events::{
|
||||||
StateEventType, TimelineEventType,
|
TimelineEventType,
|
||||||
room::{
|
room::{
|
||||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||||
member::{MembershipState, RoomMemberEventContent},
|
member::{MembershipState, RoomMemberEventContent},
|
||||||
@@ -25,9 +25,11 @@ use serde_json::{
|
|||||||
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
|
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::{AuthSet, StateMap, test_utils::not_found};
|
||||||
use crate::{
|
use crate::{
|
||||||
matrix::{Event, Pdu, pdu::EventHash},
|
Result,
|
||||||
state_res::{self as state_res, Error, Result, StateMap},
|
matrix::{Event, EventHash, PduEvent, event::TypeExt},
|
||||||
|
utils::stream::IterStream,
|
||||||
};
|
};
|
||||||
|
|
||||||
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
|
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
|
||||||
@@ -35,6 +37,12 @@ static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
|
|||||||
#[cfg(tuwunel_bench)]
|
#[cfg(tuwunel_bench)]
|
||||||
#[cfg_attr(tuwunel_bench, bench)]
|
#[cfg_attr(tuwunel_bench, bench)]
|
||||||
fn lexico_topo_sort(c: &mut test::Bencher) {
|
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! {
|
let graph = hashmap! {
|
||||||
event_id("l") => hashset![event_id("o")],
|
event_id("l") => hashset![event_id("o")],
|
||||||
event_id("m") => hashset![event_id("n"), 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")],
|
event_id("p") => hashset![event_id("o")],
|
||||||
};
|
};
|
||||||
|
|
||||||
c.iter(|| {
|
c.iter(move || {
|
||||||
let _ = state_res::lexicographical_topological_sort(&graph, &|_| {
|
rt.block_on(async {
|
||||||
future::ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0))))
|
_ = 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(tuwunel_bench)]
|
||||||
#[cfg_attr(tuwunel_bench, bench)]
|
#[cfg_attr(tuwunel_bench, bench)]
|
||||||
fn resolution_shallow_auth_chain(c: &mut test::Bencher) {
|
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
|
// build up the DAG
|
||||||
let (state_at_bob, state_at_charlie, _) = store.set_up();
|
let (state_at_bob, state_at_charlie, _) = store.set_up();
|
||||||
|
|
||||||
c.iter(|| async {
|
let rules = RoomVersionId::V6.rules().unwrap();
|
||||||
let ev_map = store.0.clone();
|
let ev_map = store.0.clone();
|
||||||
let state_sets = [&state_at_bob, &state_at_charlie];
|
let state_sets = [state_at_bob, state_at_charlie];
|
||||||
let fetch = |id: OwnedEventId| ready(ev_map.get(&id).map(ToOwned::to_owned));
|
let auth_chains = state_sets
|
||||||
let exists = |id: OwnedEventId| ready(ev_map.get(&id).is_some());
|
.iter()
|
||||||
let auth_chain_sets: Vec<HashSet<_>> = state_sets
|
.map(|map| {
|
||||||
.iter()
|
store
|
||||||
.map(|map| {
|
.auth_event_ids(room_id(), map.values().cloned().collect())
|
||||||
store
|
.unwrap()
|
||||||
.auth_event_ids(room_id(), map.values().cloned().collect())
|
})
|
||||||
.unwrap()
|
.collect::<Vec<_>>();
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let _ = match state_res::resolve(
|
let func = async || {
|
||||||
&RoomVersionId::V6,
|
if let Err(e) = super::resolve(
|
||||||
state_sets.into_iter(),
|
&rules,
|
||||||
&auth_chain_sets,
|
state_sets.clone().into_iter().stream(),
|
||||||
&fetch,
|
auth_chains.clone().into_iter().stream(),
|
||||||
&exists,
|
&async |id| ev_map.get(&id).cloned().ok_or_else(not_found),
|
||||||
|
&async |id| ev_map.contains_key(&id),
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
| Ok(state) => state,
|
panic!("{e}")
|
||||||
| Err(e) => panic!("{e}"),
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
c.iter(move || {
|
||||||
|
rt.block_on(async {
|
||||||
|
func().await;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(tuwunel_bench)]
|
#[cfg(tuwunel_bench)]
|
||||||
#[cfg_attr(tuwunel_bench, bench)]
|
#[cfg_attr(tuwunel_bench, bench)]
|
||||||
fn resolve_deeper_event_set(c: &mut test::Bencher) {
|
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 mut inner = INITIAL_EVENTS();
|
||||||
let ban = BAN_STATE_SET();
|
let ban = BAN_STATE_SET();
|
||||||
|
|
||||||
@@ -97,66 +121,73 @@ fn resolve_deeper_event_set(c: &mut test::Bencher) {
|
|||||||
let store = TestStore(inner.clone());
|
let store = TestStore(inner.clone());
|
||||||
|
|
||||||
let state_set_a = [
|
let state_set_a = [
|
||||||
inner.get(&event_id("CREATE")).unwrap(),
|
&inner[&event_id("CREATE")],
|
||||||
inner.get(&event_id("IJR")).unwrap(),
|
&inner[&event_id("IJR")],
|
||||||
inner.get(&event_id("IMA")).unwrap(),
|
&inner[&event_id("IMA")],
|
||||||
inner.get(&event_id("IMB")).unwrap(),
|
&inner[&event_id("IMB")],
|
||||||
inner.get(&event_id("IMC")).unwrap(),
|
&inner[&event_id("IMC")],
|
||||||
inner.get(&event_id("MB")).unwrap(),
|
&inner[&event_id("MB")],
|
||||||
inner.get(&event_id("PA")).unwrap(),
|
&inner[&event_id("PA")],
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ev| {
|
.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(),
|
ev.event_id().to_owned(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<StateMap<_>>();
|
.collect::<StateMap<_>>();
|
||||||
|
|
||||||
let state_set_b = [
|
let state_set_b = [
|
||||||
inner.get(&event_id("CREATE")).unwrap(),
|
&inner[&event_id("CREATE")],
|
||||||
inner.get(&event_id("IJR")).unwrap(),
|
&inner[&event_id("IJR")],
|
||||||
inner.get(&event_id("IMA")).unwrap(),
|
&inner[&event_id("IMA")],
|
||||||
inner.get(&event_id("IMB")).unwrap(),
|
&inner[&event_id("IMB")],
|
||||||
inner.get(&event_id("IMC")).unwrap(),
|
&inner[&event_id("IMC")],
|
||||||
inner.get(&event_id("IME")).unwrap(),
|
&inner[&event_id("IME")],
|
||||||
inner.get(&event_id("PA")).unwrap(),
|
&inner[&event_id("PA")],
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ev| {
|
.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(),
|
ev.event_id().to_owned(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<StateMap<_>>();
|
.collect::<StateMap<_>>();
|
||||||
|
|
||||||
c.iter(|| async {
|
let rules = RoomVersionId::V6.rules().unwrap();
|
||||||
let state_sets = [&state_set_a, &state_set_b];
|
let state_sets = [state_set_a, state_set_b];
|
||||||
let auth_chain_sets: Vec<HashSet<_>> = state_sets
|
let auth_chains = state_sets
|
||||||
.iter()
|
.iter()
|
||||||
.map(|map| {
|
.map(|map| {
|
||||||
store
|
store
|
||||||
.auth_event_ids(room_id(), map.values().cloned().collect())
|
.auth_event_ids(room_id(), map.values().cloned().collect())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let fetch = |id: OwnedEventId| ready(inner.get(&id).map(ToOwned::to_owned));
|
let func = async || {
|
||||||
let exists = |id: OwnedEventId| ready(inner.get(&id).is_some());
|
if let Err(e) = super::resolve(
|
||||||
let _ = match state_res::resolve(
|
&rules,
|
||||||
&RoomVersionId::V6,
|
state_sets.clone().into_iter().stream(),
|
||||||
state_sets.into_iter(),
|
auth_chains.clone().into_iter().stream(),
|
||||||
&auth_chain_sets,
|
&async |id| inner.get(&id).cloned().ok_or_else(not_found),
|
||||||
&fetch,
|
&async |id| inner.contains_key(&id),
|
||||||
&exists,
|
false,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
| Ok(state) => state,
|
panic!("{e}")
|
||||||
| Err(_) => panic!("resolution failed during benchmarking"),
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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>);
|
struct TestStore<E: Event>(HashMap<OwnedEventId, E>);
|
||||||
|
|
||||||
#[allow(unused)]
|
#[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> {
|
fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result<E> {
|
||||||
self.0
|
self.0
|
||||||
.get(event_id)
|
.get(event_id)
|
||||||
.cloned()
|
.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
|
/// Returns the events that correspond to the `event_ids` sorted in the same
|
||||||
@@ -191,13 +222,12 @@ impl<E: Event + Clone> TestStore<E> {
|
|||||||
&self,
|
&self,
|
||||||
room_id: &RoomId,
|
room_id: &RoomId,
|
||||||
event_ids: Vec<OwnedEventId>,
|
event_ids: Vec<OwnedEventId>,
|
||||||
) -> Result<HashSet<OwnedEventId>> {
|
) -> Result<AuthSet<OwnedEventId>> {
|
||||||
let mut result = HashSet::new();
|
let mut result = AuthSet::new();
|
||||||
let mut stack = event_ids;
|
let mut stack = event_ids;
|
||||||
|
|
||||||
// DFS for auth event chain
|
// DFS for auth event chain
|
||||||
while !stack.is_empty() {
|
while let Some(ev_id) = stack.pop() {
|
||||||
let ev_id = stack.pop().unwrap();
|
|
||||||
if result.contains(&ev_id) {
|
if result.contains(&ev_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -226,7 +256,8 @@ impl<E: Event + Clone> TestStore<E> {
|
|||||||
let chain = self
|
let chain = self
|
||||||
.auth_event_ids(room_id, ids)?
|
.auth_event_ids(room_id, ids)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<HashSet<_>>();
|
.collect::<AuthSet<_>>();
|
||||||
|
|
||||||
auth_chain_sets.push(chain);
|
auth_chain_sets.push(chain);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +265,7 @@ impl<E: Event + Clone> TestStore<E> {
|
|||||||
let common = auth_chain_sets
|
let common = auth_chain_sets
|
||||||
.iter()
|
.iter()
|
||||||
.skip(1)
|
.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
|
Ok(auth_chain_sets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -247,7 +278,7 @@ impl<E: Event + Clone> TestStore<E> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestStore<Pdu> {
|
impl TestStore<PduEvent> {
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
fn set_up(
|
fn set_up(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -261,8 +292,9 @@ impl TestStore<Pdu> {
|
|||||||
&[],
|
&[],
|
||||||
&[],
|
&[],
|
||||||
);
|
);
|
||||||
let cre = create_event.event_id().to_owned();
|
let cre = create_event.event_id();
|
||||||
self.0.insert(cre.clone(), create_event.clone());
|
self.0
|
||||||
|
.insert(cre.to_owned(), create_event.clone());
|
||||||
|
|
||||||
let alice_mem = to_pdu_event(
|
let alice_mem = to_pdu_event(
|
||||||
"IMA",
|
"IMA",
|
||||||
@@ -270,8 +302,8 @@ impl TestStore<Pdu> {
|
|||||||
TimelineEventType::RoomMember,
|
TimelineEventType::RoomMember,
|
||||||
Some(alice().to_string().as_str()),
|
Some(alice().to_string().as_str()),
|
||||||
member_content_join(),
|
member_content_join(),
|
||||||
&[cre.clone()],
|
&[cre.to_owned()],
|
||||||
&[cre.clone()],
|
&[cre.to_owned()],
|
||||||
);
|
);
|
||||||
self.0
|
self.0
|
||||||
.insert(alice_mem.event_id().to_owned(), alice_mem.clone());
|
.insert(alice_mem.event_id().to_owned(), alice_mem.clone());
|
||||||
@@ -282,7 +314,7 @@ impl TestStore<Pdu> {
|
|||||||
TimelineEventType::RoomJoinRules,
|
TimelineEventType::RoomJoinRules,
|
||||||
Some(""),
|
Some(""),
|
||||||
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
|
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()],
|
&[alice_mem.event_id().to_owned()],
|
||||||
);
|
);
|
||||||
self.0
|
self.0
|
||||||
@@ -296,7 +328,7 @@ impl TestStore<Pdu> {
|
|||||||
TimelineEventType::RoomMember,
|
TimelineEventType::RoomMember,
|
||||||
Some(bob().to_string().as_str()),
|
Some(bob().to_string().as_str()),
|
||||||
member_content_join(),
|
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()],
|
&[join_rules.event_id().to_owned()],
|
||||||
);
|
);
|
||||||
self.0
|
self.0
|
||||||
@@ -308,7 +340,7 @@ impl TestStore<Pdu> {
|
|||||||
TimelineEventType::RoomMember,
|
TimelineEventType::RoomMember,
|
||||||
Some(charlie().to_string().as_str()),
|
Some(charlie().to_string().as_str()),
|
||||||
member_content_join(),
|
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()],
|
&[join_rules.event_id().to_owned()],
|
||||||
);
|
);
|
||||||
self.0
|
self.0
|
||||||
@@ -316,30 +348,33 @@ impl TestStore<Pdu> {
|
|||||||
|
|
||||||
let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem]
|
let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ev| {
|
.map(|e| {
|
||||||
(
|
(
|
||||||
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
|
e.event_type()
|
||||||
ev.event_id().to_owned(),
|
.with_state_key(e.state_key().unwrap()),
|
||||||
|
e.event_id().to_owned(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<StateMap<_>>();
|
.collect::<StateMap<_>>();
|
||||||
|
|
||||||
let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem]
|
let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ev| {
|
.map(|e| {
|
||||||
(
|
(
|
||||||
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
|
e.event_type()
|
||||||
ev.event_id().to_owned(),
|
.with_state_key(e.state_key().unwrap()),
|
||||||
|
e.event_id().to_owned(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<StateMap<_>>();
|
.collect::<StateMap<_>>();
|
||||||
|
|
||||||
let expected = [&create_event, &alice_mem, &join_rules, &bob_mem, &charlie_mem]
|
let expected = [&create_event, &alice_mem, &join_rules, &bob_mem, &charlie_mem]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ev| {
|
.map(|e| {
|
||||||
(
|
(
|
||||||
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
|
e.event_type()
|
||||||
ev.event_id().to_owned(),
|
.with_state_key(e.state_key().unwrap()),
|
||||||
|
e.event_id().to_owned(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<StateMap<_>>();
|
.collect::<StateMap<_>>();
|
||||||
@@ -352,7 +387,7 @@ fn event_id(id: &str) -> OwnedEventId {
|
|||||||
if id.contains('$') {
|
if id.contains('$') {
|
||||||
return id.try_into().unwrap();
|
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") }
|
fn alice() -> &'static UserId { user_id!("@alice:foo") }
|
||||||
@@ -381,7 +416,7 @@ fn to_pdu_event<S>(
|
|||||||
content: Box<RawJsonValue>,
|
content: Box<RawJsonValue>,
|
||||||
auth_events: &[S],
|
auth_events: &[S],
|
||||||
prev_events: &[S],
|
prev_events: &[S],
|
||||||
) -> Pdu
|
) -> PduEvent
|
||||||
where
|
where
|
||||||
S: AsRef<str>,
|
S: AsRef<str>,
|
||||||
{
|
{
|
||||||
@@ -391,28 +426,31 @@ where
|
|||||||
let id = if id.contains('$') {
|
let id = if id.contains('$') {
|
||||||
id.to_owned()
|
id.to_owned()
|
||||||
} else {
|
} else {
|
||||||
format!("${}:foo", id)
|
format!("${id}:foo")
|
||||||
};
|
};
|
||||||
|
|
||||||
let auth_events = auth_events
|
let auth_events = auth_events
|
||||||
.iter()
|
.iter()
|
||||||
.map(AsRef::as_ref)
|
.map(AsRef::as_ref)
|
||||||
.map(event_id)
|
.map(event_id)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let prev_events = prev_events
|
let prev_events = prev_events
|
||||||
.iter()
|
.iter()
|
||||||
.map(AsRef::as_ref)
|
.map(AsRef::as_ref)
|
||||||
.map(event_id)
|
.map(event_id)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
Pdu {
|
let state_key = state_key.map(ToOwned::to_owned);
|
||||||
|
PduEvent {
|
||||||
event_id: id.try_into().unwrap(),
|
event_id: id.try_into().unwrap(),
|
||||||
room_id: room_id().to_owned(),
|
room_id: room_id().to_owned(),
|
||||||
sender: sender.to_owned(),
|
sender: sender.to_owned(),
|
||||||
|
origin: None,
|
||||||
origin_server_ts: ts.try_into().unwrap(),
|
origin_server_ts: ts.try_into().unwrap(),
|
||||||
state_key: state_key.map(Into::into),
|
state_key: state_key.map(Into::into),
|
||||||
kind: ev_type,
|
kind: ev_type,
|
||||||
content,
|
content,
|
||||||
origin: None,
|
|
||||||
redacts: None,
|
redacts: None,
|
||||||
unsigned: None,
|
unsigned: None,
|
||||||
auth_events,
|
auth_events,
|
||||||
@@ -420,12 +458,14 @@ where
|
|||||||
depth: uint!(0),
|
depth: uint!(0),
|
||||||
hashes: EventHash::default(),
|
hashes: EventHash::default(),
|
||||||
signatures: None,
|
signatures: None,
|
||||||
|
#[cfg(test)]
|
||||||
|
rejected: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// all graphs start with these input events
|
// all graphs start with these input events
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
|
fn INITIAL_EVENTS() -> HashMap<OwnedEventId, PduEvent> {
|
||||||
vec![
|
vec![
|
||||||
to_pdu_event::<&EventId>(
|
to_pdu_event::<&EventId>(
|
||||||
"CREATE",
|
"CREATE",
|
||||||
@@ -507,7 +547,7 @@ fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
|
|||||||
|
|
||||||
// all graphs start with these input events
|
// all graphs start with these input events
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
fn BAN_STATE_SET() -> HashMap<OwnedEventId, Pdu> {
|
fn BAN_STATE_SET() -> HashMap<OwnedEventId, PduEvent> {
|
||||||
vec![
|
vec![
|
||||||
to_pdu_event(
|
to_pdu_event(
|
||||||
"PA",
|
"PA",
|
||||||
|
|||||||
@@ -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
119
src/core/matrix/state_res/event_auth/auth_types.rs
Normal file
119
src/core/matrix/state_res/event_auth/auth_types.rs
Normal 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 sender’s current `m.room.member` event, if any.
|
||||||
|
if *event_type != TimelineEventType::RoomCreate {
|
||||||
|
// v1-v11, the `m.room.create` event.
|
||||||
|
if !rules.room_create_event_id_as_room_id || always_create {
|
||||||
|
auth_types.push((StateEventType::RoomCreate, "".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
auth_types.push((StateEventType::RoomPowerLevels, "".into()));
|
||||||
|
auth_types.push((StateEventType::RoomMember, sender.as_str().into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If type is `m.room.member`:
|
||||||
|
if *event_type == TimelineEventType::RoomMember {
|
||||||
|
auth_types_for_member_event(&mut auth_types, state_key, content, rules)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(auth_types)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auth_types_for_member_event(
|
||||||
|
auth_types: &mut AuthTypes,
|
||||||
|
state_key: Option<&str>,
|
||||||
|
content: &RawJsonValue,
|
||||||
|
rules: &AuthorizationRules,
|
||||||
|
) -> Result {
|
||||||
|
// The target’s current `m.room.member` event, if any.
|
||||||
|
let Some(state_key) = state_key else {
|
||||||
|
return Err!("missing `state_key` field for `m.room.member` event");
|
||||||
|
};
|
||||||
|
|
||||||
|
let key = (StateEventType::RoomMember, state_key.into());
|
||||||
|
if !auth_types.contains(&key) {
|
||||||
|
auth_types.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = RoomMemberEventContent::new(content);
|
||||||
|
let membership = content.membership()?;
|
||||||
|
|
||||||
|
// If `membership` is `join`, `invite` or `knock`, the current
|
||||||
|
// `m.room.join_rules` event, if any.
|
||||||
|
if matches!(
|
||||||
|
membership,
|
||||||
|
MembershipState::Join | MembershipState::Invite | MembershipState::Knock
|
||||||
|
) {
|
||||||
|
let key = (StateEventType::RoomJoinRules, "".into());
|
||||||
|
if !auth_types.contains(&key) {
|
||||||
|
auth_types.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If `membership` is `invite` and `content` contains a `third_party_invite`
|
||||||
|
// property, the current `m.room.third_party_invite` event with `state_key`
|
||||||
|
// matching `content.third_party_invite.signed.token`, if any.
|
||||||
|
if membership == MembershipState::Invite {
|
||||||
|
let third_party_invite = content.third_party_invite()?;
|
||||||
|
if let Some(third_party_invite) = third_party_invite {
|
||||||
|
let token = third_party_invite.token()?.into();
|
||||||
|
let key = (StateEventType::RoomThirdPartyInvite, token);
|
||||||
|
if !auth_types.contains(&key) {
|
||||||
|
auth_types.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If `content.join_authorised_via_users_server` is present, and the room
|
||||||
|
// version supports restricted rooms, then the `m.room.member` event with
|
||||||
|
// `state_key` matching `content.join_authorised_via_users_server`.
|
||||||
|
//
|
||||||
|
// Note: And the membership is join (https://github.com/matrix-org/matrix-spec/pull/2100)
|
||||||
|
if membership == MembershipState::Join && rules.restricted_join_rule {
|
||||||
|
let join_authorised_via_users_server = content.join_authorised_via_users_server()?;
|
||||||
|
if let Some(user_id) = join_authorised_via_users_server {
|
||||||
|
let key = (StateEventType::RoomMember, user_id.as_str().into());
|
||||||
|
if !auth_types.contains(&key) {
|
||||||
|
auth_types.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
587
src/core/matrix/state_res/event_auth/room_member.rs
Normal file
587
src/core/matrix/state_res/event_auth/room_member.rs
Normal 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 sender’s current membership state is not join, reject.
|
||||||
|
let sender_membership = sender_membership?;
|
||||||
|
if sender_membership != MembershipState::Join {
|
||||||
|
return Err!("cannot invite user if sender is not joined");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since v1, if target user’s current membership state is join or ban, reject.
|
||||||
|
let current_target_user_membership = current_target_user_membership?;
|
||||||
|
if matches!(current_target_user_membership, MembershipState::Join | MembershipState::Ban) {
|
||||||
|
return Err!("cannot invite user that is joined or banned");
|
||||||
|
}
|
||||||
|
|
||||||
|
let creators = room_create_event.creators(rules)?;
|
||||||
|
let sender_power_level =
|
||||||
|
room_power_levels_event.user_power_level(room_member_event.sender(), creators, rules)?;
|
||||||
|
|
||||||
|
let invite_power_level =
|
||||||
|
room_power_levels_event.get_as_int_or_default(RoomPowerLevelsIntField::Invite, rules)?;
|
||||||
|
|
||||||
|
// Since v1, if the sender’s power level is greater than or equal to the invite
|
||||||
|
// level, allow. Otherwise, reject.
|
||||||
|
if sender_power_level < invite_power_level {
|
||||||
|
return Err!("sender does not have enough power to invite");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the `third_party_invite` from the `m.room.member` event passes
|
||||||
|
/// the authorization rules.
|
||||||
|
#[tracing::instrument(level = "trace", skip_all)]
|
||||||
|
async fn check_third_party_invite<Fetch, Fut, Pdu>(
|
||||||
|
room_member_event: &RoomMemberEvent<Pdu>,
|
||||||
|
third_party_invite: &ThirdPartyInvite,
|
||||||
|
target_user: &UserId,
|
||||||
|
fetch_state: &Fetch,
|
||||||
|
) -> Result
|
||||||
|
where
|
||||||
|
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
|
||||||
|
Fut: Future<Output = Result<Pdu>> + Send,
|
||||||
|
Pdu: Event,
|
||||||
|
{
|
||||||
|
let current_target_user_membership = fetch_state.user_membership(target_user).await?;
|
||||||
|
|
||||||
|
// Since v1, if target user is banned, reject.
|
||||||
|
if current_target_user_membership == MembershipState::Ban {
|
||||||
|
return Err!("cannot invite user that is banned");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since v1, if content.third_party_invite does not have a signed property,
|
||||||
|
// reject. Since v1, if signed does not have mxid and token properties, reject.
|
||||||
|
let third_party_invite_token = third_party_invite.token()?;
|
||||||
|
let third_party_invite_mxid = third_party_invite.mxid()?;
|
||||||
|
|
||||||
|
// Since v1, if mxid does not match state_key, reject.
|
||||||
|
if target_user != third_party_invite_mxid {
|
||||||
|
return Err!("third-party invite mxid does not match target user");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since v1, if there is no m.room.third_party_invite event in the current room
|
||||||
|
// state with state_key matching token, reject.
|
||||||
|
let Some(room_third_party_invite_event) = fetch_state
|
||||||
|
.room_third_party_invite_event(third_party_invite_token)
|
||||||
|
.await
|
||||||
|
else {
|
||||||
|
return Err!("no `m.room.third_party_invite` in room state matches the token");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Since v1, if sender does not match sender of the m.room.third_party_invite,
|
||||||
|
// reject.
|
||||||
|
if room_member_event.sender() != room_third_party_invite_event.sender() {
|
||||||
|
return Err!(
|
||||||
|
"sender of `m.room.third_party_invite` does not match sender of `m.room.member`"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let signatures = third_party_invite.signatures()?;
|
||||||
|
let public_keys = room_third_party_invite_event.public_keys()?;
|
||||||
|
let signed_canonical_json = third_party_invite.signed_canonical_json()?;
|
||||||
|
|
||||||
|
// Since v1, if any signature in signed matches any public key in the
|
||||||
|
// m.room.third_party_invite event, allow.
|
||||||
|
for entity_signatures_value in signatures.values() {
|
||||||
|
let Some(entity_signatures) = entity_signatures_value.as_object() else {
|
||||||
|
return Err!(Request(InvalidParam(
|
||||||
|
"unexpected format of `signatures` field in `third_party_invite.signed` of \
|
||||||
|
`m.room.member` event: expected a map of string to object, got \
|
||||||
|
{entity_signatures_value:?}"
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
// We will ignore any error from now on, we just want to find a signature that
|
||||||
|
// can be verified from a public key.
|
||||||
|
|
||||||
|
for (key_id, signature_value) in entity_signatures {
|
||||||
|
let Ok(parsed_key_id) = <&SigningKeyId<AnyKeyName>>::try_from(key_id.as_str()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(signature_str) = signature_value.as_str() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(signature) = Base64::<Standard>::parse(signature_str) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let algorithm = parsed_key_id.algorithm();
|
||||||
|
for encoded_public_key in &public_keys {
|
||||||
|
let Ok(public_key) = encoded_public_key.decode() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if verify_canonical_json_bytes(
|
||||||
|
&algorithm,
|
||||||
|
&public_key,
|
||||||
|
signature.as_bytes(),
|
||||||
|
signed_canonical_json.as_bytes(),
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, reject.
|
||||||
|
Err!(
|
||||||
|
"no signature on third-party invite matches a public key in `m.room.third_party_invite` \
|
||||||
|
event"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the given event passes the `m.room.member` authorization rules
|
||||||
|
/// with a membership of `leave`.
|
||||||
|
#[tracing::instrument(level = "trace", skip_all)]
|
||||||
|
async fn check_room_member_leave<Fetch, Fut, Pdu>(
|
||||||
|
room_member_event: &RoomMemberEvent<Pdu>,
|
||||||
|
target_user: &UserId,
|
||||||
|
rules: &AuthorizationRules,
|
||||||
|
room_create_event: &RoomCreateEvent<Pdu>,
|
||||||
|
fetch_state: &Fetch,
|
||||||
|
) -> Result
|
||||||
|
where
|
||||||
|
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
|
||||||
|
Fut: Future<Output = Result<Pdu>> + Send,
|
||||||
|
Pdu: Event,
|
||||||
|
{
|
||||||
|
let (sender_membership, room_power_levels_event, current_target_user_membership) = join3(
|
||||||
|
fetch_state.user_membership(room_member_event.sender()),
|
||||||
|
fetch_state.room_power_levels_event(),
|
||||||
|
fetch_state.user_membership(target_user),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let sender_membership = sender_membership?;
|
||||||
|
|
||||||
|
// v1-v6, if the sender matches state_key, allow if and only if that user’s
|
||||||
|
// current membership state is invite or join.
|
||||||
|
// Since v7, if the sender matches state_key, allow if and only if that user’s
|
||||||
|
// current membership state is invite, join, or knock.
|
||||||
|
if room_member_event.sender() == target_user {
|
||||||
|
let membership_is_invite_or_join =
|
||||||
|
matches!(sender_membership, MembershipState::Join | MembershipState::Invite);
|
||||||
|
let membership_is_knock = rules.knocking && sender_membership == MembershipState::Knock;
|
||||||
|
|
||||||
|
return if membership_is_invite_or_join || membership_is_knock {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err!("cannot leave if not joined, invited or knocked")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since v1, if the sender’s current membership state is not join, reject.
|
||||||
|
if sender_membership != MembershipState::Join {
|
||||||
|
return Err!("cannot kick if sender is not joined");
|
||||||
|
}
|
||||||
|
|
||||||
|
let creators = room_create_event.creators(rules)?;
|
||||||
|
let current_target_user_membership = current_target_user_membership?;
|
||||||
|
|
||||||
|
let sender_power_level = room_power_levels_event.user_power_level(
|
||||||
|
room_member_event.sender(),
|
||||||
|
creators.clone(),
|
||||||
|
rules,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let ban_power_level =
|
||||||
|
room_power_levels_event.get_as_int_or_default(RoomPowerLevelsIntField::Ban, rules)?;
|
||||||
|
|
||||||
|
// Since v1, if the target user’s current membership state is ban, and the
|
||||||
|
// sender’s power level is less than the ban level, reject.
|
||||||
|
if current_target_user_membership == MembershipState::Ban
|
||||||
|
&& sender_power_level < ban_power_level
|
||||||
|
{
|
||||||
|
return Err!("sender does not have enough power to unban");
|
||||||
|
}
|
||||||
|
|
||||||
|
let kick_power_level =
|
||||||
|
room_power_levels_event.get_as_int_or_default(RoomPowerLevelsIntField::Kick, rules)?;
|
||||||
|
|
||||||
|
let target_user_power_level =
|
||||||
|
room_power_levels_event.user_power_level(target_user, creators, rules)?;
|
||||||
|
|
||||||
|
// Since v1, if the sender’s power level is greater than or equal to the kick
|
||||||
|
// level, and the target user’s power level is less than the sender’s power
|
||||||
|
// level, allow.
|
||||||
|
//
|
||||||
|
// Otherwise, reject.
|
||||||
|
if sender_power_level >= kick_power_level && target_user_power_level < sender_power_level {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err!("sender does not have enough power to kick target user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the given event passes the `m.room.member` authorization rules
|
||||||
|
/// with a membership of `ban`.
|
||||||
|
#[tracing::instrument(level = "trace", skip_all)]
|
||||||
|
async fn check_room_member_ban<Fetch, Fut, Pdu>(
|
||||||
|
room_member_event: &RoomMemberEvent<Pdu>,
|
||||||
|
target_user: &UserId,
|
||||||
|
rules: &AuthorizationRules,
|
||||||
|
room_create_event: &RoomCreateEvent<Pdu>,
|
||||||
|
fetch_state: &Fetch,
|
||||||
|
) -> Result
|
||||||
|
where
|
||||||
|
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
|
||||||
|
Fut: Future<Output = Result<Pdu>> + Send,
|
||||||
|
Pdu: Event,
|
||||||
|
{
|
||||||
|
let (sender_membership, room_power_levels_event) = join(
|
||||||
|
fetch_state.user_membership(room_member_event.sender()),
|
||||||
|
fetch_state.room_power_levels_event(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Since v1, if the sender’s current membership state is not join, reject.
|
||||||
|
let sender_membership = sender_membership?;
|
||||||
|
if sender_membership != MembershipState::Join {
|
||||||
|
return Err!("cannot ban if sender is not joined");
|
||||||
|
}
|
||||||
|
|
||||||
|
let creators = room_create_event.creators(rules)?;
|
||||||
|
|
||||||
|
let sender_power_level = room_power_levels_event.user_power_level(
|
||||||
|
room_member_event.sender(),
|
||||||
|
creators.clone(),
|
||||||
|
rules,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let ban_power_level =
|
||||||
|
room_power_levels_event.get_as_int_or_default(RoomPowerLevelsIntField::Ban, rules)?;
|
||||||
|
|
||||||
|
let target_user_power_level =
|
||||||
|
room_power_levels_event.user_power_level(target_user, creators, rules)?;
|
||||||
|
|
||||||
|
// If the sender’s power level is greater than or equal to the ban level, and
|
||||||
|
// the target user’s power level is less than the sender’s power level, allow.
|
||||||
|
//
|
||||||
|
// Otherwise, reject.
|
||||||
|
if sender_power_level >= ban_power_level && target_user_power_level < sender_power_level {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err!("sender does not have enough power to ban target user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the given event passes the `m.room.member` authorization rules
|
||||||
|
/// with a membership of `knock`.
|
||||||
|
#[tracing::instrument(level = "trace", skip_all)]
|
||||||
|
async fn check_room_member_knock<Fetch, Fut, Pdu>(
|
||||||
|
room_member_event: &RoomMemberEvent<Pdu>,
|
||||||
|
target_user: &UserId,
|
||||||
|
rules: &AuthorizationRules,
|
||||||
|
fetch_state: &Fetch,
|
||||||
|
) -> Result
|
||||||
|
where
|
||||||
|
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
|
||||||
|
Fut: Future<Output = Result<Pdu>> + Send,
|
||||||
|
Pdu: Event,
|
||||||
|
{
|
||||||
|
let sender = room_member_event.sender();
|
||||||
|
let (join_rule, sender_membership) =
|
||||||
|
join(fetch_state.join_rule(), fetch_state.user_membership(sender)).await;
|
||||||
|
|
||||||
|
// v7-v9, if the join_rule is anything other than knock, reject.
|
||||||
|
// Since v10, if the join_rule is anything other than knock or knock_restricted,
|
||||||
|
// reject.
|
||||||
|
let join_rule = join_rule?;
|
||||||
|
if join_rule != JoinRule::Knock
|
||||||
|
&& (rules.knock_restricted_join_rule && !matches!(join_rule, JoinRule::KnockRestricted))
|
||||||
|
{
|
||||||
|
return Err!(
|
||||||
|
"join rule is not set to knock or knock_restricted, knocking is not allowed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since v7, if sender does not match state_key, reject.
|
||||||
|
if room_member_event.sender() != target_user {
|
||||||
|
return Err!("cannot make another user knock, sender does not match target user");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since v7, if the sender’s current membership is not ban, invite, or join,
|
||||||
|
// allow. Otherwise, reject.
|
||||||
|
let sender_membership = sender_membership?;
|
||||||
|
if !matches!(
|
||||||
|
sender_membership,
|
||||||
|
MembershipState::Ban | MembershipState::Invite | MembershipState::Join
|
||||||
|
) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err!("cannot knock if user is banned, invited or joined")
|
||||||
|
}
|
||||||
|
}
|
||||||
2605
src/core/matrix/state_res/event_auth/room_member/tests.rs
Normal file
2605
src/core/matrix/state_res/event_auth/room_member/tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
1054
src/core/matrix/state_res/event_auth/tests.rs
Normal file
1054
src/core/matrix/state_res/event_auth/tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
1274
src/core/matrix/state_res/event_auth/tests/room_power_levels.rs
Normal file
1274
src/core/matrix/state_res/event_auth/tests/room_power_levels.rs
Normal file
File diff suppressed because it is too large
Load Diff
461
src/core/matrix/state_res/event_format.rs
Normal file
461
src/core/matrix/state_res/event_format.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/core/matrix/state_res/events.rs
Normal file
50
src/core/matrix/state_res/events.rs
Normal 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 someone’s ability to do something in the room.
|
||||||
|
pub(super) fn is_power_event<Pdu>(event: &Pdu) -> bool
|
||||||
|
where
|
||||||
|
Pdu: crate::matrix::Event,
|
||||||
|
{
|
||||||
|
use ruma::events::{TimelineEventType, room::member::MembershipState};
|
||||||
|
|
||||||
|
match event.event_type() {
|
||||||
|
| TimelineEventType::RoomPowerLevels
|
||||||
|
| TimelineEventType::RoomJoinRules
|
||||||
|
| TimelineEventType::RoomCreate => event.state_key() == Some(""),
|
||||||
|
| TimelineEventType::RoomMember => {
|
||||||
|
let content = RoomMemberEventContent::new(event.content());
|
||||||
|
if content.membership().is_ok_and(|membership| {
|
||||||
|
matches!(membership, MembershipState::Leave | MembershipState::Ban)
|
||||||
|
}) {
|
||||||
|
return Some(event.sender().as_str()) != event.state_key();
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
},
|
||||||
|
| _ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
152
src/core/matrix/state_res/events/create.rs
Normal file
152
src/core/matrix/state_res/events/create.rs
Normal 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 }
|
||||||
|
}
|
||||||
74
src/core/matrix/state_res/events/join_rules.rs
Normal file
74
src/core/matrix/state_res/events/join_rules.rs
Normal 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>);
|
||||||
207
src/core/matrix/state_res/events/member.rs
Normal file
207
src/core/matrix/state_res/events/member.rs
Normal 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}"
|
||||||
|
)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
401
src/core/matrix/state_res/events/power_levels.rs
Normal file
401
src/core/matrix/state_res/events/power_levels.rs
Normal 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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/core/matrix/state_res/events/third_party_invite.rs
Normal file
61
src/core/matrix/state_res/events/third_party_invite.rs
Normal 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 }
|
||||||
|
}
|
||||||
74
src/core/matrix/state_res/fetch_state.rs
Normal file
74
src/core/matrix/state_res/fetch_state.rs
Normal 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
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
213
src/core/matrix/state_res/resolve.rs
Normal file
213
src/core/matrix/state_res/resolve.rs
Normal 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 weren’t picked in step 1 and order them by
|
||||||
|
// the mainline ordering based on the power level in the partially resolved
|
||||||
|
// state obtained in step 2.
|
||||||
|
let sorted_remaining_events: 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)
|
||||||
|
}
|
||||||
40
src/core/matrix/state_res/resolve/auth_difference.rs
Normal file
40
src/core/matrix/state_res/resolve/auth_difference.rs
Normal 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 doesn’t appear in every auth chain.
|
||||||
|
/// If _Ci_ is the full auth chain of _Si_, then the auth difference is ∪_Ci_ −
|
||||||
|
/// ∩_Ci_.
|
||||||
|
///
|
||||||
|
/// ## Arguments
|
||||||
|
///
|
||||||
|
/// * `auth_chains` - The list of full recursive sets of `auth_events`. Inputs
|
||||||
|
/// must be sorted.
|
||||||
|
///
|
||||||
|
/// ## Returns
|
||||||
|
///
|
||||||
|
/// Outputs the event IDs that are not present in all the auth chains.
|
||||||
|
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()
|
||||||
|
}
|
||||||
124
src/core/matrix/state_res/resolve/conflicted_subgraph.rs
Normal file
124
src/core/matrix/state_res/resolve/conflicted_subgraph.rs
Normal 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()))
|
||||||
|
}
|
||||||
203
src/core/matrix/state_res/resolve/iterative_auth_check.rs
Normal file
203
src/core/matrix/state_res/resolve/iterative_auth_check.rs
Normal 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
|
||||||
|
/// event’s auth_events is used if the auth event is not rejected.
|
||||||
|
///
|
||||||
|
/// ## Arguments
|
||||||
|
///
|
||||||
|
/// * `rules` - The authorization rules for the current room version.
|
||||||
|
/// * `events` - The sorted state events to apply to the `partial_state`.
|
||||||
|
/// * `state` - The current state that was partially resolved for the room.
|
||||||
|
/// * `fetch_event` - Function to fetch an event in the room given its event ID.
|
||||||
|
///
|
||||||
|
/// ## Returns
|
||||||
|
///
|
||||||
|
/// Returns the partially resolved state, or an `Err(_)` if one of the state
|
||||||
|
/// events in the room has an unexpected format.
|
||||||
|
#[tracing::instrument(
|
||||||
|
name = "iterative_auth",
|
||||||
|
level = "debug",
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
states = ?state.len(),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub(super) async fn iterative_auth_check<'b, SortedPowerEvents, Fetch, Fut, Pdu>(
|
||||||
|
rules: &RoomVersionRules,
|
||||||
|
events: SortedPowerEvents,
|
||||||
|
state: StateMap<OwnedEventId>,
|
||||||
|
fetch: &Fetch,
|
||||||
|
) -> Result<StateMap<OwnedEventId>>
|
||||||
|
where
|
||||||
|
SortedPowerEvents: Stream<Item = &'b EventId> + Send,
|
||||||
|
Fetch: Fn(OwnedEventId) -> Fut + Sync,
|
||||||
|
Fut: Future<Output = Result<Pdu>> + Send,
|
||||||
|
Pdu: Event,
|
||||||
|
{
|
||||||
|
events
|
||||||
|
.map(Ok)
|
||||||
|
.wide_and_then(async |event_id| {
|
||||||
|
let event = fetch(event_id.to_owned()).await?;
|
||||||
|
let state_key: StateKey = event
|
||||||
|
.state_key()
|
||||||
|
.ok_or_else(|| err!(Request(InvalidParam("Missing state_key"))))?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
Ok((event_id, state_key, event))
|
||||||
|
})
|
||||||
|
.try_fold(state, |state, (event_id, state_key, event)| {
|
||||||
|
auth_check(rules, state, event_id, state_key, event, fetch)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
name = "check",
|
||||||
|
level = "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)
|
||||||
|
}
|
||||||
171
src/core/matrix/state_res/resolve/mainline_sort.rs
Normal file
171
src/core/matrix/state_res/resolve/mainline_sort.rs
Normal 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 x’s
|
||||||
|
/// origin_server_ts is less than y’s origin_server_ts; or
|
||||||
|
/// 3. the mainline positions of the events are the same and the events have the
|
||||||
|
/// same origin_server_ts, but x’s event_id is less than y’s event_id.
|
||||||
|
///
|
||||||
|
/// ## Arguments
|
||||||
|
///
|
||||||
|
/// * `events` - The list of event IDs to sort.
|
||||||
|
/// * `power_level` - The power level event in the current state.
|
||||||
|
/// * `fetch_event` - Function to fetch an event in the room given its event ID.
|
||||||
|
///
|
||||||
|
/// ## Returns
|
||||||
|
///
|
||||||
|
/// Returns the sorted list of event IDs, or an `Err(_)` if one the event in the
|
||||||
|
/// room has an unexpected format.
|
||||||
|
#[tracing::instrument(
|
||||||
|
level = "debug",
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
?power_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
|
||||||
|
}
|
||||||
273
src/core/matrix/state_res/resolve/power_sort.rs
Normal file
273
src/core/matrix/state_res/resolve/power_sort.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/core/matrix/state_res/resolve/split_conflicted.rs
Normal file
70
src/core/matrix/state_res/resolve/split_conflicted.rs
Normal 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)
|
||||||
|
}
|
||||||
840
src/core/matrix/state_res/resolve/tests.rs
Normal file
840
src/core/matrix/state_res/resolve/tests.rs
Normal 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],
|
||||||
|
],);
|
||||||
|
}
|
||||||
163
src/core/matrix/state_res/resolve/topological_sort.rs
Normal file
163
src/core/matrix/state_res/resolve/topological_sort.rs
Normal 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. x’s sender has greater power level than y’s sender, when looking at their
|
||||||
|
/// respective auth_events; or
|
||||||
|
/// 2. the senders have the same power level, but x’s origin_server_ts is less
|
||||||
|
/// than y’s origin_server_ts; or
|
||||||
|
/// 3. the senders have the same power level and the events have the same
|
||||||
|
/// origin_server_ts, but x’s event_id is less than y’s event_id.
|
||||||
|
///
|
||||||
|
/// The reverse topological power ordering can be found by sorting the events
|
||||||
|
/// using Kahn’s algorithm for topological sorting, and at each step selecting,
|
||||||
|
/// among all the candidate vertices, the smallest vertex using the above
|
||||||
|
/// comparison relation.
|
||||||
|
///
|
||||||
|
/// ## 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)
|
||||||
|
}
|
||||||
@@ -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}`"))),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +1,53 @@
|
|||||||
use std::{
|
use std::{
|
||||||
borrow::Borrow,
|
borrow::Borrow,
|
||||||
collections::{BTreeMap, HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
sync::atomic::{AtomicU64, Ordering::SeqCst},
|
pin::Pin,
|
||||||
|
slice,
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicU64, Ordering::SeqCst},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use futures::future::ready;
|
|
||||||
use ruma::{
|
use ruma::{
|
||||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, ServerSignatures,
|
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId, event_id,
|
||||||
UserId, event_id,
|
|
||||||
events::{
|
events::{
|
||||||
TimelineEventType,
|
StateEventType, TimelineEventType,
|
||||||
room::{
|
room::{
|
||||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||||
member::{MembershipState, RoomMemberEventContent},
|
member::{MembershipState, RoomMemberEventContent},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
int, room_id, uint, user_id,
|
int, room_id,
|
||||||
|
room_version_rules::{AuthorizationRules, RoomVersionRules},
|
||||||
|
uint, user_id,
|
||||||
};
|
};
|
||||||
use serde_json::{
|
use serde_json::{
|
||||||
json,
|
json,
|
||||||
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
|
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::{
|
use crate::{
|
||||||
Result, info,
|
Error, Result, err, info,
|
||||||
matrix::{Event, EventTypeExt, Pdu, StateMap, pdu::EventHash},
|
matrix::{Event, EventHash, EventTypeExt, PduEvent, StateKey},
|
||||||
|
utils::stream::IterStream,
|
||||||
};
|
};
|
||||||
|
|
||||||
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
|
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
pub(crate) async fn do_check(
|
pub(super) fn not_found() -> Error { err!(Request(NotFound("Test event not found"))) }
|
||||||
events: &[Pdu],
|
|
||||||
|
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>>,
|
edges: Vec<Vec<OwnedEventId>>,
|
||||||
expected_state_ids: Vec<OwnedEventId>,
|
expected_state_ids: Vec<OwnedEventId>,
|
||||||
) {
|
) {
|
||||||
@@ -79,35 +95,32 @@ pub(crate) async fn do_check(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// event_id -> Pdu
|
// event_id -> PduEvent
|
||||||
let mut event_map: HashMap<OwnedEventId, Pdu> = HashMap::new();
|
let mut event_map: HashMap<OwnedEventId, PduEvent> = HashMap::new();
|
||||||
// event_id -> StateMap<OwnedEventId>
|
// event_id -> StateMap<OwnedEventId>
|
||||||
let mut state_at_event: HashMap<OwnedEventId, StateMap<OwnedEventId>> = HashMap::new();
|
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
|
// Resolve the current state and add it to the state_at_event map then continue
|
||||||
// on in "time"
|
// on in "time"
|
||||||
for node in super::lexicographical_topological_sort(&graph, &|_id| async {
|
for node in super::topological_sort(&graph, &async |_id| {
|
||||||
Ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0))))
|
Ok((int!(0).into(), MilliSecondsSinceUnixEpoch(uint!(0))))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.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 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() {
|
let state_before: StateMap<OwnedEventId> = if prev_events.is_empty() {
|
||||||
HashMap::new()
|
StateMap::new()
|
||||||
} else if prev_events.len() == 1 {
|
} else if prev_events.len() == 1 {
|
||||||
state_at_event
|
state_at_event[prev_events.iter().next().unwrap()].clone()
|
||||||
.get(prev_events.iter().next().unwrap())
|
|
||||||
.unwrap()
|
|
||||||
.clone()
|
|
||||||
} else {
|
} else {
|
||||||
let state_sets = prev_events
|
let state_sets = prev_events
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|k| state_at_event.get(k))
|
.filter_map(|k| state_at_event.get(k).cloned())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
@@ -121,21 +134,27 @@ pub(crate) async fn do_check(
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
);
|
);
|
||||||
|
|
||||||
let auth_chain_sets: Vec<_> = state_sets
|
let auth_chain_sets = state_sets
|
||||||
.iter()
|
.iter()
|
||||||
.map(|map| {
|
.map(|map| {
|
||||||
store
|
store
|
||||||
.auth_event_ids(room_id(), map.values().cloned().collect())
|
.auth_event_ids(room_id(), map.values().cloned().collect())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let event_map = &event_map;
|
let state_sets = state_sets.into_iter().stream();
|
||||||
let fetch = |id: OwnedEventId| ready(event_map.get(&id).cloned());
|
|
||||||
let exists = |id: OwnedEventId| ready(event_map.get(&id).is_some());
|
let rules = RoomVersionRules::V6;
|
||||||
let resolved =
|
let resolved = super::resolve(
|
||||||
super::resolve(&RoomVersionId::V6, state_sets, &auth_chain_sets, &fetch, &exists)
|
&rules,
|
||||||
.await;
|
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 {
|
match resolved {
|
||||||
| Ok(state) => state,
|
| Ok(state) => state,
|
||||||
@@ -147,13 +166,15 @@ pub(crate) async fn do_check(
|
|||||||
|
|
||||||
let ty = fake_event.event_type();
|
let ty = fake_event.event_type();
|
||||||
let key = fake_event.state_key().unwrap();
|
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(
|
let auth_types = auth_types_for_event(
|
||||||
fake_event.event_type(),
|
fake_event.event_type(),
|
||||||
fake_event.sender(),
|
fake_event.sender(),
|
||||||
fake_event.state_key(),
|
fake_event.state_key(),
|
||||||
fake_event.content(),
|
fake_event.content(),
|
||||||
|
&AuthorizationRules::V6,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -183,7 +204,7 @@ pub(crate) async fn do_check(
|
|||||||
store.0.insert(ev_id.to_owned(), event.clone());
|
store.0.insert(ev_id.to_owned(), event.clone());
|
||||||
|
|
||||||
state_at_event.insert(node, state_after);
|
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();
|
let mut expected_state = StateMap::new();
|
||||||
@@ -228,24 +249,23 @@ pub(crate) async fn do_check(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::exhaustive_structs)]
|
#[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> {
|
impl TestStore {
|
||||||
pub(crate) fn get_event(&self, _: &RoomId, event_id: &EventId) -> Result<E> {
|
pub(super) fn get_event(&self, _: &RoomId, event_id: &EventId) -> Result<PduEvent> {
|
||||||
self.0
|
self.0
|
||||||
.get(event_id)
|
.get(event_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or_else(|| super::Error::NotFound(format!("{event_id} not found")))
|
.ok_or_else(|| event_not_found(event_id))
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a Vec of the related auth events to the given `event`.
|
/// 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,
|
&self,
|
||||||
room_id: &RoomId,
|
room_id: &RoomId,
|
||||||
event_ids: Vec<OwnedEventId>,
|
event_ids: Vec<OwnedEventId>,
|
||||||
) -> Result<HashSet<OwnedEventId>> {
|
) -> Result<AuthSet<OwnedEventId>> {
|
||||||
let mut result = HashSet::new();
|
let mut result = AuthSet::new();
|
||||||
let mut stack = event_ids;
|
let mut stack = event_ids;
|
||||||
|
|
||||||
// DFS for auth event chain
|
// DFS for auth event chain
|
||||||
@@ -267,8 +287,8 @@ impl<E: Event + Clone> TestStore<E> {
|
|||||||
|
|
||||||
// A StateStore implementation for testing
|
// A StateStore implementation for testing
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
impl TestStore<Pdu> {
|
impl TestStore {
|
||||||
pub(crate) fn set_up(
|
pub(super) fn set_up(
|
||||||
&mut self,
|
&mut self,
|
||||||
) -> (StateMap<OwnedEventId>, StateMap<OwnedEventId>, StateMap<OwnedEventId>) {
|
) -> (StateMap<OwnedEventId>, StateMap<OwnedEventId>, StateMap<OwnedEventId>) {
|
||||||
let create_event = to_pdu_event::<&EventId>(
|
let create_event = to_pdu_event::<&EventId>(
|
||||||
@@ -289,8 +309,8 @@ impl TestStore<Pdu> {
|
|||||||
TimelineEventType::RoomMember,
|
TimelineEventType::RoomMember,
|
||||||
Some(alice().as_str()),
|
Some(alice().as_str()),
|
||||||
member_content_join(),
|
member_content_join(),
|
||||||
&[cre.clone()],
|
slice::from_ref(&cre),
|
||||||
&[cre.clone()],
|
slice::from_ref(&cre),
|
||||||
);
|
);
|
||||||
self.0
|
self.0
|
||||||
.insert(alice_mem.event_id().to_owned(), alice_mem.clone());
|
.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('$') {
|
if id.contains('$') {
|
||||||
return id.try_into().unwrap();
|
return id.try_into().unwrap();
|
||||||
}
|
}
|
||||||
@@ -378,33 +398,35 @@ pub(crate) fn event_id(id: &str) -> OwnedEventId {
|
|||||||
format!("${id}:foo").try_into().unwrap()
|
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()
|
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()
|
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,
|
id: &str,
|
||||||
sender: &UserId,
|
sender: &UserId,
|
||||||
ev_type: TimelineEventType,
|
ev_type: TimelineEventType,
|
||||||
state_key: Option<&str>,
|
state_key: Option<&str>,
|
||||||
content: Box<RawJsonValue>,
|
content: Box<RawJsonValue>,
|
||||||
) -> Pdu {
|
) -> PduEvent {
|
||||||
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
|
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
|
||||||
let id = if id.contains('$') {
|
let id = if id.contains('$') {
|
||||||
id.to_owned()
|
id.to_owned()
|
||||||
@@ -412,15 +434,16 @@ pub(crate) fn to_init_pdu_event(
|
|||||||
format!("${id}:foo")
|
format!("${id}:foo")
|
||||||
};
|
};
|
||||||
|
|
||||||
Pdu {
|
let state_key = state_key.map(ToOwned::to_owned);
|
||||||
|
PduEvent {
|
||||||
event_id: id.try_into().unwrap(),
|
event_id: id.try_into().unwrap(),
|
||||||
room_id: room_id().to_owned(),
|
room_id: room_id().to_owned(),
|
||||||
sender: sender.to_owned(),
|
sender: sender.to_owned(),
|
||||||
|
origin: None,
|
||||||
origin_server_ts: ts.try_into().unwrap(),
|
origin_server_ts: ts.try_into().unwrap(),
|
||||||
state_key: state_key.map(Into::into),
|
state_key: state_key.map(Into::into),
|
||||||
kind: ev_type,
|
kind: ev_type,
|
||||||
content,
|
content,
|
||||||
origin: None,
|
|
||||||
redacts: None,
|
redacts: None,
|
||||||
unsigned: None,
|
unsigned: None,
|
||||||
auth_events: vec![],
|
auth_events: vec![],
|
||||||
@@ -428,10 +451,11 @@ pub(crate) fn to_init_pdu_event(
|
|||||||
depth: uint!(0),
|
depth: uint!(0),
|
||||||
hashes: EventHash::default(),
|
hashes: EventHash::default(),
|
||||||
signatures: None,
|
signatures: None,
|
||||||
|
rejected: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn to_pdu_event<S>(
|
pub(super) fn to_pdu_event<S>(
|
||||||
id: &str,
|
id: &str,
|
||||||
sender: &UserId,
|
sender: &UserId,
|
||||||
ev_type: TimelineEventType,
|
ev_type: TimelineEventType,
|
||||||
@@ -439,7 +463,7 @@ pub(crate) fn to_pdu_event<S>(
|
|||||||
content: Box<RawJsonValue>,
|
content: Box<RawJsonValue>,
|
||||||
auth_events: &[S],
|
auth_events: &[S],
|
||||||
prev_events: &[S],
|
prev_events: &[S],
|
||||||
) -> Pdu
|
) -> PduEvent
|
||||||
where
|
where
|
||||||
S: AsRef<str>,
|
S: AsRef<str>,
|
||||||
{
|
{
|
||||||
@@ -460,15 +484,16 @@ where
|
|||||||
.map(event_id)
|
.map(event_id)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
Pdu {
|
let state_key = state_key.map(ToOwned::to_owned);
|
||||||
|
PduEvent {
|
||||||
event_id: id.try_into().unwrap(),
|
event_id: id.try_into().unwrap(),
|
||||||
room_id: room_id().to_owned(),
|
room_id: room_id().to_owned(),
|
||||||
sender: sender.to_owned(),
|
sender: sender.to_owned(),
|
||||||
|
origin: None,
|
||||||
origin_server_ts: ts.try_into().unwrap(),
|
origin_server_ts: ts.try_into().unwrap(),
|
||||||
state_key: state_key.map(Into::into),
|
state_key: state_key.map(Into::into),
|
||||||
kind: ev_type,
|
kind: ev_type,
|
||||||
content,
|
content,
|
||||||
origin: None,
|
|
||||||
redacts: None,
|
redacts: None,
|
||||||
unsigned: None,
|
unsigned: None,
|
||||||
auth_events,
|
auth_events,
|
||||||
@@ -476,12 +501,153 @@ where
|
|||||||
depth: uint!(0),
|
depth: uint!(0),
|
||||||
hashes: EventHash::default(),
|
hashes: EventHash::default(),
|
||||||
signatures: None,
|
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
|
// all graphs start with these input events
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub(crate) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
|
pub(super) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, PduEvent> {
|
||||||
vec![
|
vec![
|
||||||
to_pdu_event::<&EventId>(
|
to_pdu_event::<&EventId>(
|
||||||
"CREATE",
|
"CREATE",
|
||||||
@@ -561,9 +727,88 @@ pub(crate) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
|
|||||||
.collect()
|
.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
|
// all graphs start with these input events
|
||||||
#[allow(non_snake_case)]
|
#[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>(
|
vec![to_pdu_event::<&EventId>(
|
||||||
"CREATE",
|
"CREATE",
|
||||||
alice(),
|
alice(),
|
||||||
@@ -579,9 +824,99 @@ pub(crate) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap<OwnedEventId, Pdu> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[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"]
|
vec!["START", "IMC", "IMB", "IJR", "IPOWER", "IMA", "CREATE"]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(event_id)
|
.map(event_id)
|
||||||
.collect::<Vec<_>>()
|
.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"],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
214
src/core/tests/it/fixtures/MSC4297-problem-A/pdus-hydra.json
Normal file
214
src/core/tests/it/fixtures/MSC4297-problem-A/pdus-hydra.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
223
src/core/tests/it/fixtures/MSC4297-problem-A/pdus-v11.json
Normal file
223
src/core/tests/it/fixtures/MSC4297-problem-A/pdus-v11.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
239
src/core/tests/it/fixtures/MSC4297-problem-B/pdus-hydra.json
Normal file
239
src/core/tests/it/fixtures/MSC4297-problem-B/pdus-hydra.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
251
src/core/tests/it/fixtures/MSC4297-problem-B/pdus-v11.json
Normal file
251
src/core/tests/it/fixtures/MSC4297-problem-B/pdus-v11.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
129
src/core/tests/it/fixtures/bootstrap-private-chat.json
Normal file
129
src/core/tests/it/fixtures/bootstrap-private-chat.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
64
src/core/tests/it/fixtures/origin-server-ts-tiebreak.json
Normal file
64
src/core/tests/it/fixtures/origin-server-ts-tiebreak.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
3
src/core/tests/it/main.rs
Normal file
3
src/core/tests/it/main.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
//! Integration tests entrypoint.
|
||||||
|
|
||||||
|
mod resolve;
|
||||||
700
src/core/tests/it/resolve.rs
Normal file
700
src/core/tests/it/resolve.rs
Normal 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)
|
||||||
|
}
|
||||||
56
src/core/tests/it/resolve/snapshot_tests.rs
Normal file
56
src/core/tests/it/resolve/snapshot_tests.rs
Normal 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"]
|
||||||
|
);
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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
Reference in New Issue
Block a user