State-reset and security mitigations.

Upgrade Ruma to present.

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

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

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

Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
Jason Volk
2025-08-11 18:45:28 +00:00
parent 2c6dd78502
commit 628597c318
134 changed files with 14961 additions and 4935 deletions
+2
View File
@@ -0,0 +1,2 @@
[env]
RUMA_UNSTABLE_EXHAUSTIVE_TYPES = "true"
Generated
+60 -31
View File
@@ -720,6 +720,18 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "console"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"windows-sys 0.59.0",
]
[[package]]
name = "const-oid"
version = "0.9.6"
@@ -1143,6 +1155,12 @@ dependencies = [
"serde",
]
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "enum-as-inner"
version = "0.6.1"
@@ -1989,6 +2007,18 @@ version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]]
name = "insta"
version = "1.43.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371"
dependencies = [
"console",
"once_cell",
"serde",
"similar",
]
[[package]]
name = "interpolate_name"
version = "0.2.4"
@@ -3439,8 +3469,8 @@ dependencies = [
[[package]]
name = "ruma"
version = "0.10.1"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.12.6"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"assign",
"js_int",
@@ -3451,7 +3481,6 @@ dependencies = [
"ruma-events",
"ruma-federation-api",
"ruma-identifiers-validation",
"ruma-identity-service-api",
"ruma-push-gateway-api",
"ruma-signatures",
"web-time",
@@ -3459,8 +3488,8 @@ dependencies = [
[[package]]
name = "ruma-appservice-api"
version = "0.10.0"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.12.2"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"js_int",
"ruma-common",
@@ -3471,8 +3500,8 @@ dependencies = [
[[package]]
name = "ruma-client-api"
version = "0.18.0"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.20.4"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"as_variant",
"assign",
@@ -3494,8 +3523,8 @@ dependencies = [
[[package]]
name = "ruma-common"
version = "0.13.0"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.15.4"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"as_variant",
"base64",
@@ -3522,12 +3551,13 @@ dependencies = [
"uuid",
"web-time",
"wildmatch",
"zeroize",
]
[[package]]
name = "ruma-events"
version = "0.28.1"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.30.5"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"as_variant",
"indexmap",
@@ -3547,12 +3577,13 @@ dependencies = [
"url",
"web-time",
"wildmatch",
"zeroize",
]
[[package]]
name = "ruma-federation-api"
version = "0.9.0"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.11.2"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"bytes",
"headers",
@@ -3573,27 +3604,17 @@ dependencies = [
[[package]]
name = "ruma-identifiers-validation"
version = "0.9.5"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.10.1"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"js_int",
"thiserror 2.0.12",
]
[[package]]
name = "ruma-identity-service-api"
version = "0.9.0"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
dependencies = [
"js_int",
"ruma-common",
"serde",
]
[[package]]
name = "ruma-macros"
version = "0.13.0"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.15.2"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"cfg-if",
"proc-macro-crate",
@@ -3607,8 +3628,8 @@ dependencies = [
[[package]]
name = "ruma-push-gateway-api"
version = "0.9.0"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.11.0"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"js_int",
"ruma-common",
@@ -3619,8 +3640,8 @@ dependencies = [
[[package]]
name = "ruma-signatures"
version = "0.15.0"
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
version = "0.17.1"
source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f"
dependencies = [
"base64",
"ed25519-dalek",
@@ -4206,6 +4227,12 @@ dependencies = [
"quote",
]
[[package]]
name = "similar"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]]
name = "simple_asn1"
version = "0.6.3"
@@ -4977,6 +5004,7 @@ dependencies = [
"hardened_malloc-rs",
"http",
"http-body-util",
"insta",
"ipaddress",
"itertools 0.14.0",
"jsonwebtoken",
@@ -4996,6 +5024,7 @@ dependencies = [
"serde_json",
"serde_regex",
"serde_yaml",
"similar",
"smallstr",
"smallvec",
"thiserror 2.0.12",
+13 -8
View File
@@ -219,6 +219,12 @@ features = [
"webp",
]
[workspace.dependencies.insta]
version = "1.43.1"
features = [
"json",
]
[workspace.dependencies.ipaddress]
version = "0.1"
@@ -311,20 +317,18 @@ default-features = false
[workspace.dependencies.ruma]
git = "https://github.com/matrix-construct/ruma"
rev = "0155c2b33233bec9dece79d5134a9574b347f4c1"
rev = "3d3acddfcf96891f1203ca3c36d8f41932ede50f"
features = [
"compat",
"__compat",
"rand",
"appservice-api-c",
"client-api",
"federation-api",
"markdown",
"push-gateway-api-c",
"unstable-exhaustive-types",
"ring-compat",
"compat-upload-signatures",
"identifiers-validation",
"unstable-unspecified",
"unstable-msc2448",
"unstable-msc2666",
"unstable-msc2867",
@@ -332,10 +336,8 @@ features = [
"unstable-msc3026",
"unstable-msc3061",
"unstable-msc3245",
"unstable-msc3266",
"unstable-msc3381", # polls
"unstable-msc3489", # beacon / live location
"unstable-msc3575",
"unstable-msc3930", # polls push rules
"unstable-msc4075",
"unstable-msc4095",
@@ -343,9 +345,9 @@ features = [
"unstable-msc4125",
"unstable-msc4186",
"unstable-msc4203", # sending to-device events to appservices
"unstable-msc4210", # remove legacy mentions
"unstable-msc4311",
"unstable-extensible-events",
"unstable-pdu",
"unstable-hydra",
]
[workspace.dependencies.rustls]
@@ -425,6 +427,9 @@ default-features = false
version = "0.10"
default-features = false
[workspace.dependencies.similar]
version = "2.7.0"
[workspace.dependencies.smallstr]
version = "0.3"
features = [
+4 -2
View File
@@ -87,9 +87,12 @@ pub(super) async fn parse_pdu(&self) -> Result {
}
let string = self.body[1..self.body.len().saturating_sub(1)].join("\n");
let rules = RoomVersionId::V6
.rules()
.expect("rules for V6 rooms");
match serde_json::from_str(&string) {
| Err(e) => return Err!("Invalid json in command body: {e}"),
| Ok(value) => match ruma::signatures::reference_hash(&value, &RoomVersionId::V6) {
| Ok(value) => match ruma::signatures::reference_hash(&value, &rules) {
| Err(e) => return Err!("Could not parse PDU JSON: {e:?}"),
| Ok(hash) => {
let event_id = OwnedEventId::parse(format!("${hash}"));
@@ -252,7 +255,6 @@ pub(super) async fn get_remote_pdu(
.sending
.send_federation_request(&server, ruma::api::federation::event::get_event::v1::Request {
event_id: event_id.clone(),
include_unredacted_content: None,
})
.await
{
+23 -15
View File
@@ -195,6 +195,7 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
self.services
.admin
.make_user_admin(&user_id)
.boxed()
.await?;
warn!("Granting {user_id} admin privileges as the first user");
}
@@ -725,32 +726,38 @@ pub(super) async fn force_demote(&self, user_id: String, room_id: OwnedRoomOrAli
.lock(&room_id)
.await;
let room_power_levels: Option<RoomPowerLevelsEventContent> = self
let room_power_levels: Option<RoomPowerLevels> = self
.services
.rooms
.state_accessor
.room_state_get_content(&room_id, &StateEventType::RoomPowerLevels, "")
.get_power_levels(&room_id)
.await
.ok();
let user_can_demote_self = room_power_levels
let user_can_change_self = room_power_levels
.as_ref()
.is_some_and(|power_levels_content| {
RoomPowerLevels::from(power_levels_content.clone())
.user_can_change_user_power_level(&user_id, &user_id)
}) || self
.services
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
.await
.is_ok_and(|event| event.sender() == user_id);
.is_some_and(|power_levels| {
power_levels.user_can_change_user_power_level(&user_id, &user_id)
});
let user_can_demote_self = user_can_change_self
|| self
.services
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
.await
.is_ok_and(|event| event.sender() == user_id);
if !user_can_demote_self {
return Err!("User is not allowed to modify their own power levels in the room.",);
return Err!("User is not allowed to modify their own power levels in the room.");
}
let mut power_levels_content = room_power_levels.unwrap_or_default();
let mut power_levels_content: RoomPowerLevelsEventContent = room_power_levels
.map(TryInto::try_into)
.transpose()?
.unwrap_or_default();
power_levels_content.users.remove(&user_id);
let event_id = self
@@ -783,6 +790,7 @@ pub(super) async fn make_user_admin(&self, user_id: String) -> Result {
self.services
.admin
.make_user_admin(&user_id)
.boxed()
.await?;
self.write_str(&format!("{user_id} has been granted admin privileges.",))
+17 -25
View File
@@ -11,10 +11,7 @@
},
uiaa::{AuthFlow, AuthType, UiaaInfo},
},
events::{
StateEventType,
room::power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
events::{StateEventType, room::power_levels::RoomPowerLevelsEventContent},
};
use tuwunel_core::{
Err, Error, Result, err, info,
@@ -60,10 +57,7 @@ pub(crate) async fn change_password_route(
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
..Default::default()
};
match &body.auth {
@@ -189,10 +183,7 @@ pub(crate) async fn deactivate_route(
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
..Default::default()
};
match &body.auth {
@@ -331,21 +322,18 @@ pub async fn full_user_deactivate(
let room_power_levels = services
.rooms
.state_accessor
.room_state_get_content::<RoomPowerLevelsEventContent>(
room_id,
&StateEventType::RoomPowerLevels,
"",
)
.get_power_levels(room_id)
.await
.ok();
let user_can_demote_self =
room_power_levels
.as_ref()
.is_some_and(|power_levels_content| {
RoomPowerLevels::from(power_levels_content.clone())
.user_can_change_user_power_level(user_id, user_id)
}) || services
let user_can_change_self = room_power_levels
.as_ref()
.is_some_and(|power_levels| {
power_levels.user_can_change_user_power_level(user_id, user_id)
});
let user_can_demote_self = user_can_change_self
|| services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, "")
@@ -353,7 +341,11 @@ pub async fn full_user_deactivate(
.is_ok_and(|event| event.sender() == user_id);
if user_can_demote_self {
let mut power_levels_content = room_power_levels.unwrap_or_default();
let mut power_levels_content: RoomPowerLevelsEventContent = room_power_levels
.map(TryInto::try_into)
.transpose()?
.unwrap_or_default();
power_levels_content.users.remove(user_id);
// ignore errors so deactivation doesn't fail
+6 -3
View File
@@ -3,9 +3,12 @@
use axum::extract::State;
use ruma::{
RoomVersionId,
api::client::discovery::get_capabilities::{
self, Capabilities, GetLoginTokenCapability, RoomVersionStability,
RoomVersionsCapability, ThirdPartyIdChangesCapability,
api::client::discovery::{
get_capabilities,
get_capabilities::v3::{
Capabilities, GetLoginTokenCapability, RoomVersionStability, RoomVersionsCapability,
ThirdPartyIdChangesCapability,
},
},
};
use serde_json::json;
+2 -8
View File
@@ -145,10 +145,7 @@ pub(crate) async fn delete_device_route(
// UIAA
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
..Default::default()
};
match &body.auth {
@@ -224,10 +221,7 @@ pub(crate) async fn delete_devices_route(
// UIAA
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
..Default::default()
};
match &body.auth {
+24 -30
View File
@@ -16,18 +16,15 @@
},
federation,
},
directory::{Filter, PublicRoomJoinRule, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
directory::{Filter, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
events::{
StateEventType,
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
room::join_rules::{JoinRule, RoomJoinRulesEventContent},
},
uint,
};
use tuwunel_core::{
Err, Result, err, info,
Err, Result, err, info, is_true,
matrix::Event,
utils::{
TryFutureExtExt,
@@ -51,7 +48,7 @@ pub(crate) async fn get_public_rooms_filtered_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_public_rooms_filtered::v3::Request>,
) -> Result<get_public_rooms_filtered::v3::Response> {
check_banned(&services, body.server.as_deref())?;
check_server_banned(&services, body.server.as_deref())?;
let response = get_public_rooms_filtered_helper(
&services,
@@ -80,7 +77,7 @@ pub(crate) async fn get_public_rooms_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_public_rooms::v3::Request>,
) -> Result<get_public_rooms::v3::Response> {
check_banned(&services, body.server.as_deref())?;
check_server_banned(&services, body.server.as_deref())?;
let response = get_public_rooms_filtered_helper(
&services,
@@ -393,15 +390,11 @@ async fn user_can_publish_room(
match services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomPowerLevels, "")
.get_power_levels(room_id)
.await
{
| Ok(event) => serde_json::from_str(event.content().get())
.map_err(|_| err!(Database("Invalid event content for m.room.power_levels")))
.map(|content: RoomPowerLevelsEventContent| {
RoomPowerLevels::from(content)
.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)
}),
| Ok(power_levels) =>
Ok(power_levels.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)),
| _ => {
match services
.rooms
@@ -453,7 +446,7 @@ async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> Public
.state_accessor
.room_state_get_content(&room_id, &StateEventType::RoomJoinRules, "")
.map_ok(|c: RoomJoinRulesEventContent| match c.join_rule {
| JoinRule::Public => PublicRoomJoinRule::Public,
| JoinRule::Public => "public".into(),
| JoinRule::Knock => "knock".into(),
| JoinRule::KnockRestricted(_) => "knock_restricted".into(),
| _ => "invite".into(),
@@ -497,24 +490,25 @@ async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> Public
}
}
fn check_banned(services: &Services, server: Option<&ServerName>) -> Result {
fn check_server_banned(services: &Services, server: Option<&ServerName>) -> Result {
let Some(server) = server else {
return Ok(());
};
let forbidden_remote_directory = services
.config
.forbidden_remote_room_directory_server_names
.is_match(server.host());
let conditions = [
services
.config
.forbidden_remote_room_directory_server_names
.is_match(server.host()),
services
.config
.forbidden_remote_server_names
.is_match(server.host()),
];
let forbidden_remote_server = services
.config
.forbidden_remote_server_names
.is_match(server.host());
if forbidden_remote_directory || forbidden_remote_server {
Err!(Request(Forbidden("Server is banned on this homeserver.")))
} else {
Ok(())
if conditions.iter().any(is_true!()) {
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
}
Ok(())
}
+9 -10
View File
@@ -3,7 +3,8 @@
use axum::extract::State;
use futures::{StreamExt, stream::FuturesUnordered};
use ruma::{
OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
CanonicalJsonObject, CanonicalJsonValue, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId,
UserId,
api::{
client::{
error::ErrorKind,
@@ -162,10 +163,7 @@ pub(crate) async fn upload_signing_keys_route(
// UIAA
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
..Default::default()
};
match check_for_new_keys(
@@ -599,18 +597,19 @@ fn add_unsigned_device_display_name(
include_display_names: bool,
) -> Result {
if let Some(display_name) = metadata.display_name {
let mut object = keys.deserialize_as::<serde_json::Map<String, serde_json::Value>>()?;
let mut object = keys.deserialize_as_unchecked::<CanonicalJsonObject>()?;
let unsigned = object
.entry("unsigned")
.or_insert_with(|| json!({}));
if let serde_json::Value::Object(unsigned_object) = unsigned {
.entry("unsigned".into())
.or_insert_with(CanonicalJsonValue::default);
if let CanonicalJsonValue::Object(unsigned_object) = unsigned {
if include_display_names {
unsigned_object.insert("device_display_name".to_owned(), display_name.into());
} else {
unsigned_object.insert(
"device_display_name".to_owned(),
Some(metadata.device_id.as_str().to_owned()).into(),
CanonicalJsonValue::String(metadata.device_id.as_str().to_owned()),
);
}
}
+9 -8
View File
@@ -7,8 +7,9 @@
events::room::member::{MembershipState, RoomMemberEventContent},
};
use tuwunel_core::{
Err, Result, debug_error, err, info,
Err, Result, err,
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
warn,
};
use tuwunel_service::Services;
@@ -27,8 +28,8 @@ pub(crate) async fn invite_user_route(
let sender_user = body.sender_user();
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
debug_error!(
"User {sender_user} is not an admin and attempted to send an invite to room {}",
warn!(
"{sender_user} is not an admin and attempted to send an invite to {}",
&body.room_id
);
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
@@ -104,10 +105,7 @@ pub(crate) async fn invite_helper(
is_direct: bool,
) -> Result {
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
info!(
"User {sender_user} is not an admin and attempted to send an invite to room \
{room_id}"
);
warn!("{sender_user} is not an admin and attempted to send an invite to {room_id}");
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
}
@@ -156,7 +154,10 @@ pub(crate) async fn invite_helper(
.sending
.convert_to_outgoing_federation_event(pdu_json.clone())
.await,
invite_room_state,
invite_room_state: invite_room_state
.into_iter()
.map(Into::into)
.collect(),
via: services
.rooms
.state_cache
+26 -31
View File
@@ -25,10 +25,9 @@
use tuwunel_core::{
Err, Result, debug, debug_info, debug_warn, err, error, info,
matrix::{
StateKey,
event::{gen_event_id, gen_event_id_canonical_json},
pdu::{PduBuilder, PduEvent},
state_res,
room_version, state_res,
},
result::FlatOk,
trace,
@@ -551,7 +550,13 @@ async fn join_room_by_id_helper_remote(
})
.ready_filter_map(Result::ok)
.fold(HashMap::new(), async |mut state, (event_id, value)| {
let pdu = match PduEvent::from_id_val(&event_id, value.clone()) {
let pdu = if value["type"] == "m.room.create" {
PduEvent::from_rid_val(room_id, &event_id, value.clone())
} else {
PduEvent::from_id_val(&event_id, value.clone())
};
let pdu = match pdu {
| Ok(pdu) => pdu,
| Err(e) => {
debug_warn!("Invalid PDU in send_join response: {e:?}: {value:#?}");
@@ -604,36 +609,26 @@ async fn join_room_by_id_helper_remote(
drop(cork);
debug!("Running send_join auth check");
let fetch_state = &state;
let state_fetch = |k: StateEventType, s: StateKey| async move {
let shortstatekey = services
.rooms
.short
.get_shortstatekey(&k, &s)
.await
.ok()?;
let event_id = fetch_state.get(&shortstatekey)?;
services
.rooms
.timeline
.get_pdu(event_id)
.await
.ok()
};
let auth_check = state_res::event_auth::auth_check(
&state_res::RoomVersion::new(&room_version_id)?,
state_res::auth_check(
&room_version::rules(&room_version_id)?,
&parsed_join_pdu,
None, // TODO: third party invite
|k, s| state_fetch(k.clone(), s.into()),
)
.await
.map_err(|e| err!(Request(Forbidden(warn!("Auth check failed: {e:?}")))))?;
&async |event_id| services.rooms.timeline.get_pdu(&event_id).await,
&async |event_type, state_key| {
let shortstatekey = services
.rooms
.short
.get_shortstatekey(&event_type, state_key.as_str())
.await?;
if !auth_check {
return Err!(Request(Forbidden("Auth check failed")));
}
let event_id = state.get(&shortstatekey).ok_or_else(|| {
err!(Request(NotFound("Missing fetch_state {shortstatekey:?}")))
})?;
services.rooms.timeline.get_pdu(event_id).await
},
)
.boxed()
.await?;
info!("Compressing state from send_join");
let compressed: CompressedState = services
+31 -9
View File
@@ -8,7 +8,10 @@
RoomVersionId, UserId,
api::{
client::knock::knock_room,
federation::{self},
federation::{
membership::RawStrippedState,
{self},
},
},
canonical_json::to_canonical_value,
events::{
@@ -17,7 +20,7 @@
},
};
use tuwunel_core::{
Err, Result, debug, debug_info, debug_warn, err, info,
Err, Result, debug, debug_info, debug_warn, err, extract_variant, info,
matrix::{
event::{Event, gen_event_id},
pdu::{PduBuilder, PduEvent},
@@ -346,7 +349,7 @@ async fn knock_room_helper_local(
let knock_event = knock_event_stub;
info!("Asking {remote_server} for send_knock in room {room_id}");
let send_knock_request = federation::knock::send_knock::v1::Request {
let send_knock_request = federation::membership::create_knock_event::v1::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
pdu: services
@@ -384,7 +387,13 @@ async fn knock_room_helper_local(
.get_content::<RoomMemberEventContent>()
.expect("we just created this"),
sender_user,
Some(send_knock_response.knock_room_state),
Some(
send_knock_response
.knock_room_state
.into_iter()
.filter_map(|s| extract_variant!(s, RawStrippedState::Stripped))
.collect(),
),
None,
false,
)
@@ -477,7 +486,7 @@ async fn knock_room_helper_remote(
let knock_event = knock_event_stub;
info!("Asking {remote_server} for send_knock in room {room_id}");
let send_knock_request = federation::knock::send_knock::v1::Request {
let send_knock_request = federation::membership::create_knock_event::v1::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
pdu: services
@@ -507,7 +516,14 @@ async fn knock_room_helper_remote(
let state = send_knock_response
.knock_room_state
.iter()
.map(|event| serde_json::from_str::<CanonicalJsonObject>(event.clone().into_json().get()))
.map(|event| {
serde_json::from_str::<CanonicalJsonObject>(
extract_variant!(event.clone(), RawStrippedState::Stripped)
.expect("Raw<AnyStrippedStateEvent>")
.json()
.get(),
)
})
.filter_map(Result::ok);
let mut state_map: HashMap<u64, OwnedEventId> = HashMap::new();
@@ -594,7 +610,13 @@ async fn knock_room_helper_remote(
.get_content::<RoomMemberEventContent>()
.expect("we just created this"),
sender_user,
Some(send_knock_response.knock_room_state),
Some(
send_knock_response
.knock_room_state
.into_iter()
.filter_map(|s| extract_variant!(s, RawStrippedState::Stripped))
.collect(),
),
None,
false,
)
@@ -628,7 +650,7 @@ async fn make_knock_request(
sender_user: &UserId,
room_id: &RoomId,
servers: &[OwnedServerName],
) -> Result<(federation::knock::create_knock_event_template::v1::Response, OwnedServerName)> {
) -> Result<(federation::membership::prepare_knock_event::v1::Response, OwnedServerName)> {
let mut make_knock_response_and_server =
Err!(BadServerResponse("No server available to assist in knocking."));
@@ -645,7 +667,7 @@ async fn make_knock_request(
.sending
.send_federation_request(
remote_server,
federation::knock::create_knock_event_template::v1::Request {
federation::membership::prepare_knock_event::v1::Request {
room_id: room_id.to_owned(),
user_id: sender_user.to_owned(),
ver: services
+10 -9
View File
@@ -70,15 +70,16 @@ pub(crate) async fn banned_room_check(
if let Some(room_id) = room_id {
if services.rooms.metadata.is_banned(room_id).await
|| services
.config
.forbidden_remote_server_names
.is_match(
room_id
.server_name()
.expect("legacy room mxid")
.host(),
) {
|| (room_id.server_name().is_some()
&& services
.config
.forbidden_remote_server_names
.is_match(
room_id
.server_name()
.expect("legacy room mxid")
.host(),
)) {
warn!(
"User {user_id} who is not an admin attempted to send an invite for or \
attempted to join a banned room or banned room server name: {room_id}"
+1 -1
View File
@@ -60,7 +60,7 @@
pub(super) use openid::*;
pub(super) use presence::*;
pub(super) use profile::*;
pub use profile::{update_all_rooms, update_avatar_url, update_displayname};
pub use profile::{update_avatar_url, update_displayname};
pub(super) use push::*;
pub(super) use read_marker::*;
pub(super) use redact::*;
+8 -4
View File
@@ -2,7 +2,7 @@
use axum::extract::State;
use futures::{
StreamExt, TryStreamExt,
FutureExt, StreamExt, TryStreamExt,
future::{join, join3, join4},
};
use ruma::{
@@ -368,7 +368,9 @@ pub async fn update_displayname(
.collect()
.await;
update_all_rooms(services, all_joined_rooms, user_id).await;
update_all_rooms(services, all_joined_rooms, user_id)
.boxed()
.await;
}
pub async fn update_avatar_url(
@@ -421,10 +423,12 @@ pub async fn update_avatar_url(
.collect()
.await;
update_all_rooms(services, all_joined_rooms, user_id).await;
update_all_rooms(services, all_joined_rooms, user_id)
.boxed()
.await;
}
pub async fn update_all_rooms(
async fn update_all_rooms(
services: &Services,
all_joined_rooms: Vec<(PduBuilder, &OwnedRoomId)>,
user_id: &UserId,
+7 -3
View File
@@ -316,7 +316,7 @@ pub(crate) async fn register_route(
stages: vec![AuthType::RegistrationToken],
}],
completed: Vec::new(),
params: Box::default(),
params: Default::default(),
session: None,
auth_error: None,
};
@@ -326,7 +326,7 @@ pub(crate) async fn register_route(
uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
completed: Vec::new(),
params: Box::default(),
params: Default::default(),
session: None,
auth_error: None,
};
@@ -523,7 +523,11 @@ pub(crate) async fn register_route(
.await
.is_ok_and(is_equal_to!(1))
{
services.admin.make_user_admin(&user_id).await?;
services
.admin
.make_user_admin(&user_id)
.boxed()
.await?;
warn!("Granting {user_id} admin privileges as the first user");
}
+8 -11
View File
@@ -15,6 +15,8 @@
use crate::Ruma;
const REASON_MAX_LEN: usize = 750;
/// # `POST /_matrix/client/v3/rooms/{roomId}/report`
///
/// Reports an abusive room to homeserver admins
@@ -29,18 +31,13 @@ pub(crate) async fn report_room_route(
info!(
"Received room report by user {sender_user} for room {} with reason: \"{}\"",
body.room_id,
body.reason.as_deref().unwrap_or("")
body.room_id, body.reason,
);
if body
.reason
.as_ref()
.is_some_and(|s| s.len() > 750)
{
return Err!(Request(
InvalidParam("Reason too long, should be 750 characters or fewer",)
));
if body.reason.len().gt(&REASON_MAX_LEN) {
return Err!(Request(InvalidParam(
"Reason too long, should be {REASON_MAX_LEN} characters or fewer"
)));
}
delay_response().await;
@@ -64,7 +61,7 @@ pub(crate) async fn report_room_route(
"@room Room report received from {} -\n\nRoom ID: {}\n\nReport Reason: {}",
sender_user.to_owned(),
body.room_id,
body.reason.as_deref().unwrap_or("")
body.reason,
)))
.await
.ok();
+337 -171
View File
@@ -1,10 +1,13 @@
use std::collections::BTreeMap;
use axum::extract::State;
use futures::FutureExt;
use futures::{FutureExt, future::OptionFuture};
use ruma::{
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId,
api::client::room::{self, create_room},
api::client::room::{
self, create_room,
create_room::v3::{CreationContent, RoomPreset},
},
events::{
TimelineEventType,
room::{
@@ -16,19 +19,21 @@
member::{MembershipState, RoomMemberEventContent},
name::RoomNameEventContent,
power_levels::RoomPowerLevelsEventContent,
topic::RoomTopicEventContent,
topic::{RoomTopicEventContent, TopicContentBlock},
},
},
int,
room_version_rules::{RoomIdFormatVersion, RoomVersionRules},
serde::{JsonObject, Raw},
};
use serde_json::{json, value::to_raw_value};
use tuwunel_core::{
Err, Result, debug_info, debug_warn, err, info,
matrix::{StateKey, pdu::PduBuilder},
matrix::{StateKey, pdu::PduBuilder, room_version},
utils::BoolExt,
warn,
};
use tuwunel_service::{Services, appservice::RegistrationInfo};
use tuwunel_service::{Services, appservice::RegistrationInfo, rooms::state::RoomMutexGuard};
use crate::{Ruma, client::invite_helper};
@@ -53,164 +58,61 @@ pub(crate) async fn create_room_route(
State(services): State<crate::State>,
body: Ruma<create_room::v3::Request>,
) -> Result<create_room::v3::Response> {
use create_room::v3::RoomPreset;
can_create_room_check(&services, &body).await?;
can_publish_directory_check(&services, &body).await?;
let sender_user = body.sender_user();
// Figure out preset. We need it for preset specific events
let preset = body
.preset
.clone()
.unwrap_or(match &body.visibility {
| room::Visibility::Public => RoomPreset::PublicChat,
| _ => RoomPreset::PrivateChat, // Room visibility should not be custom
});
if !services.globals.allow_room_creation()
&& body.appservice_info.is_none()
&& !services.users.is_admin(sender_user).await
{
return Err!(Request(Forbidden("Room creation has been disabled.",)));
}
let alias: OptionFuture<_> = body
.room_alias_name
.as_ref()
.map(|alias| room_alias_check(&services, alias, body.appservice_info.as_ref()))
.into();
let room_id: OwnedRoomId = match &body.room_id {
| Some(custom_room_id) => custom_room_id_check(&services, custom_room_id)?,
| _ => RoomId::new(&services.server.name),
};
// check if room ID doesn't already exist instead of erroring on auth check
if services
.rooms
.short
.get_shortroomid(&room_id)
.await
.is_ok()
{
return Err!(Request(RoomInUse("Room with that custom room ID already exists",)));
}
if body.visibility == room::Visibility::Public
&& services
.server
.config
.lockdown_public_room_directory
&& !services.users.is_admin(sender_user).await
&& body.appservice_info.is_none()
{
warn!(
"Non-admin user {sender_user} tried to publish {room_id} to the room directory \
while \"lockdown_public_room_directory\" is enabled"
);
if services.server.config.admin_room_notices {
// Determine room version
let (room_version, version_rules) = body
.room_version
.as_ref()
.map_or(Ok(&services.server.config.default_room_version), |version| {
services
.admin
.notice(&format!(
"Non-admin user {sender_user} tried to publish {room_id} to the room \
directory while \"lockdown_public_room_directory\" is enabled"
))
.await;
}
return Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")));
}
let _short_id = services
.rooms
.short
.get_or_create_shortroomid(&room_id)
.await;
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
let alias: Option<OwnedRoomAliasId> = match body.room_alias_name.as_ref() {
| Some(alias) =>
Some(room_alias_check(&services, alias, body.appservice_info.as_ref()).await?),
| _ => None,
};
let room_version = match body.room_version.clone() {
| Some(room_version) =>
if services
.server
.supported_room_version(&room_version)
{
room_version
} else {
return Err!(Request(UnsupportedRoomVersion(
"This server does not support that room version."
)));
},
| None => services
.server
.config
.default_room_version
.clone(),
};
.supported_room_version(version)
.then_ok_or_else(version, || {
err!(Request(UnsupportedRoomVersion(
"This server does not support room version {version:?}"
)))
})
})
.and_then(|version| Ok((version, room_version::rules(version)?)))?;
let create_content = match &body.creation_content {
| Some(content) => {
use RoomVersionId::*;
let mut content = content
.deserialize_as::<CanonicalJsonObject>()
.map_err(|e| {
err!(Request(BadJson(error!(
"Failed to deserialise content as canonical JSON: {e}"
))))
})?;
match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
content.insert(
"creator".into(),
json!(&sender_user).try_into().map_err(|e| {
err!(Request(BadJson(debug_error!("Invalid creation content: {e}"))))
})?,
);
},
| _ => {
// V11+ removed the "creator" key
},
}
content.insert(
"room_version".into(),
json!(room_version.as_str())
.try_into()
.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
);
content
},
| None => {
use RoomVersionId::*;
let content = match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
RoomCreateEventContent::new_v1(sender_user.to_owned()),
| _ => RoomCreateEventContent::new_v11(),
};
let mut content =
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())
.unwrap();
content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
content
},
};
// Error on existing alias before committing to creation.
let alias = alias.await.transpose()?;
// Increment and hold the counter; the room will sync atomically to clients
// which is preferable.
let next_count = services.globals.next_count();
// 1. The room create event
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_content)?,
state_key: Some(StateKey::new()),
..Default::default()
},
sender_user,
&room_id,
&state_lock,
)
.boxed()
.await?;
// 1. Create the create event.
let (room_id, state_lock) = match version_rules.room_id_format {
| RoomIdFormatVersion::V1 =>
create_create_event_legacy(&services, &body, room_version, &version_rules).await?,
| RoomIdFormatVersion::V2 =>
create_create_event(&services, &body, &preset, room_version, &version_rules)
.await
.map_err(|e| {
err!(Request(InvalidParam("Error while creating m.room.create event: {e}")))
})?,
};
// 2. Let the room creator join
let sender_user = body.sender_user();
services
.rooms
.timeline
@@ -230,17 +132,14 @@ pub(crate) async fn create_room_route(
.await?;
// 3. Power levels
// Figure out preset. We need it for preset specific events
let preset = body
.preset
.clone()
.unwrap_or(match &body.visibility {
| room::Visibility::Public => RoomPreset::PublicChat,
| _ => RoomPreset::PrivateChat, // Room visibility should not be custom
});
let mut users = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]);
let mut users = if !version_rules
.authorization
.explicitly_privilege_room_creators
{
BTreeMap::from_iter([(sender_user.to_owned(), int!(100))])
} else {
BTreeMap::new()
};
if preset == RoomPreset::TrustedPrivateChat {
for invite in &body.invite {
@@ -260,11 +159,17 @@ pub(crate) async fn create_room_route(
continue;
}
users.insert(invite.clone(), int!(100));
if !version_rules
.authorization
.additional_room_creators
{
users.insert(invite.clone(), int!(100));
}
}
}
let power_levels_content = default_power_levels_content(
&version_rules,
body.power_level_content_override.as_ref(),
&body.visibility,
users,
@@ -365,7 +270,7 @@ pub(crate) async fn create_room_route(
// 6. Events listed in initial_state
for event in &body.initial_state {
let mut pdu_builder = event
.deserialize_as::<PduBuilder>()
.deserialize_as_unchecked::<PduBuilder>()
.map_err(|e| {
err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}"))))
})?;
@@ -421,7 +326,10 @@ pub(crate) async fn create_room_route(
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomTopicEventContent { topic: topic.clone() }),
PduBuilder::state(String::new(), &RoomTopicEventContent {
topic: topic.clone(),
topic_block: TopicContentBlock::default(),
}),
sender_user,
&room_id,
&state_lock,
@@ -488,25 +396,226 @@ pub(crate) async fn create_room_route(
Ok(create_room::v3::Response::new(room_id))
}
async fn create_create_event(
services: &Services,
body: &Ruma<create_room::v3::Request>,
preset: &RoomPreset,
room_version: &RoomVersionId,
version_rules: &RoomVersionRules,
) -> Result<(OwnedRoomId, RoomMutexGuard)> {
let _sender_user = body.sender_user();
let mut create_content = match &body.creation_content {
| Some(content) => {
let mut content = content
.deserialize_as_unchecked::<CanonicalJsonObject>()
.map_err(|e| {
err!(Request(BadJson(error!(
"Failed to deserialise content as canonical JSON: {e}"
))))
})?;
content.insert(
"room_version".into(),
json!(room_version.as_str())
.try_into()
.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
);
content
},
| None => {
let content = RoomCreateEventContent::new_v11();
let mut content =
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
content
},
};
if version_rules
.authorization
.additional_room_creators
{
let mut additional_creators = body
.creation_content
.as_ref()
.and_then(|c| {
c.deserialize_as_unchecked::<CreationContent>()
.ok()
})
.unwrap_or_default()
.additional_creators;
if *preset == RoomPreset::TrustedPrivateChat {
additional_creators.extend(body.invite.clone());
}
additional_creators.sort();
additional_creators.dedup();
if !additional_creators.is_empty() {
create_content
.insert("additional_creators".into(), json!(additional_creators).try_into()?);
}
}
// 1. The room create event, using a placeholder room_id
let room_id = ruma::room_id!("!thiswillbereplaced").to_owned();
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
let create_event_id = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_content)?,
state_key: Some(StateKey::new()),
..Default::default()
},
body.sender_user(),
&room_id,
&state_lock,
)
.boxed()
.await?;
drop(state_lock);
// The real room_id is now the event_id.
let room_id = OwnedRoomId::from_parts('!', create_event_id.localpart(), None)?;
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
Ok((room_id, state_lock))
}
async fn create_create_event_legacy(
services: &Services,
body: &Ruma<create_room::v3::Request>,
room_version: &RoomVersionId,
_version_rules: &RoomVersionRules,
) -> Result<(OwnedRoomId, RoomMutexGuard)> {
let room_id: OwnedRoomId = match &body.room_id {
| None => RoomId::new_v1(&services.server.name),
| Some(custom_id) => custom_room_id_check(services, custom_id).await?,
};
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
let _short_id = services
.rooms
.short
.get_or_create_shortroomid(&room_id)
.await;
let create_content = match &body.creation_content {
| Some(content) => {
use RoomVersionId::*;
let mut content = content
.deserialize_as_unchecked::<CanonicalJsonObject>()
.map_err(|e| {
err!(Request(BadJson(error!(
"Failed to deserialise content as canonical JSON: {e}"
))))
})?;
match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
content.insert(
"creator".into(),
json!(body.sender_user())
.try_into()
.map_err(|e| {
err!(Request(BadJson(debug_error!(
"Invalid creation content: {e}"
))))
})?,
);
},
| _ => {
// V11+ removed the "creator" key
},
}
content.insert(
"room_version".into(),
json!(room_version.as_str())
.try_into()
.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
);
content
},
| None => {
use RoomVersionId::*;
let content = match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
RoomCreateEventContent::new_v1(body.sender_user().to_owned()),
| _ => RoomCreateEventContent::new_v11(),
};
let mut content =
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
content
},
};
// 1. The room create event
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_content)?,
state_key: Some(StateKey::new()),
..Default::default()
},
body.sender_user(),
&room_id,
&state_lock,
)
.boxed()
.await?;
Ok((room_id, state_lock))
}
/// creates the power_levels_content for the PDU builder
fn default_power_levels_content(
version_rules: &RoomVersionRules,
power_level_content_override: Option<&Raw<RoomPowerLevelsEventContent>>,
visibility: &room::Visibility,
users: BTreeMap<OwnedUserId, Int>,
) -> Result<serde_json::Value> {
use serde_json::to_value;
let mut power_levels_content =
to_value(RoomPowerLevelsEventContent { users, ..Default::default() })?;
let mut power_levels_content = RoomPowerLevelsEventContent::new(&version_rules.authorization);
power_levels_content.users = users;
let mut power_levels_content = to_value(power_levels_content)?;
// secure proper defaults of sensitive/dangerous permissions that moderators
// (power level 50) should not have easy access to
power_levels_content["events"]["m.room.power_levels"] = to_value(100)?;
power_levels_content["events"]["m.room.server_acl"] = to_value(100)?;
power_levels_content["events"]["m.room.tombstone"] = to_value(100)?;
power_levels_content["events"]["m.room.encryption"] = to_value(100)?;
power_levels_content["events"]["m.room.history_visibility"] = to_value(100)?;
if version_rules
.authorization
.explicitly_privilege_room_creators
{
power_levels_content["events"]["m.room.tombstone"] = to_value(150)?;
} else {
power_levels_content["events"]["m.room.tombstone"] = to_value(100)?;
}
// always allow users to respond (not post new) to polls. this is primarily
// useful in read-only announcement rooms that post a public poll.
power_levels_content["events"]["org.matrix.msc3381.poll.response"] = to_value(0)?;
@@ -599,7 +708,7 @@ async fn room_alias_check(
}
/// if a room is being created with a custom room ID, run our checks against it
fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<OwnedRoomId> {
async fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<OwnedRoomId> {
// apply forbidden room alias checks to custom room IDs too
if services
.globals
@@ -623,8 +732,65 @@ fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<Own
let server_name = services.globals.server_name();
let full_room_id = format!("!{custom_room_id}:{server_name}");
OwnedRoomId::parse(full_room_id)
.map_err(Into::into)
let room_id = OwnedRoomId::parse(full_room_id)
.inspect(|full_room_id| debug_info!(?full_room_id, "Full custom room ID"))
.inspect_err(|e| warn!(?e, ?custom_room_id, "Failed to create room with custom room ID",))
.inspect_err(|e| {
warn!(?e, ?custom_room_id, "Failed to create room with custom room ID");
})?;
// check if room ID doesn't already exist instead of erroring on auth check
if services
.rooms
.short
.get_shortroomid(&room_id)
.await
.is_ok()
{
return Err!(Request(RoomInUse("Room with that custom room ID already exists",)));
}
Ok(room_id)
}
async fn can_publish_directory_check(
services: &Services,
body: &Ruma<create_room::v3::Request>,
) -> Result {
if !services
.server
.config
.lockdown_public_room_directory
|| body.appservice_info.is_some()
|| body.visibility != room::Visibility::Public
|| services.users.is_admin(body.sender_user()).await
{
return Ok(());
}
let msg = format!(
"Non-admin user {} tried to publish new to the directory while \
lockdown_public_room_directory is enabled",
body.sender_user(),
);
warn!("{msg}");
if services.server.config.admin_room_notices {
services.admin.notice(&msg).await;
}
Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")))
}
async fn can_create_room_check(
services: &Services,
body: &Ruma<create_room::v3::Request>,
) -> Result {
if !services.globals.allow_room_creation()
&& body.appservice_info.is_none()
&& !services.users.is_admin(body.sender_user()).await
{
return Err!(Request(Forbidden("Room creation has been disabled.",)));
}
Ok(())
}
+43 -49
View File
@@ -12,7 +12,7 @@
federation::space::{SpaceHierarchyParentSummary, get_hierarchy},
},
events::room::member::MembershipState,
space::SpaceRoomJoinRule::{self, *},
room::{JoinRuleSummary, RoomSummary},
};
use tuwunel_core::{
Err, Result, debug_warn, trace,
@@ -34,8 +34,8 @@
pub(crate) async fn get_room_summary_legacy(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_summary::msc3266::Request>,
) -> Result<RumaResponse<get_summary::msc3266::Response>> {
body: Ruma<get_summary::v1::Request>,
) -> Result<RumaResponse<get_summary::v1::Response>> {
get_room_summary(State(services), InsecureClientIp(client), body)
.boxed()
.await
@@ -51,8 +51,8 @@ pub(crate) async fn get_room_summary_legacy(
pub(crate) async fn get_room_summary(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_summary::msc3266::Request>,
) -> Result<get_summary::msc3266::Response> {
body: Ruma<get_summary::v1::Request>,
) -> Result<get_summary::v1::Response> {
let (room_id, servers) = services
.rooms
.alias
@@ -73,7 +73,7 @@ async fn room_summary_response(
room_id: &RoomId,
servers: &[OwnedServerName],
sender_user: Option<&UserId>,
) -> Result<get_summary::msc3266::Response> {
) -> Result<get_summary::v1::Response> {
if services
.rooms
.state_cache
@@ -85,23 +85,12 @@ async fn room_summary_response(
.await;
}
let room =
remote_room_summary_hierarchy_response(services, room_id, servers, sender_user).await?;
let summary = remote_room_summary_hierarchy_response(services, room_id, servers, sender_user)
.await?
.summary;
Ok(get_summary::msc3266::Response {
room_id: room_id.to_owned(),
canonical_alias: room.canonical_alias,
avatar_url: room.avatar_url,
guest_can_join: room.guest_can_join,
name: room.name,
num_joined_members: room.num_joined_members,
topic: room.topic,
world_readable: room.world_readable,
join_rule: room.join_rule,
room_type: room.room_type,
room_version: room.room_version,
encryption: room.encryption,
allowed_room_ids: room.allowed_room_ids,
Ok(get_summary::v1::Response {
summary,
membership: sender_user
.is_some()
.then_some(MembershipState::Leave),
@@ -112,7 +101,7 @@ async fn local_room_summary_response(
services: &Services,
room_id: &RoomId,
sender_user: Option<&UserId>,
) -> Result<get_summary::msc3266::Response> {
) -> Result<get_summary::v1::Response> {
trace!(?sender_user, "Sending local room summary response for {room_id:?}");
let join_rule = services
.rooms
@@ -139,7 +128,7 @@ async fn local_room_summary_response(
&join_rule.clone().into(),
guest_can_join,
world_readable,
join_rule.allowed_rooms(),
join_rule.allowed_room_ids(),
sender_user,
)
.await?;
@@ -224,24 +213,22 @@ async fn local_room_summary_response(
membership,
);
Ok(get_summary::msc3266::Response {
room_id: room_id.to_owned(),
canonical_alias,
avatar_url,
guest_can_join,
name,
num_joined_members: num_joined_members.try_into().unwrap_or_default(),
topic,
world_readable,
room_type,
room_version,
encryption,
Ok(get_summary::v1::Response {
summary: RoomSummary {
room_id: room_id.to_owned(),
canonical_alias,
avatar_url,
guest_can_join,
name,
num_joined_members: num_joined_members.try_into().unwrap_or_default(),
topic,
world_readable,
room_type,
room_version,
encryption,
join_rule: join_rule.into(),
},
membership,
allowed_room_ids: join_rule
.allowed_rooms()
.map(Into::into)
.collect(),
join_rule: join_rule.into(),
})
}
@@ -277,10 +264,11 @@ async fn remote_room_summary_hierarchy_response(
while let Some(Ok(response)) = requests.next().await {
trace!("{response:?}");
let room = response.room.clone();
if room.room_id != room_id {
let summary = &room.summary;
if summary.room_id != room_id {
debug_warn!(
"Room ID {} returned does not belong to the requested room ID {}",
room.room_id,
summary.room_id,
room_id
);
continue;
@@ -289,10 +277,10 @@ async fn remote_room_summary_hierarchy_response(
return user_can_see_summary(
services,
room_id,
&room.join_rule,
room.guest_can_join,
room.world_readable,
room.allowed_room_ids.iter().map(AsRef::as_ref),
&summary.join_rule,
summary.guest_can_join,
summary.world_readable,
summary.join_rule.allowed_room_ids(),
sender_user,
)
.await
@@ -308,7 +296,7 @@ async fn remote_room_summary_hierarchy_response(
async fn user_can_see_summary<'a, I>(
services: &Services,
room_id: &RoomId,
join_rule: &SpaceRoomJoinRule,
join_rule: &JoinRuleSummary,
guest_can_join: bool,
world_readable: bool,
allowed_room_ids: I,
@@ -317,17 +305,23 @@ async fn user_can_see_summary<'a, I>(
where
I: Iterator<Item = &'a RoomId> + Send,
{
let is_public_room = matches!(join_rule, Public | Knock | KnockRestricted);
let is_public_room = matches!(
join_rule,
JoinRuleSummary::Public | JoinRuleSummary::Knock | JoinRuleSummary::KnockRestricted(_)
);
match sender_user {
| Some(sender_user) => {
let user_can_see_state_events = services
.rooms
.state_accessor
.user_can_see_state_events(sender_user, room_id);
let is_guest = services
.users
.is_deactivated(sender_user)
.unwrap_or(false);
let user_in_allowed_restricted_room = allowed_room_ids.stream().any(|room| {
services
.rooms
+17 -7
View File
@@ -4,7 +4,7 @@
use futures::StreamExt;
use ruma::{
CanonicalJsonObject, RoomId, RoomVersionId,
api::client::{error::ErrorKind, room::upgrade_room},
api::client::room::upgrade_room,
events::{
StateEventType, TimelineEventType,
room::{
@@ -14,11 +14,12 @@
},
},
int,
room_version_rules::RoomIdFormatVersion,
};
use serde_json::{json, value::to_raw_value};
use tuwunel_core::{
Err, Error, Result, err,
matrix::{Event, StateKey, pdu::PduBuilder},
Err, Result, err,
matrix::{Event, StateKey, pdu::PduBuilder, room_version},
};
use crate::Ruma;
@@ -61,14 +62,23 @@ pub(crate) async fn upgrade_room_route(
.server
.supported_room_version(&body.new_version)
{
return Err(Error::BadRequest(
ErrorKind::UnsupportedRoomVersion,
return Err!(Request(UnsupportedRoomVersion(
"This server does not support that room version.",
));
)));
}
if matches!(body.new_version, RoomVersionId::V12) {
return Err!(Request(UnsupportedRoomVersion(
"Upgrading to version 12 is still under development.",
)));
}
let room_version_rules = room_version::rules(&body.new_version)?;
let room_id_format = &room_version_rules.room_id_format;
assert!(*room_id_format == RoomIdFormatVersion::V1, "TODO");
// Create a replacement room
let replacement_room = RoomId::new(services.globals.server_name());
let replacement_room = RoomId::new_v1(services.globals.server_name());
let _short_id = services
.rooms
+2
View File
@@ -1,3 +1,4 @@
use futures::FutureExt;
use ruma::{OwnedUserId, UserId};
use tuwunel_core::{Err, Result, debug};
use tuwunel_service::Services;
@@ -63,6 +64,7 @@ pub(super) async fn ldap_login(
services
.admin
.make_user_admin(lowercased_user_id)
.boxed()
.await?;
} else if !is_ldap_admin && is_tuwunel_admin {
services
+1 -4
View File
@@ -56,10 +56,7 @@ pub(crate) async fn login_token_route(
let mut uiaainfo = uiaa::UiaaInfo {
flows: vec![password_flow],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
..Default::default()
};
match &body.auth {
+52 -51
View File
@@ -2,7 +2,7 @@
use futures::{FutureExt, TryFutureExt, TryStreamExt};
use ruma::{
OwnedEventId, RoomId, UserId,
api::client::state::{get_state_events, get_state_events_for_key, send_state_event},
api::client::state::{get_state_event_for_key, get_state_events, send_state_event},
events::{
AnyStateEventContent, StateEventType,
room::{
@@ -107,8 +107,8 @@ pub(crate) async fn get_state_events_route(
/// readable
pub(crate) async fn get_state_events_for_key_route(
State(services): State<crate::State>,
body: Ruma<get_state_events_for_key::v3::Request>,
) -> Result<get_state_events_for_key::v3::Response> {
body: Ruma<get_state_event_for_key::v3::Request>,
) -> Result<get_state_event_for_key::v3::Response> {
let sender_user = body.sender_user();
if !services
@@ -140,7 +140,7 @@ pub(crate) async fn get_state_events_for_key_route(
.as_ref()
.is_some_and(|f| f.to_lowercase().eq("event"));
Ok(get_state_events_for_key::v3::Response {
Ok(get_state_event_for_key::v3::Response {
content: event_format.or(|| event.get_content_as_value()),
event: event_format.then(|| {
json!({
@@ -167,8 +167,8 @@ pub(crate) async fn get_state_events_for_key_route(
/// readable
pub(crate) async fn get_state_events_for_empty_key_route(
State(services): State<crate::State>,
body: Ruma<get_state_events_for_key::v3::Request>,
) -> Result<RumaResponse<get_state_events_for_key::v3::Response>> {
body: Ruma<get_state_event_for_key::v3::Request>,
) -> Result<RumaResponse<get_state_event_for_key::v3::Response>> {
get_state_events_for_key_route(State(services), body)
.await
.map(RumaResponse)
@@ -222,7 +222,7 @@ async fn allowed_to_send_state_event(
| StateEventType::RoomServerAcl => {
// prevents common ACL paw-guns as ACL management is difficult and prone to
// irreversible mistakes
match json.deserialize_as::<RoomServerAclEventContent>() {
match json.deserialize_as_unchecked::<RoomServerAclEventContent>() {
| Ok(acl_content) => {
if acl_content.allow_is_empty() {
return Err!(Request(BadJson(debug_warn!(
@@ -282,7 +282,7 @@ async fn allowed_to_send_state_event(
// admin room is a sensitive room, it should not ever be made public
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
if admin_room_id == room_id {
match json.deserialize_as::<RoomJoinRulesEventContent>() {
match json.deserialize_as_unchecked::<RoomJoinRulesEventContent>() {
| Ok(join_rule) =>
if join_rule.join_rule == JoinRule::Public {
return Err!(Request(Forbidden(
@@ -301,7 +301,7 @@ async fn allowed_to_send_state_event(
| StateEventType::RoomHistoryVisibility => {
// admin room is a sensitive room, it should not ever be made world readable
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
match json.deserialize_as::<RoomHistoryVisibilityEventContent>() {
match json.deserialize_as_unchecked::<RoomHistoryVisibilityEventContent>() {
| Ok(visibility_content) => {
if admin_room_id == room_id
&& visibility_content.history_visibility
@@ -322,7 +322,7 @@ async fn allowed_to_send_state_event(
}
},
| StateEventType::RoomCanonicalAlias => {
match json.deserialize_as::<RoomCanonicalAliasEventContent>() {
match json.deserialize_as_unchecked::<RoomCanonicalAliasEventContent>() {
| Ok(canonical_alias_content) => {
let mut aliases = canonical_alias_content.alt_aliases.clone();
@@ -354,52 +354,53 @@ async fn allowed_to_send_state_event(
},
}
},
| StateEventType::RoomMember => match json.deserialize_as::<RoomMemberEventContent>() {
| Ok(membership_content) => {
let Ok(_state_key) = UserId::parse(state_key) else {
return Err!(Request(BadJson(
"Membership event has invalid or non-existent state key"
)));
};
if let Some(authorising_user) =
membership_content.join_authorized_via_users_server
{
if membership_content.membership != MembershipState::Join {
| StateEventType::RoomMember =>
match json.deserialize_as_unchecked::<RoomMemberEventContent>() {
| Ok(membership_content) => {
let Ok(_state_key) = UserId::parse(state_key) else {
return Err!(Request(BadJson(
"join_authorised_via_users_server is only for member joins"
"Membership event has invalid or non-existent state key"
)));
}
};
if !services.globals.user_is_local(&authorising_user) {
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} does not belong to this \
homeserver"
)));
}
if let Some(authorising_user) =
membership_content.join_authorized_via_users_server
{
if membership_content.membership != MembershipState::Join {
return Err!(Request(BadJson(
"join_authorised_via_users_server is only for member joins"
)));
}
services
.rooms
.state_cache
.is_joined(&authorising_user, room_id)
.map(is_false!())
.map(BoolExt::into_result)
.map_err(|()| {
err!(Request(InvalidParam(
"Authorising user {authorising_user} is not in the room. They \
cannot authorise the join."
)))
})
.await?;
}
if !services.globals.user_is_local(&authorising_user) {
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} does not belong to this \
homeserver"
)));
}
services
.rooms
.state_cache
.is_joined(&authorising_user, room_id)
.map(is_false!())
.map(BoolExt::into_result)
.map_err(|()| {
err!(Request(InvalidParam(
"Authorising user {authorising_user} is not in the room. \
They cannot authorise the join."
)))
})
.await?;
}
},
| Err(e) => {
return Err!(Request(BadJson(
"Membership content must have a valid JSON body with at least a valid \
membership state: {e}"
)));
},
},
| Err(e) => {
return Err!(Request(BadJson(
"Membership content must have a valid JSON body with at least a valid \
membership state: {e}"
)));
},
},
| _ => (),
}
+17 -7
View File
@@ -14,11 +14,11 @@
api::client::{
filter::FilterDefinition,
sync::sync_events::{
self, DeviceLists, UnreadNotificationsCount,
self, DeviceLists, StrippedState, UnreadNotificationsCount,
v3::{
Ephemeral, Filter, GlobalAccountData, InviteState, InvitedRoom, JoinedRoom,
KnockState, KnockedRoom, LeftRoom, Presence, RoomAccountData, RoomSummary, Rooms,
State as RoomState, Timeline, ToDevice,
State as RoomState, StateEvents, Timeline, ToDevice,
},
},
uiaa::UiaaResponse,
@@ -295,7 +295,12 @@ async fn build_sync_events(
}
let invited_room = InvitedRoom {
invite_state: InviteState { events: invite_state },
invite_state: InviteState {
events: invite_state
.into_iter()
.map(Raw::cast::<StrippedState>)
.collect(),
},
};
invited_rooms.insert(room_id, invited_room);
@@ -320,7 +325,12 @@ async fn build_sync_events(
}
let knocked_room = KnockedRoom {
knock_state: KnockState { events: knock_state },
knock_state: KnockState {
events: knock_state
.into_iter()
.map(Raw::cast::<StrippedState>)
.collect(),
},
};
knocked_rooms.insert(room_id, knocked_room);
@@ -540,7 +550,7 @@ async fn handle_left_room(
prev_batch: Some(next_batch.to_string()),
events: Vec::new(),
},
state: RoomState { events: vec![event.into_format()] },
state: RoomState::Before(StateEvents { events: vec![event.into_format()] }),
}));
}
@@ -635,7 +645,7 @@ async fn handle_left_room(
prev_batch: Some(next_batch.to_string()),
events: Vec::new(), // and so we dont need to set this to empty vec
},
state: RoomState { events: left_state_events },
state: RoomState::Before(StateEvents { events: left_state_events }),
}))
}
@@ -1042,7 +1052,7 @@ async fn load_joined_room(
let joined_room = JoinedRoom {
account_data: RoomAccountData { events: account_data_events },
ephemeral: Ephemeral { events: edus },
state: RoomState { events: state_events },
state: RoomState::Before(StateEvents { events: state_events }),
summary: RoomSummary {
joined_member_count: joined_member_count.map(ruma_from_u64),
invited_member_count: invited_member_count.map(ruma_from_u64),
+13 -3
View File
@@ -13,7 +13,10 @@
};
use ruma::{
DeviceId, OwnedEventId, OwnedRoomId, RoomId, UInt, UserId,
api::client::sync::sync_events::{self, DeviceLists, UnreadNotificationsCount},
api::client::sync::sync_events::{
self, DeviceLists, StrippedState, UnreadNotificationsCount,
v5::request::ExtensionRoomConfig,
},
directory::RoomTypeFilter,
events::{
AnyRawAccountDataEvent, AnySyncEphemeralRoomEvent, StateEventType, TimelineEventType,
@@ -647,7 +650,11 @@ async fn process_rooms<'a, Rooms>(
name: room_name.or(hero_name),
initial: Some(roomsince == &0),
is_dm: None,
invite_state,
invite_state: invite_state.map(|s| {
s.into_iter()
.map(Raw::cast::<StrippedState>)
.collect()
}),
unread_notifications: UnreadNotificationsCount {
highlight_count: Some(
services
@@ -727,7 +734,10 @@ async fn collect_account_data(
.await;
if let Some(rooms) = &body.extensions.account_data.rooms {
for room in rooms {
for room in rooms
.iter()
.filter_map(|erc| extract_variant!(erc, ExtensionRoomConfig::Room))
{
account_data.rooms.insert(
room.clone(),
services
+16 -20
View File
@@ -146,7 +146,7 @@ pub(crate) async fn set_profile_key_route(
)));
}
let Some(profile_key_value) = body.kv_pair.get(&body.key_name) else {
let Some(profile_key_value) = body.kv_pair.get(&body.key) else {
return Err!(Request(BadJson(
"The key does not match the URL field key, or JSON body is empty (use DELETE)"
)));
@@ -164,7 +164,7 @@ pub(crate) async fn set_profile_key_route(
return Err!(Request(BadJson("Key names cannot be longer than 128 bytes")));
}
if body.key_name == "displayname" {
if body.key == "displayname" {
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
@@ -180,7 +180,7 @@ pub(crate) async fn set_profile_key_route(
&all_joined_rooms,
)
.await;
} else if body.key_name == "avatar_url" {
} else if body.key == "avatar_url" {
let mxc = ruma::OwnedMxcUri::from(profile_key_value.to_string());
let all_joined_rooms: Vec<OwnedRoomId> = services
@@ -193,11 +193,9 @@ pub(crate) async fn set_profile_key_route(
update_avatar_url(&services, &body.user_id, Some(mxc), None, &all_joined_rooms).await;
} else {
services.users.set_profile_key(
&body.user_id,
&body.key_name,
Some(profile_key_value.clone()),
);
services
.users
.set_profile_key(&body.user_id, &body.key, Some(profile_key_value.clone()));
}
if services.config.allow_local_presence {
@@ -233,7 +231,7 @@ pub(crate) async fn delete_profile_key_route(
)));
}
if body.key_name == "displayname" {
if body.key == "displayname" {
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
@@ -243,7 +241,7 @@ pub(crate) async fn delete_profile_key_route(
.await;
update_displayname(&services, &body.user_id, None, &all_joined_rooms).await;
} else if body.key_name == "avatar_url" {
} else if body.key == "avatar_url" {
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
@@ -256,7 +254,7 @@ pub(crate) async fn delete_profile_key_route(
} else {
services
.users
.set_profile_key(&body.user_id, &body.key_name, None);
.set_profile_key(&body.user_id, &body.key, None);
}
if services.config.allow_local_presence {
@@ -379,14 +377,12 @@ pub(crate) async fn get_profile_key_route(
.users
.set_timezone(&body.user_id, response.tz.clone());
match response.custom_profile_fields.get(&body.key_name) {
match response.custom_profile_fields.get(&body.key) {
| Some(value) => {
profile_key_value.insert(body.key_name.clone(), value.clone());
services.users.set_profile_key(
&body.user_id,
&body.key_name,
Some(value.clone()),
);
profile_key_value.insert(body.key.clone(), value.clone());
services
.users
.set_profile_key(&body.user_id, &body.key, Some(value.clone()));
},
| _ => {
return Err!(Request(NotFound("The requested profile key does not exist.")));
@@ -409,11 +405,11 @@ pub(crate) async fn get_profile_key_route(
match services
.users
.profile_key(&body.user_id, &body.key_name)
.profile_key(&body.user_id, &body.key)
.await
{
| Ok(value) => {
profile_key_value.insert(body.key_name.clone(), value);
profile_key_value.insert(body.key.clone(), value);
},
| _ => {
return Err!(Request(NotFound("The requested profile key does not exist.")));
+11 -14
View File
@@ -1,12 +1,9 @@
use axum::{Json, extract::State, response::IntoResponse};
use ruma::api::client::{
discovery::{
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
discover_support::{self, Contact},
},
error::ErrorKind,
use ruma::api::client::discovery::{
discover_homeserver::{self, HomeserverInfo},
discover_support::{self, Contact},
};
use tuwunel_core::{Error, Result};
use tuwunel_core::{Err, Result};
use crate::Ruma;
@@ -19,13 +16,12 @@ pub(crate) async fn well_known_client(
) -> Result<discover_homeserver::Response> {
let client_url = match services.server.config.well_known.client.as_ref() {
| Some(url) => url.to_string(),
| None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
| None => return Err!(Request(NotFound("Not found."))),
};
Ok(discover_homeserver::Response {
homeserver: HomeserverInfo { base_url: client_url.clone() },
homeserver: HomeserverInfo { base_url: client_url },
identity_server: None,
sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }),
tile_server: None,
})
}
@@ -54,7 +50,7 @@ pub(crate) async fn well_known_support(
// support page or role must be either defined for this to be valid
if support_page.is_none() && role.is_none() {
return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
return Err!(Request(NotFound("Not found.")));
}
let email_address = services
@@ -63,6 +59,7 @@ pub(crate) async fn well_known_support(
.well_known
.support_email
.clone();
let matrix_id = services
.server
.config
@@ -72,7 +69,7 @@ pub(crate) async fn well_known_support(
// if a role is specified, an email address or matrix id is required
if role.is_some() && (email_address.is_none() && matrix_id.is_none()) {
return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
return Err!(Request(NotFound("Not found.")));
}
// TODO: support defining multiple contacts in the config
@@ -86,7 +83,7 @@ pub(crate) async fn well_known_support(
// support page or role+contacts must be either defined for this to be valid
if contacts.is_empty() && support_page.is_none() {
return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
return Err!(Request(NotFound("Not found.")));
}
Ok(discover_support::Response { contacts, support_page })
@@ -103,7 +100,7 @@ pub(crate) async fn syncv3_client_server_json(
| Some(url) => url.to_string(),
| None => match services.server.config.well_known.server.as_ref() {
| Some(url) => url.to_string(),
| None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
| None => return Err!(Request(NotFound("Not found."))),
},
};
+1 -1
View File
@@ -1,4 +1,4 @@
#![type_length_limit = "65536"] //TODO: reduce me
#![type_length_limit = "262144"] //TODO: REDUCE ME
#![allow(clippy::toplevel_ref_arg)]
pub mod client;
+18 -12
View File
@@ -69,7 +69,10 @@ pub(super) async fn auth(
json_body: Option<&CanonicalJsonValue>,
metadata: &Metadata,
) -> Result<Auth> {
use AuthScheme::{AccessToken, AccessTokenOptional, AppserviceToken, ServerSignatures};
use AuthScheme::{
AccessToken, AccessTokenOptional, AppserviceToken, AppserviceTokenOptional,
ServerSignatures,
};
use Error::BadRequest;
use ErrorKind::UnknownToken;
use Token::{Appservice, Expired, Invalid, User};
@@ -129,7 +132,7 @@ pub(super) async fn auth(
| (AccessToken, Appservice(info)) => Ok(auth_appservice(services, request, info).await?),
| (AccessToken, Token::None) => match metadata {
| (AccessToken | AppserviceToken, Token::None) => match metadata {
| &get_turn_server_info::v3::Request::METADATA
if services.server.config.turn_allow_guests =>
Ok(Auth::default()),
@@ -137,22 +140,25 @@ pub(super) async fn auth(
| _ => Err!(Request(MissingToken("Missing access token."))),
},
| (AccessToken | AccessTokenOptional | AuthScheme::None, User(user)) => Ok(Auth {
| (
AccessToken | AccessTokenOptional | AppserviceTokenOptional | AuthScheme::None,
User(user),
) => Ok(Auth {
sender_user: Some(user.0),
sender_device: Some(user.1),
_expires_at: user.2,
..Auth::default()
}),
//TODO: add AppserviceTokenOptional
| (AccessTokenOptional | AppserviceToken | AuthScheme::None, Appservice(info)) =>
Ok(Auth {
appservice_info: Some(*info),
..Auth::default()
}),
| (
AccessTokenOptional | AppserviceTokenOptional | AppserviceToken | AuthScheme::None,
Appservice(info),
) => Ok(Auth {
appservice_info: Some(*info),
..Auth::default()
}),
//TODO: add AppserviceTokenOptional
| (AccessTokenOptional | AppserviceToken | AuthScheme::None, Token::None) =>
| (AccessTokenOptional | AppserviceTokenOptional | AuthScheme::None, Token::None) =>
Ok(Auth::default()),
}
}
@@ -306,7 +312,7 @@ async fn auth_server(
let keys: PubKeys = [(x_matrix.key.to_string(), key.key)].into();
let keys: PubKeyMap = [(origin.as_str().into(), keys)].into();
if let Err(e) = ruma::signatures::verify_json(&keys, authorization) {
if let Err(e) = ruma::signatures::verify_json(&keys, &authorization) {
debug_error!("Failed to verify federation request from {origin}: {e}");
if request.parts.uri.to_string().contains('@') {
warn!(
+12 -11
View File
@@ -64,17 +64,18 @@ pub(crate) async fn get_hierarchy_route(
})
.unzip()
.map(|(children, inaccessible_children): (Vec<_>, Vec<_>)| {
(
children
.into_iter()
.flatten()
.map(Into::into)
.collect(),
inaccessible_children
.into_iter()
.flatten()
.collect(),
)
let children = children
.into_iter()
.flatten()
.map(|parent| parent.summary)
.collect();
let inaccessible_children = inaccessible_children
.into_iter()
.flatten()
.collect();
(children, inaccessible_children)
})
.await;
+11 -3
View File
@@ -3,12 +3,15 @@
use base64::{Engine as _, engine::general_purpose};
use ruma::{
CanonicalJsonValue, OwnedUserId, UserId,
api::{client::error::ErrorKind, federation::membership::create_invite},
api::{
client::error::ErrorKind,
federation::membership::{RawStrippedState, create_invite},
},
events::room::member::{MembershipState, RoomMemberEventContent},
serde::JsonObject,
};
use tuwunel_core::{
Err, Error, Result, err,
Err, Error, Result, err, extract_variant,
matrix::{Event, PduEvent, event::gen_event_id},
utils,
utils::hash::sha256,
@@ -119,7 +122,12 @@ pub(crate) async fn create_invite_route(
return Err!(Request(Forbidden("This server does not allow room invites.")));
}
let mut invite_state = body.invite_room_state.clone();
let mut invite_state: Vec<_> = body
.invite_room_state
.clone()
.into_iter()
.filter_map(|s| extract_variant!(s, RawStrippedState::Stripped))
.collect();
let mut event: JsonObject = serde_json::from_str(body.event.get())
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
+4 -4
View File
@@ -2,7 +2,7 @@
use axum::extract::State;
use ruma::{
RoomVersionId,
api::{client::error::ErrorKind, federation::knock::create_knock_event_template},
api::{client::error::ErrorKind, federation::membership::prepare_knock_event},
events::room::member::{MembershipState, RoomMemberEventContent},
};
use serde_json::value::to_raw_value;
@@ -15,8 +15,8 @@
/// Creates a knock template.
pub(crate) async fn create_knock_event_template_route(
State(services): State<crate::State>,
body: Ruma<create_knock_event_template::v1::Request>,
) -> Result<create_knock_event_template::v1::Response> {
body: Ruma<prepare_knock_event::v1::Request>,
) -> Result<prepare_knock_event::v1::Response> {
if !services
.rooms
.metadata
@@ -124,7 +124,7 @@ pub(crate) async fn create_knock_event_template_route(
// room v3 and above removed the "event_id" field from remote PDU format
super::maybe_strip_event_id(&mut pdu_json, &room_version_id)?;
Ok(create_knock_event_template::v1::Response {
Ok(prepare_knock_event::v1::Response {
room_version: room_version_id,
event: to_raw_value(&pdu_json).expect("CanonicalJson can be serialized to JSON"),
})
+1
View File
@@ -354,6 +354,7 @@ pub(crate) async fn create_join_event_v2_route(
create_join_event(&services, body.origin(), &body.room_id, &body.pdu)
.boxed()
.await?;
let room_state = create_join_event::v2::RoomState {
members_omitted: false,
auth_chain,
+13 -6
View File
@@ -3,7 +3,7 @@
use ruma::{
OwnedServerName, OwnedUserId,
RoomVersionId::*,
api::federation::knock::send_knock,
api::federation::membership::create_knock_event,
events::{
StateEventType,
room::member::{MembershipState, RoomMemberEventContent},
@@ -23,8 +23,8 @@
/// Submits a signed knock event.
pub(crate) async fn create_knock_event_v1_route(
State(services): State<crate::State>,
body: Ruma<send_knock::v1::Request>,
) -> Result<send_knock::v1::Response> {
body: Ruma<create_knock_event::v1::Request>,
) -> Result<create_knock_event::v1::Response> {
if services
.config
.forbidden_remote_server_names
@@ -189,7 +189,14 @@ pub(crate) async fn create_knock_event_v1_route(
.send_pdu_room(&body.room_id, &pdu_id)
.await?;
let knock_room_state = services.rooms.state.summary_stripped(&pdu).await;
Ok(send_knock::v1::Response { knock_room_state })
Ok(create_knock_event::v1::Response {
knock_room_state: services
.rooms
.state
.summary_stripped(&pdu)
.await
.into_iter()
.map(Into::into)
.collect(),
})
}
+2
View File
@@ -119,7 +119,9 @@ hardened_malloc-rs.workspace = true
hardened_malloc-rs.optional = true
[dev-dependencies]
insta.workspace = true
maplit.workspace = true
similar.workspace = true
[lints]
workspace = true
+44 -7
View File
@@ -665,20 +665,33 @@ pub struct Config {
pub allow_room_creation: bool,
/// Set to false to disable users from joining or creating room versions
/// that aren't officially supported by tuwunel.
/// that aren't officially supported by tuwunel. Unstable room versions may
/// have flawed specifications or our implementation may be non-conforming.
/// Correct operation may not be guaranteed, but incorrect operation may be
/// tolerable and unnoticed.
///
/// tuwunel officially supports room versions 6 - 11.
/// tuwunel officially supports room versions 6+. tuwunel has slightly
/// experimental (though works fine in practice) support for versions 3 - 5.
///
/// tuwunel has slightly experimental (though works fine in practice)
/// support for versions 3 - 5.
/// default: true
#[serde(default = "true_fn")]
pub allow_unstable_room_versions: bool,
/// Set to true to enable experimental room versions.
///
/// Unlike unstable room versions these versions are either under
/// development, protype spec-changes, or somehow present a serious risk to
/// the server's operation or database corruption. This is for developer use
/// only.
#[serde(default)]
pub allow_experimental_room_versions: bool,
/// Default room version tuwunel will create rooms with.
///
/// Per spec, room version 11 is the default.
///
/// default: 11
/// The default is prescribed by the spec, but may be selected by developer
/// recommendation. To prevent stale documentation we no longer list it
/// here. It is only advised to override this if you know what you are
/// doing, and by doing so, updates with new versions are precluded.
#[serde(default = "default_default_room_version")]
pub default_room_version: RoomVersionId,
@@ -1885,6 +1898,30 @@ pub struct Config {
#[serde(default)]
pub allow_invalid_tls_certificates: bool,
/// Backport state-reset security fixes to all room versions.
///
/// This option applies the State Resolution 2.1 mitigation developed during
/// project Hydra for room version 12 to all prior State Resolution 2.0 room
/// versions (all room versions supported by this server). These mitigations
/// increase resilience to state-resets without any new definition of
/// correctness; therefor it is safe to set this to true for existing rooms.
///
/// Furthermore, state-reset attacks are not consistent as they result in
/// rooms without any single consensus, therefor it is unnecessary to set
/// this to false to match other servers which set this to false or simply
/// lack support; even if replicating the post-reset state suffered by other
/// servers is somehow desired.
///
/// This option exists for developer and debug use, and as a failsafe in
/// lieu of hardcoding it.
///
/// This currently defaults to false as a matter of development until
/// real-world testing can shake out any implementation issues rather than
/// jeopardize existing rooms, but otherwise will default to true at the
/// next point release or patch.
#[serde(default)]
pub hydra_backports: bool,
// external structure; separate section
#[serde(default)]
pub blurhashing: BlurhashConfig,
+2 -2
View File
@@ -118,6 +118,8 @@ pub enum Error {
Mxc(#[from] ruma::MxcUriError),
#[error(transparent)]
Mxid(#[from] ruma::IdParseError),
#[error(transparent)]
PowerLevels(#[from] ruma::events::room::power_levels::PowerLevelsError),
#[error("from {0}: {1}")]
Redaction(ruma::OwnedServerName, ruma::canonical_json::RedactionError),
#[error("{0}: {1}")]
@@ -126,8 +128,6 @@ pub enum Error {
Ruma(#[from] ruma::api::client::error::Error),
#[error(transparent)]
Signatures(#[from] ruma::signatures::Error),
#[error(transparent)]
StateRes(#[from] crate::state_res::Error),
#[error("uiaa")]
Uiaa(ruma::api::client::uiaa::UiaaInfo),
+1 -4
View File
@@ -41,10 +41,7 @@ fn from(error: Error) -> Self {
message: error.message(),
};
Self::MatrixError(ruma::api::client::error::Error {
status_code: error.status_code(),
body,
})
Self::MatrixError(ruma::api::client::error::Error::new(error.status_code(), body))
}
}
+19 -4
View File
@@ -2,10 +2,19 @@
use std::iter::once;
use ruma::{RoomVersionId, api::client::discovery::get_capabilities::RoomVersionStability};
use ruma::{RoomVersionId, api::client::discovery::get_capabilities::v3::RoomVersionStability};
use crate::{at, is_equal_to};
/// Partially supported non-compliant room versions
pub const UNSTABLE_ROOM_VERSIONS: &[RoomVersionId] = &[
RoomVersionId::V1,
RoomVersionId::V2,
RoomVersionId::V3,
RoomVersionId::V4,
RoomVersionId::V5,
];
/// Supported and stable room versions
pub const STABLE_ROOM_VERSIONS: &[RoomVersionId] = &[
RoomVersionId::V6,
@@ -16,9 +25,8 @@
RoomVersionId::V11,
];
/// Experimental, partially supported room versions
pub const UNSTABLE_ROOM_VERSIONS: &[RoomVersionId] =
&[RoomVersionId::V2, RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5];
/// Experimental and prototype room versions under development.
pub const EXPERIMENTAL_ROOM_VERSIONS: &[RoomVersionId] = &[RoomVersionId::V12];
type RoomVersion = (RoomVersionId, RoomVersionStability);
@@ -31,8 +39,15 @@ pub fn supported_room_version(&self, version: &RoomVersionId) -> bool {
#[inline]
pub fn supported_room_versions(&self) -> impl Iterator<Item = RoomVersionId> + '_ {
let experimental_room_versions = EXPERIMENTAL_ROOM_VERSIONS
.iter()
.cloned()
.zip(once(RoomVersionStability::Unstable).cycle())
.filter(|_| self.config.allow_experimental_room_versions);
Self::available_room_versions()
.filter(|(_, stability)| self.supported_stability(stability))
.chain(experimental_room_versions)
.map(at!(0))
}
+22 -2
View File
@@ -4,6 +4,7 @@
mod id;
mod redact;
mod relation;
pub mod state_key;
mod type_ext;
mod unsigned;
@@ -16,12 +17,23 @@
use serde::Deserialize;
use serde_json::{Value as JsonValue, value::RawValue as RawJsonValue};
pub use self::{filter::Matches, id::*, relation::RelationTypeEqual, type_ext::TypeExt};
use super::{pdu::Pdu, state_key::StateKey};
pub use self::{
filter::Matches,
id::*,
relation::RelationTypeEqual,
state_key::{StateKey, TypeStateKey},
type_ext::TypeExt,
};
use super::pdu::Pdu;
use crate::{Result, utils};
/// Abstraction of a PDU so users can have their own PDU types.
pub trait Event: Clone + Debug + Send + Sync {
#[inline]
fn is_type_and_state_key(&self, kind: &TimelineEventType, state_key: &str) -> bool {
self.kind() == kind && self.state_key() == Some(state_key)
}
/// Serialize into a Ruma JSON format, consuming.
#[inline]
fn into_format<T>(self) -> T
@@ -152,6 +164,11 @@ fn as_mut_pdu(&mut self) -> &mut Pdu { unimplemented!("not a mutable Pdu") }
/// All the authenticating events for this event.
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_;
/// All the authenticating events for this event.
fn auth_events_into(
self,
) -> impl IntoIterator<IntoIter = impl Iterator<Item = OwnedEventId>> + Send;
/// The event's content.
fn content(&self) -> &RawJsonValue;
@@ -167,6 +184,9 @@ fn as_mut_pdu(&mut self) -> &mut Pdu { unimplemented!("not a mutable Pdu") }
/// If this event is a redaction event this is the event it redacts.
fn redacts(&self) -> Option<&EventId>;
/// see: https://spec.matrix.org/v1.14/rooms/v11/#rejected-events
fn rejected(&self) -> bool;
/// The `RoomId` of this event.
fn room_id(&self) -> &RoomId;
+33 -3
View File
@@ -1,8 +1,8 @@
use ruma::{
events::{
AnyMessageLikeEvent, AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent,
AnySyncTimelineEvent, AnyTimelineEvent, StateEvent, room::member::RoomMemberEventContent,
space::child::HierarchySpaceChildEvent,
AnyMessageLikeEvent, AnyStateEvent, AnyStrippedStateEvent, AnySyncMessageLikeEvent,
AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent, StateEvent,
room::member::RoomMemberEventContent, space::child::HierarchySpaceChildEvent,
},
serde::Raw,
};
@@ -106,6 +106,36 @@ fn from(event: Ref<'a, E>) -> Self {
}
}
impl<E: Event> From<Owned<E>> for Raw<AnySyncMessageLikeEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<AnySyncMessageLikeEvent> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let (redacts, content) = redact::copy(event);
let mut json = json!({
"content": content,
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"sender": event.sender(),
"type": event.kind(),
});
if let Some(redacts) = &redacts {
json["redacts"] = json!(redacts);
}
if let Some(state_key) = event.state_key() {
json["state_key"] = json!(state_key);
}
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}
impl<E: Event> From<Owned<E>> for Raw<AnyStateEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
+4 -4
View File
@@ -1,7 +1,7 @@
use ruma::{CanonicalJsonObject, OwnedEventId, RoomVersionId};
use serde_json::value::RawValue as RawJsonValue;
use crate::{Result, err};
use crate::{Result, err, matrix::room_version};
/// Generates a correct eventId for the incoming pdu.
///
@@ -24,8 +24,8 @@ pub fn gen_event_id(
value: &CanonicalJsonObject,
room_version_id: &RoomVersionId,
) -> Result<OwnedEventId> {
let reference_hash = ruma::signatures::reference_hash(value, room_version_id)?;
let event_id: OwnedEventId = format!("${reference_hash}").try_into()?;
let room_version_rules = room_version::rules(room_version_id)?;
let reference_hash = ruma::signatures::reference_hash(value, &room_version_rules)?;
Ok(event_id)
OwnedEventId::from_parts('$', &reference_hash, None).map_err(Into::into)
}
+17
View File
@@ -0,0 +1,17 @@
use std::cmp::Ordering;
use ruma::events::StateEventType;
use smallstr::SmallString;
pub type TypeStateKey = (StateEventType, StateKey);
pub type StateKey = SmallString<[u8; INLINE_SIZE]>;
const INLINE_SIZE: usize = 48;
#[inline]
#[must_use]
pub fn cmp(a: &TypeStateKey, b: &TypeStateKey) -> Ordering { a.0.cmp(&b.0).then(a.1.cmp(&b.1)) }
#[inline]
#[must_use]
pub fn rcmp(a: &TypeStateKey, b: &TypeStateKey) -> Ordering { b.0.cmp(&a.0).then(b.1.cmp(&a.1)) }
+2 -2
View File
@@ -21,12 +21,12 @@ fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, Stat
impl TypeExt for TimelineEventType {
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
(self.into(), state_key.into())
(self.to_cow_str().into(), state_key.into())
}
}
impl TypeExt for &TimelineEventType {
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
(self.clone().into(), state_key.into())
(self.to_cow_str().into(), state_key.into())
}
}
+5 -5
View File
@@ -2,10 +2,10 @@
pub mod event;
pub mod pdu;
pub mod state_key;
pub mod room_version;
pub mod state_res;
pub use event::{Event, TypeExt as EventTypeExt};
pub use pdu::{Pdu, PduBuilder, PduCount, PduEvent, PduId, RawPduId, ShortId};
pub use state_key::StateKey;
pub use state_res::{RoomVersion, StateMap, TypeStateKey};
pub use event::{Event, StateKey, TypeExt as EventTypeExt, TypeStateKey, state_key};
pub use pdu::{EventHash, Pdu, PduBuilder, PduCount, PduEvent, PduId, RawPduId, ShortId};
pub use room_version::{RoomVersion, RoomVersionRules};
pub use state_res::{StateMap, events};
+61
View File
@@ -67,8 +67,25 @@ pub struct Pdu {
// BTreeMap<Box<ServerName>, BTreeMap<ServerSigningKeyId, String>>
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signatures: Option<Box<RawJsonValue>>,
//TODO: https://spec.matrix.org/v1.14/rooms/v11/#rejected-events
#[cfg(test)]
#[serde(default, skip_serializing)]
pub rejected: bool,
}
/// The [maximum size allowed] for a PDU.
/// [maximum size allowed]: https://spec.matrix.org/latest/client-server-api/#size-limits
pub const MAX_PDU_BYTES: usize = 65_535;
/// The [maximum length allowed] for the `prev_events` array of a PDU.
/// [maximum length allowed]: https://spec.matrix.org/latest/rooms/v1/#event-format
pub const MAX_PREV_EVENTS: usize = 20;
/// The [maximum length allowed] for the `auth_events` array of a PDU.
/// [maximum length allowed]: https://spec.matrix.org/latest/rooms/v1/#event-format
pub const MAX_AUTH_EVENTS: usize = 10;
impl Pdu {
pub fn from_id_val(event_id: &EventId, mut json: CanonicalJsonObject) -> Result<Self> {
let event_id = CanonicalJsonValue::String(event_id.into());
@@ -77,6 +94,20 @@ pub fn from_id_val(event_id: &EventId, mut json: CanonicalJsonObject) -> Result<
.and_then(serde_json::from_value)
.map_err(Into::into)
}
pub fn from_rid_val(
room_id: &RoomId,
event_id: &EventId,
mut json: CanonicalJsonObject,
) -> Result<Self> {
let event_id = CanonicalJsonValue::String(event_id.into());
let room_id = CanonicalJsonValue::String(room_id.into());
json.insert("event_id".into(), event_id);
json.insert("room_id".into(), room_id);
serde_json::to_value(json)
.and_then(serde_json::from_value)
.map_err(Into::into)
}
}
impl Event for Pdu
@@ -88,6 +119,13 @@ fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Sen
self.auth_events.iter().map(AsRef::as_ref)
}
#[inline]
fn auth_events_into(
self,
) -> impl IntoIterator<IntoIter = impl Iterator<Item = OwnedEventId>> + Send {
self.auth_events.into_iter()
}
#[inline]
fn content(&self) -> &RawJsonValue { &self.content }
@@ -107,6 +145,14 @@ fn prev_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Sen
#[inline]
fn redacts(&self) -> Option<&EventId> { self.redacts.as_deref() }
#[cfg(test)]
#[inline]
fn rejected(&self) -> bool { self.rejected }
#[cfg(not(test))]
#[inline]
fn rejected(&self) -> bool { false }
#[inline]
fn room_id(&self) -> &RoomId { &self.room_id }
@@ -144,6 +190,13 @@ fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Sen
self.auth_events.iter().map(AsRef::as_ref)
}
#[inline]
fn auth_events_into(
self,
) -> impl IntoIterator<IntoIter = impl Iterator<Item = OwnedEventId>> + Send {
self.auth_events.iter().map(ToOwned::to_owned)
}
#[inline]
fn content(&self) -> &RawJsonValue { &self.content }
@@ -163,6 +216,14 @@ fn prev_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Sen
#[inline]
fn redacts(&self) -> Option<&EventId> { self.redacts.as_deref() }
#[cfg(test)]
#[inline]
fn rejected(&self) -> bool { self.rejected }
#[cfg(not(test))]
#[inline]
fn rejected(&self) -> bool { false }
#[inline]
fn room_id(&self) -> &RoomId { &self.room_id }
+3 -3
View File
@@ -2,7 +2,7 @@
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedEventId,
events::{EventContent, MessageLikeEventType, StateEventType, TimelineEventType},
events::{MessageLikeEventContent, StateEventContent, TimelineEventType},
};
use serde::Deserialize;
use serde_json::value::{RawValue as RawJsonValue, to_raw_value};
@@ -33,7 +33,7 @@ pub struct Builder {
impl Builder {
pub fn state<S, T>(state_key: S, content: &T) -> Self
where
T: EventContent<EventType = StateEventType>,
T: StateEventContent,
S: Into<StateKey>,
{
Self {
@@ -47,7 +47,7 @@ pub fn state<S, T>(state_key: S, content: &T) -> Self
pub fn timeline<T>(content: &T) -> Self
where
T: EventContent<EventType = MessageLikeEventType>,
T: MessageLikeEventContent,
{
Self {
event_type: content.event_type().into(),
+7 -1
View File
@@ -10,7 +10,13 @@ pub fn redact(&mut self, room_version_id: &RoomVersionId, reason: JsonValue) ->
let mut content = serde_json::from_str(self.content.get())
.map_err(|e| err!(Request(BadJson("Failed to deserialize content into type: {e}"))))?;
redact_content_in_place(&mut content, room_version_id, self.kind.to_string())
let room_version_rules = room_version_id.rules().ok_or_else(|| {
err!(Request(UnsupportedRoomVersion(
"Cannot redact event for unknown room version {room_version_id:?}."
)))
})?;
redact_content_in_place(&mut content, &room_version_rules.redaction, self.kind.to_string())
.map_err(|e| Error::Redaction(self.sender.server_name().to_owned(), e))?;
let reason = serde_json::to_value(reason).expect("Failed to preserialize reason");
+23
View File
@@ -0,0 +1,23 @@
use ruma::{RoomVersionId, events::room::create::RoomCreateEventContent};
pub use ruma::{RoomVersionId as RoomVersion, room_version_rules::RoomVersionRules};
use crate::{Result, err, matrix::Event};
pub fn rules(room_version_id: &RoomVersionId) -> Result<RoomVersionRules> {
room_version_id.rules().ok_or_else(|| {
err!(Request(UnsupportedRoomVersion(
"Unknown or unsupported room version {room_version_id:?}.",
)))
})
}
pub fn from_create_event<Pdu: Event>(create_event: &Pdu) -> Result<RoomVersionId> {
let content: RoomCreateEventContent = create_event.get_content()?;
Ok(from_create_content(&content).clone())
}
#[inline]
#[must_use]
pub fn from_create_content(content: &RoomCreateEventContent) -> &RoomVersionId {
&content.room_version
}
-5
View File
@@ -1,5 +0,0 @@
use smallstr::SmallString;
pub type StateKey = SmallString<[u8; INLINE_SIZE]>;
const INLINE_SIZE: usize = 48;
+142 -102
View File
@@ -1,18 +1,18 @@
#![cfg_attr(not(tuwunel_bench), allow(unused_imports, dead_code))]
#[cfg(tuwunel_bench)]
extern crate test;
use std::{
borrow::Borrow,
collections::{HashMap, HashSet},
collections::HashMap,
sync::atomic::{AtomicU64, Ordering::SeqCst},
};
use futures::{future, future::ready};
use maplit::{btreemap, hashmap, hashset};
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, Signatures, UserId,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, UserId,
events::{
StateEventType, TimelineEventType,
TimelineEventType,
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
@@ -25,9 +25,11 @@
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
};
use super::{AuthSet, StateMap, test_utils::not_found};
use crate::{
matrix::{Event, Pdu, pdu::EventHash},
state_res::{self as state_res, Error, Result, StateMap},
Result,
matrix::{Event, EventHash, PduEvent, event::TypeExt},
utils::stream::IterStream,
};
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
@@ -35,6 +37,12 @@
#[cfg(tuwunel_bench)]
#[cfg_attr(tuwunel_bench, bench)]
fn lexico_topo_sort(c: &mut test::Bencher) {
use maplit::{hashmap, hashset};
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
let graph = hashmap! {
event_id("l") => hashset![event_id("o")],
event_id("m") => hashset![event_id("n"), event_id("o")],
@@ -43,9 +51,12 @@ fn lexico_topo_sort(c: &mut test::Bencher) {
event_id("p") => hashset![event_id("o")],
};
c.iter(|| {
let _ = state_res::lexicographical_topological_sort(&graph, &|_| {
future::ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0))))
c.iter(move || {
rt.block_on(async {
_ = super::topological_sort(&graph, &async |_id| {
Ok((int!(0).into(), MilliSecondsSinceUnixEpoch(uint!(0))))
})
.await;
});
});
}
@@ -53,43 +64,56 @@ fn lexico_topo_sort(c: &mut test::Bencher) {
#[cfg(tuwunel_bench)]
#[cfg_attr(tuwunel_bench, bench)]
fn resolution_shallow_auth_chain(c: &mut test::Bencher) {
let mut store = TestStore(hashmap! {});
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
let mut store = TestStore(maplit::hashmap! {});
// build up the DAG
let (state_at_bob, state_at_charlie, _) = store.set_up();
c.iter(|| async {
let ev_map = store.0.clone();
let state_sets = [&state_at_bob, &state_at_charlie];
let fetch = |id: OwnedEventId| ready(ev_map.get(&id).map(ToOwned::to_owned));
let exists = |id: OwnedEventId| ready(ev_map.get(&id).is_some());
let auth_chain_sets: Vec<HashSet<_>> = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect();
let rules = RoomVersionId::V6.rules().unwrap();
let ev_map = store.0.clone();
let state_sets = [state_at_bob, state_at_charlie];
let auth_chains = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect::<Vec<_>>();
let _ = match state_res::resolve(
&RoomVersionId::V6,
state_sets.into_iter(),
&auth_chain_sets,
&fetch,
&exists,
let func = async || {
if let Err(e) = super::resolve(
&rules,
state_sets.clone().into_iter().stream(),
auth_chains.clone().into_iter().stream(),
&async |id| ev_map.get(&id).cloned().ok_or_else(not_found),
&async |id| ev_map.contains_key(&id),
false,
)
.await
{
| Ok(state) => state,
| Err(e) => panic!("{e}"),
};
panic!("{e}")
}
};
c.iter(move || {
rt.block_on(async {
func().await;
});
});
}
#[cfg(tuwunel_bench)]
#[cfg_attr(tuwunel_bench, bench)]
fn resolve_deeper_event_set(c: &mut test::Bencher) {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
let mut inner = INITIAL_EVENTS();
let ban = BAN_STATE_SET();
@@ -97,66 +121,73 @@ fn resolve_deeper_event_set(c: &mut test::Bencher) {
let store = TestStore(inner.clone());
let state_set_a = [
inner.get(&event_id("CREATE")).unwrap(),
inner.get(&event_id("IJR")).unwrap(),
inner.get(&event_id("IMA")).unwrap(),
inner.get(&event_id("IMB")).unwrap(),
inner.get(&event_id("IMC")).unwrap(),
inner.get(&event_id("MB")).unwrap(),
inner.get(&event_id("PA")).unwrap(),
&inner[&event_id("CREATE")],
&inner[&event_id("IJR")],
&inner[&event_id("IMA")],
&inner[&event_id("IMB")],
&inner[&event_id("IMC")],
&inner[&event_id("MB")],
&inner[&event_id("PA")],
]
.iter()
.map(|ev| {
(
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
ev.event_type()
.with_state_key(ev.state_key().unwrap()),
ev.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
let state_set_b = [
inner.get(&event_id("CREATE")).unwrap(),
inner.get(&event_id("IJR")).unwrap(),
inner.get(&event_id("IMA")).unwrap(),
inner.get(&event_id("IMB")).unwrap(),
inner.get(&event_id("IMC")).unwrap(),
inner.get(&event_id("IME")).unwrap(),
inner.get(&event_id("PA")).unwrap(),
&inner[&event_id("CREATE")],
&inner[&event_id("IJR")],
&inner[&event_id("IMA")],
&inner[&event_id("IMB")],
&inner[&event_id("IMC")],
&inner[&event_id("IME")],
&inner[&event_id("PA")],
]
.iter()
.map(|ev| {
(
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
ev.event_type()
.with_state_key(ev.state_key().unwrap()),
ev.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
c.iter(|| async {
let state_sets = [&state_set_a, &state_set_b];
let auth_chain_sets: Vec<HashSet<_>> = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect();
let rules = RoomVersionId::V6.rules().unwrap();
let state_sets = [state_set_a, state_set_b];
let auth_chains = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect::<Vec<_>>();
let fetch = |id: OwnedEventId| ready(inner.get(&id).map(ToOwned::to_owned));
let exists = |id: OwnedEventId| ready(inner.get(&id).is_some());
let _ = match state_res::resolve(
&RoomVersionId::V6,
state_sets.into_iter(),
&auth_chain_sets,
&fetch,
&exists,
let func = async || {
if let Err(e) = super::resolve(
&rules,
state_sets.clone().into_iter().stream(),
auth_chains.clone().into_iter().stream(),
&async |id| inner.get(&id).cloned().ok_or_else(not_found),
&async |id| inner.contains_key(&id),
false,
)
.await
{
| Ok(state) => state,
| Err(_) => panic!("resolution failed during benchmarking"),
};
panic!("{e}")
}
};
c.iter(move || {
rt.block_on(async {
func().await;
});
});
}
@@ -168,12 +199,12 @@ fn resolve_deeper_event_set(c: &mut test::Bencher) {
struct TestStore<E: Event>(HashMap<OwnedEventId, E>);
#[allow(unused)]
impl<E: Event + Clone> TestStore<E> {
impl<E: Event> TestStore<E> {
fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result<E> {
self.0
.get(event_id)
.cloned()
.ok_or_else(|| Error::NotFound(format!("{} not found", event_id)))
.ok_or_else(not_found)
}
/// Returns the events that correspond to the `event_ids` sorted in the same
@@ -191,13 +222,12 @@ fn auth_event_ids(
&self,
room_id: &RoomId,
event_ids: Vec<OwnedEventId>,
) -> Result<HashSet<OwnedEventId>> {
let mut result = HashSet::new();
) -> Result<AuthSet<OwnedEventId>> {
let mut result = AuthSet::new();
let mut stack = event_ids;
// DFS for auth event chain
while !stack.is_empty() {
let ev_id = stack.pop().unwrap();
while let Some(ev_id) = stack.pop() {
if result.contains(&ev_id) {
continue;
}
@@ -226,7 +256,8 @@ fn auth_chain_diff(
let chain = self
.auth_event_ids(room_id, ids)?
.into_iter()
.collect::<HashSet<_>>();
.collect::<AuthSet<_>>();
auth_chain_sets.push(chain);
}
@@ -234,7 +265,7 @@ fn auth_chain_diff(
let common = auth_chain_sets
.iter()
.skip(1)
.fold(first, |a, b| a.intersection(b).cloned().collect::<HashSet<_>>());
.fold(first, |a, b| a.intersection(b).cloned().collect::<AuthSet<_>>());
Ok(auth_chain_sets
.into_iter()
@@ -247,7 +278,7 @@ fn auth_chain_diff(
}
}
impl TestStore<Pdu> {
impl TestStore<PduEvent> {
#[allow(clippy::type_complexity)]
fn set_up(
&mut self,
@@ -261,8 +292,9 @@ fn set_up(
&[],
&[],
);
let cre = create_event.event_id().to_owned();
self.0.insert(cre.clone(), create_event.clone());
let cre = create_event.event_id();
self.0
.insert(cre.to_owned(), create_event.clone());
let alice_mem = to_pdu_event(
"IMA",
@@ -270,8 +302,8 @@ fn set_up(
TimelineEventType::RoomMember,
Some(alice().to_string().as_str()),
member_content_join(),
&[cre.clone()],
&[cre.clone()],
&[cre.to_owned()],
&[cre.to_owned()],
);
self.0
.insert(alice_mem.event_id().to_owned(), alice_mem.clone());
@@ -282,7 +314,7 @@ fn set_up(
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
&[cre.clone(), alice_mem.event_id().to_owned()],
&[cre.to_owned(), alice_mem.event_id().to_owned()],
&[alice_mem.event_id().to_owned()],
);
self.0
@@ -296,7 +328,7 @@ fn set_up(
TimelineEventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_join(),
&[cre.clone(), join_rules.event_id().to_owned()],
&[cre.to_owned(), join_rules.event_id().to_owned()],
&[join_rules.event_id().to_owned()],
);
self.0
@@ -308,7 +340,7 @@ fn set_up(
TimelineEventType::RoomMember,
Some(charlie().to_string().as_str()),
member_content_join(),
&[cre, join_rules.event_id().to_owned()],
&[cre.to_owned(), join_rules.event_id().to_owned()],
&[join_rules.event_id().to_owned()],
);
self.0
@@ -316,30 +348,33 @@ fn set_up(
let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem]
.iter()
.map(|ev| {
.map(|e| {
(
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
ev.event_id().to_owned(),
e.event_type()
.with_state_key(e.state_key().unwrap()),
e.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem]
.iter()
.map(|ev| {
.map(|e| {
(
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
ev.event_id().to_owned(),
e.event_type()
.with_state_key(e.state_key().unwrap()),
e.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
let expected = [&create_event, &alice_mem, &join_rules, &bob_mem, &charlie_mem]
.iter()
.map(|ev| {
.map(|e| {
(
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
ev.event_id().to_owned(),
e.event_type()
.with_state_key(e.state_key().unwrap()),
e.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
@@ -352,7 +387,7 @@ fn event_id(id: &str) -> OwnedEventId {
if id.contains('$') {
return id.try_into().unwrap();
}
format!("${}:foo", id).try_into().unwrap()
format!("${id}:foo").try_into().unwrap()
}
fn alice() -> &'static UserId { user_id!("@alice:foo") }
@@ -381,7 +416,7 @@ fn to_pdu_event<S>(
content: Box<RawJsonValue>,
auth_events: &[S],
prev_events: &[S],
) -> Pdu
) -> PduEvent
where
S: AsRef<str>,
{
@@ -391,28 +426,31 @@ fn to_pdu_event<S>(
let id = if id.contains('$') {
id.to_owned()
} else {
format!("${}:foo", id)
format!("${id}:foo")
};
let auth_events = auth_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
let prev_events = prev_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
Pdu {
let state_key = state_key.map(ToOwned::to_owned);
PduEvent {
event_id: id.try_into().unwrap(),
room_id: room_id().to_owned(),
sender: sender.to_owned(),
origin: None,
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
kind: ev_type,
content,
origin: None,
redacts: None,
unsigned: None,
auth_events,
@@ -420,12 +458,14 @@ fn to_pdu_event<S>(
depth: uint!(0),
hashes: EventHash::default(),
signatures: None,
#[cfg(test)]
rejected: false,
}
}
// all graphs start with these input events
#[allow(non_snake_case)]
fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
fn INITIAL_EVENTS() -> HashMap<OwnedEventId, PduEvent> {
vec![
to_pdu_event::<&EventId>(
"CREATE",
@@ -507,7 +547,7 @@ fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
// all graphs start with these input events
#[allow(non_snake_case)]
fn BAN_STATE_SET() -> HashMap<OwnedEventId, Pdu> {
fn BAN_STATE_SET() -> HashMap<OwnedEventId, PduEvent> {
vec![
to_pdu_event(
"PA",
-23
View File
@@ -1,23 +0,0 @@
use serde_json::Error as JsonError;
use thiserror::Error;
/// Represents the various errors that arise when resolving state.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
/// A deserialization error.
#[error(transparent)]
SerdeJson(#[from] JsonError),
/// The given option or version is unsupported.
#[error("Unsupported room version: {0}")]
Unsupported(String),
/// The given event was not found.
#[error("Not found error: {0}")]
NotFound(String),
/// Invalid fields in the given PDU.
#[error("Invalid PDU: {0}")]
InvalidPdu(String),
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,119 @@
use ruma::{
UserId,
events::{StateEventType, TimelineEventType, room::member::MembershipState},
room_version_rules::AuthorizationRules,
};
use serde_json::value::RawValue as RawJsonValue;
use super::super::{TypeStateKey, events::member::RoomMemberEventContent};
use crate::{Err, Result, arrayvec::ArrayVec, matrix::pdu::MAX_AUTH_EVENTS};
pub type AuthTypes = ArrayVec<TypeStateKey, MAX_AUTH_EVENTS>;
/// Get the list of [relevant auth events] required to authorize the event of
/// the given type.
///
/// Returns a list of `(event_type, state_key)` tuples.
///
/// # Errors
///
/// Returns an `Err(_)` if a field could not be deserialized because `content`
/// does not respect the expected format for the `event_type`.
///
/// [relevant auth events]: https://spec.matrix.org/latest/server-server-api/#auth-events-selection
pub fn auth_types_for_event(
event_type: &TimelineEventType,
sender: &UserId,
state_key: Option<&str>,
content: &RawJsonValue,
rules: &AuthorizationRules,
always_create: bool,
) -> Result<AuthTypes> {
let mut auth_types = AuthTypes::new();
// The `auth_events` for the `m.room.create` event in a room is empty.
// For other events, it should be the following subset of the room state:
//
// - The `m.room.create` event.
// - The current `m.room.power_levels` event, if any.
// - The senders current `m.room.member` event, if any.
if *event_type != TimelineEventType::RoomCreate {
// v1-v11, the `m.room.create` event.
if !rules.room_create_event_id_as_room_id || always_create {
auth_types.push((StateEventType::RoomCreate, "".into()));
}
auth_types.push((StateEventType::RoomPowerLevels, "".into()));
auth_types.push((StateEventType::RoomMember, sender.as_str().into()));
}
// If type is `m.room.member`:
if *event_type == TimelineEventType::RoomMember {
auth_types_for_member_event(&mut auth_types, state_key, content, rules)?;
}
Ok(auth_types)
}
fn auth_types_for_member_event(
auth_types: &mut AuthTypes,
state_key: Option<&str>,
content: &RawJsonValue,
rules: &AuthorizationRules,
) -> Result {
// The targets current `m.room.member` event, if any.
let Some(state_key) = state_key else {
return Err!("missing `state_key` field for `m.room.member` event");
};
let key = (StateEventType::RoomMember, state_key.into());
if !auth_types.contains(&key) {
auth_types.push(key);
}
let content = RoomMemberEventContent::new(content);
let membership = content.membership()?;
// If `membership` is `join`, `invite` or `knock`, the current
// `m.room.join_rules` event, if any.
if matches!(
membership,
MembershipState::Join | MembershipState::Invite | MembershipState::Knock
) {
let key = (StateEventType::RoomJoinRules, "".into());
if !auth_types.contains(&key) {
auth_types.push(key);
}
}
// If `membership` is `invite` and `content` contains a `third_party_invite`
// property, the current `m.room.third_party_invite` event with `state_key`
// matching `content.third_party_invite.signed.token`, if any.
if membership == MembershipState::Invite {
let third_party_invite = content.third_party_invite()?;
if let Some(third_party_invite) = third_party_invite {
let token = third_party_invite.token()?.into();
let key = (StateEventType::RoomThirdPartyInvite, token);
if !auth_types.contains(&key) {
auth_types.push(key);
}
}
}
// If `content.join_authorised_via_users_server` is present, and the room
// version supports restricted rooms, then the `m.room.member` event with
// `state_key` matching `content.join_authorised_via_users_server`.
//
// Note: And the membership is join (https://github.com/matrix-org/matrix-spec/pull/2100)
if membership == MembershipState::Join && rules.restricted_join_rule {
let join_authorised_via_users_server = content.join_authorised_via_users_server()?;
if let Some(user_id) = join_authorised_via_users_server {
let key = (StateEventType::RoomMember, user_id.as_str().into());
if !auth_types.contains(&key) {
auth_types.push(key);
}
}
}
Ok(())
}
@@ -0,0 +1,587 @@
use std::borrow::Borrow;
use futures::future::{join, join3};
use ruma::{
AnyKeyName, SigningKeyId, UserId,
events::{StateEventType, room::member::MembershipState},
room_version_rules::AuthorizationRules,
serde::{Base64, base64::Standard},
signatures::verify_canonical_json_bytes,
};
use crate::{
Err, Result, debug, err, is_equal_to,
matrix::{Event, StateKey},
};
#[cfg(test)]
mod tests;
#[cfg(test)]
use super::test_utils;
use super::{
FetchStateExt,
events::{
JoinRule, RoomCreateEvent, RoomMemberEvent, RoomPowerLevelsIntField,
member::ThirdPartyInvite, power_levels::RoomPowerLevelsEventOptionExt,
},
};
/// Check whether the given event passes the `m.room.roomber` authorization
/// rules.
///
/// This assumes that `ruma_signatures::verify_event()` was called previously,
/// as some authorization rules depend on the signatures being valid on the
/// event.
#[tracing::instrument(level = "trace", skip_all)]
pub(super) async fn check_room_member<Fetch, Fut, Pdu>(
room_member_event: &RoomMemberEvent<Pdu>,
rules: &AuthorizationRules,
room_create_event: &RoomCreateEvent<Pdu>,
fetch_state: &Fetch,
) -> Result
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
debug!("starting m.room.member check");
// Since v1, if there is no state_key property, or no membership property in
// content, reject.
let Some(state_key) = room_member_event.state_key() else {
return Err!("missing `state_key` field in `m.room.member` event");
};
let target_user = <&UserId>::try_from(state_key)
.map_err(|e| err!("invalid `state_key` field in `m.room.member` event: {e}"))?;
let target_membership = room_member_event.membership()?;
// These checks are done `in ruma_signatures::verify_event()`:
//
// Since v8, if content has a join_authorised_via_users_server property:
//
// - Since v8, if the event is not validly signed by the homeserver of the user
// ID denoted by the key, reject.
match target_membership {
// Since v1, if membership is join:
| MembershipState::Join =>
check_room_member_join(
room_member_event,
target_user,
rules,
room_create_event,
fetch_state,
)
.await,
// Since v1, if membership is invite:
| MembershipState::Invite =>
check_room_member_invite(
room_member_event,
target_user,
rules,
room_create_event,
fetch_state,
)
.await,
// Since v1, if membership is leave:
| MembershipState::Leave =>
check_room_member_leave(
room_member_event,
target_user,
rules,
room_create_event,
fetch_state,
)
.await,
// Since v1, if membership is ban:
| MembershipState::Ban =>
check_room_member_ban(
room_member_event,
target_user,
rules,
room_create_event,
fetch_state,
)
.await,
// Since v7, if membership is knock:
| MembershipState::Knock if rules.knocking =>
check_room_member_knock(room_member_event, target_user, rules, fetch_state).await,
// Since v1, otherwise, the membership is unknown. Reject.
| _ => Err!("unknown membership"),
}
}
/// Check whether the given event passes the `m.room.member` authorization rules
/// with a membership of `join`.
#[tracing::instrument(level = "trace", skip_all)]
async fn check_room_member_join<Fetch, Fut, Pdu>(
room_member_event: &RoomMemberEvent<Pdu>,
target_user: &UserId,
rules: &AuthorizationRules,
room_create_event: &RoomCreateEvent<Pdu>,
fetch_state: &Fetch,
) -> Result
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let mut creators = room_create_event.creators(rules)?;
let mut prev_events = room_member_event.prev_events();
let prev_event_is_room_create_event = prev_events
.next()
.is_some_and(|event_id| event_id.borrow() == room_create_event.event_id().borrow());
let prev_event_is_only_room_create_event =
prev_event_is_room_create_event && prev_events.next().is_none();
// v1-v10, if the only previous event is an m.room.create and the state_key is
// the creator, allow.
// Since v11, if the only previous event is an m.room.create and the state_key
// is the sender of the m.room.create, allow.
if prev_event_is_only_room_create_event && creators.any(is_equal_to!(*target_user)) {
return Ok(());
}
// Since v1, if the sender does not match state_key, reject.
if room_member_event.sender() != target_user {
return Err!("sender of join event must match target user");
}
let (current_membership, join_rule) =
join(fetch_state.user_membership(target_user), fetch_state.join_rule()).await;
// Since v1, if the sender is banned, reject.
let current_membership = current_membership?;
if current_membership == MembershipState::Ban {
return Err!("banned user cannot join room");
}
// v1-v6, if the join_rule is invite then allow if membership state is invite or
// join.
// Since v7, if the join_rule is invite or knock then allow if membership state
// is invite or join.
let join_rule = join_rule?;
if (join_rule == JoinRule::Invite || rules.knocking && join_rule == JoinRule::Knock)
&& matches!(current_membership, MembershipState::Invite | MembershipState::Join)
{
return Ok(());
}
// v8-v9, if the join_rule is restricted:
// Since v10, if the join_rule is restricted or knock_restricted:
if rules.restricted_join_rule && matches!(join_rule, JoinRule::Restricted)
|| rules.knock_restricted_join_rule && matches!(join_rule, JoinRule::KnockRestricted)
{
// Since v8, if membership state is join or invite, allow.
if matches!(current_membership, MembershipState::Join | MembershipState::Invite) {
return Ok(());
}
// Since v8, if the join_authorised_via_users_server key in content is not a
// user with sufficient permission to invite other users, reject.
//
// Otherwise, allow.
let Some(authorized_via_user) = room_member_event.join_authorised_via_users_server()?
else {
// The field is absent, we cannot authorize.
return Err!(
"cannot join restricted room without `join_authorised_via_users_server` field \
if not invited"
);
};
// The member needs to be in the room to have any kind of permission.
let authorized_via_user_membership = fetch_state
.user_membership(&authorized_via_user)
.await?;
if authorized_via_user_membership != MembershipState::Join {
return Err!("`join_authorised_via_users_server` is not joined");
}
let room_power_levels_event = fetch_state.room_power_levels_event().await;
let authorized_via_user_power_level =
room_power_levels_event.user_power_level(&authorized_via_user, creators, rules)?;
let invite_power_level = room_power_levels_event
.get_as_int_or_default(RoomPowerLevelsIntField::Invite, rules)?;
if authorized_via_user_power_level < invite_power_level {
return Err!("`join_authorised_via_users_server` does not have enough power");
}
return Ok(());
}
// Since v1, if the join_rule is public, allow. Otherwise, reject.
if join_rule != JoinRule::Public {
return Err!("cannot join a room that is not `public`");
}
Ok(())
}
/// Check whether the given event passes the `m.room.member` authorization rules
/// with a membership of `invite`.
#[tracing::instrument(level = "trace", skip_all)]
async fn check_room_member_invite<Fetch, Fut, Pdu>(
room_member_event: &RoomMemberEvent<Pdu>,
target_user: &UserId,
rules: &AuthorizationRules,
room_create_event: &RoomCreateEvent<Pdu>,
fetch_state: &Fetch,
) -> Result
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let third_party_invite = room_member_event.third_party_invite()?;
// Since v1, if content has a third_party_invite property:
if let Some(third_party_invite) = third_party_invite {
return check_third_party_invite(
room_member_event,
&third_party_invite,
target_user,
fetch_state,
)
.await;
}
let sender_user = room_member_event.sender();
let (sender_membership, current_target_user_membership, room_power_levels_event) = join3(
fetch_state.user_membership(sender_user),
fetch_state.user_membership(target_user),
fetch_state.room_power_levels_event(),
)
.await;
// Since v1, if the senders current membership state is not join, reject.
let sender_membership = sender_membership?;
if sender_membership != MembershipState::Join {
return Err!("cannot invite user if sender is not joined");
}
// Since v1, if target users current membership state is join or ban, reject.
let current_target_user_membership = current_target_user_membership?;
if matches!(current_target_user_membership, MembershipState::Join | MembershipState::Ban) {
return Err!("cannot invite user that is joined or banned");
}
let creators = room_create_event.creators(rules)?;
let sender_power_level =
room_power_levels_event.user_power_level(room_member_event.sender(), creators, rules)?;
let invite_power_level =
room_power_levels_event.get_as_int_or_default(RoomPowerLevelsIntField::Invite, rules)?;
// Since v1, if the senders power level is greater than or equal to the invite
// level, allow. Otherwise, reject.
if sender_power_level < invite_power_level {
return Err!("sender does not have enough power to invite");
}
Ok(())
}
/// Check whether the `third_party_invite` from the `m.room.member` event passes
/// the authorization rules.
#[tracing::instrument(level = "trace", skip_all)]
async fn check_third_party_invite<Fetch, Fut, Pdu>(
room_member_event: &RoomMemberEvent<Pdu>,
third_party_invite: &ThirdPartyInvite,
target_user: &UserId,
fetch_state: &Fetch,
) -> Result
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let current_target_user_membership = fetch_state.user_membership(target_user).await?;
// Since v1, if target user is banned, reject.
if current_target_user_membership == MembershipState::Ban {
return Err!("cannot invite user that is banned");
}
// Since v1, if content.third_party_invite does not have a signed property,
// reject. Since v1, if signed does not have mxid and token properties, reject.
let third_party_invite_token = third_party_invite.token()?;
let third_party_invite_mxid = third_party_invite.mxid()?;
// Since v1, if mxid does not match state_key, reject.
if target_user != third_party_invite_mxid {
return Err!("third-party invite mxid does not match target user");
}
// Since v1, if there is no m.room.third_party_invite event in the current room
// state with state_key matching token, reject.
let Some(room_third_party_invite_event) = fetch_state
.room_third_party_invite_event(third_party_invite_token)
.await
else {
return Err!("no `m.room.third_party_invite` in room state matches the token");
};
// Since v1, if sender does not match sender of the m.room.third_party_invite,
// reject.
if room_member_event.sender() != room_third_party_invite_event.sender() {
return Err!(
"sender of `m.room.third_party_invite` does not match sender of `m.room.member`"
);
}
let signatures = third_party_invite.signatures()?;
let public_keys = room_third_party_invite_event.public_keys()?;
let signed_canonical_json = third_party_invite.signed_canonical_json()?;
// Since v1, if any signature in signed matches any public key in the
// m.room.third_party_invite event, allow.
for entity_signatures_value in signatures.values() {
let Some(entity_signatures) = entity_signatures_value.as_object() else {
return Err!(Request(InvalidParam(
"unexpected format of `signatures` field in `third_party_invite.signed` of \
`m.room.member` event: expected a map of string to object, got \
{entity_signatures_value:?}"
)));
};
// We will ignore any error from now on, we just want to find a signature that
// can be verified from a public key.
for (key_id, signature_value) in entity_signatures {
let Ok(parsed_key_id) = <&SigningKeyId<AnyKeyName>>::try_from(key_id.as_str()) else {
continue;
};
let Some(signature_str) = signature_value.as_str() else {
continue;
};
let Ok(signature) = Base64::<Standard>::parse(signature_str) else {
continue;
};
let algorithm = parsed_key_id.algorithm();
for encoded_public_key in &public_keys {
let Ok(public_key) = encoded_public_key.decode() else {
continue;
};
if verify_canonical_json_bytes(
&algorithm,
&public_key,
signature.as_bytes(),
signed_canonical_json.as_bytes(),
)
.is_ok()
{
return Ok(());
}
}
}
}
// Otherwise, reject.
Err!(
"no signature on third-party invite matches a public key in `m.room.third_party_invite` \
event"
)
}
/// Check whether the given event passes the `m.room.member` authorization rules
/// with a membership of `leave`.
#[tracing::instrument(level = "trace", skip_all)]
async fn check_room_member_leave<Fetch, Fut, Pdu>(
room_member_event: &RoomMemberEvent<Pdu>,
target_user: &UserId,
rules: &AuthorizationRules,
room_create_event: &RoomCreateEvent<Pdu>,
fetch_state: &Fetch,
) -> Result
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let (sender_membership, room_power_levels_event, current_target_user_membership) = join3(
fetch_state.user_membership(room_member_event.sender()),
fetch_state.room_power_levels_event(),
fetch_state.user_membership(target_user),
)
.await;
let sender_membership = sender_membership?;
// v1-v6, if the sender matches state_key, allow if and only if that users
// current membership state is invite or join.
// Since v7, if the sender matches state_key, allow if and only if that users
// current membership state is invite, join, or knock.
if room_member_event.sender() == target_user {
let membership_is_invite_or_join =
matches!(sender_membership, MembershipState::Join | MembershipState::Invite);
let membership_is_knock = rules.knocking && sender_membership == MembershipState::Knock;
return if membership_is_invite_or_join || membership_is_knock {
Ok(())
} else {
Err!("cannot leave if not joined, invited or knocked")
};
}
// Since v1, if the senders current membership state is not join, reject.
if sender_membership != MembershipState::Join {
return Err!("cannot kick if sender is not joined");
}
let creators = room_create_event.creators(rules)?;
let current_target_user_membership = current_target_user_membership?;
let sender_power_level = room_power_levels_event.user_power_level(
room_member_event.sender(),
creators.clone(),
rules,
)?;
let ban_power_level =
room_power_levels_event.get_as_int_or_default(RoomPowerLevelsIntField::Ban, rules)?;
// Since v1, if the target users current membership state is ban, and the
// senders power level is less than the ban level, reject.
if current_target_user_membership == MembershipState::Ban
&& sender_power_level < ban_power_level
{
return Err!("sender does not have enough power to unban");
}
let kick_power_level =
room_power_levels_event.get_as_int_or_default(RoomPowerLevelsIntField::Kick, rules)?;
let target_user_power_level =
room_power_levels_event.user_power_level(target_user, creators, rules)?;
// Since v1, if the senders power level is greater than or equal to the kick
// level, and the target users power level is less than the senders power
// level, allow.
//
// Otherwise, reject.
if sender_power_level >= kick_power_level && target_user_power_level < sender_power_level {
Ok(())
} else {
Err!("sender does not have enough power to kick target user")
}
}
/// Check whether the given event passes the `m.room.member` authorization rules
/// with a membership of `ban`.
#[tracing::instrument(level = "trace", skip_all)]
async fn check_room_member_ban<Fetch, Fut, Pdu>(
room_member_event: &RoomMemberEvent<Pdu>,
target_user: &UserId,
rules: &AuthorizationRules,
room_create_event: &RoomCreateEvent<Pdu>,
fetch_state: &Fetch,
) -> Result
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let (sender_membership, room_power_levels_event) = join(
fetch_state.user_membership(room_member_event.sender()),
fetch_state.room_power_levels_event(),
)
.await;
// Since v1, if the senders current membership state is not join, reject.
let sender_membership = sender_membership?;
if sender_membership != MembershipState::Join {
return Err!("cannot ban if sender is not joined");
}
let creators = room_create_event.creators(rules)?;
let sender_power_level = room_power_levels_event.user_power_level(
room_member_event.sender(),
creators.clone(),
rules,
)?;
let ban_power_level =
room_power_levels_event.get_as_int_or_default(RoomPowerLevelsIntField::Ban, rules)?;
let target_user_power_level =
room_power_levels_event.user_power_level(target_user, creators, rules)?;
// If the senders power level is greater than or equal to the ban level, and
// the target users power level is less than the senders power level, allow.
//
// Otherwise, reject.
if sender_power_level >= ban_power_level && target_user_power_level < sender_power_level {
Ok(())
} else {
Err!("sender does not have enough power to ban target user")
}
}
/// Check whether the given event passes the `m.room.member` authorization rules
/// with a membership of `knock`.
#[tracing::instrument(level = "trace", skip_all)]
async fn check_room_member_knock<Fetch, Fut, Pdu>(
room_member_event: &RoomMemberEvent<Pdu>,
target_user: &UserId,
rules: &AuthorizationRules,
fetch_state: &Fetch,
) -> Result
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let sender = room_member_event.sender();
let (join_rule, sender_membership) =
join(fetch_state.join_rule(), fetch_state.user_membership(sender)).await;
// v7-v9, if the join_rule is anything other than knock, reject.
// Since v10, if the join_rule is anything other than knock or knock_restricted,
// reject.
let join_rule = join_rule?;
if join_rule != JoinRule::Knock
&& (rules.knock_restricted_join_rule && !matches!(join_rule, JoinRule::KnockRestricted))
{
return Err!(
"join rule is not set to knock or knock_restricted, knocking is not allowed"
);
}
// Since v7, if sender does not match state_key, reject.
if room_member_event.sender() != target_user {
return Err!("cannot make another user knock, sender does not match target user");
}
// Since v7, if the senders current membership is not ban, invite, or join,
// allow. Otherwise, reject.
let sender_membership = sender_membership?;
if !matches!(
sender_membership,
MembershipState::Ban | MembershipState::Invite | MembershipState::Join
) {
Ok(())
} else {
Err!("cannot knock if user is banned, invited or joined")
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+461
View File
@@ -0,0 +1,461 @@
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, ID_MAX_BYTES, RoomId, int,
room_version_rules::EventFormatRules,
};
use serde_json::to_string as to_json_string;
use crate::{
Err, Result, err,
matrix::pdu::{MAX_AUTH_EVENTS, MAX_PDU_BYTES, MAX_PREV_EVENTS},
};
/// Check that the given canonicalized PDU respects the event format of the room
/// version and the [size limits] from the Matrix specification.
///
/// This is part of the [checks performed on receipt of a PDU].
///
/// This checks the following and enforces their size limits:
///
/// * Full PDU
/// * `sender`
/// * `room_id`
/// * `type`
/// * `event_id`
/// * `state_key`
/// * `prev_events`
/// * `auth_events`
/// * `depth`
///
/// Returns an `Err(_)` if the JSON is malformed or if the PDU doesn't pass the
/// checks.
///
/// [size limits]: https://spec.matrix.org/latest/client-server-api/#size-limits
/// [checks performed on receipt of a PDU]: https://spec.matrix.org/latest/server-server-api/#checks-performed-on-receipt-of-a-pdu
pub fn check_pdu_format(pdu: &CanonicalJsonObject, rules: &EventFormatRules) -> Result {
// Check the PDU size, it must occur on the full PDU with signatures.
let json = to_json_string(&pdu)
.map_err(|e| err!(Request(BadJson("Failed to serialize canonical JSON: {e}"))))?;
if json.len() > MAX_PDU_BYTES {
return Err!(Request(InvalidParam(
"PDU is larger than maximum of {MAX_PDU_BYTES} bytes"
)));
}
// Check the presence, type and length of the `type` field.
let event_type = extract_required_string_field(pdu, "type")?;
// Check the presence, type and length of the `sender` field.
extract_required_string_field(pdu, "sender")?;
// Check the presence, type and length of the `room_id` field.
let room_id = (event_type != "m.room.create" || rules.require_room_create_room_id)
.then(|| extract_required_string_field(pdu, "room_id"))
.transpose()?;
// Check the presence, type and length of the `event_id` field.
if rules.require_event_id {
extract_required_string_field(pdu, "event_id")?;
}
// Check the type and length of the `state_key` field.
extract_optional_string_field(pdu, "state_key")?;
// Check the presence, type and length of the `prev_events` field.
extract_required_array_field(pdu, "prev_events", MAX_PREV_EVENTS)?;
// Check the presence, type and length of the `auth_events` field.
let auth_events = extract_required_array_field(pdu, "auth_events", MAX_AUTH_EVENTS)?;
if !rules.allow_room_create_in_auth_events {
// The only case where the room ID should be missing is for m.room.create which
// shouldn't have any auth_events.
if let Some(room_id) = room_id {
let room_create_event_reference_hash = <&RoomId>::try_from(room_id.as_str())
.map_err(|e| err!("invalid `room_id` field in PDU: {e}"))?
.strip_sigil();
for event_id in auth_events {
let CanonicalJsonValue::String(event_id) = event_id else {
return Err!(Request(InvalidParam(
"unexpected format of array item in `auth_events` field in PDU: \
expected string, got {event_id:?}"
)));
};
let reference_hash =
event_id
.strip_prefix('$')
.ok_or(err!(Request(InvalidParam(
"unexpected format of array item in `auth_events` field in PDU: \
string not beginning with the `$` sigil"
))))?;
if reference_hash == room_create_event_reference_hash {
return Err!(Request(InvalidParam(
"invalid `auth_events` field in PDU: cannot contain the `m.room.create` \
event ID"
)));
}
}
}
}
// Check the presence, type and value of the `depth` field.
match pdu.get("depth") {
| Some(CanonicalJsonValue::Integer(value)) =>
if *value < int!(0) {
return Err!(Request(InvalidParam(
"invalid `depth` field in PDU: cannot be a negative integer"
)));
},
| Some(value) => {
return Err!(Request(InvalidParam(
"unexpected format of `depth` field in PDU: expected integer, got {value:?}"
)));
},
| None => return Err!(Request(InvalidParam("missing `depth` field in PDU"))),
}
Ok(())
}
/// Extract the optional string field with the given name from the given
/// canonical JSON object.
///
/// Returns `Ok(Some(value))` if the field is present and a valid string,
/// `Ok(None)` if the field is missing and `Err(_)` if the field is not a string
/// or its length is bigger than [`ID_MAX_BYTES`].
fn extract_optional_string_field<'a>(
object: &'a CanonicalJsonObject,
field: &'a str,
) -> Result<Option<&'a String>> {
match object.get(field) {
| Some(CanonicalJsonValue::String(value)) =>
if value.len() > ID_MAX_BYTES {
Err!(Request(InvalidParam(
"invalid `{field}` field in PDU: string length is larger than maximum of \
{ID_MAX_BYTES} bytes"
)))
} else {
Ok(Some(value))
},
| Some(value) => Err!(Request(InvalidParam(
"unexpected format of `{field}` field in PDU: expected string, got {value:?}"
))),
| None => Ok(None),
}
}
/// Extract the required string field with the given name from the given
/// canonical JSON object.
///
/// Returns `Ok(value)` if the field is present and a valid string and `Err(_)`
/// if the field is missing, not a string or its length is bigger than
/// [`ID_MAX_BYTES`].
fn extract_required_string_field<'a>(
object: &'a CanonicalJsonObject,
field: &'a str,
) -> Result<&'a String> {
extract_optional_string_field(object, field)?
.ok_or_else(|| err!(Request(InvalidParam("missing `{field}` field in PDU"))))
}
/// Extract the required array field with the given name from the given
/// canonical JSON object.
///
/// Returns `Ok(value)` if the field is present and a valid array or `Err(_)` if
/// the field is missing, not an array or its length is bigger than the given
/// value.
fn extract_required_array_field<'a>(
object: &'a CanonicalJsonObject,
field: &'a str,
max_len: usize,
) -> Result<&'a [CanonicalJsonValue]> {
match object.get(field) {
| Some(CanonicalJsonValue::Array(value)) =>
if value.len() > max_len {
Err!(Request(InvalidParam(
"invalid `{field}` field in PDU: array length is larger than maximum of \
{max_len}"
)))
} else {
Ok(value)
},
| Some(value) => Err!(Request(InvalidParam(
"unexpected format of `{field}` field in PDU: expected array, got {value:?}"
))),
| None => Err!(Request(InvalidParam("missing `{field}` field in PDU"))),
}
}
#[cfg(test)]
mod tests {
use std::iter::repeat_n;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, int, room_version_rules::EventFormatRules,
};
use serde_json::{from_value as from_json_value, json};
use super::check_pdu_format;
/// Construct a PDU valid for the event format of room v1.
fn pdu_v1() -> CanonicalJsonObject {
let pdu = json!({
"auth_events": [
[
"$af232176:example.org",
{ "sha256": "abase64encodedsha256hashshouldbe43byteslong" },
],
],
"content": {
"key": "value",
},
"depth": 12,
"event_id": "$a4ecee13e2accdadf56c1025:example.com",
"hashes": {
"sha256": "thishashcoversallfieldsincasethisisredacted"
},
"origin_server_ts": 1_838_188_000,
"prev_events": [
[
"$af232176:example.org",
{ "sha256": "abase64encodedsha256hashshouldbe43byteslong" }
],
],
"room_id": "!UcYsUzyxTGDxLBEvLy:example.org",
"sender": "@alice:example.com",
"signatures": {
"example.com": {
"ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus",
},
},
"type": "m.room.message",
"unsigned": {
"age": 4612,
},
});
from_json_value(pdu).unwrap()
}
/// Construct a PDU valid for the event format of room v3.
fn pdu_v3() -> CanonicalJsonObject {
let pdu = json!({
"auth_events": [
"$base64encodedeventid",
"$adifferenteventid",
],
"content": {
"key": "value",
},
"depth": 12,
"hashes": {
"sha256": "thishashcoversallfieldsincasethisisredacted",
},
"origin_server_ts": 1_838_188_000,
"prev_events": [
"$base64encodedeventid",
"$adifferenteventid",
],
"redacts": "$some/old+event",
"room_id": "!UcYsUzyxTGDxLBEvLy:example.org",
"sender": "@alice:example.com",
"signatures": {
"example.com": {
"ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus",
},
},
"type": "m.room.message",
"unsigned": {
"age": 4612,
}
});
from_json_value(pdu).unwrap()
}
/// Construct an `m.room.create` PDU valid for the event format of
/// `org.matrix.hydra.11`.
fn room_create_hydra() -> CanonicalJsonObject {
let pdu = json!({
"auth_events": [],
"content": {
"room_version": "org.matrix.hydra.11",
},
"depth": 1,
"hashes": {
"sha256": "thishashcoversallfieldsincasethisisredacted",
},
"origin_server_ts": 1_838_188_000,
"prev_events": [],
"sender": "@alice:example.com",
"signatures": {
"example.com": {
"ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus",
},
},
"type": "m.room.create",
"unsigned": {
"age": 4612,
}
});
from_json_value(pdu).unwrap()
}
/// Construct a PDU valid for the event format of `org.matrix.hydra.11`.
fn pdu_hydra() -> CanonicalJsonObject {
let pdu = json!({
"auth_events": [
"$base64encodedeventid",
"$adifferenteventid",
],
"content": {
"key": "value",
},
"depth": 12,
"hashes": {
"sha256": "thishashcoversallfieldsincasethisisredacted",
},
"origin_server_ts": 1_838_188_000,
"prev_events": [
"$base64encodedeventid",
],
"room_id": "!roomcreatereferencehash",
"sender": "@alice:example.com",
"signatures": {
"example.com": {
"ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus",
},
},
"type": "m.room.message",
"unsigned": {
"age": 4612,
}
});
from_json_value(pdu).unwrap()
}
#[test]
fn check_pdu_format_valid_v1() {
check_pdu_format(&pdu_v1(), &EventFormatRules::V1).unwrap();
}
#[test]
fn check_pdu_format_valid_v3() {
check_pdu_format(&pdu_v3(), &EventFormatRules::V3).unwrap();
}
#[test]
fn check_pdu_format_pdu_too_big() {
// Add a lot of data in the content to reach MAX_PDU_SIZE.
let mut pdu = pdu_v3();
let content = pdu
.get_mut("content")
.unwrap()
.as_object_mut()
.unwrap();
let long_string = repeat_n('a', 66_000).collect::<String>();
content.insert("big_data".to_owned(), long_string.into());
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
#[test]
fn check_pdu_format_fields_missing() {
for field in
&["event_id", "sender", "room_id", "type", "prev_events", "auth_events", "depth"]
{
let mut pdu = pdu_v1();
pdu.remove(*field).unwrap();
check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err();
}
}
#[test]
fn check_pdu_format_strings_too_big() {
for field in &["event_id", "sender", "room_id", "type", "state_key"] {
let mut pdu = pdu_v1();
let value = repeat_n('a', 300).collect::<String>();
pdu.insert((*field).to_owned(), value.into());
check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err();
}
}
#[test]
fn check_pdu_format_strings_wrong_format() {
for field in &["event_id", "sender", "room_id", "type", "state_key"] {
let mut pdu = pdu_v1();
pdu.insert((*field).to_owned(), true.into());
check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err();
}
}
#[test]
fn check_pdu_format_arrays_too_big() {
for field in &["prev_events", "auth_events"] {
let mut pdu = pdu_v3();
let value: Vec<_> =
repeat_n(CanonicalJsonValue::from("$eventid".to_owned()), 30).collect();
pdu.insert((*field).to_owned(), value.into());
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
}
#[test]
fn check_pdu_format_arrays_wrong_format() {
for field in &["prev_events", "auth_events"] {
let mut pdu = pdu_v3();
pdu.insert((*field).to_owned(), true.into());
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
}
#[test]
fn check_pdu_format_negative_depth() {
let mut pdu = pdu_v3();
pdu.insert("depth".to_owned(), int!(-1).into())
.unwrap();
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
#[test]
fn check_pdu_format_depth_wrong_format() {
let mut pdu = pdu_v3();
pdu.insert("depth".to_owned(), true.into());
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
#[test]
fn check_pdu_format_valid_room_create_hydra() {
let pdu = room_create_hydra();
check_pdu_format(&pdu, &EventFormatRules::V12).unwrap();
}
#[test]
fn check_pdu_format_valid_hydra() {
let pdu = pdu_hydra();
check_pdu_format(&pdu, &EventFormatRules::V12).unwrap();
}
#[test]
fn check_pdu_format_hydra_with_room_create() {
let mut pdu = pdu_hydra();
pdu.get_mut("auth_events")
.unwrap()
.as_array_mut()
.unwrap()
.push("$roomcreatereferencehash".to_owned().into());
check_pdu_format(&pdu, &EventFormatRules::V12).unwrap_err();
}
}
+50
View File
@@ -0,0 +1,50 @@
//! Helper traits and types to work with events (aka PDUs).
pub mod create;
pub mod join_rules;
pub mod member;
pub mod power_levels;
pub mod third_party_invite;
pub use self::{
create::RoomCreateEvent,
join_rules::{JoinRule, RoomJoinRulesEvent},
member::{RoomMemberEvent, RoomMemberEventContent},
power_levels::{RoomPowerLevelsEvent, RoomPowerLevelsIntField},
third_party_invite::RoomThirdPartyInviteEvent,
};
/// Whether the given event is a power event.
///
/// Definition in the spec:
///
/// > A power event is a state event with type `m.room.power_levels` or
/// > `m.room.join_rules`, or a
/// > state event with type `m.room.member` where the `membership` is `leave` or
/// > `ban` and the
/// > `sender` does not match the `state_key`. The idea behind this is that
/// > power events are events
/// > that might remove someones ability to do something in the room.
pub(super) fn is_power_event<Pdu>(event: &Pdu) -> bool
where
Pdu: crate::matrix::Event,
{
use ruma::events::{TimelineEventType, room::member::MembershipState};
match event.event_type() {
| TimelineEventType::RoomPowerLevels
| TimelineEventType::RoomJoinRules
| TimelineEventType::RoomCreate => event.state_key() == Some(""),
| TimelineEventType::RoomMember => {
let content = RoomMemberEventContent::new(event.content());
if content.membership().is_ok_and(|membership| {
matches!(membership, MembershipState::Leave | MembershipState::Ban)
}) {
return Some(event.sender().as_str()) != event.state_key();
}
false
},
| _ => false,
}
}
+152
View File
@@ -0,0 +1,152 @@
//! Types to deserialize `m.room.create` events.
use std::{borrow::Cow, iter, ops::Deref};
use ruma::{
OwnedUserId, RoomVersionId, UserId, room_version_rules::AuthorizationRules,
serde::from_raw_json_value,
};
use serde::{Deserialize, de::IgnoredAny};
use crate::{Error, Result, err, matrix::Event};
/// A helper type for an [`Event`] of type `m.room.create`.
///
/// This is a type that deserializes each field lazily, when requested.
#[derive(Debug)]
pub struct RoomCreateEvent<E: Event>(E);
impl<E: Event> RoomCreateEvent<E> {
/// Construct a new `RoomCreateEvent` around the given event.
#[inline]
pub fn new(event: E) -> Self { Self(event) }
/// The version of the room.
#[allow(dead_code)]
pub fn room_version(&self) -> Result<RoomVersionId> {
#[derive(Deserialize)]
struct RoomCreateContentRoomVersion {
#[allow(dead_code)]
room_version: Option<RoomVersionId>,
}
let content: RoomCreateContentRoomVersion =
from_raw_json_value(self.content()).map_err(|err: Error| {
err!("invalid `room_version` field in `m.room.create` event: {err}")
})?;
Ok(content.room_version.unwrap_or(RoomVersionId::V1))
}
/// Whether the room is federated.
pub fn federate(&self) -> Result<bool> {
#[derive(Deserialize)]
struct RoomCreateContentFederate {
#[serde(rename = "m.federate")]
federate: Option<bool>,
}
let content: RoomCreateContentFederate =
from_raw_json_value(self.content()).map_err(|err: Error| {
err!("invalid `m.federate` field in `m.room.create` event: {err}")
})?;
Ok(content.federate.unwrap_or(true))
}
/// The creator of the room.
///
/// If the `use_room_create_sender` field of `AuthorizationRules` is set,
/// the creator is the sender of this `m.room.create` event, otherwise it
/// is deserialized from the `creator` field of this event's content.
pub fn creator(&self, rules: &AuthorizationRules) -> Result<Cow<'_, UserId>> {
#[derive(Deserialize)]
struct RoomCreateContentCreator {
creator: OwnedUserId,
}
if rules.use_room_create_sender {
Ok(Cow::Borrowed(self.sender()))
} else {
let content: RoomCreateContentCreator =
from_raw_json_value(self.content()).map_err(|err: Error| {
err!("missing or invalid `creator` field in `m.room.create` event: {err}")
})?;
Ok(Cow::Owned(content.creator))
}
}
/// The creators of the room.
///
/// If the `use_room_create_sender` field of `AuthorizationRules` is set,
/// the creator is the sender of this `m.room.create` event, otherwise it
/// is deserialized from the `creator` field of this event's content.
/// Additionally if the `explicitly_privilege_room_creators`
/// field of `AuthorizationRules` is set, any additional user IDs in
/// `additional_creators`, if present, will also be considered creators.
pub fn creators<'a>(
&'a self,
rules: &'a AuthorizationRules,
) -> Result<impl Iterator<Item = OwnedUserId> + Clone + use<'a, E>> {
let initial = self.creator(rules)?.into_owned();
let additional = self.additional_creators(rules)?;
Ok(iter::once(initial).chain(additional))
}
/// The additional creators of the room (if any).
///
/// If the `explicitly_privilege_room_creators`
/// field of `AuthorizationRules` is set, any additional user IDs in
/// `additional_creators`, if present, will also be considered creators.
///
/// This function ignores the primary room creator, and should only be used
/// in `check_room_member_join`. Otherwise, you should use `creators`
/// instead.
pub(super) fn additional_creators(
&self,
rules: &AuthorizationRules,
) -> Result<impl Iterator<Item = OwnedUserId> + Clone> {
#[derive(Deserialize)]
struct RoomCreateContentAdditionalCreators {
#[serde(default)]
additional_creators: Vec<OwnedUserId>,
}
Ok(if rules.additional_room_creators {
let mut content: RoomCreateContentAdditionalCreators =
from_raw_json_value(self.content()).map_err(|err: serde_json::Error| {
err!("invalid `additional_creators` field in `m.room.create` event: {err}")
})?;
content.additional_creators.sort();
content.additional_creators.dedup();
content.additional_creators.into_iter()
} else {
Vec::new().into_iter()
})
}
/// Whether the event has a `creator` field.
pub fn has_creator(&self) -> Result<bool> {
#[derive(Deserialize)]
struct RoomCreateContentCreator {
creator: Option<IgnoredAny>,
}
let content: RoomCreateContentCreator =
from_raw_json_value(self.content()).map_err(|err: Error| {
err!("invalid `creator` field in `m.room.create` event: {err}")
})?;
Ok(content.creator.is_some())
}
}
impl<E: Event> Deref for RoomCreateEvent<E> {
type Target = E;
#[inline]
fn deref(&self) -> &Self::Target { &self.0 }
}
@@ -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
View File
@@ -0,0 +1,207 @@
//! Types to deserialize `m.room.member` events.
use std::ops::Deref;
use ruma::{
CanonicalJsonObject, OwnedUserId, events::room::member::MembershipState,
serde::from_raw_json_value, signatures::canonical_json,
};
use serde::Deserialize;
use serde_json::value::RawValue as RawJsonValue;
use crate::{Err, Error, Result, debug_error, err, matrix::Event};
/// A helper type for an [`Event`] of type `m.room.member`.
///
/// This is a type that deserializes each field lazily, as requested.
#[derive(Debug, Clone)]
pub struct RoomMemberEvent<E: Event>(E);
impl<E: Event> RoomMemberEvent<E> {
/// Construct a new `RoomMemberEvent` around the given event.
#[inline]
pub fn new(event: E) -> Self { Self(event) }
/// The membership of the user.
#[inline]
pub fn membership(&self) -> Result<MembershipState> {
RoomMemberEventContent(self.content()).membership()
}
/// If this is a `join` event, the ID of a user on the homeserver that
/// authorized it.
#[inline]
pub fn join_authorised_via_users_server(&self) -> Result<Option<OwnedUserId>> {
RoomMemberEventContent(self.content()).join_authorised_via_users_server()
}
/// If this is an `invite` event, details about the third-party invite that
/// resulted in this event.
#[inline]
pub fn third_party_invite(&self) -> Result<Option<ThirdPartyInvite>> {
RoomMemberEventContent(self.content()).third_party_invite()
}
}
impl<E: Event> Deref for RoomMemberEvent<E> {
type Target = E;
#[inline]
fn deref(&self) -> &Self::Target { &self.0 }
}
/// Helper trait for `Option<RoomMemberEvent<E>>`.
pub(crate) trait RoomMemberEventResultExt {
/// The membership of the user.
///
/// Defaults to `leave` if there is no `m.room.member` event.
fn membership(&self) -> Result<MembershipState>;
}
impl<E: Event> RoomMemberEventResultExt for Result<RoomMemberEvent<E>> {
fn membership(&self) -> Result<MembershipState> {
match self {
| Ok(event) => event.membership(),
| Err(e) if e.is_not_found() => Ok(MembershipState::Leave),
| Err(e) if cfg!(test) => panic!("membership(): unexpected: {e}"),
| Err(e) => {
debug_error!("membership(): unexpected: {e}");
Ok(MembershipState::Leave)
},
}
}
}
/// A helper type for the raw JSON content of an event of type `m.room.member`.
pub struct RoomMemberEventContent<'a>(&'a RawJsonValue);
impl<'a> RoomMemberEventContent<'a> {
/// Construct a new `RoomMemberEventContent` around the given raw JSON
/// content.
#[inline]
#[must_use]
pub fn new(content: &'a RawJsonValue) -> Self { Self(content) }
}
impl RoomMemberEventContent<'_> {
/// The membership of the user.
pub fn membership(&self) -> Result<MembershipState> {
#[derive(Deserialize)]
struct RoomMemberContentMembership {
membership: MembershipState,
}
let content: RoomMemberContentMembership =
from_raw_json_value(self.0).map_err(|err: Error| {
err!(Request(InvalidParam(
"missing or invalid `membership` field in `m.room.member` event: {err}"
)))
})?;
Ok(content.membership)
}
/// If this is a `join` event, the ID of a user on the homeserver that
/// authorized it.
pub fn join_authorised_via_users_server(&self) -> Result<Option<OwnedUserId>> {
#[derive(Deserialize)]
struct RoomMemberContentJoinAuthorizedViaUsersServer {
join_authorised_via_users_server: Option<OwnedUserId>,
}
let content: RoomMemberContentJoinAuthorizedViaUsersServer = from_raw_json_value(self.0)
.map_err(|err: Error| {
err!(Request(InvalidParam(
"invalid `join_authorised_via_users_server` field in `m.room.member` event: \
{err}"
)))
})?;
Ok(content.join_authorised_via_users_server)
}
/// If this is an `invite` event, details about the third-party invite that
/// resulted in this event.
pub fn third_party_invite(&self) -> Result<Option<ThirdPartyInvite>> {
#[derive(Deserialize)]
struct RoomMemberContentThirdPartyInvite {
third_party_invite: Option<ThirdPartyInvite>,
}
let content: RoomMemberContentThirdPartyInvite =
from_raw_json_value(self.0).map_err(|err: Error| {
err!(Request(InvalidParam(
"invalid `third_party_invite` field in `m.room.member` event: {err}"
)))
})?;
Ok(content.third_party_invite)
}
}
/// Details about a third-party invite.
#[derive(Deserialize)]
pub struct ThirdPartyInvite {
/// Signed details about the third-party invite.
signed: CanonicalJsonObject,
}
impl ThirdPartyInvite {
/// The unique identifier for the third-party invite.
pub fn token(&self) -> Result<&str> {
let Some(token_value) = self.signed.get("token") else {
return Err!(Request(InvalidParam(
"missing `token` field in `third_party_invite.signed` of `m.room.member` event"
)));
};
token_value.as_str().ok_or_else(|| {
err!(Request(InvalidParam(
"unexpected format of `token` field in `third_party_invite.signed` of \
`m.room.member` event: expected string, got {token_value:?}"
)))
})
}
/// The Matrix ID of the user that was invited.
pub fn mxid(&self) -> Result<&str> {
let Some(mxid_value) = self.signed.get("mxid") else {
return Err!(Request(InvalidParam(
"missing `mxid` field in `third_party_invite.signed` of `m.room.member` event"
)));
};
mxid_value.as_str().ok_or_else(|| {
err!(Request(InvalidParam(
"unexpected format of `mxid` field in `third_party_invite.signed` of \
`m.room.member` event: expected string, got {mxid_value:?}"
)))
})
}
/// The signatures of the event.
pub fn signatures(&self) -> Result<&CanonicalJsonObject> {
let Some(signatures_value) = self.signed.get("signatures") else {
return Err!(Request(InvalidParam(
"missing `signatures` field in `third_party_invite.signed` of `m.room.member` \
event"
)));
};
signatures_value.as_object().ok_or_else(|| {
err!(Request(InvalidParam(
"unexpected format of `signatures` field in `third_party_invite.signed` of \
`m.room.member` event: expected object, got {signatures_value:?}"
)))
})
}
/// The `signed` object as canonical JSON string to verify the signatures.
pub fn signed_canonical_json(&self) -> Result<String> {
canonical_json(&self.signed).map_err(|error| {
err!(Request(InvalidParam(
"invalid `third_party_invite.signed` field in `m.room.member` event: {error}"
)))
})
}
}
@@ -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",
}
}
}
@@ -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
View File
@@ -0,0 +1,74 @@
use ruma::{
UserId,
events::{StateEventType, room::member::MembershipState},
};
use super::events::{
JoinRule, RoomCreateEvent, RoomJoinRulesEvent, RoomMemberEvent, RoomPowerLevelsEvent,
RoomThirdPartyInviteEvent, member::RoomMemberEventResultExt,
};
use crate::{
Result, err,
matrix::{Event, StateKey},
};
pub(super) trait FetchStateExt<Pdu: Event> {
async fn room_create_event(&self) -> Result<RoomCreateEvent<Pdu>>;
async fn user_membership(&self, user_id: &UserId) -> Result<MembershipState>;
async fn room_power_levels_event(&self) -> Option<RoomPowerLevelsEvent<Pdu>>;
async fn join_rule(&self) -> Result<JoinRule>;
async fn room_third_party_invite_event(
&self,
token: &str,
) -> Option<RoomThirdPartyInviteEvent<Pdu>>;
}
impl<Fetch, Fut, Pdu> FetchStateExt<Pdu> for &Fetch
where
Fetch: Fn(StateEventType, StateKey) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>>,
Pdu: Event,
{
async fn room_create_event(&self) -> Result<RoomCreateEvent<Pdu>> {
self(StateEventType::RoomCreate, "".into())
.await
.map(RoomCreateEvent::new)
.map_err(|e| err!("no `m.room.create` event in current state: {e}"))
}
async fn user_membership(&self, user_id: &UserId) -> Result<MembershipState> {
self(StateEventType::RoomMember, user_id.as_str().into())
.await
.map(RoomMemberEvent::new)
.membership()
}
async fn room_power_levels_event(&self) -> Option<RoomPowerLevelsEvent<Pdu>> {
self(StateEventType::RoomPowerLevels, "".into())
.await
.map(RoomPowerLevelsEvent::new)
.ok()
}
async fn join_rule(&self) -> Result<JoinRule> {
self(StateEventType::RoomJoinRules, "".into())
.await
.map(RoomJoinRulesEvent::new)
.map_err(|e| err!("no `m.room.join_rules` event in current state: {e}"))?
.join_rule()
}
async fn room_third_party_invite_event(
&self,
token: &str,
) -> Option<RoomThirdPartyInviteEvent<Pdu>> {
self(StateEventType::RoomThirdPartyInvite, token.into())
.await
.ok()
.map(RoomThirdPartyInviteEvent::new)
}
}
File diff suppressed because it is too large Load Diff
-265
View File
@@ -1,265 +0,0 @@
use std::collections::BTreeMap;
use ruma::{
Int, OwnedUserId, UserId,
events::{TimelineEventType, room::power_levels::RoomPowerLevelsEventContent},
power_levels::{NotificationPowerLevels, default_power_level},
serde::{
deserialize_v1_powerlevel, vec_deserialize_int_powerlevel_values,
vec_deserialize_v1_powerlevel_values,
},
};
use serde::Deserialize;
use serde_json::{Error, from_str as from_json_str};
use super::{Result, RoomVersion};
use crate::error;
#[derive(Deserialize)]
struct IntRoomPowerLevelsEventContent {
#[serde(default = "default_power_level")]
ban: Int,
#[serde(default)]
events: BTreeMap<TimelineEventType, Int>,
#[serde(default)]
events_default: Int,
#[serde(default)]
invite: Int,
#[serde(default = "default_power_level")]
kick: Int,
#[serde(default = "default_power_level")]
redact: Int,
#[serde(default = "default_power_level")]
state_default: Int,
#[serde(default)]
users: BTreeMap<OwnedUserId, Int>,
#[serde(default)]
users_default: Int,
#[serde(default)]
notifications: IntNotificationPowerLevels,
}
impl From<IntRoomPowerLevelsEventContent> for RoomPowerLevelsEventContent {
fn from(int_pl: IntRoomPowerLevelsEventContent) -> Self {
let IntRoomPowerLevelsEventContent {
ban,
events,
events_default,
invite,
kick,
redact,
state_default,
users,
users_default,
notifications,
} = int_pl;
let mut pl = Self::new();
pl.ban = ban;
pl.events = events;
pl.events_default = events_default;
pl.invite = invite;
pl.kick = kick;
pl.redact = redact;
pl.state_default = state_default;
pl.users = users;
pl.users_default = users_default;
pl.notifications = notifications.into();
pl
}
}
#[derive(Deserialize)]
struct IntNotificationPowerLevels {
#[serde(default = "default_power_level")]
room: Int,
}
impl Default for IntNotificationPowerLevels {
fn default() -> Self { Self { room: default_power_level() } }
}
impl From<IntNotificationPowerLevels> for NotificationPowerLevels {
fn from(int_notif: IntNotificationPowerLevels) -> Self {
let mut notif = Self::new();
notif.room = int_notif.room;
notif
}
}
#[inline]
pub(crate) fn deserialize_power_levels(
content: &str,
room_version: &RoomVersion,
) -> Option<RoomPowerLevelsEventContent> {
if room_version.integer_power_levels {
deserialize_integer_power_levels(content)
} else {
deserialize_legacy_power_levels(content)
}
}
fn deserialize_integer_power_levels(content: &str) -> Option<RoomPowerLevelsEventContent> {
match from_json_str::<IntRoomPowerLevelsEventContent>(content) {
| Ok(content) => Some(content.into()),
| Err(_) => {
error!("m.room.power_levels event is not valid with integer values");
None
},
}
}
fn deserialize_legacy_power_levels(content: &str) -> Option<RoomPowerLevelsEventContent> {
match from_json_str(content) {
| Ok(content) => Some(content),
| Err(_) => {
error!(
"m.room.power_levels event is not valid with integer or string integer values"
);
None
},
}
}
#[derive(Deserialize)]
pub(crate) struct PowerLevelsContentFields {
#[serde(
default,
deserialize_with = "vec_deserialize_v1_powerlevel_values"
)]
pub(crate) users: Vec<(OwnedUserId, Int)>,
#[serde(default, deserialize_with = "deserialize_v1_powerlevel")]
pub(crate) users_default: Int,
}
impl PowerLevelsContentFields {
pub(crate) fn get_user_power(&self, user_id: &UserId) -> Option<&Int> {
let comparator = |item: &(OwnedUserId, Int)| {
let item: &UserId = &item.0;
item.cmp(user_id)
};
self.users
.binary_search_by(comparator)
.ok()
.and_then(|idx| self.users.get(idx).map(|item| &item.1))
}
}
#[derive(Deserialize)]
struct IntPowerLevelsContentFields {
#[serde(
default,
deserialize_with = "vec_deserialize_int_powerlevel_values"
)]
users: Vec<(OwnedUserId, Int)>,
#[serde(default)]
users_default: Int,
}
impl From<IntPowerLevelsContentFields> for PowerLevelsContentFields {
fn from(pl: IntPowerLevelsContentFields) -> Self {
let IntPowerLevelsContentFields { users, users_default } = pl;
Self { users, users_default }
}
}
#[inline]
pub(crate) fn deserialize_power_levels_content_fields(
content: &str,
room_version: &RoomVersion,
) -> Result<PowerLevelsContentFields, Error> {
if room_version.integer_power_levels {
deserialize_integer_power_levels_content_fields(content)
} else {
deserialize_legacy_power_levels_content_fields(content)
}
}
fn deserialize_integer_power_levels_content_fields(
content: &str,
) -> Result<PowerLevelsContentFields, Error> {
from_json_str::<IntPowerLevelsContentFields>(content).map(Into::into)
}
fn deserialize_legacy_power_levels_content_fields(
content: &str,
) -> Result<PowerLevelsContentFields, Error> {
from_json_str(content)
}
#[derive(Deserialize)]
pub(crate) struct PowerLevelsContentInvite {
#[serde(default, deserialize_with = "deserialize_v1_powerlevel")]
pub(crate) invite: Int,
}
#[derive(Deserialize)]
struct IntPowerLevelsContentInvite {
#[serde(default)]
invite: Int,
}
impl From<IntPowerLevelsContentInvite> for PowerLevelsContentInvite {
fn from(pl: IntPowerLevelsContentInvite) -> Self {
let IntPowerLevelsContentInvite { invite } = pl;
Self { invite }
}
}
pub(crate) fn deserialize_power_levels_content_invite(
content: &str,
room_version: &RoomVersion,
) -> Result<PowerLevelsContentInvite, Error> {
if room_version.integer_power_levels {
from_json_str::<IntPowerLevelsContentInvite>(content).map(Into::into)
} else {
from_json_str(content)
}
}
#[derive(Deserialize)]
pub(crate) struct PowerLevelsContentRedact {
#[serde(
default = "default_power_level",
deserialize_with = "deserialize_v1_powerlevel"
)]
pub(crate) redact: Int,
}
#[derive(Deserialize)]
pub(crate) struct IntPowerLevelsContentRedact {
#[serde(default = "default_power_level")]
redact: Int,
}
impl From<IntPowerLevelsContentRedact> for PowerLevelsContentRedact {
fn from(pl: IntPowerLevelsContentRedact) -> Self {
let IntPowerLevelsContentRedact { redact } = pl;
Self { redact }
}
}
pub(crate) fn deserialize_power_levels_content_redact(
content: &str,
room_version: &RoomVersion,
) -> Result<PowerLevelsContentRedact, Error> {
if room_version.integer_power_levels {
from_json_str::<IntPowerLevelsContentRedact>(content).map(Into::into)
} else {
from_json_str(content)
}
}
+213
View File
@@ -0,0 +1,213 @@
#[cfg(test)]
mod tests;
mod auth_difference;
mod conflicted_subgraph;
mod iterative_auth_check;
mod mainline_sort;
mod power_sort;
mod split_conflicted;
mod topological_sort;
use std::collections::{BTreeMap, BTreeSet};
use futures::{FutureExt, Stream, StreamExt, TryFutureExt, future::OptionFuture};
use ruma::{OwnedEventId, events::StateEventType, room_version_rules::RoomVersionRules};
pub use self::topological_sort::topological_sort;
use self::{
auth_difference::auth_difference, conflicted_subgraph::conflicted_subgraph_dfs,
iterative_auth_check::iterative_auth_check, mainline_sort::mainline_sort,
power_sort::power_sort, split_conflicted::split_conflicted_state,
};
#[cfg(test)]
use super::test_utils;
use crate::{
Result, debug,
matrix::{Event, TypeStateKey},
utils::stream::{BroadbandExt, IterStream},
};
/// ConflictMap of OwnedEventId specifically.
pub type ConflictMap = StateMap<ConflictVec>;
/// A mapping of event type and state_key to some value `T`, usually an
/// `EventId`.
pub type StateMap<Id> = BTreeMap<TypeStateKey, Id>;
/// Full recursive set of `auth_events` for each event in a StateMap.
pub type AuthSet<Id> = BTreeSet<Id>;
/// List of conflicting event_ids
type ConflictVec = Vec<OwnedEventId>;
/// Apply the [state resolution] algorithm introduced in room version 2 to
/// resolve the state of a room.
///
/// ## Arguments
///
/// * `rules` - The rules to apply for the version of the current room.
///
/// * `state_maps` - The incoming states to resolve. Each `StateMap` represents
/// a possible fork in the state of a room.
///
/// * `auth_chains` - The list of full recursive sets of `auth_events` for each
/// event in the `state_maps`.
///
/// * `fetch_event` - Function to fetch an event in the room given its event ID.
///
/// ## Invariants
///
/// The caller of `resolve` must ensure that all the events are from the same
/// room.
///
/// ## Returns
///
/// The resolved room state.
///
/// [state resolution]: https://spec.matrix.org/latest/rooms/v2/#state-resolution
#[tracing::instrument(level = "debug", skip_all)]
pub async fn resolve<'a, States, AuthSets, FetchExists, ExistsFut, FetchEvent, EventFut, Pdu>(
rules: &RoomVersionRules,
state_maps: States,
auth_sets: AuthSets,
fetch: &FetchEvent,
exists: &FetchExists,
backport_css: bool,
) -> Result<StateMap<OwnedEventId>>
where
States: Stream<Item = StateMap<OwnedEventId>> + Send,
AuthSets: Stream<Item = AuthSet<OwnedEventId>> + Send,
FetchExists: Fn(OwnedEventId) -> ExistsFut + Sync,
ExistsFut: Future<Output = bool> + Send,
FetchEvent: Fn(OwnedEventId) -> EventFut + Sync,
EventFut: Future<Output = Result<Pdu>> + Send,
Pdu: Event + Clone,
{
// Split the unconflicted state map and the conflicted state set.
let (unconflicted_state, conflicted_states) = split_conflicted_state(state_maps).await;
debug!(?unconflicted_state, unconflicted = unconflicted_state.len(), "unresolved state");
debug!(?conflicted_states, conflicted = conflicted_states.len(), "unresolved states");
if conflicted_states.is_empty() {
return Ok(unconflicted_state.into_iter().collect());
}
let consider_conflicted_subgraph = rules
.state_res
.v2_rules()
.is_some_and(|rules| rules.consider_conflicted_state_subgraph)
|| backport_css;
// Since `org.matrix.hydra.11`, fetch the conflicted state subgraph.
let conflicted_subgraph: OptionFuture<_> = consider_conflicted_subgraph
.then(|| conflicted_states.clone().into_values().flatten())
.map(async |ids| conflicted_subgraph_dfs(ids.stream(), fetch))
.into();
let conflicted_subgraph = conflicted_subgraph
.await
.into_iter()
.stream()
.flatten();
// 0. The full conflicted set is the union of the conflicted state set and the
// auth difference. Don't honor events that don't exist.
let full_conflicted_set: AuthSet<_> = auth_difference(auth_sets)
.chain(conflicted_states.into_values().flatten().stream())
.broad_filter_map(async |id| exists(id.clone()).await.then_some(id))
.chain(conflicted_subgraph)
.collect::<AuthSet<_>>()
.inspect(|set| debug!(count = set.len(), "full conflicted set"))
.inspect(|set| debug!(?set, "full conflicted set"))
.await;
// 1. Select the set X of all power events that appear in the full conflicted
// set. For each such power event P, enlarge X by adding the events in the
// auth chain of P which also belong to the full conflicted set. Sort X into
// a list using the reverse topological power ordering.
let sorted_power_events: Vec<_> = power_sort(rules, &full_conflicted_set, fetch)
.inspect_ok(|list| debug!(count = list.len(), "sorted power events"))
.inspect_ok(|list| debug!(?list, "sorted power events"))
.await?;
let sorted_power_events_set: AuthSet<_> = sorted_power_events.iter().collect();
let sorted_power_events = sorted_power_events
.iter()
.stream()
.map(AsRef::as_ref);
let start_with_incoming_state = rules
.state_res
.v2_rules()
.is_none_or(|r| !r.begin_iterative_auth_checks_with_empty_state_map);
let initial_state = start_with_incoming_state
.then(|| unconflicted_state.clone())
.unwrap_or_default();
// 2. Apply the iterative auth checks algorithm, starting from the unconflicted
// state map, to the list of events from the previous step to get a partially
// resolved state.
let partially_resolved_state =
iterative_auth_check(rules, sorted_power_events, initial_state, fetch)
.inspect_ok(|map| debug!(count = map.len(), "partially resolved power state"))
.inspect_ok(|map| debug!(?map, "partially resolved power state"))
.await?;
// This "epochs" power level event
let power_ty_sk = (StateEventType::RoomPowerLevels, "".into());
let power_event = partially_resolved_state.get(&power_ty_sk);
debug!(event_id = ?power_event, "epoch power event");
let remaining_events: Vec<_> = full_conflicted_set
.into_iter()
.filter(|id| !sorted_power_events_set.contains(id))
.collect();
debug!(count = remaining_events.len(), "remaining events");
debug!(list = ?remaining_events, "remaining events");
let have_remaining_events = !remaining_events.is_empty();
let remaining_events = remaining_events
.iter()
.stream()
.map(AsRef::as_ref);
// 3. Take all remaining events that werent picked in step 1 and order them by
// the mainline ordering based on the power level in the partially resolved
// state obtained in step 2.
let sorted_remaining_events: OptionFuture<_> = have_remaining_events
.then(move || mainline_sort(power_event.cloned(), remaining_events, fetch))
.into();
let sorted_remaining_events = sorted_remaining_events
.await
.unwrap_or(Ok(Vec::new()))?;
debug!(count = sorted_remaining_events.len(), "sorted remaining events");
debug!(list = ?sorted_remaining_events, "sorted remaining events");
let sorted_remaining_events = sorted_remaining_events
.iter()
.stream()
.map(AsRef::as_ref);
// 4. Apply the iterative auth checks algorithm on the partial resolved state
// and the list of events from the previous step.
let mut resolved_state =
iterative_auth_check(rules, sorted_remaining_events, partially_resolved_state, fetch)
.await?;
// 5. Update the result by replacing any event with the event with the same key
// from the unconflicted state map, if such an event exists, to get the final
// resolved state.
resolved_state.extend(unconflicted_state);
debug!(resolved_state = resolved_state.len(), "resolved state");
debug!(?resolved_state, "resolved state");
Ok(resolved_state)
}
@@ -0,0 +1,40 @@
use std::borrow::Borrow;
use futures::{FutureExt, Stream};
use ruma::EventId;
use super::AuthSet;
use crate::utils::stream::{IterStream, ReadyExt};
/// Get the auth difference for the given auth chains.
///
/// Definition in the specification:
///
/// The auth difference is calculated by first calculating the full auth chain
/// for each state _Si_, that is the union of the auth chains for each event in
/// _Si_, and then taking every event that doesnt appear in every auth chain.
/// If _Ci_ is the full auth chain of _Si_, then the auth difference is _Ci_
/// ∩_Ci_.
///
/// ## Arguments
///
/// * `auth_chains` - The list of full recursive sets of `auth_events`. Inputs
/// must be sorted.
///
/// ## Returns
///
/// Outputs the event IDs that are not present in all the auth chains.
pub(super) fn auth_difference<'a, AuthSets, Id>(auth_sets: AuthSets) -> impl Stream<Item = Id>
where
AuthSets: Stream<Item = AuthSet<Id>>,
Id: Borrow<EventId> + Clone + Eq + Ord + Send + 'a,
{
auth_sets
.ready_fold_default(|ret: AuthSet<Id>, set| {
ret.symmetric_difference(&set)
.cloned()
.collect::<AuthSet<Id>>()
})
.map(|set: AuthSet<Id>| set.into_iter().stream())
.flatten_stream()
}
@@ -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()))
}
@@ -0,0 +1,203 @@
use futures::{Stream, StreamExt, TryFutureExt, TryStreamExt};
use ruma::{
EventId, OwnedEventId,
events::{StateEventType, TimelineEventType},
room_version_rules::RoomVersionRules,
};
use super::{
super::{auth_types_for_event, check_state_dependent_auth_rules},
StateMap,
};
use crate::{
Error, Result, debug_warn, err, error,
matrix::{Event, EventTypeExt, StateKey},
trace,
utils::stream::{IterStream, ReadyExt, TryReadyExt, TryWidebandExt},
};
/// Perform the iterative auth checks to the given list of events.
///
/// Definition in the specification:
///
/// The iterative auth checks algorithm takes as input an initial room state and
/// a sorted list of state events, and constructs a new room state by iterating
/// through the event list and applying the state event to the room state if the
/// state event is allowed by the authorization rules. If the state event is not
/// allowed by the authorization rules, then the event is ignored. If a
/// (event_type, state_key) key that is required for checking the authorization
/// rules is not present in the state, then the appropriate state event from the
/// events auth_events is used if the auth event is not rejected.
///
/// ## Arguments
///
/// * `rules` - The authorization rules for the current room version.
/// * `events` - The sorted state events to apply to the `partial_state`.
/// * `state` - The current state that was partially resolved for the room.
/// * `fetch_event` - Function to fetch an event in the room given its event ID.
///
/// ## Returns
///
/// Returns the partially resolved state, or an `Err(_)` if one of the state
/// events in the room has an unexpected format.
#[tracing::instrument(
name = "iterative_auth",
level = "debug",
skip_all,
fields(
states = ?state.len(),
)
)]
pub(super) async fn iterative_auth_check<'b, SortedPowerEvents, Fetch, Fut, Pdu>(
rules: &RoomVersionRules,
events: SortedPowerEvents,
state: StateMap<OwnedEventId>,
fetch: &Fetch,
) -> Result<StateMap<OwnedEventId>>
where
SortedPowerEvents: Stream<Item = &'b EventId> + Send,
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
events
.map(Ok)
.wide_and_then(async |event_id| {
let event = fetch(event_id.to_owned()).await?;
let state_key: StateKey = event
.state_key()
.ok_or_else(|| err!(Request(InvalidParam("Missing state_key"))))?
.into();
Ok((event_id, state_key, event))
})
.try_fold(state, |state, (event_id, state_key, event)| {
auth_check(rules, state, event_id, state_key, event, fetch)
})
.await
}
#[tracing::instrument(
name = "check",
level = "debug",
skip_all,
fields(
%event_id,
%state_key,
)
)]
async fn auth_check<Fetch, Fut, Pdu>(
rules: &RoomVersionRules,
mut state: StateMap<OwnedEventId>,
event_id: &EventId,
state_key: StateKey,
event: Pdu,
fetch: &Fetch,
) -> Result<StateMap<OwnedEventId>>
where
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
let Ok(auth_types) = auth_types_for_event(
event.event_type(),
event.sender(),
Some(&state_key),
event.content(),
&rules.authorization,
true,
)
.inspect_err(|e| error!("failed to get auth types for event: {e}")) else {
return Ok(state);
};
let auth_types_events = auth_types
.stream()
.ready_filter_map(|key| {
state
.get(&key)
.map(move |auth_event_id| (auth_event_id, key))
})
.filter_map(async |(id, key)| {
fetch(id.clone())
.inspect_err(|e| debug_warn!(%id, "missing auth event: {e}"))
.inspect_err(|e| debug_assert!(!cfg!(test), "missing auth {id:?}: {e:?}"))
.map_ok(move |auth_event| (key, auth_event))
.await
.ok()
})
.ready_filter_map(|(key, auth_event)| {
auth_event
.rejected()
.eq(&false)
.then_some((key, auth_event))
})
.map(Ok);
// If the `m.room.create` event is not in the auth events, we need to add it,
// because it's always part of the state and required in the auth rules.
let also_need_create_event = *event.event_type() != TimelineEventType::RoomCreate
&& rules
.authorization
.room_create_event_id_as_room_id;
let also_create_id: Option<OwnedEventId> = also_need_create_event
.then(|| event.room_id().as_event_id().ok())
.flatten();
let auth_events = event
.auth_events()
.chain(also_create_id.as_deref().into_iter())
.stream()
.filter_map(async |id| {
fetch(id.to_owned())
.inspect_err(|e| debug_warn!(%id, "missing auth event: {e}"))
.inspect_err(|e| debug_assert!(!cfg!(test), "missing auth {id:?}: {e:?}"))
.await
.ok()
})
.map(Result::<Pdu, Error>::Ok)
.ready_try_filter_map(|auth_event| {
let state_key = auth_event
.state_key()
.ok_or_else(|| err!(Request(InvalidParam("Missing state_key"))))?;
let key_val = auth_event
.rejected()
.eq(&false)
.then_some((auth_event.event_type().with_state_key(state_key), auth_event));
Ok(key_val)
});
let auth_events: Vec<_> = auth_events
.chain(auth_types_events)
.try_collect()
.map_ok(|mut vec: Vec<_>| {
vec.sort_by(|a, b| a.0.cmp(&b.0));
vec.reverse();
vec.dedup_by(|a, b| a.0.eq(&b.0));
vec
})
.await?;
let fetch_state = async |ty: StateEventType, key: StateKey| -> Result<Pdu> {
trace!(?ty, ?key, auth_events = auth_events.len(), "fetch state");
auth_events
.binary_search_by(|a| ty.cmp(&a.0.0).then(key.cmp(&a.0.1)))
.map(|i| auth_events[i].1.clone())
.map_err(|_| err!(Request(NotFound("Missing auth_event {ty:?},{key:?}"))))
};
// Add authentic event to the partially resolved state.
if check_state_dependent_auth_rules(rules, &event, &fetch_state)
.await
.inspect_err(|e| debug_warn!("event failed auth check: {e}"))
.is_ok()
{
let key = event.event_type().with_state_key(state_key);
state.insert(key, event_id.to_owned());
}
Ok(state)
}
@@ -0,0 +1,171 @@
use std::collections::HashMap;
use futures::{Stream, StreamExt, TryStreamExt, pin_mut};
use ruma::{EventId, OwnedEventId, events::TimelineEventType};
use crate::{
Result,
matrix::Event,
trace,
utils::stream::{IterStream, TryReadyExt, WidebandExt},
};
/// Perform mainline ordering of the given events.
///
/// Definition in the spec:
/// Given mainline positions calculated from P, the mainline ordering based on P
/// of a set of events is the ordering, from smallest to largest, using the
/// following comparison relation on events: for events x and y, x < y if
///
/// 1. the mainline position of x is greater than the mainline position of y
/// (i.e. the auth chain of x is based on an earlier event in the mainline
/// than y); or
/// 2. the mainline positions of the events are the same, but xs
/// origin_server_ts is less than ys origin_server_ts; or
/// 3. the mainline positions of the events are the same and the events have the
/// same origin_server_ts, but xs event_id is less than ys event_id.
///
/// ## Arguments
///
/// * `events` - The list of event IDs to sort.
/// * `power_level` - The power level event in the current state.
/// * `fetch_event` - Function to fetch an event in the room given its event ID.
///
/// ## Returns
///
/// Returns the sorted list of event IDs, or an `Err(_)` if one the event in the
/// room has an unexpected format.
#[tracing::instrument(
level = "debug",
skip_all,
fields(
?power_level,
)
)]
pub(super) async fn mainline_sort<'a, RemainingEvents, Fetch, Fut, Pdu>(
mut power_level: Option<OwnedEventId>,
events: RemainingEvents,
fetch: &Fetch,
) -> Result<Vec<OwnedEventId>>
where
RemainingEvents: Stream<Item = &'a EventId> + Send,
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
// Populate the mainline of the power level.
let mut mainline = vec![];
while let Some(power_level_event_id) = power_level {
let power_level_event = fetch(power_level_event_id).await?;
mainline.push(power_level_event.event_id().to_owned());
power_level = get_power_levels_auth_event(&power_level_event, fetch)
.await?
.map(|event| event.event_id().to_owned());
}
let mainline_map: HashMap<_, _> = mainline
.iter()
.rev()
.enumerate()
.map(|(idx, event_id)| (event_id.clone(), idx))
.collect();
let order_map: HashMap<_, _> = events
.wide_filter_map(async |event_id| {
let event = fetch(event_id.to_owned()).await.ok()?;
let position = mainline_position(&event, &mainline_map, fetch)
.await
.ok()?;
let event_id = event.event_id().to_owned();
let origin_server_ts = event.origin_server_ts();
Some((event_id, (position, origin_server_ts)))
})
.collect()
.await;
let mut sorted_event_ids: Vec<_> = order_map.keys().cloned().collect();
sorted_event_ids.sort_by(|a, b| {
let (a_pos, a_ots) = &order_map[a];
let (b_pos, b_ots) = &order_map[b];
a_pos
.cmp(b_pos)
.then(a_ots.cmp(b_ots))
.then(a.cmp(b))
});
Ok(sorted_event_ids)
}
/// Get the mainline position of the given event from the given mainline map.
///
/// ## Arguments
///
/// * `event` - The event to compute the mainline position of.
/// * `mainline_map` - The mainline map of the m.room.power_levels event.
/// * `fetch` - Function to fetch an event in the room given its event ID.
///
/// ## Returns
///
/// Returns the mainline position of the event, or an `Err(_)` if one of the
/// events in the auth chain of the event was not found.
#[tracing::instrument(
level = "trace",
skip_all,
fields(
event = ?event.event_id(),
mainline = mainline_map.len(),
)
)]
async fn mainline_position<Fetch, Fut, Pdu>(
event: &Pdu,
mainline_map: &HashMap<OwnedEventId, usize>,
fetch: &Fetch,
) -> Result<usize>
where
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>>,
Pdu: Event,
{
let mut current_event = Some(event.clone());
while let Some(event) = current_event {
trace!(event_id = ?event.event_id(), "mainline");
// If the current event is in the mainline map, return its position.
if let Some(position) = mainline_map.get(event.event_id()) {
return Ok(*position);
}
// Look for the power levels event in the auth events.
current_event = get_power_levels_auth_event(&event, fetch).await?;
}
// Did not find a power level event so we default to zero.
Ok(0)
}
#[allow(clippy::redundant_closure)]
#[tracing::instrument(level = "trace", skip_all)]
async fn get_power_levels_auth_event<Fetch, Fut, Pdu>(
event: &Pdu,
fetch: &Fetch,
) -> Result<Option<Pdu>>
where
Fetch: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<Pdu>>,
Pdu: Event,
{
let power_level_event = event
.auth_events()
.try_stream()
.map_ok(ToOwned::to_owned)
.and_then(|auth_event_id| fetch(auth_event_id))
.ready_try_skip_while(|auth_event| {
Ok(!auth_event.is_type_and_state_key(&TimelineEventType::RoomPowerLevels, ""))
});
pin_mut!(power_level_event);
power_level_event.try_next().await
}
@@ -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,
}
}
@@ -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
View File
@@ -0,0 +1,840 @@
use std::collections::HashMap;
use futures::StreamExt;
use maplit::{hashmap, hashset};
use rand::seq::SliceRandom;
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedEventId,
events::{
StateEventType, TimelineEventType,
room::join_rules::{JoinRule, RoomJoinRulesEventContent},
},
int,
room_version_rules::RoomVersionRules,
uint,
};
use serde_json::{json, value::to_raw_value as to_raw_json_value};
use super::{
AuthSet, StateMap,
test_utils::{
INITIAL_EVENTS, TestStore, alice, bob, charlie, do_check, ella, event_id,
member_content_ban, member_content_join, not_found, room_id, to_init_pdu_event,
to_pdu_event, zara,
},
};
use crate::{
debug,
matrix::{Event, EventTypeExt, PduEvent},
utils::stream::IterStream,
};
async fn test_event_sort() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let rules = RoomVersionRules::V6;
let events = INITIAL_EVENTS();
let auth_chain: AuthSet<OwnedEventId> = AuthSet::new();
let sorted_power_events = super::power_sort(&rules, &auth_chain, &async |id| {
events.get(&id).cloned().ok_or_else(not_found)
})
.await
.unwrap();
let sorted_power_events = sorted_power_events
.iter()
.stream()
.map(AsRef::as_ref);
let resolved_power =
super::iterative_auth_check(&rules, sorted_power_events, StateMap::new(), &async |id| {
events.get(&id).cloned().ok_or_else(not_found)
})
.await
.expect("iterative auth check failed on resolved events");
// don't remove any events so we know it sorts them all correctly
let mut events_to_sort = events.keys().cloned().collect::<Vec<_>>();
events_to_sort.shuffle(&mut rand::thread_rng());
let power_level = resolved_power
.get(&(StateEventType::RoomPowerLevels, "".into()))
.cloned();
let events_to_sort = events_to_sort.iter().stream().map(AsRef::as_ref);
let sorted_event_ids = super::mainline_sort(power_level, events_to_sort, &async |id| {
events.get(&id).cloned().ok_or_else(not_found)
})
.await
.unwrap();
assert_eq!(
vec![
"$CREATE:foo",
"$IMA:foo",
"$IPOWER:foo",
"$IJR:foo",
"$IMB:foo",
"$IMC:foo",
"$START:foo",
"$END:foo"
],
sorted_event_ids
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
);
}
#[tokio::test]
async fn test_sort() {
for _ in 0..20 {
// since we shuffle the eventIds before we sort them introducing randomness
// seems like we should test this a few times
test_event_sort().await;
}
}
#[tokio::test]
async fn ban_vs_power_level() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let events = &[
to_init_pdu_event(
"PA",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
),
to_init_pdu_event(
"MA",
alice(),
TimelineEventType::RoomMember,
Some(alice().to_string().as_str()),
member_content_join(),
),
to_init_pdu_event(
"MB",
alice(),
TimelineEventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_ban(),
),
to_init_pdu_event(
"PB",
bob(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
),
];
let edges = vec![vec!["END", "MB", "MA", "PA", "START"], vec!["END", "PA", "PB"]]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["PA", "MA", "MB"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(events, edges, expected_state_ids).await;
}
#[tokio::test]
async fn topic_basic() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let events = &[
to_init_pdu_event(
"T1",
alice(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
to_init_pdu_event(
"PA1",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
),
to_init_pdu_event(
"T2",
alice(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
to_init_pdu_event(
"PA2",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 0 } })).unwrap(),
),
to_init_pdu_event(
"PB",
bob(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
),
to_init_pdu_event(
"T3",
bob(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
];
let edges =
vec![vec!["END", "PA2", "T2", "PA1", "T1", "START"], vec!["END", "T3", "PB", "PA1"]]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["PA2", "T2"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(events, edges, expected_state_ids).await;
}
#[tokio::test]
async fn topic_reset() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let events = &[
to_init_pdu_event(
"T1",
alice(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
to_init_pdu_event(
"PA",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
),
to_init_pdu_event(
"T2",
bob(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
to_init_pdu_event(
"MB",
alice(),
TimelineEventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_ban(),
),
];
let edges = vec![vec!["END", "MB", "T2", "PA", "T1", "START"], vec!["END", "T1"]]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["T1", "MB", "PA"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(events, edges, expected_state_ids).await;
}
#[tokio::test]
async fn join_rule_evasion() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let events = &[
to_init_pdu_event(
"JR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Private)).unwrap(),
),
to_init_pdu_event(
"ME",
ella(),
TimelineEventType::RoomMember,
Some(ella().to_string().as_str()),
member_content_join(),
),
];
let edges = vec![vec!["END", "JR", "START"], vec!["END", "ME", "START"]]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec![event_id("JR")];
do_check(events, edges, expected_state_ids).await;
}
#[tokio::test]
async fn offtopic_power_level() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let events = &[
to_init_pdu_event(
"PA",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
),
to_init_pdu_event(
"PB",
bob(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50, charlie(): 50 } }))
.unwrap(),
),
to_init_pdu_event(
"PC",
charlie(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50, charlie(): 0 } }))
.unwrap(),
),
];
let edges = vec![vec!["END", "PC", "PB", "PA", "START"], vec!["END", "PA"]]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["PC"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(events, edges, expected_state_ids).await;
}
#[tokio::test]
async fn topic_setting() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let events = vec![
to_init_pdu_event(
"T1",
alice(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
to_init_pdu_event(
"PA1",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
),
to_init_pdu_event(
"T2",
alice(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
to_init_pdu_event(
"PA2",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 0 } })).unwrap(),
),
to_init_pdu_event(
"PB",
bob(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
),
to_init_pdu_event(
"T3",
bob(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
to_init_pdu_event(
"MZ1",
zara(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
to_init_pdu_event(
"T4",
alice(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
),
];
let edges = vec![vec!["END", "T4", "MZ1", "PA2", "T2", "PA1", "T1", "START"], vec![
"END", "MZ1", "T3", "PB", "PA1",
]]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["T4", "PA2"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(&events, edges, expected_state_ids).await;
}
#[tokio::test]
async fn test_event_map_none() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let mut store = TestStore(hashmap! {});
// build up the DAG
let (state_at_bob, state_at_charlie, expected) = store.set_up();
let ev_map = store.0.clone();
let state_sets = [state_at_bob, state_at_charlie];
let auth_chains = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect::<Vec<_>>();
let rules = RoomVersionRules::V1;
let resolved = match super::resolve(
&rules,
state_sets.into_iter().stream(),
auth_chains.into_iter().stream(),
&async |id| ev_map.get(&id).cloned().ok_or_else(not_found),
&async |id| ev_map.contains_key(&id),
false,
)
.await
{
| Ok(state) => state,
| Err(e) => panic!("{e}"),
};
assert_eq!(expected, resolved);
}
#[tokio::test]
async fn test_reverse_topological_power_sort() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let graph = hashmap! {
event_id("l") => hashset![event_id("o")],
event_id("m") => hashset![event_id("n"), event_id("o")],
event_id("n") => hashset![event_id("o")],
event_id("o") => hashset![], // "o" has zero outgoing edges but 4 incoming edges
event_id("p") => hashset![event_id("o")],
};
let res = super::topological_sort(&graph, &async |_id| {
Ok((int!(0).into(), MilliSecondsSinceUnixEpoch(uint!(0))))
})
.await
.unwrap();
assert_eq!(
vec!["o", "l", "n", "m", "p"],
res.iter()
.map(ToString::to_string)
.map(|s| s.replace('$', "").replace(":foo", ""))
.collect::<Vec<_>>()
);
}
#[tokio::test]
async fn ban_with_auth_chains() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let ban = BAN_STATE_SET();
let edges = vec![vec!["END", "MB", "PA", "START"], vec!["END", "IME", "MB"]]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["PA", "MB"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(&ban.values().cloned().collect::<Vec<_>>(), edges, expected_state_ids).await;
}
#[tokio::test]
async fn ban_with_auth_chains2() {
_ = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
);
let init = INITIAL_EVENTS();
let ban = BAN_STATE_SET();
let mut inner = init.clone();
inner.extend(ban);
let store = TestStore(inner.clone());
let state_set_a = [
&inner[&event_id("CREATE")],
&inner[&event_id("IJR")],
&inner[&event_id("IMA")],
&inner[&event_id("IMB")],
&inner[&event_id("IMC")],
&inner[&event_id("MB")],
&inner[&event_id("PA")],
]
.iter()
.map(|ev| {
(
ev.event_type()
.with_state_key(ev.state_key().unwrap()),
ev.event_id.clone(),
)
})
.collect::<StateMap<_>>();
let state_set_b = [
&inner[&event_id("CREATE")],
&inner[&event_id("IJR")],
&inner[&event_id("IMA")],
&inner[&event_id("IMB")],
&inner[&event_id("IMC")],
&inner[&event_id("IME")],
&inner[&event_id("PA")],
]
.iter()
.map(|ev| {
(
ev.event_type()
.with_state_key(ev.state_key().unwrap()),
ev.event_id.clone(),
)
})
.collect::<StateMap<_>>();
let ev_map = &store.0;
let state_sets = [state_set_a, state_set_b];
let auth_chains = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect::<Vec<_>>();
let resolved = match super::resolve(
&RoomVersionRules::V6,
state_sets.into_iter().stream(),
auth_chains.into_iter().stream(),
&async |id| ev_map.get(&id).cloned().ok_or_else(not_found),
&async |id| ev_map.contains_key(&id),
false,
)
.await
{
| Ok(state) => state,
| Err(e) => panic!("{e}"),
};
debug!(
resolved = ?resolved
.iter()
.map(|((ty, key), id)| format!("(({ty}{key:?}), {id})"))
.collect::<Vec<_>>(),
"resolved state",
);
let expected = [
"$CREATE:foo",
"$IJR:foo",
"$PA:foo",
"$IMA:foo",
"$IMB:foo",
"$IMC:foo",
"$MB:foo",
];
for id in expected.iter().map(|i| event_id(i)) {
// make sure our resolved events are equal to the expected list
assert!(resolved.values().any(|eid| eid == &id) || init.contains_key(&id), "{id}");
}
assert_eq!(expected.len(), resolved.len());
}
#[tokio::test]
async fn join_rule_with_auth_chain() {
let join_rule = JOIN_RULE();
let edges = vec![vec!["END", "JR", "START"], vec!["END", "IMZ", "START"]]
.into_iter()
.map(|list| list.into_iter().map(event_id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let expected_state_ids = vec!["JR"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>();
do_check(&join_rule.values().cloned().collect::<Vec<_>>(), edges, expected_state_ids).await;
}
#[allow(non_snake_case)]
fn BAN_STATE_SET() -> HashMap<OwnedEventId, PduEvent> {
vec![
to_pdu_event(
"PA",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
&["CREATE", "IMA", "IPOWER"], // auth_events
&["START"], // prev_events
),
to_pdu_event(
"PB",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["END"],
),
to_pdu_event(
"MB",
alice(),
TimelineEventType::RoomMember,
Some(ella().as_str()),
member_content_ban(),
&["CREATE", "IMA", "PB"],
&["PA"],
),
to_pdu_event(
"IME",
ella(),
TimelineEventType::RoomMember,
Some(ella().as_str()),
member_content_join(),
&["CREATE", "IJR", "PA"],
&["MB"],
),
]
.into_iter()
.map(|ev| (ev.event_id.clone(), ev))
.collect()
}
#[allow(non_snake_case)]
fn JOIN_RULE() -> HashMap<OwnedEventId, PduEvent> {
vec![
to_pdu_event(
"JR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&json!({ "join_rule": "invite" })).unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["START"],
),
to_pdu_event(
"IMZ",
zara(),
TimelineEventType::RoomPowerLevels,
Some(zara().as_str()),
member_content_join(),
&["CREATE", "JR", "IPOWER"],
&["START"],
),
]
.into_iter()
.map(|ev| (ev.event_id.clone(), ev))
.collect()
}
macro_rules! state_set {
($($kind:expr => $key:expr => $id:expr),* $(,)?) => {{
#[allow(unused_mut)]
let mut x = StateMap::new();
$(
x.insert(($kind, $key.into()), $id);
)*
x
}};
}
#[tokio::test]
async fn split_conflicted_state_set_conflicted_unique_state_keys() {
let (unconflicted, conflicted) = super::split_conflicted_state(
[
state_set![StateEventType::RoomMember => "@a:hs1" => 0],
state_set![StateEventType::RoomMember => "@b:hs1" => 1],
state_set![StateEventType::RoomMember => "@c:hs1" => 2],
]
.into_iter()
.stream(),
)
.await;
let (unconflicted, conflicted): (StateMap<_>, StateMap<_>) =
(unconflicted.into_iter().collect(), conflicted.into_iter().collect());
assert_eq!(unconflicted, StateMap::new());
assert_eq!(conflicted, state_set![
StateEventType::RoomMember => "@a:hs1" => vec![0],
StateEventType::RoomMember => "@b:hs1" => vec![1],
StateEventType::RoomMember => "@c:hs1" => vec![2],
],);
}
#[tokio::test]
async fn split_conflicted_state_set_conflicted_same_state_key() {
let (unconflicted, conflicted) = super::split_conflicted_state(
[
state_set![StateEventType::RoomMember => "@a:hs1" => 0],
state_set![StateEventType::RoomMember => "@a:hs1" => 1],
state_set![StateEventType::RoomMember => "@a:hs1" => 2],
]
.into_iter()
.stream(),
)
.await;
let (unconflicted, mut conflicted): (StateMap<_>, StateMap<_>) =
(unconflicted.into_iter().collect(), conflicted.into_iter().collect());
// HashMap iteration order is random, so sort this before asserting on it
for v in conflicted.values_mut() {
v.sort_unstable();
}
assert_eq!(unconflicted, StateMap::new());
assert_eq!(conflicted, state_set![
StateEventType::RoomMember => "@a:hs1" => vec![0, 1, 2],
],);
}
#[tokio::test]
async fn split_conflicted_state_set_unconflicted() {
let (unconflicted, conflicted) = super::split_conflicted_state(
[
state_set![StateEventType::RoomMember => "@a:hs1" => 0],
state_set![StateEventType::RoomMember => "@a:hs1" => 0],
state_set![StateEventType::RoomMember => "@a:hs1" => 0],
]
.into_iter()
.stream(),
)
.await;
let (unconflicted, conflicted): (StateMap<_>, StateMap<_>) =
(unconflicted.into_iter().collect(), conflicted.into_iter().collect());
assert_eq!(unconflicted, state_set![
StateEventType::RoomMember => "@a:hs1" => 0,
],);
assert_eq!(conflicted, StateMap::new());
}
#[tokio::test]
async fn split_conflicted_state_set_mixed() {
let (unconflicted, conflicted) = super::split_conflicted_state(
[
state_set![StateEventType::RoomMember => "@a:hs1" => 0],
state_set![
StateEventType::RoomMember => "@a:hs1" => 0,
StateEventType::RoomMember => "@b:hs1" => 1,
],
state_set![
StateEventType::RoomMember => "@a:hs1" => 0,
StateEventType::RoomMember => "@c:hs1" => 2,
],
]
.into_iter()
.stream(),
)
.await;
let (unconflicted, conflicted): (StateMap<_>, StateMap<_>) =
(unconflicted.into_iter().collect(), conflicted.into_iter().collect());
assert_eq!(unconflicted, state_set![
StateEventType::RoomMember => "@a:hs1" => 0,
],);
assert_eq!(conflicted, state_set![
StateEventType::RoomMember => "@b:hs1" => vec![1],
StateEventType::RoomMember => "@c:hs1" => vec![2],
],);
}
@@ -0,0 +1,163 @@
use std::{
cmp::{Ordering, Reverse},
collections::{BinaryHeap, HashMap, HashSet},
};
use futures::TryStreamExt;
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, events::room::power_levels::UserPowerLevel,
};
use crate::{Result, utils::stream::IterStream};
#[derive(PartialEq, Eq)]
struct TieBreaker<'a> {
power_level: UserPowerLevel,
origin_server_ts: MilliSecondsSinceUnixEpoch,
event_id: &'a EventId,
}
type PduInfo = (UserPowerLevel, MilliSecondsSinceUnixEpoch);
// NOTE: the power level comparison is "backwards" intentionally.
impl Ord for TieBreaker<'_> {
fn cmp(&self, other: &Self) -> Ordering {
other
.power_level
.cmp(&self.power_level)
.then(self.origin_server_ts.cmp(&other.origin_server_ts))
.then(self.event_id.cmp(other.event_id))
}
}
impl PartialOrd for TieBreaker<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
/// Sorts the given event graph using reverse topological power ordering.
///
/// Definition in the specification:
///
/// The reverse topological power ordering of a set of events is the
/// lexicographically smallest topological ordering based on the DAG formed by
/// auth events. The reverse topological power ordering is ordered from earliest
/// event to latest. For comparing two topological orderings to determine which
/// is the lexicographically smallest, the following comparison relation on
/// events is used: for events x and y, x < y if
///
/// 1. xs sender has greater power level than ys sender, when looking at their
/// respective auth_events; or
/// 2. the senders have the same power level, but xs origin_server_ts is less
/// than ys origin_server_ts; or
/// 3. the senders have the same power level and the events have the same
/// origin_server_ts, but xs event_id is less than ys event_id.
///
/// The reverse topological power ordering can be found by sorting the events
/// using Kahns algorithm for topological sorting, and at each step selecting,
/// among all the candidate vertices, the smallest vertex using the above
/// comparison relation.
///
/// ## Arguments
///
/// * `graph` - The graph to sort. A map of event ID to its auth events that are
/// in the full conflicted set.
///
/// * `query` - Function to obtain a (power level, origin_server_ts) of an event
/// for breaking ties.
///
/// ## Returns
///
/// Returns the ordered list of event IDs from earliest to latest.
#[tracing::instrument(
level = "debug",
skip_all,
fields(
graph = graph.len(),
)
)]
pub async fn topological_sort<Query, Fut>(
graph: &HashMap<OwnedEventId, HashSet<OwnedEventId>>,
query: &Query,
) -> Result<Vec<OwnedEventId>>
where
Query: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Result<PduInfo>> + Send,
{
// We consider that the DAG is directed from most recent events to oldest
// events, so an event is an incoming edge to its auth events. zero_outdegs:
// Vec of events that have an outdegree of zero (no outgoing edges), i.e. the
// oldest events. incoming_edges_map: Map of event to the list of events that
// reference it in its auth events.
let init = (Vec::new(), HashMap::<OwnedEventId, HashSet<OwnedEventId>>::new());
// Populate the list of events with an outdegree of zero, and the map of
// incoming edges.
let (zero_outdeg, incoming_edges) = graph
.iter()
.try_stream()
.try_fold(
init,
async |(mut zero_outdeg, mut incoming_edges), (event_id, outgoing_edges)| {
if outgoing_edges.is_empty() {
let (power_level, origin_server_ts) = query(event_id.clone()).await?;
// `Reverse` because `BinaryHeap` sorts largest -> smallest and we need
// smallest -> largest.
zero_outdeg.push(Reverse(TieBreaker {
power_level,
origin_server_ts,
event_id,
}));
}
incoming_edges.entry(event_id.into()).or_default();
for auth_event_id in outgoing_edges {
incoming_edges
.entry(auth_event_id.into())
.or_default()
.insert(event_id.into());
}
Ok((zero_outdeg, incoming_edges))
},
)
.await?;
// Map of event to the list of events in its auth events.
let mut outgoing_edges_map = graph.clone();
// Use a BinaryHeap to keep the events with an outdegree of zero sorted.
let mut heap = BinaryHeap::from(zero_outdeg);
let mut sorted = vec![];
// Apply Kahn's algorithm.
// https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm
while let Some(Reverse(item)) = heap.pop() {
for parent_id in incoming_edges
.get(item.event_id)
.expect("event_id in heap should also be in incoming_edges")
{
let outgoing_edges = outgoing_edges_map
.get_mut(parent_id)
.expect("outgoing_edges should contain all event_ids");
outgoing_edges.remove(item.event_id);
if !outgoing_edges.is_empty() {
continue;
}
// Push on the heap once all the outgoing edges have been removed.
let (power_level, origin_server_ts) = query(parent_id.clone()).await?;
heap.push(Reverse(TieBreaker {
power_level,
origin_server_ts,
event_id: parent_id.as_ref(),
}));
}
sorted.push(item.event_id.into());
}
Ok(sorted)
}
-150
View File
@@ -1,150 +0,0 @@
use ruma::RoomVersionId;
use super::{Error, Result};
#[derive(Debug)]
#[allow(clippy::exhaustive_enums)]
pub enum RoomDisposition {
/// A room version that has a stable specification.
Stable,
/// A room version that is not yet fully specified.
Unstable,
}
#[derive(Debug)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub enum EventFormatVersion {
/// $id:server event id format
V1,
/// MSC1659-style $hash event id format: introduced for room v3
V2,
/// MSC1884-style $hash format: introduced for room v4
V3,
}
#[derive(Debug)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub enum StateResolutionVersion {
/// State resolution for rooms at version 1.
V1,
/// State resolution for room at version 2 or later.
V2,
}
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[allow(clippy::struct_excessive_bools)]
pub struct RoomVersion {
/// The stability of this room.
pub disposition: RoomDisposition,
/// The format of the EventId.
pub event_format: EventFormatVersion,
/// Which state resolution algorithm is used.
pub state_res: StateResolutionVersion,
// FIXME: not sure what this one means?
pub enforce_key_validity: bool,
/// `m.room.aliases` had special auth rules and redaction rules
/// before room version 6.
///
/// before MSC2261/MSC2432,
pub special_case_aliases_auth: bool,
/// Strictly enforce canonical json, do not allow:
/// * Integers outside the range of [-2 ^ 53 + 1, 2 ^ 53 - 1]
/// * Floats
/// * NaN, Infinity, -Infinity
pub strict_canonicaljson: bool,
/// Verify notifications key while checking m.room.power_levels.
///
/// bool: MSC2209: Check 'notifications'
pub limit_notifications_power_levels: bool,
/// Extra rules when verifying redaction events.
pub extra_redaction_checks: bool,
/// Allow knocking in event authentication.
///
/// See [room v7 specification](https://spec.matrix.org/latest/rooms/v7/) for more information.
pub allow_knocking: bool,
/// Adds support for the restricted join rule.
///
/// See: [MSC3289](https://github.com/matrix-org/matrix-spec-proposals/pull/3289) for more information.
pub restricted_join_rules: bool,
/// Adds support for the knock_restricted join rule.
///
/// See: [MSC3787](https://github.com/matrix-org/matrix-spec-proposals/pull/3787) for more information.
pub knock_restricted_join_rule: bool,
/// Enforces integer power levels.
///
/// See: [MSC3667](https://github.com/matrix-org/matrix-spec-proposals/pull/3667) for more information.
pub integer_power_levels: bool,
/// Determine the room creator using the `m.room.create` event's `sender`,
/// instead of the event content's `creator` field.
///
/// See: [MSC2175](https://github.com/matrix-org/matrix-spec-proposals/pull/2175) for more information.
pub use_room_create_sender: bool,
}
impl RoomVersion {
pub const V1: Self = Self {
disposition: RoomDisposition::Stable,
event_format: EventFormatVersion::V1,
state_res: StateResolutionVersion::V1,
enforce_key_validity: false,
special_case_aliases_auth: true,
strict_canonicaljson: false,
limit_notifications_power_levels: false,
extra_redaction_checks: true,
allow_knocking: false,
restricted_join_rules: false,
knock_restricted_join_rule: false,
integer_power_levels: false,
use_room_create_sender: false,
};
pub const V10: Self = Self {
knock_restricted_join_rule: true,
integer_power_levels: true,
..Self::V9
};
pub const V11: Self = Self {
use_room_create_sender: true,
..Self::V10
};
pub const V2: Self = Self {
state_res: StateResolutionVersion::V2,
..Self::V1
};
pub const V3: Self = Self {
event_format: EventFormatVersion::V2,
extra_redaction_checks: false,
..Self::V2
};
pub const V4: Self = Self {
event_format: EventFormatVersion::V3,
..Self::V3
};
pub const V5: Self = Self { enforce_key_validity: true, ..Self::V4 };
pub const V6: Self = Self {
special_case_aliases_auth: false,
strict_canonicaljson: true,
limit_notifications_power_levels: true,
..Self::V5
};
pub const V7: Self = Self { allow_knocking: true, ..Self::V6 };
pub const V8: Self = Self { restricted_join_rules: true, ..Self::V7 };
pub const V9: Self = Self::V8;
pub fn new(version: &RoomVersionId) -> Result<Self> {
Ok(match version {
| RoomVersionId::V1 => Self::V1,
| RoomVersionId::V2 => Self::V2,
| RoomVersionId::V3 => Self::V3,
| RoomVersionId::V4 => Self::V4,
| RoomVersionId::V5 => Self::V5,
| RoomVersionId::V6 => Self::V6,
| RoomVersionId::V7 => Self::V7,
| RoomVersionId::V8 => Self::V8,
| RoomVersionId::V9 => Self::V9,
| RoomVersionId::V10 => Self::V10,
| RoomVersionId::V11 => Self::V11,
| ver => return Err(Error::Unsupported(format!("found version `{ver}`"))),
})
}
}
+401 -66
View File
@@ -1,37 +1,53 @@
use std::{
borrow::Borrow,
collections::{BTreeMap, HashMap, HashSet},
sync::atomic::{AtomicU64, Ordering::SeqCst},
collections::{HashMap, HashSet},
pin::Pin,
slice,
sync::{
Arc,
atomic::{AtomicU64, Ordering::SeqCst},
},
};
use futures::future::ready;
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, ServerSignatures,
UserId, event_id,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId, event_id,
events::{
TimelineEventType,
StateEventType, TimelineEventType,
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
},
},
int, room_id, uint, user_id,
int, room_id,
room_version_rules::{AuthorizationRules, RoomVersionRules},
uint, user_id,
};
use serde_json::{
json,
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
};
use super::auth_types_for_event;
use super::{AuthSet, StateMap, auth_types_for_event, events::RoomCreateEvent};
use crate::{
Result, info,
matrix::{Event, EventTypeExt, Pdu, StateMap, pdu::EventHash},
Error, Result, err, info,
matrix::{Event, EventHash, EventTypeExt, PduEvent, StateKey},
utils::stream::IterStream,
};
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
pub(crate) async fn do_check(
events: &[Pdu],
pub(super) fn not_found() -> Error { err!(Request(NotFound("Test event not found"))) }
pub(super) fn event_not_found(event_id: &EventId) -> Error {
err!(Request(NotFound("Test event not found: {event_id:?}")))
}
pub(super) fn state_not_found(ty: &StateEventType, sk: &str) -> Error {
err!(Request(NotFound("Test state not found: ({ty:?},{sk:?})")))
}
pub(super) async fn do_check(
events: &[PduEvent],
edges: Vec<Vec<OwnedEventId>>,
expected_state_ids: Vec<OwnedEventId>,
) {
@@ -79,35 +95,32 @@ pub(crate) async fn do_check(
}
}
// event_id -> Pdu
let mut event_map: HashMap<OwnedEventId, Pdu> = HashMap::new();
// event_id -> PduEvent
let mut event_map: HashMap<OwnedEventId, PduEvent> = HashMap::new();
// event_id -> StateMap<OwnedEventId>
let mut state_at_event: HashMap<OwnedEventId, StateMap<OwnedEventId>> = HashMap::new();
// Resolve the current state and add it to the state_at_event map then continue
// on in "time"
for node in super::lexicographical_topological_sort(&graph, &|_id| async {
Ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0))))
for node in super::topological_sort(&graph, &async |_id| {
Ok((int!(0).into(), MilliSecondsSinceUnixEpoch(uint!(0))))
})
.await
.unwrap()
{
let fake_event = fake_event_map.get(&node).unwrap();
let fake_event = &fake_event_map[&node];
let event_id = fake_event.event_id().to_owned();
let prev_events = graph.get(&node).unwrap();
let prev_events = &graph[&node];
let state_before: StateMap<OwnedEventId> = if prev_events.is_empty() {
HashMap::new()
StateMap::new()
} else if prev_events.len() == 1 {
state_at_event
.get(prev_events.iter().next().unwrap())
.unwrap()
.clone()
state_at_event[prev_events.iter().next().unwrap()].clone()
} else {
let state_sets = prev_events
.iter()
.filter_map(|k| state_at_event.get(k))
.filter_map(|k| state_at_event.get(k).cloned())
.collect::<Vec<_>>();
info!(
@@ -121,21 +134,27 @@ pub(crate) async fn do_check(
.collect::<Vec<_>>()
);
let auth_chain_sets: Vec<_> = state_sets
let auth_chain_sets = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect();
.collect::<Vec<_>>();
let event_map = &event_map;
let fetch = |id: OwnedEventId| ready(event_map.get(&id).cloned());
let exists = |id: OwnedEventId| ready(event_map.get(&id).is_some());
let resolved =
super::resolve(&RoomVersionId::V6, state_sets, &auth_chain_sets, &fetch, &exists)
.await;
let state_sets = state_sets.into_iter().stream();
let rules = RoomVersionRules::V6;
let resolved = super::resolve(
&rules,
state_sets,
auth_chain_sets.into_iter().stream(),
&async |id| event_map.get(&id).cloned().ok_or_else(not_found),
&async |id| event_map.contains_key(&id),
false,
)
.await;
match resolved {
| Ok(state) => state,
@@ -147,13 +166,15 @@ pub(crate) async fn do_check(
let ty = fake_event.event_type();
let key = fake_event.state_key().unwrap();
state_after.insert(ty.with_state_key(key), event_id.to_owned());
state_after.insert(ty.with_state_key(key), event_id.clone());
let auth_types = auth_types_for_event(
fake_event.event_type(),
fake_event.sender(),
fake_event.state_key(),
fake_event.content(),
&AuthorizationRules::V6,
false,
)
.unwrap();
@@ -183,7 +204,7 @@ pub(crate) async fn do_check(
store.0.insert(ev_id.to_owned(), event.clone());
state_at_event.insert(node, state_after);
event_map.insert(event_id.to_owned(), store.0.get(ev_id).unwrap().clone());
event_map.insert(event_id.clone(), store.0[ev_id].clone());
}
let mut expected_state = StateMap::new();
@@ -228,24 +249,23 @@ pub(crate) async fn do_check(
}
#[allow(clippy::exhaustive_structs)]
pub(crate) struct TestStore<E: Event>(pub(crate) HashMap<OwnedEventId, E>);
pub(super) struct TestStore(pub(super) HashMap<OwnedEventId, PduEvent>);
impl<E: Event + Clone> TestStore<E> {
pub(crate) fn get_event(&self, _: &RoomId, event_id: &EventId) -> Result<E> {
impl TestStore {
pub(super) fn get_event(&self, _: &RoomId, event_id: &EventId) -> Result<PduEvent> {
self.0
.get(event_id)
.cloned()
.ok_or_else(|| super::Error::NotFound(format!("{event_id} not found")))
.map_err(Into::into)
.ok_or_else(|| event_not_found(event_id))
}
/// Returns a Vec of the related auth events to the given `event`.
pub(crate) fn auth_event_ids(
pub(super) fn auth_event_ids(
&self,
room_id: &RoomId,
event_ids: Vec<OwnedEventId>,
) -> Result<HashSet<OwnedEventId>> {
let mut result = HashSet::new();
) -> Result<AuthSet<OwnedEventId>> {
let mut result = AuthSet::new();
let mut stack = event_ids;
// DFS for auth event chain
@@ -267,8 +287,8 @@ pub(crate) fn auth_event_ids(
// A StateStore implementation for testing
#[allow(clippy::type_complexity)]
impl TestStore<Pdu> {
pub(crate) fn set_up(
impl TestStore {
pub(super) fn set_up(
&mut self,
) -> (StateMap<OwnedEventId>, StateMap<OwnedEventId>, StateMap<OwnedEventId>) {
let create_event = to_pdu_event::<&EventId>(
@@ -289,8 +309,8 @@ pub(crate) fn set_up(
TimelineEventType::RoomMember,
Some(alice().as_str()),
member_content_join(),
&[cre.clone()],
&[cre.clone()],
slice::from_ref(&cre),
slice::from_ref(&cre),
);
self.0
.insert(alice_mem.event_id().to_owned(), alice_mem.clone());
@@ -370,7 +390,7 @@ pub(crate) fn set_up(
}
}
pub(crate) fn event_id(id: &str) -> OwnedEventId {
pub(super) fn event_id(id: &str) -> OwnedEventId {
if id.contains('$') {
return id.try_into().unwrap();
}
@@ -378,33 +398,35 @@ pub(crate) fn event_id(id: &str) -> OwnedEventId {
format!("${id}:foo").try_into().unwrap()
}
pub(crate) fn alice() -> &'static UserId { user_id!("@alice:foo") }
pub(super) fn alice() -> &'static UserId { user_id!("@alice:foo") }
pub(crate) fn bob() -> &'static UserId { user_id!("@bob:foo") }
pub(super) fn bob() -> &'static UserId { user_id!("@bob:foo") }
pub(crate) fn charlie() -> &'static UserId { user_id!("@charlie:foo") }
pub(super) fn charlie() -> &'static UserId { user_id!("@charlie:foo") }
pub(crate) fn ella() -> &'static UserId { user_id!("@ella:foo") }
pub(super) fn ella() -> &'static UserId { user_id!("@ella:foo") }
pub(crate) fn zara() -> &'static UserId { user_id!("@zara:foo") }
pub(super) fn zara() -> &'static UserId { user_id!("@zara:foo") }
pub(crate) fn room_id() -> &'static RoomId { room_id!("!test:foo") }
pub(super) fn room_id() -> &'static RoomId { room_id!("!test:foo") }
pub(crate) fn member_content_ban() -> Box<RawJsonValue> {
pub(crate) fn hydra_room_id() -> &'static RoomId { room_id!("!CREATE") }
pub(super) fn member_content_ban() -> Box<RawJsonValue> {
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Ban)).unwrap()
}
pub(crate) fn member_content_join() -> Box<RawJsonValue> {
pub(super) fn member_content_join() -> Box<RawJsonValue> {
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap()
}
pub(crate) fn to_init_pdu_event(
pub(super) fn to_init_pdu_event(
id: &str,
sender: &UserId,
ev_type: TimelineEventType,
state_key: Option<&str>,
content: Box<RawJsonValue>,
) -> Pdu {
) -> PduEvent {
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
let id = if id.contains('$') {
id.to_owned()
@@ -412,15 +434,16 @@ pub(crate) fn to_init_pdu_event(
format!("${id}:foo")
};
Pdu {
let state_key = state_key.map(ToOwned::to_owned);
PduEvent {
event_id: id.try_into().unwrap(),
room_id: room_id().to_owned(),
sender: sender.to_owned(),
origin: None,
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
kind: ev_type,
content,
origin: None,
redacts: None,
unsigned: None,
auth_events: vec![],
@@ -428,10 +451,11 @@ pub(crate) fn to_init_pdu_event(
depth: uint!(0),
hashes: EventHash::default(),
signatures: None,
rejected: false,
}
}
pub(crate) fn to_pdu_event<S>(
pub(super) fn to_pdu_event<S>(
id: &str,
sender: &UserId,
ev_type: TimelineEventType,
@@ -439,7 +463,7 @@ pub(crate) fn to_pdu_event<S>(
content: Box<RawJsonValue>,
auth_events: &[S],
prev_events: &[S],
) -> Pdu
) -> PduEvent
where
S: AsRef<str>,
{
@@ -460,15 +484,16 @@ pub(crate) fn to_pdu_event<S>(
.map(event_id)
.collect::<Vec<_>>();
Pdu {
let state_key = state_key.map(ToOwned::to_owned);
PduEvent {
event_id: id.try_into().unwrap(),
room_id: room_id().to_owned(),
sender: sender.to_owned(),
origin: None,
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
kind: ev_type,
content,
origin: None,
redacts: None,
unsigned: None,
auth_events,
@@ -476,12 +501,153 @@ pub(crate) fn to_pdu_event<S>(
depth: uint!(0),
hashes: EventHash::default(),
signatures: None,
rejected: false,
}
}
/// Same as `to_pdu_event()`, but uses the default m.room.create event ID to
/// generate the room ID.
pub(super) fn to_hydra_pdu_event<S>(
id: &str,
sender: &UserId,
ev_type: TimelineEventType,
state_key: Option<&str>,
content: Box<RawJsonValue>,
auth_events: &[S],
prev_events: &[S],
) -> PduEvent
where
S: AsRef<str>,
{
fn event_id(id: &str) -> OwnedEventId {
if id.contains('$') {
id.try_into().unwrap()
} else {
format!("${id}").try_into().unwrap()
}
}
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
let auth_events = auth_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
let prev_events = prev_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
let state_key = state_key.map(ToOwned::to_owned);
PduEvent {
event_id: event_id(id),
room_id: hydra_room_id().to_owned(),
sender: sender.to_owned(),
origin: None,
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
kind: ev_type,
content,
redacts: None,
unsigned: None,
auth_events,
prev_events,
depth: uint!(0),
hashes: EventHash::default(),
signatures: None,
rejected: false,
}
}
pub(super) fn room_redaction_pdu_event<S>(
id: &str,
sender: &UserId,
redacts: OwnedEventId,
content: Box<RawJsonValue>,
auth_events: &[S],
prev_events: &[S],
) -> PduEvent
where
S: AsRef<str>,
{
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
let id = if id.contains('$') {
id.to_owned()
} else {
format!("${id}:foo")
};
let auth_events = auth_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
let prev_events = prev_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
PduEvent {
event_id: id.try_into().unwrap(),
room_id: room_id().to_owned(),
sender: sender.to_owned(),
origin: None,
origin_server_ts: ts.try_into().unwrap(),
state_key: None,
kind: TimelineEventType::RoomRedaction,
content,
redacts: Some(redacts),
unsigned: None,
auth_events,
prev_events,
depth: uint!(0),
hashes: EventHash::default(),
signatures: None,
rejected: false,
}
}
pub(super) fn room_create_hydra_pdu_event(
id: &str,
sender: &UserId,
content: Box<RawJsonValue>,
) -> PduEvent {
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
let eid = if id.contains('$') {
id.to_owned()
} else {
format!("${id}")
};
let rid = if id.contains('!') {
id.to_owned()
} else {
format!("!{id}")
};
PduEvent {
event_id: eid.try_into().unwrap(),
room_id: rid.try_into().unwrap(),
sender: sender.to_owned(),
origin: None,
origin_server_ts: ts.try_into().unwrap(),
state_key: Some(StateKey::new()),
kind: TimelineEventType::RoomCreate,
content,
redacts: None,
unsigned: None,
auth_events: vec![],
prev_events: vec![],
depth: uint!(0),
hashes: EventHash::default(),
signatures: None,
rejected: false,
}
}
// all graphs start with these input events
#[allow(non_snake_case)]
pub(crate) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
pub(super) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, PduEvent> {
vec![
to_pdu_event::<&EventId>(
"CREATE",
@@ -561,9 +727,88 @@ pub(crate) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
.collect()
}
/// Batch of initial events to use for incoming events from room version
/// `org.matrix.hydra.11` onwards.
#[allow(non_snake_case)]
pub(super) fn INITIAL_HYDRA_EVENTS() -> HashMap<OwnedEventId, PduEvent> {
vec![
room_create_hydra_pdu_event(
"CREATE",
alice(),
to_raw_json_value(&json!({ "room_version": "org.matrix.hydra.11" })).unwrap(),
),
to_hydra_pdu_event(
"IMA",
alice(),
TimelineEventType::RoomMember,
Some(alice().as_str()),
member_content_join(),
&["CREATE"],
&["CREATE"],
),
to_hydra_pdu_event(
"IPOWER",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
&["CREATE", "IMA"],
&["IMA"],
),
to_hydra_pdu_event(
"IJR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["IPOWER"],
),
to_hydra_pdu_event(
"IMB",
bob(),
TimelineEventType::RoomMember,
Some(bob().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IJR"],
),
to_hydra_pdu_event(
"IMC",
charlie(),
TimelineEventType::RoomMember,
Some(charlie().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IMB"],
),
to_hydra_pdu_event::<&EventId>(
"START",
charlie(),
TimelineEventType::RoomMessage,
Some("dummy"),
to_raw_json_value(&json!({})).unwrap(),
&[],
&[],
),
to_hydra_pdu_event::<&EventId>(
"END",
charlie(),
TimelineEventType::RoomMessage,
Some("dummy"),
to_raw_json_value(&json!({})).unwrap(),
&[],
&[],
),
]
.into_iter()
.map(|ev| (ev.event_id().to_owned(), ev))
.collect()
}
// all graphs start with these input events
#[allow(non_snake_case)]
pub(crate) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap<OwnedEventId, Pdu> {
pub(super) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap<OwnedEventId, PduEvent> {
vec![to_pdu_event::<&EventId>(
"CREATE",
alice(),
@@ -579,9 +824,99 @@ pub(crate) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap<OwnedEventId, Pdu> {
}
#[allow(non_snake_case)]
pub(crate) fn INITIAL_EDGES() -> Vec<OwnedEventId> {
pub(super) fn INITIAL_EDGES() -> Vec<OwnedEventId> {
vec!["START", "IMC", "IMB", "IJR", "IPOWER", "IMA", "CREATE"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>()
}
pub(super) fn init_subscriber() -> tracing::dispatcher::DefaultGuard {
tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_test_writer()
.finish(),
)
}
/// Wrapper around a state map.
pub(super) struct TestStateMap(HashMap<StateEventType, HashMap<String, PduEvent>>);
impl TestStateMap {
/// Construct a `TestStateMap` from the given event map.
pub(super) fn new(events: &HashMap<OwnedEventId, PduEvent>) -> Arc<Self> {
let mut state_map: HashMap<StateEventType, HashMap<String, PduEvent>> = HashMap::new();
for event in events.values() {
let event_type = StateEventType::from(event.event_type().to_string());
state_map
.entry(event_type)
.or_default()
.insert(event.state_key().unwrap().to_owned(), event.clone());
}
Arc::new(Self(state_map))
}
/// Get the event with the given event type and state key.
pub(super) fn get(
self: &Arc<Self>,
event_type: &StateEventType,
state_key: &str,
) -> Result<PduEvent> {
self.0
.get(event_type)
.ok_or_else(|| state_not_found(event_type, state_key))?
.get(state_key)
.cloned()
.ok_or_else(|| state_not_found(event_type, state_key))
}
/// A function to get a state event from this map.
pub(super) fn fetch_state_fn(
self: &Arc<Self>,
) -> impl Fn(StateEventType, StateKey) -> Pin<Box<dyn Future<Output = Result<PduEvent>> + Send>>
{
move |event_type: StateEventType, state_key: StateKey| {
let s = self.clone();
Box::pin(async move { s.get(&event_type, state_key.as_str()) })
}
}
/// The `m.room.create` event contained in this map.
///
/// Panics if there is no `m.room.create` event in this map.
pub(super) fn room_create_event(self: &Arc<Self>) -> RoomCreateEvent<PduEvent> {
RoomCreateEvent::new(self.get(&StateEventType::RoomCreate, "").unwrap())
}
}
/// Create an `m.room.third_party_invite` event with the given sender.
pub(super) fn room_third_party_invite(sender: &UserId) -> PduEvent {
let content = json!({
"display_name": "o...@g...",
"key_validity_url": "https://identity.local/_matrix/identity/v2/pubkey/isvalid",
"public_key": "Gb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE",
"public_keys": [
{
"key_validity_url": "https://identity.local/_matrix/identity/v2/pubkey/isvalid",
"public_key": "Gb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE"
},
{
"key_validity_url": "https://identity.local/_matrix/identity/v2/pubkey/ephemeral/isvalid",
"public_key": "Kxdvv7lo0O6JVI7yimFgmYPfpLGnctcpYjuypP5zx/c"
}
]
});
to_pdu_event(
"THIRDPARTY",
sender,
TimelineEventType::RoomThirdPartyInvite,
Some("somerandomtoken"),
to_raw_json_value(&content).unwrap(),
&["CREATE", "IJR", "IPOWER"],
&["IPOWER"],
)
}
@@ -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"
]
}
]
@@ -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"
]
@@ -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"
]
}
]
@@ -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"
]
@@ -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"
]
}
]
@@ -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
View File
@@ -0,0 +1,3 @@
//! Integration tests entrypoint.
mod resolve;
+700
View File
@@ -0,0 +1,700 @@
//! State resolution integration tests.
#![cfg(test)]
use std::{
cmp::Ordering,
collections::{BTreeSet, HashMap},
error::Error,
fs,
path::Path,
};
use ruma::{
OwnedEventId, RoomVersionId,
events::{StateEventType, TimelineEventType},
room_version_rules::{AuthorizationRules, RoomVersionRules, StateResolutionV2Rules},
};
use serde::{Deserialize, Serialize};
use serde_json::{
Error as JsonError, Value as JsonValue, from_str as from_json_str,
to_string_pretty as to_json_string_pretty, to_value as to_json_value,
};
use similar::{Algorithm, udiff::unified_diff};
use tracing_subscriber::EnvFilter;
use tuwunel_core::{
Result, err,
matrix::{
Event, Pdu, StateKey, StateMap,
state_res::{AuthSet, resolve},
},
utils::stream::IterStream,
};
/// Create a new snapshot test.
///
/// # Arguments
///
/// * The test function's name.
/// * A list of JSON files relative to `tests/it/fixtures` to load PDUs to
/// resolve from.
macro_rules! snapshot_test {
($name:ident, $paths:expr $(,)?) => {
#[tokio::test]
async fn $name() {
let crate::resolve::Snapshots {
resolved_state,
} = crate::resolve::test_resolve(&$paths).await;
insta::with_settings!({
description => "Resolved state",
omit_expression => true,
snapshot_suffix => "resolved_state",
}, {
insta::assert_json_snapshot!(&resolved_state);
});
}
};
}
/// Create a new snapshot test, attempting to resolve multiple contrived states.
///
/// # Arguments
///
/// * The test function's name.
/// * A list of JSON files relative to `tests/it/fixtures` to load PDUs to
/// resolve from.
/// * A list of JSON files relative to `tests/it/fixtures` to load event IDs
/// forming contrived states to resolve.
macro_rules! snapshot_test_contrived_states {
($name:ident, $pdus_path:expr, $state_set_paths:expr $(,)?) => {
#[tokio::test]
async fn $name() {
let crate::resolve::Snapshots {
resolved_state,
} = crate::resolve::test_contrived_states(&$pdus_path, &$state_set_paths).await;
insta::with_settings!({
description => "Resolved state",
omit_expression => true,
snapshot_suffix => "resolved_state",
}, {
insta::assert_json_snapshot!(&resolved_state);
});
}
};
}
// This module must be defined lexically after the `snapshot_test` macro.
mod snapshot_tests;
/// Extract `.content.room_version` from a PDU.
#[derive(Deserialize)]
struct ExtractRoomVersion {
room_version: RoomVersionId,
}
/// Type describing a resolved state event.
#[derive(Serialize)]
struct ResolvedStateEvent {
kind: StateEventType,
state_key: StateKey,
event_id: OwnedEventId,
// Ignored in `PartialEq` and `Ord` because we don't want to consider it while sorting.
content: JsonValue,
}
impl PartialEq for ResolvedStateEvent {
fn eq(&self, other: &Self) -> bool {
self.kind == other.kind
&& self.state_key == other.state_key
&& self.event_id == other.event_id
}
}
impl Eq for ResolvedStateEvent {}
impl Ord for ResolvedStateEvent {
fn cmp(&self, other: &Self) -> Ordering {
Ordering::Equal
.then(self.kind.cmp(&other.kind))
.then(self.state_key.cmp(&other.state_key))
.then(self.event_id.cmp(&other.event_id))
}
}
impl PartialOrd for ResolvedStateEvent {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
/// Information to be captured in snapshot assertions
struct Snapshots {
/// The resolved state of the room.
resolved_state: BTreeSet<ResolvedStateEvent>,
}
fn snapshot_test_prelude(
paths: &[&str],
) -> (Vec<Vec<Pdu>>, RoomVersionRules, AuthorizationRules, StateResolutionV2Rules) {
// Run `cargo test -- --show-output` to view traces, set `RUST_LOG` to control
// filtering.
let subscriber = tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_test_writer()
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE)
.finish();
tracing::subscriber::set_global_default(subscriber).ok();
let fixtures_path = Path::new("tests/it/fixtures");
let pdu_batches = paths
.iter()
.map(|x| {
from_json_str(
&fs::read_to_string(fixtures_path.join(x))
.expect("should be able to read JSON file of PDUs"),
)
.expect("should be able to deserialize JSON file of PDUs")
})
.collect::<Vec<Vec<Pdu>>>();
let room_version_id = {
let first_pdu = pdu_batches
.first()
.expect("there should be at least one file of PDUs")
.first()
.expect("there should be at least one PDU in the first file");
assert_eq!(
first_pdu.kind,
TimelineEventType::RoomCreate,
"the first PDU in the first file should be an m.room.create event",
);
from_json_str::<ExtractRoomVersion>(first_pdu.content.get())
.expect("the m.room.create PDU's content should be valid")
.room_version
};
let rules = room_version_id
.rules()
.expect("room version should be supported");
let auth_rules = rules.clone().authorization;
let state_res_rules = rules
.state_res
.v2_rules()
.copied()
.expect("resolve only supports state resolution version 2");
(pdu_batches, rules, auth_rules, state_res_rules)
}
/// Reshape the data a bit to make the diff and snapshots easier to compare.
fn reshape(
pdus_by_id: &HashMap<OwnedEventId, Pdu>,
x: StateMap<OwnedEventId>,
) -> Result<BTreeSet<ResolvedStateEvent>, JsonError> {
x.into_iter()
.map(|((kind, state_key), event_id)| {
Ok(ResolvedStateEvent {
kind,
state_key,
content: to_json_value(pdus_by_id[&event_id].content())?,
event_id,
})
})
.collect()
}
/// Test a list of JSON files containing a list of PDUs and return the results.
///
/// State resolution is run both atomically for all PDUs and in batches of PDUs
/// by file.
async fn test_resolve(paths: &[&str]) -> Snapshots {
let (pdu_batches, rules, auth_rules, state_res_rules) = snapshot_test_prelude(paths);
// Resolve PDUs iteratively, using the ordering of `prev_events`.
let iteratively_resolved_state = resolve_iteratively(
&rules,
&auth_rules,
&state_res_rules,
pdu_batches.iter().flat_map(|x| x.iter()),
)
.await
.expect("iterative state resolution should succeed");
// Resolve PDUs in batches by file
let mut pdus_by_id = HashMap::new();
let mut batched_resolved_state = None;
for pdus in &pdu_batches {
batched_resolved_state = Some(
resolve_batch(
&rules,
&auth_rules,
&state_res_rules,
pdus,
&mut pdus_by_id,
&mut batched_resolved_state,
)
.await
.expect("batched state resolution step should succeed"),
);
}
let batched_resolved_state =
batched_resolved_state.expect("batched state resolution should have run at least once");
// Resolve all PDUs in a single step
let atomic_resolved_state = resolve_batch(
&rules,
&auth_rules,
&state_res_rules,
pdu_batches.iter().flat_map(|x| x.iter()),
&mut HashMap::new(),
&mut None,
)
.await
.expect("atomic state resolution should succeed");
let iteratively_resolved_state = reshape(&pdus_by_id, iteratively_resolved_state)
.expect("should be able to reshape iteratively resolved state");
let batched_resolved_state = reshape(&pdus_by_id, batched_resolved_state)
.expect("should be able to reshape batched resolved state");
let atomic_resolved_state = reshape(&pdus_by_id, atomic_resolved_state)
.expect("should be able to reshape atomic resolved state");
let assert_states_match = |first_resolved_state: &BTreeSet<ResolvedStateEvent>,
second_resolved_state: &BTreeSet<ResolvedStateEvent>,
first_name: &str,
second_name: &str| {
if first_resolved_state != second_resolved_state {
let diff = unified_diff(
Algorithm::default(),
&to_json_string_pretty(first_resolved_state)
.expect("should be able to serialize first resolved state"),
&to_json_string_pretty(second_resolved_state)
.expect("should be able to serialize second resolved state"),
3,
Some((first_name, second_name)),
);
panic!(
"{first_name} and {second_name} results should match; but they differ:\n{diff}"
);
}
};
assert_states_match(
&iteratively_resolved_state,
&batched_resolved_state,
"iterative",
"batched",
);
assert_states_match(&batched_resolved_state, &atomic_resolved_state, "batched", "atomic");
Snapshots {
resolved_state: iteratively_resolved_state,
}
}
/// Test a list of JSON files containing a list of PDUs and a list of JSON files
/// containing the event IDs that form a contrived state and return the results.
#[tracing::instrument(parent = None, name = "test", skip_all)]
async fn test_contrived_states(pdus_paths: &[&str], state_sets_paths: &[&str]) -> Snapshots {
let (pdu_batches, rules, _auth_rules, _state_res_rules) = snapshot_test_prelude(pdus_paths);
let pdus = pdu_batches
.into_iter()
.flat_map(IntoIterator::into_iter)
.collect::<Vec<_>>();
let pdus_by_id: HashMap<OwnedEventId, Pdu> = pdus
.clone()
.into_iter()
.map(|pdu| (pdu.event_id().to_owned(), pdu.clone()))
.collect();
let fixtures_path = Path::new("tests/it/fixtures");
let state_sets = state_sets_paths
.iter()
.map(|x| {
from_json_str::<Vec<OwnedEventId>>(
&fs::read_to_string(fixtures_path.join(x))
.expect("should be able to read JSON file of PDUs"),
)
.expect("should be able to deserialize JSON file of PDUs")
.into_iter()
.map(|event_id| {
pdus_by_id
.get(&event_id)
.map(|pdu| {
(
(
pdu.event_type().to_string().into(),
pdu.state_key
.clone()
.expect("All PDUs must be state events"),
),
event_id,
)
})
.expect("Event IDs in JSON file must be in PDUs JSON")
})
.collect()
})
.collect::<Vec<StateMap<OwnedEventId>>>();
let mut auth_chain_sets = Vec::new();
for state_map in &state_sets {
let mut auth_chain = AuthSet::new();
for event_id in state_map.values() {
let pdu = pdus_by_id
.get(event_id)
.expect("We already confirmed all state set event ids have pdus");
auth_chain.extend(
auth_events_dfs(&pdus_by_id, pdu).expect("Auth events DFS should not fail"),
);
}
auth_chain_sets.push(auth_chain);
}
let exists = async |x| pdus_by_id.contains_key(&x);
let fetch = async |x| {
pdus_by_id
.get(&x)
.cloned()
.ok_or_else(|| err!(Request(NotFound("event not found"))))
};
let resolved_state = resolve(
&rules,
state_sets.into_iter().stream(),
auth_chain_sets.into_iter().stream(),
&fetch,
&exists,
false,
)
.await
.expect("atomic state resolution should succeed");
Snapshots {
resolved_state: reshape(&pdus_by_id, resolved_state)
.expect("should be able to reshape atomic resolved state"),
}
}
/// Perform state resolution on a batch of PDUs.
///
/// This function can be used to resolve the state of a room in a single call if
/// all PDUs are provided at once, or across multiple calls if given PDUs in
/// batches in a loop. The latter form simulates the case commonly experienced
/// by homeservers during normal operation.
///
/// # Arguments
///
/// * `rules`: The rules of the room version.
/// * `pdus`: An iterator of [`Pdu`]s to resolve, either alone or against the
/// `prev_state`.
/// * `pdus_by_id`: A map of [`OwnedEventId`]s to the [`Pdu`] with that ID.
/// * Should be empty for the first call.
/// * Should not be mutated outside of this function.
/// * `prev_state`: The state returned by a previous call to this function, if
/// any.
/// * Should be `None` for the first call.
/// * Should not be mutated outside of this function.
async fn resolve_batch<'a, I, II>(
rules: &'a RoomVersionRules,
_auth_rules: &'a AuthorizationRules,
_state_res_rules: &'a StateResolutionV2Rules,
pdus: II,
pdus_by_id: &'a mut HashMap<OwnedEventId, Pdu>,
prev_state: &'a mut Option<StateMap<OwnedEventId>>,
) -> Result<StateMap<OwnedEventId>, Box<dyn Error>>
where
I: Iterator<Item = &'a Pdu> + Send + 'a,
II: IntoIterator<IntoIter = I> + Clone + Send + 'a,
Pdu: Send + Sync + 'a,
&'a Pdu: Send + 'a,
{
let mut state_sets = prev_state
.take()
.map(|x| vec![x])
.unwrap_or_default();
for pdu in pdus.clone() {
// Insert each state event into its own StateMap because we don't know any valid
// groupings.
let mut state_map = StateMap::new();
state_map.insert(
(
pdu.event_type().to_string().into(),
pdu.state_key()
.ok_or("all PDUs should be state events")?
.into(),
),
pdu.event_id().to_owned(),
);
state_sets.push(state_map);
}
pdus_by_id.extend(
pdus.clone()
.into_iter()
.map(|pdu| (pdu.event_id().to_owned(), pdu.to_owned())),
);
let mut auth_chain_sets = Vec::new();
for pdu in pdus {
auth_chain_sets.push(auth_events_dfs(&*pdus_by_id, pdu)?);
}
let fetch = async |x| {
pdus_by_id
.get(&x)
.cloned()
.ok_or_else(|| err!(Request(NotFound("event not found"))))
};
let exists = async |x| pdus_by_id.contains_key(&x);
resolve(
rules,
state_sets.into_iter().stream(),
auth_chain_sets.into_iter().stream(),
&fetch,
&exists,
false,
)
.await
.map_err(Into::into)
}
/// Perform state resolution on a batch of PDUs iteratively, one-by-one.
///
/// This function walks the `prev_events` of each PDU forward, resolving each
/// pdu against the state(s) of it's `prev_events`, to emulate what would happen
/// in a regular room a server is participating in.
///
/// # Arguments
///
/// * `auth_rules`: The authorization rules of the room version.
/// * `state_res_rules`: The state resolution rules of the room version.
/// * `pdus`: An iterator of [`Pdu`]s to resolve, with the following
/// assumptions:
/// * `prev_events` of each PDU points to another provided state event.
///
/// # Returns
///
/// The state resolved by resolving all the leaves (PDUs which don't have any
/// other PDUs pointing to it via `prev_events`).
async fn resolve_iteratively<'a, I, II>(
rules: &'a RoomVersionRules,
_auth_rules: &'a AuthorizationRules,
_state_res_rules: &'a StateResolutionV2Rules,
pdus: II,
) -> Result<StateMap<OwnedEventId>, Box<dyn Error>>
where
I: Iterator<Item = &'a Pdu>,
II: IntoIterator<IntoIter = I> + Clone,
{
let mut forward_prev_events_graph: HashMap<OwnedEventId, Vec<_>> = HashMap::new();
let mut stack = Vec::new();
for pdu in pdus.clone() {
let mut has_prev_events = false;
for prev_event in pdu.prev_events() {
forward_prev_events_graph
.entry(prev_event.into())
.or_default()
.push(pdu.event_id().into());
has_prev_events = true;
}
if pdu.event_type() == &TimelineEventType::RoomCreate && !has_prev_events {
stack.push(pdu.event_id().to_owned());
}
}
let pdus_by_id: HashMap<OwnedEventId, Pdu> = pdus
.clone()
.into_iter()
.map(|pdu| (pdu.event_id().to_owned(), pdu.to_owned()))
.collect();
let exists = async |x| pdus_by_id.contains_key(&x);
let fetch = async |x| {
pdus_by_id
.get(&x)
.cloned()
.ok_or_else(|| err!(Request(NotFound("event not found"))))
};
let mut state_at_events: HashMap<OwnedEventId, StateMap<OwnedEventId>> = HashMap::new();
let mut leaves = Vec::new();
'outer: while let Some(event_id) = stack.pop() {
let mut states_before_event = Vec::new();
let mut auth_chain_sets = Vec::new();
let current_pdu = pdus_by_id
.get(&event_id)
.expect("every pdu should be available");
for prev_event in current_pdu.prev_events() {
let Some(state_at_event) = state_at_events.get(prev_event) else {
// State for a prev event is not known, we will come back to this event on a
// later iteration.
continue 'outer;
};
for pdu in state_at_event.values().map(|event_id| {
pdus_by_id
.get(event_id)
.expect("every pdu should be available")
}) {
auth_chain_sets.push(auth_events_dfs(&pdus_by_id, pdu)?);
}
states_before_event.push(state_at_event.clone());
}
if states_before_event.is_empty() {
// initial event, nothing to resolve
state_at_events.insert(
event_id.clone(),
StateMap::from_iter([(
(
current_pdu.event_type().to_string().into(),
current_pdu
.state_key()
.expect("all pdus are state events")
.into(),
),
event_id.clone(),
)]),
);
} else {
let state_before_event = resolve(
rules,
states_before_event.clone().into_iter().stream(),
auth_chain_sets.clone().into_iter().stream(),
&fetch,
&exists,
false,
)
.await?;
let mut proposed_state_at_event = state_before_event.clone();
proposed_state_at_event.insert(
(
current_pdu.event_type().to_string().into(),
current_pdu
.state_key()
.expect("all pdus are state events")
.into(),
),
event_id.clone(),
);
auth_chain_sets.push(auth_events_dfs(&pdus_by_id, current_pdu)?);
let state_at_event = resolve(
rules,
[state_before_event, proposed_state_at_event]
.into_iter()
.stream(),
auth_chain_sets.into_iter().stream(),
&fetch,
&exists,
false,
)
.await?;
state_at_events.insert(event_id.clone(), state_at_event);
}
if let Some(prev_events) = forward_prev_events_graph.get(&event_id) {
stack.extend(prev_events.iter().cloned());
} else {
// pdu is a leaf: no `prev_events` point to it.
leaves.push(event_id);
}
}
assert!(
state_at_events.len() == pdus_by_id.len(),
"Not all events have a state calculated! This is likely due to an event having a \
`prev_events` which points to a non-existent PDU."
);
let mut leaf_states = Vec::new();
let mut auth_chain_sets = Vec::new();
for leaf in leaves {
let state_at_event = state_at_events
.get(&leaf)
.expect("states at all events are known");
for pdu in state_at_event.values().map(|event_id| {
pdus_by_id
.get(event_id)
.expect("every pdu should be available")
}) {
auth_chain_sets.push(auth_events_dfs(&pdus_by_id, pdu)?);
}
leaf_states.push(state_at_event.clone());
}
resolve(
rules,
leaf_states.into_iter().stream(),
auth_chain_sets.into_iter().stream(),
&fetch,
&exists,
false,
)
.await
.map_err(Into::into)
}
/// Depth-first search for the `auth_events` of the given PDU.
///
/// # Errors
///
/// Fails if `pdus` does not contain a PDU that appears in the recursive
/// `auth_events` of `pdu`.
fn auth_events_dfs(
pdus_by_id: &HashMap<OwnedEventId, Pdu>,
pdu: &Pdu,
) -> Result<AuthSet<OwnedEventId>, Box<dyn Error>> {
let mut out = AuthSet::new();
let mut stack = pdu
.auth_events()
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
while let Some(event_id) = stack.pop() {
if out.contains(&event_id) {
continue;
}
out.insert(event_id.clone());
stack.extend(
pdus_by_id
.get(&event_id)
.ok_or_else(|| format!("missing required PDU: {event_id}"))?
.auth_events()
.map(ToOwned::to_owned),
);
}
Ok(out)
}
@@ -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