From 81027ebd29f1359afed353d51c81c84cabcd6b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 18 Jul 2025 16:15:41 +0200 Subject: [PATCH 1/9] Upgrade Ruma --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 576b78a73ad..ede4f63de92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4483,7 +4483,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.12.5" -source = "git+https://github.com/ruma/ruma?rev=7bae3d0c0b8edf008899ac2b04cf9722182eef68#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" dependencies = [ "assign", "js_int", @@ -4499,7 +4499,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.20.4" -source = "git+https://github.com/ruma/ruma?rev=7bae3d0c0b8edf008899ac2b04cf9722182eef68#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" dependencies = [ "as_variant", "assign", @@ -4522,7 +4522,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.15.4" -source = "git+https://github.com/ruma/ruma?rev=7bae3d0c0b8edf008899ac2b04cf9722182eef68#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" dependencies = [ "as_variant", "base64", @@ -4555,7 +4555,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.30.4" -source = "git+https://github.com/ruma/ruma?rev=7bae3d0c0b8edf008899ac2b04cf9722182eef68#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" dependencies = [ "as_variant", "indexmap", @@ -4581,7 +4581,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.11.2" -source = "git+https://github.com/ruma/ruma?rev=7bae3d0c0b8edf008899ac2b04cf9722182eef68#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" dependencies = [ "headers", "http", @@ -4599,7 +4599,7 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.4.1" -source = "git+https://github.com/ruma/ruma?rev=7bae3d0c0b8edf008899ac2b04cf9722182eef68#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" dependencies = [ "as_variant", "html5ever", @@ -4610,7 +4610,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.10.1" -source = "git+https://github.com/ruma/ruma?rev=7bae3d0c0b8edf008899ac2b04cf9722182eef68#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" dependencies = [ "js_int", "thiserror 2.0.11", @@ -4619,7 +4619,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.15.2" -source = "git+https://github.com/ruma/ruma?rev=7bae3d0c0b8edf008899ac2b04cf9722182eef68#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" dependencies = [ "cfg-if", "proc-macro-crate", diff --git a/Cargo.toml b/Cargo.toml index 91b2cb18c62..5435d5d427e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ proptest = { version = "1.6.0", default-features = false, features = ["std"] } rand = "0.8.5" reqwest = { version = "0.12.12", default-features = false } rmp-serde = "1.3.0" -ruma = { git = "https://github.com/ruma/ruma", rev = "7bae3d0c0b8edf008899ac2b04cf9722182eef68", features = [ +ruma = { git = "https://github.com/ruma/ruma", rev = "de19ebaf71af620eb17abaefd92e43153f9d041d", features = [ "client-api-c", "compat-upload-signatures", "compat-arbitrary-length-ids", @@ -77,7 +77,7 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "7bae3d0c0b8edf008899ac2b04 "unstable-msc4278", "unstable-msc4286", ] } -ruma-common = { git = "https://github.com/ruma/ruma", rev = "7bae3d0c0b8edf008899ac2b04cf9722182eef68" } +ruma-common = { git = "https://github.com/ruma/ruma", rev = "de19ebaf71af620eb17abaefd92e43153f9d041d" } sentry = "0.36.0" sentry-tracing = "0.36.0" serde = { version = "1.0.217", features = ["rc"] } From 51418d20f9b714632a87bc203c047df2c64c817d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 18 Jul 2025 16:22:32 +0200 Subject: [PATCH 2/9] refactor: Adapt to the deprecation of event_id in PreviousRoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- .../src/response_processors/state_events.rs | 22 +++++++++---------- crates/matrix-sdk-base/src/room/tombstone.rs | 5 ++--- testing/matrix-sdk-test/src/event_factory.rs | 4 ++-- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/crates/matrix-sdk-base/src/response_processors/state_events.rs b/crates/matrix-sdk-base/src/response_processors/state_events.rs index 1a80dc220c8..e7d5376470a 100644 --- a/crates/matrix-sdk-base/src/response_processors/state_events.rs +++ b/crates/matrix-sdk-base/src/response_processors/state_events.rs @@ -558,7 +558,7 @@ mod tests { // Predecessor of room 1 is room 0. event_factory .create(sender, RoomVersionId::try_from("42").unwrap()) - .predecessor(room_id_0, tombstone_event_id), + .predecessor(room_id_0), ), ) .build_sync_response(); @@ -597,7 +597,7 @@ mod tests { // Predecessor of room 2 is room 1. event_factory .create(sender, RoomVersionId::try_from("43").unwrap()) - .predecessor(room_id_1, tombstone_event_id), + .predecessor(room_id_1), ), ) .build_sync_response(); @@ -666,7 +666,7 @@ mod tests { // Predecessor of room 1 is room 0. event_factory .create(sender, RoomVersionId::try_from("42").unwrap()) - .predecessor(room_id_0, event_id!("$ev0")), + .predecessor(room_id_0), ) .add_timeline_event( // Successor of room 1 is room 2. @@ -681,7 +681,7 @@ mod tests { // Predecessor of room 2 is room 1. event_factory .create(sender, RoomVersionId::try_from("43").unwrap()) - .predecessor(room_id_1, event_id!("$ev1")), + .predecessor(room_id_1), ), ) .build_sync_response(); @@ -792,7 +792,7 @@ mod tests { // Predecessor of room 1 is room 0. event_factory .create(sender, RoomVersionId::try_from("42").unwrap()) - .predecessor(room_id_0, tombstone_event_id), + .predecessor(room_id_0), ), ) .build_sync_response(); @@ -832,7 +832,7 @@ mod tests { // Predecessor of room 2 is room 1. event_factory .create(sender, RoomVersionId::try_from("43").unwrap()) - .predecessor(room_id_1, tombstone_event_id), + .predecessor(room_id_1), ) .add_timeline_event( // Successor of room 2 is room 0. @@ -900,7 +900,7 @@ mod tests { JoinedRoomBuilder::new(room_id_0).add_timeline_event( event_factory .create(sender, RoomVersionId::try_from("42").unwrap()) - .predecessor(room_id_0, tombstone_event_id) + .predecessor(room_id_0) .event_id(tombstone_event_id), ), ) @@ -942,7 +942,7 @@ mod tests { // Predecessor of room 0 is room 0 event_factory .create(sender, RoomVersionId::try_from("42").unwrap()) - .predecessor(room_id_0, tombstone_event_id), + .predecessor(room_id_0), ), ) .build_sync_response(); @@ -982,7 +982,7 @@ mod tests { // Predecessor of room 0 is room 2 event_factory .create(sender, RoomVersionId::try_from("42").unwrap()) - .predecessor(room_id_2, event_id!("$ev2")), + .predecessor(room_id_2), ) .add_timeline_event( // Successor of room 0 is room 1 @@ -997,7 +997,7 @@ mod tests { // Predecessor of room 1 is room 0 event_factory .create(sender, RoomVersionId::try_from("43").unwrap()) - .predecessor(room_id_0, event_id!("$ev0")), + .predecessor(room_id_0), ) .add_timeline_event( // Successor of room 1 is room 2 @@ -1012,7 +1012,7 @@ mod tests { // Predecessor of room 2 is room 1 event_factory .create(sender, RoomVersionId::try_from("44").unwrap()) - .predecessor(room_id_1, event_id!("$ev1")), + .predecessor(room_id_1), ) .add_timeline_event( // Successor of room 2 is room 0 diff --git a/crates/matrix-sdk-base/src/room/tombstone.rs b/crates/matrix-sdk-base/src/room/tombstone.rs index 14aeff99e03..631cf02729f 100644 --- a/crates/matrix-sdk-base/src/room/tombstone.rs +++ b/crates/matrix-sdk-base/src/room/tombstone.rs @@ -111,7 +111,7 @@ mod tests { use matrix_sdk_test::{ JoinedRoomBuilder, SyncResponseBuilder, async_test, event_factory::EventFactory, }; - use ruma::{RoomVersionId, event_id, room_id, user_id}; + use ruma::{RoomVersionId, room_id, user_id}; use crate::{RoomState, test_utils::logged_in_base_client}; @@ -226,7 +226,6 @@ mod tests { let sender = user_id!("@mnt_io:matrix.org"); let room_id = room_id!("!r1"); let predecessor_room_id = room_id!("!r0"); - let predecessor_last_event_id = event_id!("$ev42"); let room = client.get_or_create_room(room_id, RoomState::Joined); let mut sync_builder = SyncResponseBuilder::new(); @@ -235,7 +234,7 @@ mod tests { JoinedRoomBuilder::new(room_id).add_timeline_event( EventFactory::new() .create(sender, RoomVersionId::V11) - .predecessor(predecessor_room_id, predecessor_last_event_id) + .predecessor(predecessor_room_id) .into_raw_sync(), ), ) diff --git a/testing/matrix-sdk-test/src/event_factory.rs b/testing/matrix-sdk-test/src/event_factory.rs index f28c768496f..e3018130669 100644 --- a/testing/matrix-sdk-test/src/event_factory.rs +++ b/testing/matrix-sdk-test/src/event_factory.rs @@ -438,8 +438,8 @@ impl EventBuilder { impl EventBuilder { /// Define the predecessor fields. - pub fn predecessor(mut self, room_id: &RoomId, event_id: &EventId) -> Self { - self.content.predecessor = Some(PreviousRoom::new(room_id.to_owned(), event_id.to_owned())); + pub fn predecessor(mut self, room_id: &RoomId) -> Self { + self.content.predecessor = Some(PreviousRoom::new(room_id.to_owned())); self } From 06c85e12bb2021895760924336d6eb26ad7775e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 18 Jul 2025 16:27:29 +0200 Subject: [PATCH 3/9] refactor: Adapt to addition of additional_creators in RoomCreateEventContent --- bindings/matrix-sdk-ffi/src/room/room_info.rs | 6 ++- crates/matrix-sdk-base/src/room/create.rs | 42 +++++++++++++++++-- crates/matrix-sdk-base/src/room/mod.rs | 6 +-- crates/matrix-sdk-base/src/room/room_info.rs | 10 ++--- crates/matrix-sdk-base/src/utils.rs | 11 ++--- crates/matrix-sdk-sqlite/src/state_store.rs | 6 +-- 6 files changed, 58 insertions(+), 23 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room/room_info.rs b/bindings/matrix-sdk-ffi/src/room/room_info.rs index 189d2e0c87a..1f23a1255c7 100644 --- a/bindings/matrix-sdk-ffi/src/room/room_info.rs +++ b/bindings/matrix-sdk-ffi/src/room/room_info.rs @@ -17,7 +17,7 @@ use crate::{ pub struct RoomInfo { id: String, encryption_state: EncryptionState, - creator: Option, + creators: Option>, /// The room's name from the room state event if received from sync, or one /// that's been computed otherwise. display_name: Option, @@ -102,7 +102,9 @@ impl RoomInfo { Ok(Self { id: room.room_id().to_string(), encryption_state: room.encryption_state(), - creator: room.creator().as_ref().map(ToString::to_string), + creators: room + .creators() + .map(|creators| creators.into_iter().map(Into::into).collect()), display_name: room.cached_display_name().map(|name| name.to_string()), raw_name: room.name(), topic: room.topic(), diff --git a/crates/matrix-sdk-base/src/room/create.rs b/crates/matrix-sdk-base/src/room/create.rs index 1f283703432..92764726e8b 100644 --- a/crates/matrix-sdk-base/src/room/create.rs +++ b/crates/matrix-sdk-base/src/room/create.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use matrix_sdk_common::ROOM_VERSION_RULES_FALLBACK; use ruma::{ OwnedUserId, RoomVersionId, assign, events::{ @@ -69,18 +70,38 @@ pub struct RoomCreateWithCreatorEventContent { /// This is currently only used for spaces. #[serde(skip_serializing_if = "Option::is_none", rename = "type")] pub room_type: Option, + + /// Additional room creators, considered to have "infinite" power level, in + /// room versions 12 onwards. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub additional_creators: Vec, } impl RoomCreateWithCreatorEventContent { /// Constructs a `RoomCreateWithCreatorEventContent` with the given original /// content and sender. pub fn from_event_content(content: RoomCreateEventContent, sender: OwnedUserId) -> Self { - let RoomCreateEventContent { federate, room_version, predecessor, room_type, .. } = content; - Self { creator: sender, federate, room_version, predecessor, room_type } + let RoomCreateEventContent { + federate, + room_version, + predecessor, + room_type, + additional_creators, + .. + } = content; + Self { + creator: sender, + federate, + room_version, + predecessor, + room_type, + additional_creators, + } } fn into_event_content(self) -> (RoomCreateEventContent, OwnedUserId) { - let Self { creator, federate, room_version, predecessor, room_type } = self; + let Self { creator, federate, room_version, predecessor, room_type, additional_creators } = + self; #[allow(deprecated)] let content = assign!(RoomCreateEventContent::new_v11(), { @@ -89,10 +110,25 @@ impl RoomCreateWithCreatorEventContent { room_version, predecessor, room_type, + additional_creators, }); (content, creator) } + + /// Get the creators of the room from this content, according to the room + /// version. + pub(crate) fn creators(&self) -> Vec { + let rules = self.room_version.rules().unwrap_or(ROOM_VERSION_RULES_FALLBACK); + + if rules.authorization.explicitly_privilege_room_creators { + std::iter::once(self.creator.clone()) + .chain(self.additional_creators.iter().cloned()) + .collect() + } else { + vec![self.creator.clone()] + } + } } /// Redacted form of [`RoomCreateWithCreatorEventContent`]. diff --git a/crates/matrix-sdk-base/src/room/mod.rs b/crates/matrix-sdk-base/src/room/mod.rs index a24c7741b09..b8cb3bff6ee 100644 --- a/crates/matrix-sdk-base/src/room/mod.rs +++ b/crates/matrix-sdk-base/src/room/mod.rs @@ -157,9 +157,9 @@ impl Room { &self.room_id } - /// Get a copy of the room creator. - pub fn creator(&self) -> Option { - self.inner.read().creator().map(ToOwned::to_owned) + /// Get a copy of the room creators. + pub fn creators(&self) -> Option> { + self.inner.read().creators() } /// Get our own user id. diff --git a/crates/matrix-sdk-base/src/room/room_info.rs b/crates/matrix-sdk-base/src/room/room_info.rs index 1c1d33caeb4..8e287d383d7 100644 --- a/crates/matrix-sdk-base/src/room/room_info.rs +++ b/crates/matrix-sdk-base/src/room/room_info.rs @@ -24,7 +24,7 @@ use matrix_sdk_common::{ }; use ruma::{ EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, - OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId, UserId, + OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId, api::client::sync::sync_events::v3::RoomSummary as RumaSummary, assign, events::{ @@ -882,11 +882,11 @@ impl RoomInfo { } } - /// Get the creator of this room. - pub fn creator(&self) -> Option<&UserId> { + /// Get the creators of this room. + pub fn creators(&self) -> Option> { match self.base_info.create.as_ref()? { - MinimalStateEvent::Original(ev) => Some(&ev.content.creator), - MinimalStateEvent::Redacted(ev) => Some(&ev.content.creator), + MinimalStateEvent::Original(ev) => Some(ev.content.creators()), + MinimalStateEvent::Redacted(ev) => Some(ev.content.creators()), } } diff --git a/crates/matrix-sdk-base/src/utils.rs b/crates/matrix-sdk-base/src/utils.rs index 2b49af57951..13e73679594 100644 --- a/crates/matrix-sdk-base/src/utils.rs +++ b/crates/matrix-sdk-base/src/utils.rs @@ -233,13 +233,10 @@ impl From<&StrippedRoomNameEvent> for MinimalStateEvent { impl From<&StrippedRoomCreateEvent> for MinimalStateEvent { fn from(event: &StrippedRoomCreateEvent) -> Self { - let content = RoomCreateWithCreatorEventContent { - creator: event.sender.clone(), - federate: event.content.federate, - room_version: event.content.room_version.clone(), - predecessor: event.content.predecessor.clone(), - room_type: event.content.room_type.clone(), - }; + let content = RoomCreateWithCreatorEventContent::from_event_content( + event.content.clone(), + event.sender.clone(), + ); Self::Original(OriginalMinimalStateEvent { content, event_id: None }) } } diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index 6ccef6737c5..195a9d2cbe5 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -2463,15 +2463,15 @@ mod migration_tests { let room_a = room_infos.iter().find(|r| r.room_id() == room_a_id).unwrap(); assert_eq!(room_a.name(), Some(room_a_name)); - assert_eq!(room_a.creator(), Some(room_a_create_sender)); + assert_eq!(room_a.creators(), Some(vec![room_a_create_sender.to_owned()])); let room_b = room_infos.iter().find(|r| r.room_id() == room_b_id).unwrap(); assert_eq!(room_b.name(), None); - assert_eq!(room_b.creator(), None); + assert_eq!(room_b.creators(), None); let room_c = room_infos.iter().find(|r| r.room_id() == room_c_id).unwrap(); assert_eq!(room_c.name(), None); - assert_eq!(room_c.creator(), Some(room_c_create_sender)); + assert_eq!(room_c.creators(), Some(vec![room_c_create_sender.to_owned()])); } #[async_test] From 99993a3cb5b618065a01c2e4b63a68f3214aa8ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 23 Jul 2025 09:27:57 +0200 Subject: [PATCH 4/9] refactor: Adapt to changes in power levels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- bindings/matrix-sdk-ffi/src/client.rs | 3 +- bindings/matrix-sdk-ffi/src/room_member.rs | 61 +++++++++++-- .../src/deserialized_responses.rs | 11 ++- .../src/response_processors/state_events.rs | 1 + crates/matrix-sdk-base/src/room/members.rs | 26 ++++-- crates/matrix-sdk-base/src/room/mod.rs | 26 +++--- crates/matrix-sdk-base/src/room/room_info.rs | 8 +- crates/matrix-sdk-base/src/sliding_sync.rs | 16 +++- .../src/store/integration_tests.rs | 3 +- crates/matrix-sdk-base/src/store/mod.rs | 30 +++++-- .../src/timeline/event_item/mod.rs | 18 +++- .../matrix-sdk-ui/src/timeline/tests/mod.rs | 3 +- crates/matrix-sdk/src/error.rs | 6 +- .../src/latest_events/latest_event.rs | 11 ++- crates/matrix-sdk/src/room/member.rs | 87 ++++++++++++++----- crates/matrix-sdk/src/room/mod.rs | 21 +++-- crates/matrix-sdk/src/room/power_levels.rs | 12 ++- crates/matrix-sdk/src/test_utils/mocks/mod.rs | 11 ++- .../tests/integration/room/joined.rs | 16 ++-- testing/matrix-sdk-test/src/event_factory.rs | 3 +- 20 files changed, 273 insertions(+), 100 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 22fdcf3d233..f1703213adc 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -78,6 +78,7 @@ use ruma::{ RoomAccountDataEvent as RumaRoomAccountDataEvent, }, push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat}, + room_version_rules::AuthorizationRules, OwnedDeviceId, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName, }; use serde::{Deserialize, Serialize}; @@ -1911,7 +1912,7 @@ pub struct PowerLevels { impl From for RoomPowerLevelsEventContent { fn from(value: PowerLevels) -> Self { - let mut power_levels = RoomPowerLevelsEventContent::new(); + let mut power_levels = RoomPowerLevelsEventContent::new(&AuthorizationRules::V1); if let Some(users_default) = value.users_default { power_levels.users_default = users_default.into(); diff --git a/bindings/matrix-sdk-ffi/src/room_member.rs b/bindings/matrix-sdk-ffi/src/room_member.rs index 3e38dbf6259..2b84c22f5b1 100644 --- a/bindings/matrix-sdk-ffi/src/room_member.rs +++ b/bindings/matrix-sdk-ffi/src/room_member.rs @@ -1,5 +1,5 @@ use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole}; -use ruma::UserId; +use ruma::{events::room::power_levels::UserPowerLevel, UserId}; use crate::error::{ClientError, NotYetImplemented}; @@ -57,16 +57,22 @@ impl TryFrom for Member } } +/// Get the suggested role for the given power level. +/// +/// Returns an error if the value of the power level is out of range for numbers +/// accepted in canonical JSON. #[matrix_sdk_ffi_macros::export] -pub fn suggested_role_for_power_level(power_level: i64) -> RoomMemberRole { +pub fn suggested_role_for_power_level( + power_level: PowerLevel, +) -> Result { // It's not possible to expose the constructor on the Enum through Uniffi ☹️ - RoomMemberRole::suggested_role_for_power_level(power_level) + Ok(RoomMemberRole::suggested_role_for_power_level(power_level.try_into()?)) } #[matrix_sdk_ffi_macros::export] -pub fn suggested_power_level_for_role(role: RoomMemberRole) -> i64 { +pub fn suggested_power_level_for_role(role: RoomMemberRole) -> PowerLevel { // It's not possible to expose methods on an Enum through Uniffi ☹️ - role.suggested_power_level() + role.suggested_power_level().into() } /// Generates a `matrix.to` permalink to the given userID. @@ -83,8 +89,8 @@ pub struct RoomMember { pub avatar_url: Option, pub membership: MembershipState, pub is_name_ambiguous: bool, - pub power_level: i64, - pub normalized_power_level: i64, + pub power_level: PowerLevel, + pub normalized_power_level: PowerLevel, pub is_ignored: bool, pub suggested_role_for_power_level: RoomMemberRole, pub membership_change_reason: Option, @@ -100,8 +106,8 @@ impl TryFrom for RoomMember { avatar_url: m.avatar_url().map(|a| a.to_string()), membership: m.membership().clone().try_into()?, is_name_ambiguous: m.name_ambiguous(), - power_level: m.power_level(), - normalized_power_level: m.normalized_power_level(), + power_level: m.power_level().into(), + normalized_power_level: m.normalized_power_level().into(), is_ignored: m.is_ignored(), suggested_role_for_power_level: m.suggested_role_for_power_level(), membership_change_reason: m.event().reason().map(|s| s.to_owned()), @@ -130,3 +136,40 @@ impl TryFrom for RoomMemberWithSende }) } } + +#[derive(Clone, uniffi::Enum)] +pub enum PowerLevel { + /// The user is a room creator and has infinite power level. + /// + /// This power level was introduced in room version 12. + Infinite, + + /// The user has the given power level. + Value { value: i64 }, +} + +impl From for PowerLevel { + fn from(value: UserPowerLevel) -> Self { + match value { + UserPowerLevel::Infinite => Self::Infinite, + UserPowerLevel::Int(value) => Self::Value { value: value.into() }, + _ => unimplemented!(), + } + } +} + +impl TryFrom for UserPowerLevel { + type Error = ClientError; + + fn try_from(value: PowerLevel) -> Result { + Ok(match value { + PowerLevel::Infinite => Self::Infinite, + PowerLevel::Value { value } => { + Self::Int(value.try_into().map_err(|err| ClientError::Generic { + msg: "Power level is out of range".to_owned(), + details: Some(format!("{err:?}")), + })?) + } + }) + } +} diff --git a/crates/matrix-sdk-base/src/deserialized_responses.rs b/crates/matrix-sdk-base/src/deserialized_responses.rs index 498345ae799..eda956b5060 100644 --- a/crates/matrix-sdk-base/src/deserialized_responses.rs +++ b/crates/matrix-sdk-base/src/deserialized_responses.rs @@ -30,6 +30,7 @@ use ruma::{ power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, }, }, + room_version_rules::AuthorizationRules, serde::Raw, }; use serde::Serialize; @@ -517,10 +518,14 @@ impl MemberEvent { impl SyncOrStrippedState { /// The power levels of the event. - pub fn power_levels(&self) -> RoomPowerLevels { + pub fn power_levels( + &self, + rules: &AuthorizationRules, + creators: Vec, + ) -> RoomPowerLevels { match self { - Self::Sync(e) => e.power_levels(), - Self::Stripped(e) => e.power_levels(), + Self::Sync(e) => e.power_levels(rules, creators), + Self::Stripped(e) => e.power_levels(rules, creators), } } } diff --git a/crates/matrix-sdk-base/src/response_processors/state_events.rs b/crates/matrix-sdk-base/src/response_processors/state_events.rs index e7d5376470a..6eede82a30c 100644 --- a/crates/matrix-sdk-base/src/response_processors/state_events.rs +++ b/crates/matrix-sdk-base/src/response_processors/state_events.rs @@ -1080,6 +1080,7 @@ mod tests { .add_joined_room( JoinedRoomBuilder::new(&DEFAULT_TEST_ROOM_ID) .add_timeline_event(room_name) + .add_state_event(StateTestEvent::Create) .add_state_event(StateTestEvent::PowerLevels), ) .build_sync_response(); diff --git a/crates/matrix-sdk-base/src/room/members.rs b/crates/matrix-sdk-base/src/room/members.rs index 2ee0add35ac..80a3aa30293 100644 --- a/crates/matrix-sdk-base/src/room/members.rs +++ b/crates/matrix-sdk-base/src/room/members.rs @@ -27,7 +27,7 @@ use ruma::{ presence::PresenceEvent, room::{ member::{MembershipState, RoomMemberEventContent}, - power_levels::{PowerLevelAction, RoomPowerLevels}, + power_levels::{PowerLevelAction, RoomPowerLevels, UserPowerLevel}, }, }, }; @@ -270,19 +270,27 @@ impl RoomMember { /// Get the normalized power level of this member. /// /// The normalized power level depends on the maximum power level that can - /// be found in a certain room, positive values are always in the range of - /// 0-100. - pub fn normalized_power_level(&self) -> i64 { + /// be found in a certain room, positive values that are not `Infinite` are + /// always in the range of 0-100. + pub fn normalized_power_level(&self) -> UserPowerLevel { + let UserPowerLevel::Int(power_level) = self.power_level() else { + return UserPowerLevel::Infinite; + }; + + let mut power_level = i64::from(power_level); + if self.max_power_level > 0 { - (self.power_level() * 100) / self.max_power_level - } else { - self.power_level() + power_level = (power_level * 100) / self.max_power_level; } + + UserPowerLevel::Int( + power_level.try_into().expect("normalized power level should fit in Int"), + ) } /// Get the power level of this member. - pub fn power_level(&self) -> i64 { - self.power_levels.for_user(self.user_id()).into() + pub fn power_level(&self) -> UserPowerLevel { + self.power_levels.for_user(self.user_id()) } /// Whether this user can ban other users based on the power levels. diff --git a/crates/matrix-sdk-base/src/room/mod.rs b/crates/matrix-sdk-base/src/room/mod.rs index b8cb3bff6ee..ac3e4439d1f 100644 --- a/crates/matrix-sdk-base/src/room/mod.rs +++ b/crates/matrix-sdk-base/src/room/mod.rs @@ -49,7 +49,6 @@ pub use room_info::{ }; use ruma::{ EventId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, UserId, - assign, events::{ direct::OwnedDirectUserIdentifier, receipt::{Receipt, ReceiptThread, ReceiptType}, @@ -58,10 +57,9 @@ use ruma::{ guest_access::GuestAccess, history_visibility::HistoryVisibility, join_rules::JoinRule, - power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, + power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent, RoomPowerLevelsSource}, }, }, - int, room::RoomType, }; #[cfg(feature = "e2e-encryption")] @@ -364,13 +362,16 @@ impl Room { /// Get the current power levels of this room. pub async fn power_levels(&self) -> Result { - Ok(self + let power_levels_content = self .store .get_state_event_static::(self.room_id()) .await? .ok_or(Error::InsufficientData)? - .deserialize()? - .power_levels()) + .deserialize()?; + let creators = self.creators().ok_or(Error::InsufficientData)?; + let rules = self.inner.read().room_version_rules_or_default(); + + Ok(power_levels_content.power_levels(&rules.authorization, creators)) } /// Get the current power levels of this room, or a sensible default if they @@ -380,14 +381,13 @@ impl Room { return power_levels; } - // As a fallback, create the default power levels of a room, with the creator at - // level 100. - let creator = self.creator(); - assign!( - RoomPowerLevelsEventContent::new(), - { users: creator.into_iter().map(|user_id| (user_id, int!(100))).collect() } + // As a fallback, create the default power levels of a room. + let rules = self.inner.read().room_version_rules_or_default(); + RoomPowerLevels::new( + RoomPowerLevelsSource::None, + &rules.authorization, + self.creators().into_iter().flatten(), ) - .into() } /// Get the `m.room.name` of this room. diff --git a/crates/matrix-sdk-base/src/room/room_info.rs b/crates/matrix-sdk-base/src/room/room_info.rs index 8e287d383d7..cc4b20f6866 100644 --- a/crates/matrix-sdk-base/src/room/room_info.rs +++ b/crates/matrix-sdk-base/src/room/room_info.rs @@ -49,7 +49,7 @@ use ruma::{ tag::{TagEventContent, TagName, Tags}, }, room::RoomType, - room_version_rules::{RedactionRules, RoomVersionRules}, + room_version_rules::{AuthorizationRules, RedactionRules, RoomVersionRules}, serde::Raw, }; use serde::{Deserialize, Serialize}; @@ -230,7 +230,8 @@ impl BaseRoomInfo { self.tombstone = Some(t.into()); } AnySyncStateEvent::RoomPowerLevels(p) => { - self.max_power_level = p.power_levels().max().into(); + // The rules and creators do not affect the max power level. + self.max_power_level = p.power_levels(&AuthorizationRules::V1, vec![]).max().into(); } AnySyncStateEvent::CallMember(m) => { let Some(o_ev) = m.as_original() else { @@ -306,7 +307,8 @@ impl BaseRoomInfo { self.tombstone = Some(t.into()); } AnyStrippedStateEvent::RoomPowerLevels(p) => { - self.max_power_level = p.power_levels().max().into(); + // The rules and creators do not affect the max power level. + self.max_power_level = p.power_levels(&AuthorizationRules::V1, vec![]).max().into(); } AnyStrippedStateEvent::CallMember(_) => { // Ignore stripped call state events. Rooms that are not in Joined or Left state diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index b552c787da0..b6941e9c068 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -1315,6 +1315,17 @@ mod tests { let client = logged_in_base_client(Some(own_user_id)).await; let room_id = room_id!("!r:e.uk"); + // The room create event. + let create = json!({ + "sender":"@ignacio:example.com", + "state_key":"", + "type":"m.room.create", + "event_id": "$idc", + "origin_server_ts": 12344415, + "content":{ "room_version": "11" }, + "room_id": room_id, + }); + // Give the current user invite or kick permissions in this room let power_levels = json!({ "sender":"@alice:example.com", @@ -1340,7 +1351,10 @@ mod tests { // When the sliding sync response contains a timeline let events = &[knock_event]; let mut room = room_with_timeline(events); - room.required_state.push(Raw::new(&power_levels).unwrap().cast_unchecked()); + room.required_state.extend([ + Raw::new(&create).unwrap().cast_unchecked(), + Raw::new(&power_levels).unwrap().cast_unchecked(), + ]); let response = response_with_room(room_id, room); client .process_sliding_sync(&response, &RequestedRequiredStates::default()) diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index ffe0c78abfb..dfefab2409f 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -30,6 +30,7 @@ use ruma::{ }, }, owned_event_id, owned_mxc_uri, room_id, + room_version_rules::AuthorizationRules, serde::Raw, uint, user_id, }; @@ -1961,7 +1962,7 @@ fn first_receipt_event_id() -> &'static EventId { } fn power_level_event() -> Raw { - let content = RoomPowerLevelsEventContent::default(); + let content = RoomPowerLevelsEventContent::new(&AuthorizationRules::V1); let event = json!({ "event_id": "$h29iv0s8:example.com", diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index ec149fbe845..a389c059f9b 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -32,6 +32,7 @@ use std::{ use eyeball_im::{Vector, VectorDiff}; use futures_util::Stream; +use matrix_sdk_common::ROOM_VERSION_RULES_FALLBACK; use once_cell::sync::OnceCell; #[cfg(any(test, feature = "testing"))] @@ -54,6 +55,7 @@ use ruma::{ presence::PresenceEvent, receipt::ReceiptEventContent, room::{ + create::RoomCreateEventContent, member::{RoomMemberEventContent, StrippedRoomMemberEvent}, power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, redaction::SyncRoomRedactionEvent, @@ -66,7 +68,7 @@ use tokio::sync::{Mutex, RwLock, broadcast}; use tracing::warn; use crate::{ - MinimalRoomMemberEvent, Room, RoomStateFilter, SessionMeta, + MinimalRoomMemberEvent, Room, RoomCreateWithCreatorEventContent, RoomStateFilter, SessionMeta, deserialized_responses::DisplayName, event_cache::store as event_cache_store, room::{RoomInfo, RoomInfoNotableUpdate, RoomState}, @@ -641,16 +643,28 @@ impl StateChanges { self.any_state_static_for_key::(room_id, user_id) } + /// Get the create event for the given room from an event contained in these + /// `StateChanges`, if any. + pub(crate) fn create(&self, room_id: &RoomId) -> Option { + self.any_state_static_for_key::(room_id, &EmptyStateKey) + .map(|event| { + RoomCreateWithCreatorEventContent::from_event_content(event.content, event.sender) + }) + // Fallback to the content in the room info. + .or_else(|| self.room_infos.get(room_id)?.create().cloned()) + } + /// Get the power levels for the given room from an event contained in these /// `StateChanges`, if any. pub(crate) fn power_levels(&self, room_id: &RoomId) -> Option { - Some( - self.any_state_static_for_key::( - room_id, - &EmptyStateKey, - )? - .power_levels(), - ) + let power_levels_content = self + .any_state_static_for_key::(room_id, &EmptyStateKey)?; + + let create_content = self.create(room_id)?; + let rules = create_content.room_version.rules().unwrap_or(ROOM_VERSION_RULES_FALLBACK); + let creators = create_content.creators(); + + Some(power_levels_content.power_levels(&rules.authorization, creators)) } } diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs index 4eecca2a7d4..2c87ac9d892 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -866,8 +866,20 @@ mod tests { ); let client = logged_in_client(None).await; - // Add power levels state event, otherwise the knock state event can't be used - // as the latest event + // Add create and power levels state event, otherwise the knock state event + // can't be used as the latest event + let create_event = sync_state_event!({ + "type": "m.room.create", + "content": { "room_version": "11" }, + "event_id": "$143278582443PhrSm:example.org", + "origin_server_ts": 143273580, + "room_id": room_id, + "sender": user_id, + "state_key": "", + "unsigned": { + "age": 1235 + } + }); let power_level_event = sync_state_event!({ "type": "m.room.power_levels", "content": {}, @@ -881,7 +893,7 @@ mod tests { } }); let mut room = http::response::Room::new(); - room.required_state.push(power_level_event); + room.required_state.extend([create_event, power_level_event]); // And the room is stored in the client so it can be extracted when needed let response = response_with_room(room_id, room); diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index 4998bd74f20..23505e42912 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -52,7 +52,7 @@ use ruma::{ power_levels::NotificationPowerLevels, push::{PushConditionPowerLevelsCtx, PushConditionRoomCtx, Ruleset}, room_id, - room_version_rules::RoomVersionRules, + room_version_rules::{AuthorizationRules, RoomPowerLevelsRules, RoomVersionRules}, serde::Raw, uint, }; @@ -411,6 +411,7 @@ impl RoomDataProvider for TestRoomDataProvider { BTreeMap::new(), int!(0), NotificationPowerLevels::new(), + RoomPowerLevelsRules::new(&AuthorizationRules::V1, None), ); let push_condition_room_ctx = assign!( PushConditionRoomCtx::new( diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 11611f937c6..e2038605292 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -37,7 +37,7 @@ use ruma::{ }, error::{FromHttpResponseError, IntoHttpError}, }, - events::tag::InvalidUserTagName, + events::{room::power_levels::PowerLevelsError, tag::InvalidUserTagName}, push::{InsertPushRuleError, RemovePushRuleError}, IdParseError, }; @@ -415,6 +415,10 @@ pub enum Error { /// An error happened while attempting to reply to an event. #[error(transparent)] ReplyError(#[from] ReplyError), + + /// An error happened while attempting to change power levels. + #[error("power levels error: {0}")] + PowerLevels(#[from] PowerLevelsError), } #[rustfmt::skip] // stop rustfmt breaking the `` in docs across multiple lines diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index f673750a6b9..b2e5c9e4301 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -239,7 +239,10 @@ fn find_and_map( mod tests { use assert_matches::assert_matches; use matrix_sdk_test::event_factory::EventFactory; - use ruma::{event_id, user_id}; + use ruma::{ + event_id, events::room::power_levels::RoomPowerLevelsSource, + room_version_rules::AuthorizationRules, user_id, + }; use super::{find_and_map, LatestEventValue}; @@ -427,7 +430,7 @@ mod tests { #[test] fn test_latest_event_knocked_state_event_with_power_levels() { - use ruma::events::room::power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}; + use ruma::events::room::power_levels::RoomPowerLevels; let user_id = user_id!("@mnt_io:matrix.org"); let other_user_id = user_id!("@other_mnt_io:server.name"); @@ -437,8 +440,8 @@ mod tests { .membership(ruma::events::room::member::MembershipState::Knock) .into_event(); - let room_power_levels_event = RoomPowerLevelsEventContent::new(); - let mut room_power_levels = RoomPowerLevels::from(room_power_levels_event); + let mut room_power_levels = + RoomPowerLevels::new(RoomPowerLevelsSource::None, &AuthorizationRules::V1, []); room_power_levels.users_default = 5.into(); // Cannot accept. Cannot decline. diff --git a/crates/matrix-sdk/src/room/member.rs b/crates/matrix-sdk/src/room/member.rs index 6b5ae84dad3..7ab91cdea57 100644 --- a/crates/matrix-sdk/src/room/member.rs +++ b/crates/matrix-sdk/src/room/member.rs @@ -1,6 +1,9 @@ use std::ops::Deref; -use ruma::events::room::MediaSource; +use ruma::{ + events::room::{power_levels::UserPowerLevel, MediaSource}, + int, +}; use crate::{ media::{MediaFormat, MediaRequestParameters}, @@ -95,6 +98,17 @@ impl RoomMember { #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] pub enum RoomMemberRole { + /// The member is a creator. + /// + /// A creator has infinite power levels and cannot be demoted, so this role + /// is immutable. A room can have several creators. + /// + /// It is available in room versions where + /// `explicitly_privilege_room_creators` in [`AuthorizationRules`] is set to + /// `true`. + /// + /// [`AuthorizationRules`]: ruma::room_version_rules::AuthorizationRules + Creator, /// The member is an administrator. Administrator, /// The member is a moderator. @@ -105,22 +119,29 @@ pub enum RoomMemberRole { impl RoomMemberRole { /// Creates the suggested role for a given power level. - pub fn suggested_role_for_power_level(power_level: i64) -> Self { - if power_level >= 100 { - Self::Administrator - } else if power_level >= 50 { - Self::Moderator - } else { - Self::User + pub fn suggested_role_for_power_level(power_level: UserPowerLevel) -> Self { + match power_level { + UserPowerLevel::Infinite => RoomMemberRole::Creator, + UserPowerLevel::Int(value) => { + if value >= int!(100) { + Self::Administrator + } else if value >= int!(50) { + Self::Moderator + } else { + Self::User + } + } + _ => unimplemented!(), } } /// Get the suggested power level for this role. - pub fn suggested_power_level(&self) -> i64 { + pub fn suggested_power_level(&self) -> UserPowerLevel { match self { - Self::Administrator => 100, - Self::Moderator => 50, - Self::User => 0, + Self::Creator => UserPowerLevel::Infinite, + Self::Administrator => UserPowerLevel::Int(int!(100)), + Self::Moderator => UserPowerLevel::Int(int!(50)), + Self::User => UserPowerLevel::Int(int!(0)), } } } @@ -133,33 +154,51 @@ mod tests { fn test_suggested_roles() { assert_eq!( RoomMemberRole::Administrator, - RoomMemberRole::suggested_role_for_power_level(100) + RoomMemberRole::suggested_role_for_power_level(int!(100).into()) + ); + assert_eq!( + RoomMemberRole::Moderator, + RoomMemberRole::suggested_role_for_power_level(int!(50).into()) + ); + assert_eq!( + RoomMemberRole::User, + RoomMemberRole::suggested_role_for_power_level(int!(0).into()) ); - assert_eq!(RoomMemberRole::Moderator, RoomMemberRole::suggested_role_for_power_level(50)); - assert_eq!(RoomMemberRole::User, RoomMemberRole::suggested_role_for_power_level(0)); } #[test] fn test_unexpected_power_levels() { assert_eq!( RoomMemberRole::Administrator, - RoomMemberRole::suggested_role_for_power_level(200) + RoomMemberRole::suggested_role_for_power_level(int!(200).into()) ); assert_eq!( RoomMemberRole::Administrator, - RoomMemberRole::suggested_role_for_power_level(101) + RoomMemberRole::suggested_role_for_power_level(int!(101).into()) + ); + assert_eq!( + RoomMemberRole::Moderator, + RoomMemberRole::suggested_role_for_power_level(int!(99).into()) + ); + assert_eq!( + RoomMemberRole::Moderator, + RoomMemberRole::suggested_role_for_power_level(int!(51).into()) + ); + assert_eq!( + RoomMemberRole::User, + RoomMemberRole::suggested_role_for_power_level(int!(-1).into()) + ); + assert_eq!( + RoomMemberRole::User, + RoomMemberRole::suggested_role_for_power_level(int!(-100).into()) ); - assert_eq!(RoomMemberRole::Moderator, RoomMemberRole::suggested_role_for_power_level(99)); - assert_eq!(RoomMemberRole::Moderator, RoomMemberRole::suggested_role_for_power_level(51)); - assert_eq!(RoomMemberRole::User, RoomMemberRole::suggested_role_for_power_level(-1)); - assert_eq!(RoomMemberRole::User, RoomMemberRole::suggested_role_for_power_level(-100)); } #[test] fn test_default_power_levels() { - assert_eq!(100, RoomMemberRole::Administrator.suggested_power_level()); - assert_eq!(50, RoomMemberRole::Moderator.suggested_power_level()); - assert_eq!(0, RoomMemberRole::User.suggested_power_level()); + assert_eq!(int!(100), RoomMemberRole::Administrator.suggested_power_level()); + assert_eq!(int!(50), RoomMemberRole::Moderator.suggested_power_level()); + assert_eq!(int!(0), RoomMemberRole::User.suggested_power_level()); assert_eq!( RoomMemberRole::Administrator, diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 4fa8b41a038..c676b5f8769 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -101,7 +101,9 @@ use ruma::{ }, name::RoomNameEventContent, pinned_events::RoomPinnedEventsEventContent, - power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, + power_levels::{ + RoomPowerLevels, RoomPowerLevelsEventContent, RoomPowerLevelsSource, UserPowerLevel, + }, server_acl::RoomServerAclEventContent, topic::RoomTopicEventContent, ImageInfo, MediaSource, ThumbnailInfo, @@ -115,6 +117,7 @@ use ruma::{ RoomAccountDataEventType, StateEventContent, StateEventType, StaticEventContent, StaticStateEventContent, SyncStateEvent, }, + int, push::{Action, PushConditionRoomCtx, Ruleset}, serde::Raw, time::Instant, @@ -2412,7 +2415,7 @@ impl Room { } } - self.send_state_event(RoomPowerLevelsEventContent::from(power_levels)).await + self.send_state_event(RoomPowerLevelsEventContent::try_from(power_levels)?).await } /// Applies a set of power level changes to this room. @@ -2422,7 +2425,7 @@ impl Room { pub async fn apply_power_level_changes(&self, changes: RoomPowerLevelChanges) -> Result<()> { let mut power_levels = self.power_levels().await?; power_levels.apply(changes)?; - self.send_state_event(RoomPowerLevelsEventContent::from(power_levels)).await?; + self.send_state_event(RoomPowerLevelsEventContent::try_from(power_levels)?).await?; Ok(()) } @@ -2430,7 +2433,11 @@ impl Room { /// /// [spec]: https://spec.matrix.org/v1.9/client-server-api/#mroompower_levels pub async fn reset_power_levels(&self) -> Result { - let default_power_levels = RoomPowerLevels::from(RoomPowerLevelsEventContent::new()); + let creators = self.creators().unwrap_or_default(); + let rules = self.clone_info().room_version_rules_or_default(); + + let default_power_levels = + RoomPowerLevels::new(RoomPowerLevelsSource::None, &rules.authorization, creators); let changes = RoomPowerLevelChanges::from(default_power_levels); self.apply_power_level_changes(changes).await?; Ok(self.power_levels().await?) @@ -2449,9 +2456,9 @@ impl Room { /// /// This method checks the `RoomPowerLevels` events instead of loading the /// member list and looking for the member. - pub async fn get_user_power_level(&self, user_id: &UserId) -> Result { + pub async fn get_user_power_level(&self, user_id: &UserId) -> Result { let event = self.power_levels().await?; - Ok(event.for_user(user_id).into()) + Ok(event.for_user(user_id)) } /// Gets a map with the `UserId` of users with power levels other than `0` @@ -2771,7 +2778,7 @@ impl Room { let max = members .iter() .max_by_key(|member| member.power_level()) - .filter(|max| max.power_level() >= 50) + .filter(|max| max.power_level() >= int!(50)) .map(|member| member.user_id().server_name()); // Sort the servers by population. diff --git a/crates/matrix-sdk/src/room/power_levels.rs b/crates/matrix-sdk/src/room/power_levels.rs index 4b272280bbd..4304260405e 100644 --- a/crates/matrix-sdk/src/room/power_levels.rs +++ b/crates/matrix-sdk/src/room/power_levels.rs @@ -199,7 +199,9 @@ pub fn power_level_user_changes( mod tests { use std::collections::BTreeMap; - use ruma::{int, power_levels::NotificationPowerLevels}; + use ruma::{ + int, power_levels::NotificationPowerLevels, room_version_rules::AuthorizationRules, + }; use super::*; @@ -424,11 +426,15 @@ mod tests { } fn default_power_levels() -> RoomPowerLevels { - default_power_levels_event_content().into() + RoomPowerLevels::new( + default_power_levels_event_content().into(), + &AuthorizationRules::V1, + [], + ) } fn default_power_levels_event_content() -> RoomPowerLevelsEventContent { - let mut content = RoomPowerLevelsEventContent::new(); + let mut content = RoomPowerLevelsEventContent::new(&AuthorizationRules::V1); content.ban = int!(50); content.invite = int!(50); content.kick = int!(50); diff --git a/crates/matrix-sdk/src/test_utils/mocks/mod.rs b/crates/matrix-sdk/src/test_utils/mocks/mod.rs index de94d969942..7725df6c30c 100644 --- a/crates/matrix-sdk/src/test_utils/mocks/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mocks/mod.rs @@ -1984,7 +1984,11 @@ impl<'a> MockEndpoint<'a, RoomSendStateEndpoint> { /// ``` /// # tokio_test::block_on(async { /// use matrix_sdk::{ - /// ruma::{room_id, event_id, events::room::power_levels::RoomPowerLevelsEventContent}, + /// ruma::{ + /// room_id, event_id, + /// events::room::power_levels::RoomPowerLevelsEventContent, + /// room_version_rules::AuthorizationRules + /// }, /// test_utils::mocks::MatrixMockServer /// }; /// use serde_json::json; @@ -2009,7 +2013,7 @@ impl<'a> MockEndpoint<'a, RoomSendStateEndpoint> { /// .mount() /// .await; /// - /// let mut content = RoomPowerLevelsEventContent::new(); + /// let mut content = RoomPowerLevelsEventContent::new(&AuthorizationRules::V1); /// // Update the power level to a non default value. /// // Otherwise it will be skipped from serialization. /// content.redact = 51.into(); @@ -2045,6 +2049,7 @@ impl<'a> MockEndpoint<'a, RoomSendStateEndpoint> { /// }, /// events::StateEventType, /// room_id, + /// room_version_rules::AuthorizationRules, /// }, /// test_utils::mocks::MatrixMockServer, /// }; @@ -2070,7 +2075,7 @@ impl<'a> MockEndpoint<'a, RoomSendStateEndpoint> { /// // The `m.room.reaction` event type should not be mocked by the server. /// assert!(response_not_mocked.is_err()); /// - /// let response = room.send_state_event(RoomPowerLevelsEventContent::new()).await?; + /// let response = room.send_state_event(RoomPowerLevelsEventContent::new(&AuthorizationRules::V1)).await?; /// // The `m.room.message` event type should be mocked by the server. /// assert_eq!( /// event_id, response.event_id, diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 34edd0177f2..7b6d8d1baa4 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -690,12 +690,12 @@ async fn test_get_power_level_for_user() { let power_level_admin = room.get_user_power_level(user_id!("@example:localhost")).await.unwrap(); - assert_eq!(power_level_admin, 100); + assert_eq!(power_level_admin, int!(100)); // This user either does not exist in the room or has no special power level let power_level_unknown = room.get_user_power_level(user_id!("@non-existing:localhost")).await.unwrap(); - assert_eq!(power_level_unknown, 0); + assert_eq!(power_level_unknown, int!(0)); } #[async_test] @@ -800,7 +800,9 @@ async fn test_call_notifications_ring_for_dms() { let (client, server) = logged_in_client_with_server().await; let mut sync_builder = SyncResponseBuilder::new(); - let room_builder = JoinedRoomBuilder::default().add_state_event(StateTestEvent::PowerLevels); + let room_builder = JoinedRoomBuilder::default() + .add_state_event(StateTestEvent::Create) + .add_state_event(StateTestEvent::PowerLevels); sync_builder.add_joined_room(room_builder); sync_builder.add_global_account_data_event(GlobalAccountDataTestEvent::Direct); @@ -845,7 +847,9 @@ async fn test_call_notifications_notify_for_rooms() { let (client, server) = logged_in_client_with_server().await; let mut sync_builder = SyncResponseBuilder::new(); - let room_builder = JoinedRoomBuilder::default().add_state_event(StateTestEvent::PowerLevels); + let room_builder = JoinedRoomBuilder::default() + .add_state_event(StateTestEvent::Create) + .add_state_event(StateTestEvent::PowerLevels); sync_builder.add_joined_room(room_builder); mock_sync(&server, sync_builder.build_json_sync_response(), None).await; @@ -895,7 +899,9 @@ async fn test_call_notifications_dont_notify_room_without_mention_powerlevel() { json!({"room": 101}); sync_builder.add_joined_room( - JoinedRoomBuilder::default().add_state_event(StateTestEvent::Custom(power_level_event)), + JoinedRoomBuilder::default() + .add_state_event(StateTestEvent::Create) + .add_state_event(StateTestEvent::Custom(power_level_event)), ); mock_sync(&server, sync_builder.build_json_sync_response(), None).await; diff --git a/testing/matrix-sdk-test/src/event_factory.rs b/testing/matrix-sdk-test/src/event_factory.rs index e3018130669..48ad077b2b5 100644 --- a/testing/matrix-sdk-test/src/event_factory.rs +++ b/testing/matrix-sdk-test/src/event_factory.rs @@ -71,6 +71,7 @@ use ruma::{ sticker::StickerEventContent, typing::TypingEventContent, }, + room_version_rules::AuthorizationRules, serde::Raw, server_name, }; @@ -875,7 +876,7 @@ impl EventFactory { &self, map: &mut BTreeMap, ) -> EventBuilder { - let mut event = RoomPowerLevelsEventContent::new(); + let mut event = RoomPowerLevelsEventContent::new(&AuthorizationRules::V1); event.users.append(map); self.event(event) } From 7c32ff2fbaa01a2e1e5669a242435ef3f75ab462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 23 Jul 2025 10:10:04 +0200 Subject: [PATCH 5/9] Add changelog entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- bindings/matrix-sdk-ffi/CHANGELOG.md | 7 +++++++ crates/matrix-sdk-base/CHANGELOG.md | 17 +++++++++++++++++ crates/matrix-sdk/CHANGELOG.md | 9 +++++++++ 3 files changed, 33 insertions(+) diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index e3b77ed4d85..3780de83476 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -25,6 +25,13 @@ All notable changes to this project will be documented in this file. ### Breaking changes: +- The `creator` field of `RoomInfo` has been renamed to `creators` and can now contain a list of + user IDs, to reflect that a room can now have several creators, as introduced in room version 12. + ([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436)) +- The `PowerLevel` type was introduced to represent power levels instead of `i64` to differentiate + the infinite power level of creators, as introduced in room version 12. It is used in + `suggested_role_for_power_level`, `suggested_power_level_for_role` and `RoomMember`. + ([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436)) - `Client::get_url` now returns a `Vec` instead of a `String`. It also throws an error when the response isn't status code 200 OK, instead of providing the error in the response body. ([#5438](https://github.com/matrix-org/matrix-rust-sdk/pull/5438)) diff --git a/crates/matrix-sdk-base/CHANGELOG.md b/crates/matrix-sdk-base/CHANGELOG.md index 1282fcc4ed6..de7bf89e683 100644 --- a/crates/matrix-sdk-base/CHANGELOG.md +++ b/crates/matrix-sdk-base/CHANGELOG.md @@ -7,6 +7,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate ### Features +- [**breaking**] `RoomCreateWithCreatorEventContent` has a new field + `additional_creators` that allows to specify additional room creators beside + the user sending the `m.room.create` event, introduced with room version 12. + ([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436)) - [**breaking**] The `RoomInfo` method now remembers the inviter at the time when the `BaseClient::room_joined()` method was called. The caller is responsible to remember the inviter before a server request to join the room @@ -16,6 +20,19 @@ All notable changes to this project will be documented in this file. ([#5390](https://github.com/matrix-org/matrix-rust-sdk/pull/5390)) ### Refactor +- [**breaking**] `SyncOrStrippedState::power_levels()` + takes `AuthorizationRules` and a list of creators, because creators can have + infinite power levels, as introduced in room version 12. + ([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436)) +- [**breaking**] `RoomMember::power_level()` and + `RoomMember::normalized_power_level()` now use `UserPowerLevel` to represent + power levels instead of `i64` to differentiate the infinite power level of + creators, as introduced in room version 12. + ([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436)) +- [**breaking**] The `creator()` methods of `Room` and `RoomInfo` have been + renamed to `creators()` and can now return a list of user IDs, to reflect that + a room can have several creators, as introduced in room version 12. + ([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436)) - [**breaking**] `RoomInfo::room_version_or_default()` was replaced with `room_version_rules_or_default()`. The room version should only be used for display purposes. The rules contain flags for all the differences in behavior diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 5d8e3834695..513c1bdcf59 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file. ### Features +- [**breaking**] `RoomMemberRole` has a new `Creator` variant, that + differentiates room creators with infinite power levels, as introduced in room + version 12. + ([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436)) - Add `Account::fetch_account_data_static` to fetch account data from the server with a statically-known type, with a signature similar to `Account::account_data`. @@ -21,6 +25,11 @@ All notable changes to this project will be documented in this file. ### Refactor +- [**breaking**] `RoomMemberRole::suggested_role_for_power_level()` and + `RoomMemberRole::suggested_power_level()` now use `UserPowerLevel` to represent + power levels instead of `i64` to differentiate the infinite power level of + creators, as introduced in room version 12. + ([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436)) - [**breaking**] The `reason` argument of `Room::report_room()` is now required, due to a clarification in the spec. ([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337)) From 58a0a0d1496e41b991221f89f73d66d4e69b5b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 23 Jul 2025 11:04:30 +0200 Subject: [PATCH 6/9] Fix docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk/src/room/member.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/room/member.rs b/crates/matrix-sdk/src/room/member.rs index 7ab91cdea57..31521c5119d 100644 --- a/crates/matrix-sdk/src/room/member.rs +++ b/crates/matrix-sdk/src/room/member.rs @@ -100,8 +100,8 @@ impl RoomMember { pub enum RoomMemberRole { /// The member is a creator. /// - /// A creator has infinite power levels and cannot be demoted, so this role - /// is immutable. A room can have several creators. + /// A creator has an infinite power level and cannot be demoted, so this + /// role is immutable. A room can have several creators. /// /// It is available in room versions where /// `explicitly_privilege_room_creators` in [`AuthorizationRules`] is set to From 903ac696ee0ee98b87e61c10c2f2cea521f3c8cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 23 Jul 2025 11:23:15 +0200 Subject: [PATCH 7/9] Fix IndexedDB test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- .../src/state_store/migrations.rs | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs index 3474f98692e..d69d75fa0e1 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs @@ -815,9 +815,9 @@ mod tests { }, AnySyncStateEvent, StateEventType, }, - room_id, + owned_user_id, room_id, serde::Raw, - server_name, user_id, EventId, MilliSecondsSinceUnixEpoch, RoomId, UserId, + server_name, user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, RoomId, UserId, }; use serde_json::json; use uuid::Uuid; @@ -1494,10 +1494,11 @@ mod tests { room_state_store: &IdbObjectStore<'_>, room_id: &RoomId, name: Option<&str>, - create_creator: Option<&UserId>, + create_creator: Option, create_sender: Option<&UserId>, ) -> Result<()> { - let room_info_json = room_info_v1_json(room_id, RoomState::Joined, name, create_creator); + let room_info_json = + room_info_v1_json(room_id, RoomState::Joined, name, create_creator.as_deref()); room_infos_store.put_key_val( &encode_key(None, keys::ROOM_INFOS, room_id), @@ -1510,7 +1511,7 @@ mod tests { }; let create_content = match create_creator { - Some(creator) => RoomCreateEventContent::new_v1(creator.to_owned()), + Some(creator) => RoomCreateEventContent::new_v1(creator), None => RoomCreateEventContent::new_v11(), }; @@ -1518,7 +1519,7 @@ mod tests { let create_event = json!({ "content": create_content, "event_id": event_id, - "sender": create_sender.to_owned(), + "sender": create_sender, "origin_server_ts": MilliSecondsSinceUnixEpoch::now(), "state_key": "", "type": "m.room.create", @@ -1540,17 +1541,17 @@ mod tests { // Room A: with name, creator and sender. let room_a_id = room_id!("!room_a:dummy.local"); let room_a_name = "Room A"; - let room_a_creator = user_id!("@creator:dummy.local"); + let room_a_creator = owned_user_id!("@creator:dummy.local"); // Use a different sender to check that sender is used over creator in // migration. - let room_a_create_sender = user_id!("@sender:dummy.local"); + let room_a_create_sender = owned_user_id!("@sender:dummy.local"); // Room B: without name, creator and sender. let room_b_id = room_id!("!room_b:dummy.local"); // Room C: only with sender. let room_c_id = room_id!("!room_c:dummy.local"); - let room_c_create_sender = user_id!("@creator:dummy.local"); + let room_c_create_sender = owned_user_id!("@creator:dummy.local"); // Create and populate db. { @@ -1569,7 +1570,7 @@ mod tests { room_a_id, Some(room_a_name), Some(room_a_creator), - Some(room_a_create_sender), + Some(&room_a_create_sender), )?; add_room_v7(&room_infos_store, &room_state_store, room_b_id, None, None, None)?; add_room_v7( @@ -1578,7 +1579,7 @@ mod tests { room_c_id, None, None, - Some(room_c_create_sender), + Some(&room_c_create_sender), )?; tx.await.into_result()?; @@ -1594,15 +1595,15 @@ mod tests { let room_a = room_infos.iter().find(|r| r.room_id() == room_a_id).unwrap(); assert_eq!(room_a.name(), Some(room_a_name)); - assert_eq!(room_a.creator(), Some(room_a_create_sender)); + assert_eq!(room_a.creators(), Some(vec![room_a_create_sender])); let room_b = room_infos.iter().find(|r| r.room_id() == room_b_id).unwrap(); assert_eq!(room_b.name(), None); - assert_eq!(room_b.creator(), None); + assert_eq!(room_b.creators(), None); let room_c = room_infos.iter().find(|r| r.room_id() == room_c_id).unwrap(); assert_eq!(room_c.name(), None); - assert_eq!(room_c.creator(), Some(room_c_create_sender)); + assert_eq!(room_c.creators(), Some(vec![room_c_create_sender])); Ok(()) } From ae24efdca35ed1d08df85a1ee170528e6e1d8102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 23 Jul 2025 16:04:16 +0200 Subject: [PATCH 8/9] refactor(ffi): Handle unknown UserPowerLevel variant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- bindings/matrix-sdk-ffi/src/room_member.rs | 23 +++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room_member.rs b/bindings/matrix-sdk-ffi/src/room_member.rs index 2b84c22f5b1..42dce1d331e 100644 --- a/bindings/matrix-sdk-ffi/src/room_member.rs +++ b/bindings/matrix-sdk-ffi/src/room_member.rs @@ -69,10 +69,13 @@ pub fn suggested_role_for_power_level( Ok(RoomMemberRole::suggested_role_for_power_level(power_level.try_into()?)) } +/// Get the suggested power level for the given role. +/// +/// Returns an error if the value of the power level is unsupported. #[matrix_sdk_ffi_macros::export] -pub fn suggested_power_level_for_role(role: RoomMemberRole) -> PowerLevel { +pub fn suggested_power_level_for_role(role: RoomMemberRole) -> Result { // It's not possible to expose methods on an Enum through Uniffi ☹️ - role.suggested_power_level().into() + Ok(role.suggested_power_level().try_into()?) } /// Generates a `matrix.to` permalink to the given userID. @@ -106,8 +109,8 @@ impl TryFrom for RoomMember { avatar_url: m.avatar_url().map(|a| a.to_string()), membership: m.membership().clone().try_into()?, is_name_ambiguous: m.name_ambiguous(), - power_level: m.power_level().into(), - normalized_power_level: m.normalized_power_level().into(), + power_level: m.power_level().try_into()?, + normalized_power_level: m.normalized_power_level().try_into()?, is_ignored: m.is_ignored(), suggested_role_for_power_level: m.suggested_role_for_power_level(), membership_change_reason: m.event().reason().map(|s| s.to_owned()), @@ -148,12 +151,14 @@ pub enum PowerLevel { Value { value: i64 }, } -impl From for PowerLevel { - fn from(value: UserPowerLevel) -> Self { +impl TryFrom for PowerLevel { + type Error = NotYetImplemented; + + fn try_from(value: UserPowerLevel) -> Result { match value { - UserPowerLevel::Infinite => Self::Infinite, - UserPowerLevel::Int(value) => Self::Value { value: value.into() }, - _ => unimplemented!(), + UserPowerLevel::Infinite => Ok(Self::Infinite), + UserPowerLevel::Int(value) => Ok(Self::Value { value: value.into() }), + _ => Err(NotYetImplemented), } } } From 61c52f97d9c419ea91aa5e358c13345efb2d6c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 23 Jul 2025 16:08:19 +0200 Subject: [PATCH 9/9] Add lint TODO for non-exhaustive enum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/matrix-sdk/src/room/member.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/matrix-sdk/src/room/member.rs b/crates/matrix-sdk/src/room/member.rs index 31521c5119d..7b9e314bc45 100644 --- a/crates/matrix-sdk/src/room/member.rs +++ b/crates/matrix-sdk/src/room/member.rs @@ -131,6 +131,10 @@ impl RoomMemberRole { Self::User } } + // This branch is only necessary because the enum is non-exhaustive. + // TODO: Use the `non_exhaustive_omitted_patterns` lint when it becomes stable to be + // warned when a variant is added. + // Tracking issue: https://github.com/rust-lang/rust/issues/89554 _ => unimplemented!(), } }