Skip to content

Commit c143f98

Browse files
committed
refactor(room_list): only display the knock state events if the current user can act on them
That is, if their power level allows them to either invite or kick users.
1 parent f4a1898 commit c143f98

File tree

10 files changed

+193
-44
lines changed

10 files changed

+193
-44
lines changed

bindings/matrix-sdk-ffi/src/room.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,7 @@ impl Room {
634634
}
635635

636636
pub async fn get_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
637-
let power_levels = self.inner.room_power_levels().await?;
637+
let power_levels = self.inner.power_levels().await.map_err(matrix_sdk::Error::from)?;
638638
Ok(RoomPowerLevels::from(power_levels))
639639
}
640640

crates/matrix-sdk-base/src/client.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,8 @@ impl BaseClient {
789789
room: &Room,
790790
) -> Option<(Box<LatestEvent>, usize)> {
791791
let enc_events = room.latest_encrypted_events();
792+
let power_levels = room.power_levels().await.ok();
793+
let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref());
792794

793795
// Walk backwards through the encrypted events, looking for one we can decrypt
794796
for (i, event) in enc_events.iter().enumerate().rev() {
@@ -802,14 +804,13 @@ impl BaseClient {
802804
// We found an event we can decrypt
803805
if let Ok(any_sync_event) = decrypted.raw().deserialize() {
804806
// We can deserialize it to find its type
805-
match is_suitable_for_latest_event(&any_sync_event) {
807+
match is_suitable_for_latest_event(&any_sync_event, power_levels_info) {
806808
PossibleLatestEvent::YesRoomMessage(_)
807809
| PossibleLatestEvent::YesPoll(_)
808810
| PossibleLatestEvent::YesCallInvite(_)
809811
| PossibleLatestEvent::YesCallNotify(_)
810812
| PossibleLatestEvent::YesSticker(_)
811813
| PossibleLatestEvent::YesKnockedStateEvent(_) => {
812-
// The event is the right type for us to use as latest_event
813814
return Some((Box::new(LatestEvent::new(decrypted)), i));
814815
}
815816
_ => (),

crates/matrix-sdk-base/src/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,12 @@ pub enum Error {
6161
/// function with invalid parameters
6262
#[error("receive_all_members function was called with invalid parameters")]
6363
InvalidReceiveMembersParameters,
64+
65+
/// This request failed because the local data wasn't sufficient.
66+
#[error("Local cache doesn't contain all necessary data to perform the action.")]
67+
InsufficientData,
68+
69+
/// There was a [`serde_json`] deserialization error.
70+
#[error(transparent)]
71+
DeserializationError(#[from] serde_json::error::Error),
6472
}

crates/matrix-sdk-base/src/latest_event.rs

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ use ruma::events::{
1414
};
1515
use ruma::{
1616
events::{
17-
room::member::{MembershipState, SyncRoomMemberEvent},
17+
room::{
18+
member::{MembershipState, SyncRoomMemberEvent},
19+
power_levels::RoomPowerLevels,
20+
},
1821
sticker::SyncStickerEvent,
1922
AnySyncStateEvent,
2023
},
21-
MxcUri, OwnedEventId,
24+
MxcUri, OwnedEventId, UserId,
2225
};
2326
use serde::{Deserialize, Serialize};
2427

@@ -45,6 +48,7 @@ pub enum PossibleLatestEvent<'a> {
4548
YesCallNotify(&'a SyncCallNotifyEvent),
4649

4750
/// This state event is suitable - it's a knock membership change
51+
/// that can be handled by the current user.
4852
YesKnockedStateEvent(&'a SyncRoomMemberEvent),
4953

5054
// Later: YesState(),
@@ -60,7 +64,10 @@ pub enum PossibleLatestEvent<'a> {
6064
/// Decide whether an event could be stored as the latest event in a room.
6165
/// Returns a LatestEvent representing our decision.
6266
#[cfg(feature = "e2e-encryption")]
63-
pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLatestEvent<'_> {
67+
pub fn is_suitable_for_latest_event<'a>(
68+
event: &'a AnySyncTimelineEvent,
69+
power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>,
70+
) -> PossibleLatestEvent<'a> {
6471
match event {
6572
// Suitable - we have an m.room.message that was not redacted or edited
6673
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
@@ -114,10 +121,23 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat
114121

115122
// We don't currently support most state events
116123
AnySyncTimelineEvent::State(state) => {
117-
// But we make an exception for knocked state events
124+
// But we make an exception for knocked state events *if* the current user
125+
// can either accept or decline them
118126
if let AnySyncStateEvent::RoomMember(member) = state {
119127
if matches!(member.membership(), MembershipState::Knock) {
120-
return PossibleLatestEvent::YesKnockedStateEvent(member);
128+
let can_accept_or_decline_knocks = match power_levels_info {
129+
Some((own_user_id, room_power_levels)) => {
130+
room_power_levels.user_can_invite(own_user_id)
131+
|| room_power_levels.user_can_kick(own_user_id)
132+
}
133+
_ => false,
134+
};
135+
136+
// The current user can act on the knock changes, so they should be
137+
// displayed
138+
if can_accept_or_decline_knocks {
139+
return PossibleLatestEvent::YesKnockedStateEvent(member);
140+
}
121141
}
122142
}
123143
PossibleLatestEvent::NoUnsupportedEventType
@@ -345,7 +365,7 @@ mod tests {
345365
));
346366
assert_let!(
347367
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
348-
is_suitable_for_latest_event(&event)
368+
is_suitable_for_latest_event(&event, None)
349369
);
350370

351371
assert_eq!(m.content.msgtype.msgtype(), "m.image");
@@ -368,7 +388,7 @@ mod tests {
368388
));
369389
assert_let!(
370390
PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
371-
is_suitable_for_latest_event(&event)
391+
is_suitable_for_latest_event(&event, None)
372392
);
373393

374394
assert_eq!(m.content.poll_start().question.text, "do you like rust?");
@@ -392,7 +412,7 @@ mod tests {
392412
));
393413
assert_let!(
394414
PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
395-
is_suitable_for_latest_event(&event)
415+
is_suitable_for_latest_event(&event, None)
396416
);
397417
}
398418

@@ -414,7 +434,7 @@ mod tests {
414434
));
415435
assert_let!(
416436
PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) =
417-
is_suitable_for_latest_event(&event)
437+
is_suitable_for_latest_event(&event, None)
418438
);
419439
}
420440

@@ -435,7 +455,7 @@ mod tests {
435455
));
436456

437457
assert_matches!(
438-
is_suitable_for_latest_event(&event),
458+
is_suitable_for_latest_event(&event, None),
439459
PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_))
440460
);
441461
}
@@ -457,7 +477,7 @@ mod tests {
457477
));
458478

459479
assert_matches!(
460-
is_suitable_for_latest_event(&event),
480+
is_suitable_for_latest_event(&event, None),
461481
PossibleLatestEvent::NoUnsupportedMessageLikeType
462482
);
463483
}
@@ -485,7 +505,7 @@ mod tests {
485505
));
486506

487507
assert_matches!(
488-
is_suitable_for_latest_event(&event),
508+
is_suitable_for_latest_event(&event, None),
489509
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
490510
);
491511
}
@@ -507,7 +527,10 @@ mod tests {
507527
}),
508528
));
509529

510-
assert_matches!(is_suitable_for_latest_event(&event), PossibleLatestEvent::NoEncrypted);
530+
assert_matches!(
531+
is_suitable_for_latest_event(&event, None),
532+
PossibleLatestEvent::NoEncrypted
533+
);
511534
}
512535

513536
#[test]
@@ -524,7 +547,7 @@ mod tests {
524547
));
525548

526549
assert_matches!(
527-
is_suitable_for_latest_event(&event),
550+
is_suitable_for_latest_event(&event, None),
528551
PossibleLatestEvent::NoUnsupportedEventType
529552
);
530553
}
@@ -548,7 +571,7 @@ mod tests {
548571
));
549572

550573
assert_matches!(
551-
is_suitable_for_latest_event(&event),
574+
is_suitable_for_latest_event(&event, None),
552575
PossibleLatestEvent::NoUnsupportedMessageLikeType
553576
);
554577
}

crates/matrix-sdk-base/src/rooms/normal.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ use ruma::{
4343
join_rules::JoinRule,
4444
member::{MembershipState, RoomMemberEventContent},
4545
pinned_events::RoomPinnedEventsEventContent,
46+
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
4647
redaction::SyncRoomRedactionEvent,
4748
tombstone::RoomTombstoneEventContent,
4849
},
@@ -71,7 +72,7 @@ use crate::{
7172
read_receipts::RoomReadReceipts,
7273
store::{DynStateStore, Result as StoreResult, StateStoreExt},
7374
sync::UnreadNotificationsCount,
74-
MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships,
75+
Error, MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships,
7576
};
7677

7778
/// Indicates that a notable update of `RoomInfo` has been applied, and why.
@@ -508,6 +509,17 @@ impl Room {
508509
self.inner.read().base_info.max_power_level
509510
}
510511

512+
/// Get the current power levels of this room.
513+
pub async fn power_levels(&self) -> Result<RoomPowerLevels, Error> {
514+
Ok(self
515+
.store
516+
.get_state_event_static::<RoomPowerLevelsEventContent>(self.room_id())
517+
.await?
518+
.ok_or(Error::InsufficientData)?
519+
.deserialize()?
520+
.power_levels())
521+
}
522+
511523
/// Get the `m.room.name` of this room.
512524
///
513525
/// The returned string may be empty if the event has been redacted, or it's

crates/matrix-sdk-base/src/sliding_sync/mod.rs

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use ruma::{
3030
api::client::sync::sync_events::v3::{self, InvitedRoom, KnockedRoom},
3131
events::{
3232
room::member::MembershipState, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
33-
AnySyncStateEvent,
33+
AnySyncStateEvent, StateEventType,
3434
},
3535
serde::Raw,
3636
JsOption, OwnedRoomId, RoomId, UInt, UserId,
@@ -698,9 +698,28 @@ async fn cache_latest_events(
698698
let mut encrypted_events =
699699
Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity());
700700

701+
// Try to get room power levels from the current changes
702+
let power_levels_from_changes = || {
703+
let state_changes = changes?.state.get(room_info.room_id())?;
704+
let room_power_levels_state =
705+
state_changes.get(&StateEventType::RoomPowerLevels)?.values().next()?;
706+
match room_power_levels_state.deserialize().ok()? {
707+
AnySyncStateEvent::RoomPowerLevels(ev) => Some(ev.power_levels()),
708+
_ => None,
709+
}
710+
};
711+
712+
// If we didn't get any info, try getting it from local data
713+
let power_levels = match power_levels_from_changes() {
714+
Some(power_levels) => Some(power_levels),
715+
None => room.power_levels().await.ok(),
716+
};
717+
718+
let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref());
719+
701720
for event in events.iter().rev() {
702721
if let Ok(timeline_event) = event.raw().deserialize() {
703-
match is_suitable_for_latest_event(&timeline_event) {
722+
match is_suitable_for_latest_event(&timeline_event, power_levels_info) {
704723
PossibleLatestEvent::YesRoomMessage(_)
705724
| PossibleLatestEvent::YesPoll(_)
706725
| PossibleLatestEvent::YesCallInvite(_)
@@ -1740,10 +1759,23 @@ mod tests {
17401759
}
17411760

17421761
#[async_test]
1743-
async fn test_last_knock_member_state_event_from_sliding_sync_is_cached() {
1762+
async fn test_last_knock_event_from_sliding_sync_is_cached_if_user_has_permissions() {
1763+
let own_user_id = user_id!("@me:e.uk");
17441764
// Given a logged-in client
1745-
let client = logged_in_base_client(None).await;
1765+
let client = logged_in_base_client(Some(own_user_id)).await;
17461766
let room_id = room_id!("!r:e.uk");
1767+
1768+
// Give the current user invite or kick permissions in this room
1769+
let power_levels = json!({
1770+
"sender":"@alice:example.com",
1771+
"state_key":"",
1772+
"type":"m.room.power_levels",
1773+
"event_id": "$idb",
1774+
"origin_server_ts": 12344445,
1775+
"content":{ "invite": 100, "kick": 100, "users": { own_user_id: 100 } },
1776+
"room_id": room_id,
1777+
});
1778+
17471779
// And a knock member state event
17481780
let knock_event = json!({
17491781
"sender":"@alice:example.com",
@@ -1757,7 +1789,8 @@ mod tests {
17571789

17581790
// When the sliding sync response contains a timeline
17591791
let events = &[knock_event];
1760-
let room = room_with_timeline(events);
1792+
let mut room = room_with_timeline(events);
1793+
room.required_state.push(Raw::new(&power_levels).unwrap().cast());
17611794
let response = response_with_room(room_id, room);
17621795
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
17631796

@@ -1770,7 +1803,49 @@ mod tests {
17701803
}
17711804

17721805
#[async_test]
1773-
async fn test_last_member_state_event_from_sliding_sync_is_not_cached() {
1806+
async fn test_last_knock_event_from_sliding_sync_is_not_cached_without_permissions() {
1807+
let own_user_id = user_id!("@me:e.uk");
1808+
// Given a logged-in client
1809+
let client = logged_in_base_client(Some(own_user_id)).await;
1810+
let room_id = room_id!("!r:e.uk");
1811+
1812+
// Set the user as a user with no permission to invite or kick other users in
1813+
// this room
1814+
let power_levels = json!({
1815+
"sender":"@alice:example.com",
1816+
"state_key":"",
1817+
"type":"m.room.power_levels",
1818+
"event_id": "$idb",
1819+
"origin_server_ts": 12344445,
1820+
"content":{ "invite": 50, "kick": 50, "users": { own_user_id: 0 } },
1821+
"room_id": room_id,
1822+
});
1823+
1824+
// And a knock member state event
1825+
let knock_event = json!({
1826+
"sender":"@alice:example.com",
1827+
"state_key":"@alice:example.com",
1828+
"type":"m.room.member",
1829+
"event_id": "$ida",
1830+
"origin_server_ts": 12344446,
1831+
"content":{"membership": "knock"},
1832+
"room_id": room_id,
1833+
});
1834+
1835+
// When the sliding sync response contains a timeline
1836+
let events = &[knock_event];
1837+
let mut room = room_with_timeline(events);
1838+
room.required_state.push(Raw::new(&power_levels).unwrap().cast());
1839+
let response = response_with_room(room_id, room);
1840+
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
1841+
1842+
// Then the room doesn't hold the knock state event as the latest event
1843+
let client_room = client.get_room(room_id).expect("No room found");
1844+
assert!(client_room.latest_event().is_none());
1845+
}
1846+
1847+
#[async_test]
1848+
async fn test_last_non_knock_member_state_event_from_sliding_sync_is_not_cached() {
17741849
// Given a logged-in client
17751850
let client = logged_in_base_client(None).await;
17761851
let room_id = room_id!("!r:e.uk");

crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ use ruma::{
4646
},
4747
name::RoomNameEventContent,
4848
pinned_events::RoomPinnedEventsEventContent,
49-
power_levels::RoomPowerLevelsEventContent,
49+
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
5050
server_acl::RoomServerAclEventContent,
5151
third_party_invite::RoomThirdPartyInviteEventContent,
5252
tombstone::RoomTombstoneEventContent,
@@ -141,8 +141,9 @@ impl TimelineItemContent {
141141
/// `TimelineItemContent`.
142142
pub(crate) fn from_latest_event_content(
143143
event: AnySyncTimelineEvent,
144+
power_levels_info: Option<(&UserId, &RoomPowerLevels)>,
144145
) -> Option<TimelineItemContent> {
145-
match is_suitable_for_latest_event(&event) {
146+
match is_suitable_for_latest_event(&event, power_levels_info) {
146147
PossibleLatestEvent::YesRoomMessage(m) => {
147148
Some(Self::from_suitable_latest_event_content(m))
148149
}

0 commit comments

Comments
 (0)