diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 9f11366a5..9b01c4f68 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -634,7 +634,7 @@ impl Room { } pub async fn get_power_levels(&self) -> Result { - let power_levels = self.inner.room_power_levels().await?; + let power_levels = self.inner.power_levels().await.map_err(matrix_sdk::Error::from)?; Ok(RoomPowerLevels::from(power_levels)) } diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index ecc3a81a8..1033345a8 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -789,6 +789,8 @@ impl BaseClient { room: &Room, ) -> Option<(Box, usize)> { let enc_events = room.latest_encrypted_events(); + let power_levels = room.power_levels().await.ok(); + let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref()); // Walk backwards through the encrypted events, looking for one we can decrypt for (i, event) in enc_events.iter().enumerate().rev() { @@ -802,14 +804,13 @@ impl BaseClient { // We found an event we can decrypt if let Ok(any_sync_event) = decrypted.raw().deserialize() { // We can deserialize it to find its type - match is_suitable_for_latest_event(&any_sync_event) { + match is_suitable_for_latest_event(&any_sync_event, power_levels_info) { PossibleLatestEvent::YesRoomMessage(_) | PossibleLatestEvent::YesPoll(_) | PossibleLatestEvent::YesCallInvite(_) | PossibleLatestEvent::YesCallNotify(_) | PossibleLatestEvent::YesSticker(_) | PossibleLatestEvent::YesKnockedStateEvent(_) => { - // The event is the right type for us to use as latest_event return Some((Box::new(LatestEvent::new(decrypted)), i)); } _ => (), diff --git a/crates/matrix-sdk-base/src/error.rs b/crates/matrix-sdk-base/src/error.rs index bd7a8f1f1..8d1a2dd3e 100644 --- a/crates/matrix-sdk-base/src/error.rs +++ b/crates/matrix-sdk-base/src/error.rs @@ -61,4 +61,12 @@ pub enum Error { /// function with invalid parameters #[error("receive_all_members function was called with invalid parameters")] InvalidReceiveMembersParameters, + + /// This request failed because the local data wasn't sufficient. + #[error("Local cache doesn't contain all necessary data to perform the action.")] + InsufficientData, + + /// There was a [`serde_json`] deserialization error. + #[error(transparent)] + DeserializationError(#[from] serde_json::error::Error), } diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index ed94c447a..4b2f1ff9b 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -14,11 +14,14 @@ use ruma::events::{ }; use ruma::{ events::{ - room::member::{MembershipState, SyncRoomMemberEvent}, + room::{ + member::{MembershipState, SyncRoomMemberEvent}, + power_levels::RoomPowerLevels, + }, sticker::SyncStickerEvent, AnySyncStateEvent, }, - MxcUri, OwnedEventId, + MxcUri, OwnedEventId, UserId, }; use serde::{Deserialize, Serialize}; @@ -45,6 +48,7 @@ pub enum PossibleLatestEvent<'a> { YesCallNotify(&'a SyncCallNotifyEvent), /// This state event is suitable - it's a knock membership change + /// that can be handled by the current user. YesKnockedStateEvent(&'a SyncRoomMemberEvent), // Later: YesState(), @@ -60,7 +64,10 @@ pub enum PossibleLatestEvent<'a> { /// Decide whether an event could be stored as the latest event in a room. /// Returns a LatestEvent representing our decision. #[cfg(feature = "e2e-encryption")] -pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLatestEvent<'_> { +pub fn is_suitable_for_latest_event<'a>( + event: &'a AnySyncTimelineEvent, + power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>, +) -> PossibleLatestEvent<'a> { match event { // Suitable - we have an m.room.message that was not redacted or edited AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => { @@ -114,10 +121,23 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat // We don't currently support most state events AnySyncTimelineEvent::State(state) => { - // But we make an exception for knocked state events + // But we make an exception for knocked state events *if* the current user + // can either accept or decline them if let AnySyncStateEvent::RoomMember(member) = state { if matches!(member.membership(), MembershipState::Knock) { - return PossibleLatestEvent::YesKnockedStateEvent(member); + let can_accept_or_decline_knocks = match power_levels_info { + Some((own_user_id, room_power_levels)) => { + room_power_levels.user_can_invite(own_user_id) + || room_power_levels.user_can_kick(own_user_id) + } + _ => false, + }; + + // The current user can act on the knock changes, so they should be + // displayed + if can_accept_or_decline_knocks { + return PossibleLatestEvent::YesKnockedStateEvent(member); + } } } PossibleLatestEvent::NoUnsupportedEventType @@ -345,7 +365,7 @@ mod tests { )); assert_let!( PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) = - is_suitable_for_latest_event(&event) + is_suitable_for_latest_event(&event, None) ); assert_eq!(m.content.msgtype.msgtype(), "m.image"); @@ -368,7 +388,7 @@ mod tests { )); assert_let!( PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) = - is_suitable_for_latest_event(&event) + is_suitable_for_latest_event(&event, None) ); assert_eq!(m.content.poll_start().question.text, "do you like rust?"); @@ -392,7 +412,7 @@ mod tests { )); assert_let!( PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) = - is_suitable_for_latest_event(&event) + is_suitable_for_latest_event(&event, None) ); } @@ -414,7 +434,7 @@ mod tests { )); assert_let!( PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) = - is_suitable_for_latest_event(&event) + is_suitable_for_latest_event(&event, None) ); } @@ -435,7 +455,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_)) ); } @@ -457,7 +477,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::NoUnsupportedMessageLikeType ); } @@ -485,7 +505,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_)) ); } @@ -507,7 +527,10 @@ mod tests { }), )); - assert_matches!(is_suitable_for_latest_event(&event), PossibleLatestEvent::NoEncrypted); + assert_matches!( + is_suitable_for_latest_event(&event, None), + PossibleLatestEvent::NoEncrypted + ); } #[test] @@ -524,7 +547,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::NoUnsupportedEventType ); } @@ -548,7 +571,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::NoUnsupportedMessageLikeType ); } diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 3d426261f..cfe1625b7 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -43,6 +43,7 @@ use ruma::{ join_rules::JoinRule, member::{MembershipState, RoomMemberEventContent}, pinned_events::RoomPinnedEventsEventContent, + power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, redaction::SyncRoomRedactionEvent, tombstone::RoomTombstoneEventContent, }, @@ -71,7 +72,7 @@ use crate::{ read_receipts::RoomReadReceipts, store::{DynStateStore, Result as StoreResult, StateStoreExt}, sync::UnreadNotificationsCount, - MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships, + Error, MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships, }; /// Indicates that a notable update of `RoomInfo` has been applied, and why. @@ -508,6 +509,17 @@ impl Room { self.inner.read().base_info.max_power_level } + /// Get the current power levels of this room. + pub async fn power_levels(&self) -> Result { + Ok(self + .store + .get_state_event_static::(self.room_id()) + .await? + .ok_or(Error::InsufficientData)? + .deserialize()? + .power_levels()) + } + /// Get the `m.room.name` of this room. /// /// The returned string may be empty if the event has been redacted, or it's diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index a6f711b0d..f613d177b 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -30,7 +30,7 @@ use ruma::{ api::client::sync::sync_events::v3::{self, InvitedRoom, KnockedRoom}, events::{ room::member::MembershipState, AnyRoomAccountDataEvent, AnyStrippedStateEvent, - AnySyncStateEvent, + AnySyncStateEvent, StateEventType, }, serde::Raw, JsOption, OwnedRoomId, RoomId, UInt, UserId, @@ -698,9 +698,28 @@ async fn cache_latest_events( let mut encrypted_events = Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity()); + // Try to get room power levels from the current changes + let power_levels_from_changes = || { + let state_changes = changes?.state.get(room_info.room_id())?; + let room_power_levels_state = + state_changes.get(&StateEventType::RoomPowerLevels)?.values().next()?; + match room_power_levels_state.deserialize().ok()? { + AnySyncStateEvent::RoomPowerLevels(ev) => Some(ev.power_levels()), + _ => None, + } + }; + + // If we didn't get any info, try getting it from local data + let power_levels = match power_levels_from_changes() { + Some(power_levels) => Some(power_levels), + None => room.power_levels().await.ok(), + }; + + let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref()); + for event in events.iter().rev() { if let Ok(timeline_event) = event.raw().deserialize() { - match is_suitable_for_latest_event(&timeline_event) { + match is_suitable_for_latest_event(&timeline_event, power_levels_info) { PossibleLatestEvent::YesRoomMessage(_) | PossibleLatestEvent::YesPoll(_) | PossibleLatestEvent::YesCallInvite(_) @@ -1740,10 +1759,23 @@ mod tests { } #[async_test] - async fn test_last_knock_member_state_event_from_sliding_sync_is_cached() { + async fn test_last_knock_event_from_sliding_sync_is_cached_if_user_has_permissions() { + let own_user_id = user_id!("@me:e.uk"); // Given a logged-in client - let client = logged_in_base_client(None).await; + let client = logged_in_base_client(Some(own_user_id)).await; let room_id = room_id!("!r:e.uk"); + + // Give the current user invite or kick permissions in this room + let power_levels = json!({ + "sender":"@alice:example.com", + "state_key":"", + "type":"m.room.power_levels", + "event_id": "$idb", + "origin_server_ts": 12344445, + "content":{ "invite": 100, "kick": 100, "users": { own_user_id: 100 } }, + "room_id": room_id, + }); + // And a knock member state event let knock_event = json!({ "sender":"@alice:example.com", @@ -1757,7 +1789,8 @@ mod tests { // When the sliding sync response contains a timeline let events = &[knock_event]; - let room = room_with_timeline(events); + let mut room = room_with_timeline(events); + room.required_state.push(Raw::new(&power_levels).unwrap().cast()); let response = response_with_room(room_id, room); client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); @@ -1770,7 +1803,49 @@ mod tests { } #[async_test] - async fn test_last_member_state_event_from_sliding_sync_is_not_cached() { + async fn test_last_knock_event_from_sliding_sync_is_not_cached_without_permissions() { + let own_user_id = user_id!("@me:e.uk"); + // Given a logged-in client + let client = logged_in_base_client(Some(own_user_id)).await; + let room_id = room_id!("!r:e.uk"); + + // Set the user as a user with no permission to invite or kick other users in + // this room + let power_levels = json!({ + "sender":"@alice:example.com", + "state_key":"", + "type":"m.room.power_levels", + "event_id": "$idb", + "origin_server_ts": 12344445, + "content":{ "invite": 50, "kick": 50, "users": { own_user_id: 0 } }, + "room_id": room_id, + }); + + // And a knock member state event + let knock_event = json!({ + "sender":"@alice:example.com", + "state_key":"@alice:example.com", + "type":"m.room.member", + "event_id": "$ida", + "origin_server_ts": 12344446, + "content":{"membership": "knock"}, + "room_id": room_id, + }); + + // 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()); + let response = response_with_room(room_id, room); + client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); + + // Then the room doesn't hold the knock state event as the latest event + let client_room = client.get_room(room_id).expect("No room found"); + assert!(client_room.latest_event().is_none()); + } + + #[async_test] + async fn test_last_non_knock_member_state_event_from_sliding_sync_is_not_cached() { // Given a logged-in client let client = logged_in_base_client(None).await; let room_id = room_id!("!r:e.uk"); diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index d85b4d2c4..992ba6e68 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -46,7 +46,7 @@ use ruma::{ }, name::RoomNameEventContent, pinned_events::RoomPinnedEventsEventContent, - power_levels::RoomPowerLevelsEventContent, + power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, server_acl::RoomServerAclEventContent, third_party_invite::RoomThirdPartyInviteEventContent, tombstone::RoomTombstoneEventContent, @@ -141,8 +141,9 @@ impl TimelineItemContent { /// `TimelineItemContent`. pub(crate) fn from_latest_event_content( event: AnySyncTimelineEvent, + power_levels_info: Option<(&UserId, &RoomPowerLevels)>, ) -> Option { - match is_suitable_for_latest_event(&event) { + match is_suitable_for_latest_event(&event, power_levels_info) { PossibleLatestEvent::YesRoomMessage(m) => { Some(Self::from_suitable_latest_event_content(m)) } 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 c94c858cd..b91dbb72a 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -158,9 +158,18 @@ impl EventTimelineItem { let event_id = event.event_id().to_owned(); let is_own = client.user_id().map(|uid| uid == sender).unwrap_or(false); + // Get the room's power levels for calculating the latest event + let power_levels = if let Some(room) = client.get_room(room_id) { + room.power_levels().await.ok() + } else { + None + }; + let room_power_levels_info = client.user_id().zip(power_levels.as_ref()); + // If we don't (yet) know how to handle this type of message, return `None` // here. If we do, convert it into a `TimelineItemContent`. - let content = TimelineItemContent::from_latest_event_content(event)?; + let content = + TimelineItemContent::from_latest_event_content(event, room_power_levels_info)?; // We don't currently bundle any reactions with the main event. This could // conceivably be wanted in the message preview in future. @@ -780,6 +789,27 @@ 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 + let power_level_event = sync_state_event!({ + "type": "m.room.power_levels", + "content": {}, + "event_id": "$143278582443PhrSn:example.org", + "origin_server_ts": 143273581, + "room_id": room_id, + "sender": user_id, + "state_key": "", + "unsigned": { + "age": 1234 + } + }); + let mut room = http::response::Room::new(); + room.required_state.push(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); + client.process_sliding_sync_test_helper(&response).await.unwrap(); + // When we construct a timeline event from it let event = SyncTimelineEvent::new(raw_event.cast()); let timeline_item = @@ -1040,7 +1070,6 @@ mod tests { "room_id": room_id, "sender": user_id, "state_key": user_id, - "type": "m.room.member", "unsigned": { "age": 1234 } diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 5561deee3..d6a4624ec 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2139,7 +2139,7 @@ impl Room { &self, updates: Vec<(&UserId, Int)>, ) -> Result { - let mut power_levels = self.room_power_levels().await?; + let mut power_levels = self.power_levels().await?; for (user_id, new_level) in updates { if new_level == power_levels.users_default { @@ -2157,7 +2157,7 @@ impl Room { /// Any values that are `None` in the given `RoomPowerLevelChanges` will /// remain unchanged. pub async fn apply_power_level_changes(&self, changes: RoomPowerLevelChanges) -> Result<()> { - let mut power_levels = self.room_power_levels().await?; + let mut power_levels = self.power_levels().await?; power_levels.apply(changes)?; self.send_state_event(RoomPowerLevelsEventContent::from(power_levels)).await?; Ok(()) @@ -2180,7 +2180,7 @@ impl Room { let default_power_levels = RoomPowerLevels::from(RoomPowerLevelsEventContent::new()); let changes = RoomPowerLevelChanges::from(default_power_levels); self.apply_power_level_changes(changes).await?; - self.room_power_levels().await + Ok(self.power_levels().await?) } /// Gets the suggested role for the user with the provided `user_id`. @@ -2197,14 +2197,14 @@ 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 { - let event = self.room_power_levels().await?; + let event = self.power_levels().await?; Ok(event.for_user(user_id).into()) } /// Gets a map with the `UserId` of users with power levels other than `0` /// and this power level. pub async fn users_with_power_levels(&self) -> HashMap { - let power_levels = self.room_power_levels().await.ok(); + let power_levels = self.power_levels().await.ok(); let mut user_power_levels = HashMap::::new(); if let Some(power_levels) = power_levels { for (id, level) in power_levels.users.into_iter() { @@ -2487,7 +2487,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_redact_own(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_redact_own_event(user_id)) + Ok(self.power_levels().await?.user_can_redact_own_event(user_id)) } /// Returns true if the user with the given user_id is able to redact @@ -2495,7 +2495,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_redact_other(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_redact_event_of_other(user_id)) + Ok(self.power_levels().await?.user_can_redact_event_of_other(user_id)) } /// Returns true if the user with the given user_id is able to ban in the @@ -2503,7 +2503,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_ban(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_ban(user_id)) + Ok(self.power_levels().await?.user_can_ban(user_id)) } /// Returns true if the user with the given user_id is able to kick in the @@ -2511,7 +2511,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_invite(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_invite(user_id)) + Ok(self.power_levels().await?.user_can_invite(user_id)) } /// Returns true if the user with the given user_id is able to kick in the @@ -2519,7 +2519,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_kick(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_kick(user_id)) + Ok(self.power_levels().await?.user_can_kick(user_id)) } /// Returns true if the user with the given user_id is able to send a @@ -2531,7 +2531,7 @@ impl Room { user_id: &UserId, state_event: StateEventType, ) -> Result { - Ok(self.room_power_levels().await?.user_can_send_state(user_id, state_event)) + Ok(self.power_levels().await?.user_can_send_state(user_id, state_event)) } /// Returns true if the user with the given user_id is able to send a @@ -2543,7 +2543,7 @@ impl Room { user_id: &UserId, message: MessageLikeEventType, ) -> Result { - Ok(self.room_power_levels().await?.user_can_send_message(user_id, message)) + Ok(self.power_levels().await?.user_can_send_message(user_id, message)) } /// Returns true if the user with the given user_id is able to pin or unpin @@ -2552,7 +2552,7 @@ impl Room { /// The call may fail if there is an error in getting the power levels. pub async fn can_user_pin_unpin(&self, user_id: &UserId) -> Result { Ok(self - .room_power_levels() + .power_levels() .await? .user_can_send_state(user_id, StateEventType::RoomPinnedEvents)) } @@ -2562,7 +2562,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_trigger_room_notification(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_trigger_room_notification(user_id)) + Ok(self.power_levels().await?.user_can_trigger_room_notification(user_id)) } /// Get a list of servers that should know this room. diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 497df17e0..e8f294bb0 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -624,7 +624,7 @@ async fn test_reset_power_levels() { .mount(&server) .await; - let initial_power_levels = room.room_power_levels().await.unwrap(); + let initial_power_levels = room.power_levels().await.unwrap(); assert_eq!(initial_power_levels.events[&TimelineEventType::RoomAvatar], int!(100)); room.reset_power_levels().await.unwrap();