Skip to content

Commit a3725b6

Browse files
committed
fix: Do not retrieve notification events sent by ignored users
1 parent f0c7370 commit a3725b6

File tree

6 files changed

+350
-18
lines changed

6 files changed

+350
-18
lines changed

crates/matrix-sdk-ui/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file.
1818
`Timeline` where items could be inserted in the wrong _regions_, such as
1919
a remote timeline item before the `TimelineStart` virtual timeline item.
2020
([#5000](https://github.com/matrix-org/matrix-rust-sdk/pull/5000))
21+
- `NotificationClient` will filter out events sent by ignored users on `get_notification` and `get_notifications`. ([#5081](https://github.com/matrix-org/matrix-rust-sdk/pull/5081))
2122

2223
### Features
2324

crates/matrix-sdk-ui/src/notification_client.rs

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,7 @@ impl NotificationClient {
681681
if !should_notify {
682682
result.add_notification(event_id, NotificationStatus::EventFilteredOut);
683683
} else {
684-
let notification_status = NotificationItem::new(
684+
let notification_result = NotificationItem::new(
685685
&room,
686686
raw_event,
687687
push_actions.as_deref(),
@@ -690,10 +690,23 @@ impl NotificationClient {
690690
.await
691691
.map(|event| NotificationStatus::Event(Box::new(event)));
692692

693-
match notification_status {
694-
Ok(notification_item) => {
695-
result.add_notification(event_id, notification_item);
696-
}
693+
match notification_result {
694+
Ok(notification_status) => match notification_status {
695+
NotificationStatus::Event(event) => {
696+
if self.client.is_user_ignored(event.event.sender()) {
697+
result.add_notification(
698+
event_id,
699+
NotificationStatus::EventFilteredOut,
700+
);
701+
} else {
702+
result.add_notification(
703+
event_id,
704+
NotificationStatus::Event(event),
705+
);
706+
}
707+
}
708+
_ => result.add_notification(event_id, notification_status),
709+
},
697710
Err(error) => {
698711
result.mark_fetching_notification_failed(event_id, error);
699712
}
@@ -747,15 +760,19 @@ impl NotificationClient {
747760
}
748761

749762
let push_actions = timeline_event.push_actions.take();
750-
Ok(Some(
751-
NotificationItem::new(
752-
&room,
753-
RawNotificationEvent::Timeline(timeline_event.into_raw()),
754-
push_actions.as_deref(),
755-
state_events,
756-
)
757-
.await?,
758-
))
763+
let notification_item = NotificationItem::new(
764+
&room,
765+
RawNotificationEvent::Timeline(timeline_event.into_raw()),
766+
push_actions.as_deref(),
767+
state_events,
768+
)
769+
.await?;
770+
771+
if self.client.is_user_ignored(notification_item.event.sender()) {
772+
Ok(None)
773+
} else {
774+
Ok(Some(notification_item))
775+
}
759776
}
760777
}
761778

crates/matrix-sdk-ui/tests/integration/notification_client.rs

Lines changed: 230 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ use std::{
44
};
55

66
use assert_matches::assert_matches;
7-
use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server};
7+
use matrix_sdk::{
8+
config::SyncSettings,
9+
test_utils::{logged_in_client_with_server, mocks::MatrixMockServer},
10+
};
811
use matrix_sdk_test::{
9-
async_test, event_factory::EventFactory, mocks::mock_encryption_state, JoinedRoomBuilder,
10-
StateTestEvent, SyncResponseBuilder,
12+
async_test, mocks::mock_encryption_state, JoinedRoomBuilder, StateTestEvent,
13+
SyncResponseBuilder,
1114
};
1215
use matrix_sdk_ui::{
1316
notification_client::{
@@ -1068,3 +1071,227 @@ async fn test_notification_client_mixed() {
10681071
assert_eq!(item.room_computed_display_name, sender_display_name);
10691072
assert_eq!(item.is_noisy, Some(false));
10701073
}
1074+
1075+
#[async_test]
1076+
async fn test_notification_client_sliding_sync_filters_out_events_from_ignored_users() {
1077+
let server = MatrixMockServer::new().await;
1078+
let client = server.client_builder().build().await;
1079+
1080+
let sender = user_id!("@user:example.org");
1081+
let my_user_id = client.user_id().unwrap().to_owned();
1082+
1083+
let room_id = room_id!("!a98sd12bjh:example.org");
1084+
let room_name = "The Maltese Falcon";
1085+
let sender_display_name = "John Mastodon";
1086+
let sender_avatar_url = "https://example.org/avatar.jpeg";
1087+
let event_id = event_id!("$example_event_id");
1088+
1089+
let raw_event = EventFactory::new()
1090+
.room(room_id)
1091+
.sender(sender)
1092+
.text_msg("Heya")
1093+
.event_id(event_id)
1094+
.into_raw_sync();
1095+
1096+
let pos = Mutex::new(0);
1097+
Mock::given(SlidingSyncMatcher)
1098+
.respond_with(move |request: &Request| {
1099+
let partial_request: PartialSlidingSyncRequest = request.body_json().unwrap();
1100+
// Repeat the transaction id in the response, to validate sticky parameters.
1101+
let mut pos = pos.lock().unwrap();
1102+
*pos += 1;
1103+
let pos_as_str = (*pos).to_string();
1104+
ResponseTemplate::new(200).set_body_json(json!({
1105+
"txn_id": partial_request.txn_id,
1106+
"pos": pos_as_str,
1107+
"rooms": {
1108+
room_id: {
1109+
"name": room_name,
1110+
"initial": true,
1111+
1112+
"required_state": [
1113+
// Sender's member information.
1114+
{
1115+
"content": {
1116+
"avatar_url": sender_avatar_url,
1117+
"displayname": sender_display_name,
1118+
"membership": "join"
1119+
},
1120+
"room_id": room_id,
1121+
"event_id": "$151800140517rfvjc:example.org",
1122+
"membership": "join",
1123+
"origin_server_ts": 151800140,
1124+
"sender": sender,
1125+
"state_key": sender,
1126+
"type": "m.room.member",
1127+
"unsigned": {
1128+
"age": 2970366,
1129+
}
1130+
},
1131+
1132+
// Own member information.
1133+
{
1134+
"content": {
1135+
"avatar_url": null,
1136+
"displayname": "My Self",
1137+
"membership": "join"
1138+
},
1139+
"room_id": room_id,
1140+
"event_id": "$151800140517rflkc:example.org",
1141+
"membership": "join",
1142+
"origin_server_ts": 151800140,
1143+
"sender": my_user_id.clone(),
1144+
"state_key": my_user_id,
1145+
"type": "m.room.member",
1146+
"unsigned": {
1147+
"age": 2970366,
1148+
}
1149+
},
1150+
1151+
// Power levels.
1152+
{
1153+
"content": {
1154+
"ban": 50,
1155+
"events": {
1156+
"m.room.avatar": 50,
1157+
"m.room.canonical_alias": 50,
1158+
"m.room.history_visibility": 100,
1159+
"m.room.name": 50,
1160+
"m.room.power_levels": 100,
1161+
"m.room.message": 25,
1162+
},
1163+
"events_default": 0,
1164+
"invite": 0,
1165+
"kick": 50,
1166+
"redact": 50,
1167+
"state_default": 50,
1168+
"users": {
1169+
"@example:localhost": 100,
1170+
sender: 0,
1171+
},
1172+
"users_default": 0,
1173+
},
1174+
"event_id": "$15139375512JaHAW:localhost",
1175+
"origin_server_ts": 151393755,
1176+
"sender": "@example:localhost",
1177+
"state_key": "",
1178+
"type": "m.room.power_levels",
1179+
"unsigned": {
1180+
"age": 703422,
1181+
},
1182+
},
1183+
],
1184+
1185+
"timeline": [
1186+
raw_event,
1187+
]
1188+
}
1189+
},
1190+
1191+
"extensions": {
1192+
"account_data": {
1193+
"global": [{
1194+
"type": "m.ignored_user_list",
1195+
"content": {
1196+
"ignored_users": { sender: {} }
1197+
}
1198+
}]
1199+
}
1200+
}
1201+
}))
1202+
})
1203+
.mount(server.server())
1204+
.await;
1205+
1206+
let dummy_sync_service = Arc::new(SyncService::builder(client.clone()).build().await.unwrap());
1207+
let process_setup =
1208+
NotificationProcessSetup::SingleProcess { sync_service: dummy_sync_service };
1209+
let notification_client = NotificationClient::new(client, process_setup).await.unwrap();
1210+
let mut result = notification_client
1211+
.get_notifications_with_sliding_sync(&[NotificationItemsRequest {
1212+
room_id: room_id.to_owned(),
1213+
event_ids: vec![event_id.to_owned()],
1214+
}])
1215+
.await
1216+
.unwrap();
1217+
1218+
let Some(Ok(item)) = result.remove(event_id) else {
1219+
panic!("fetching notification for {event_id} failed");
1220+
};
1221+
let NotificationStatus::EventFilteredOut = item else {
1222+
panic!("notification for {event_id} was not filtered out");
1223+
};
1224+
}
1225+
1226+
#[async_test]
1227+
async fn test_notification_client_context_filters_out_events_from_ignored_users() {
1228+
let server = MatrixMockServer::new().await;
1229+
let client = server.client_builder().build().await;
1230+
1231+
let sender = user_id!("@user:example.org");
1232+
let room_id = room_id!("!a98sd12bjh:example.org");
1233+
let event_id = event_id!("$example_event_id");
1234+
1235+
server.sync_joined_room(&client, room_id).await;
1236+
1237+
// Add mock for sliding sync so we get the ignored user list from its account
1238+
// data
1239+
let pos = Mutex::new(0);
1240+
Mock::given(SlidingSyncMatcher)
1241+
.respond_with(move |request: &Request| {
1242+
let partial_request: PartialSlidingSyncRequest = request.body_json().unwrap();
1243+
// Repeat the transaction id in the response, to validate sticky parameters.
1244+
let mut pos = pos.lock().unwrap();
1245+
*pos += 1;
1246+
let pos_as_str = (*pos).to_string();
1247+
ResponseTemplate::new(200).set_body_json(json!({
1248+
"txn_id": partial_request.txn_id,
1249+
"pos": pos_as_str,
1250+
"rooms": {},
1251+
1252+
"extensions": {
1253+
"account_data": {
1254+
"global": [{
1255+
"type": "m.ignored_user_list",
1256+
"content": {
1257+
"ignored_users": { sender: {} }
1258+
}
1259+
}]
1260+
}
1261+
}
1262+
}))
1263+
})
1264+
.mount(server.server())
1265+
.await;
1266+
1267+
let event = EventFactory::new()
1268+
.room(room_id)
1269+
.sender(sender)
1270+
.text_msg("Heya")
1271+
.event_id(event_id)
1272+
.into_event();
1273+
1274+
// Mock the /context response
1275+
server.mock_room_event_context().ok(event, "start", "end").mock_once().mount().await;
1276+
1277+
let dummy_sync_service = Arc::new(SyncService::builder(client.clone()).build().await.unwrap());
1278+
let process_setup =
1279+
NotificationProcessSetup::SingleProcess { sync_service: dummy_sync_service };
1280+
let notification_client = NotificationClient::new(client, process_setup).await.unwrap();
1281+
1282+
// Call sync first so we get the list of ignored users in the notification
1283+
// client This should still work in a real life usage
1284+
let _ = notification_client
1285+
.get_notifications_with_sliding_sync(&[NotificationItemsRequest {
1286+
room_id: room_id.to_owned(),
1287+
event_ids: vec![event_id.to_owned()],
1288+
}])
1289+
.await;
1290+
1291+
// If the event is not found even though there was a mocked response for it, it
1292+
// was discarded as expected
1293+
let result =
1294+
notification_client.get_notification_with_context(room_id, event_id).await.unwrap();
1295+
1296+
assert!(result.is_none());
1297+
}

crates/matrix-sdk/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ All notable changes to this project will be documented in this file.
3333
- `Room::send_single_receipt()` and `Room::send_multiple_receipts()` now also unset the unread
3434
flag of the room if an unthreaded read receipt is sent.
3535
([#5055](https://github.com/matrix-org/matrix-rust-sdk/pull/5055))
36+
- `Client::is_user_ignored(&UserId)` can be used to check if a user is currently ignored. ([#5081](https://github.com/matrix-org/matrix-rust-sdk/pull/5081))
3637

3738
### Bug fixes
3839

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2541,6 +2541,13 @@ impl Client {
25412541
let base_room = self.inner.base_client.room_knocked(&response.room_id).await?;
25422542
Ok(Room::new(self.clone(), base_room))
25432543
}
2544+
2545+
/// Checks whether the provided `user_id` belongs to an ignored user.
2546+
pub fn is_user_ignored(&self, user_id: &UserId) -> bool {
2547+
let raw_user_id = user_id.to_string();
2548+
let current_ignored_user_list = self.subscribe_to_ignore_user_list_changes().get();
2549+
current_ignored_user_list.contains(&raw_user_id)
2550+
}
25442551
}
25452552

25462553
/// A weak reference to the inner client, useful when trying to get a handle
@@ -2608,7 +2615,7 @@ pub(crate) mod tests {
26082615
ignored_user_list::IgnoredUserListEventContent,
26092616
media_preview_config::{InviteAvatars, MediaPreviewConfigEventContent, MediaPreviews},
26102617
},
2611-
owned_room_id, room_alias_id, room_id, RoomId, ServerName, UserId,
2618+
owned_room_id, room_alias_id, room_id, user_id, RoomId, ServerName, UserId,
26122619
};
26132620
use serde_json::json;
26142621
use stream_assert::{assert_next_matches, assert_pending};
@@ -3445,4 +3452,25 @@ pub(crate) mod tests {
34453452
assert_eq!(initial_value.invite_avatars, InviteAvatars::On);
34463453
assert_eq!(initial_value.media_previews, MediaPreviews::On);
34473454
}
3455+
3456+
#[async_test]
3457+
async fn test_is_user_ignored() {
3458+
let server = MatrixMockServer::new().await;
3459+
let client = server.client_builder().build().await;
3460+
3461+
let user_id = user_id!("@alice:host");
3462+
assert!(!client.is_user_ignored(user_id));
3463+
3464+
server
3465+
.mock_sync()
3466+
.ok_and_run(&client, |builder| {
3467+
builder.add_global_account_data_event(GlobalAccountDataTestEvent::Custom(json!({
3468+
"type": "m.ignored_user_list",
3469+
"content": IgnoredUserListEventContent::users([user_id.to_owned()])
3470+
})));
3471+
})
3472+
.await;
3473+
3474+
assert!(client.is_user_ignored(user_id));
3475+
}
34483476
}

0 commit comments

Comments
 (0)