| 1 | use matrix_sdk::{ |
| 2 | Room, |
| 3 | ruma::{ |
| 4 | UInt, |
| 5 | api::client::push::get_notifications::v3, |
| 6 | event_id, |
| 7 | events::{AnySyncTimelineEvent, Mentions}, |
| 8 | owned_user_id, |
| 9 | push::{Action, EventMatchConditionData, NewConditionalPushRule, PushCondition, Ruleset}, |
| 10 | room_id, |
| 11 | serde::Raw, |
| 12 | }, |
| 13 | test_utils::mocks::MatrixMockServer, |
| 14 | }; |
| 15 | use matrix_sdk_test::{JoinedRoomBuilder, event_factory::EventFactory}; |
| 16 | use serde_json::json; |
| 17 | use std::time::Duration; |
| 18 | use wiremock::{ |
| 19 | Mock, ResponseTemplate, |
| 20 | matchers::{method, path, query_param}, |
| 21 | }; |
| 22 | |
| 23 | fn explicit_mention( |
| 24 | room_id: &matrix_sdk::ruma::RoomId, |
| 25 | event_id: &matrix_sdk::ruma::EventId, |
| 26 | sender: &matrix_sdk::ruma::UserId, |
| 27 | mentioned: &matrix_sdk::ruma::UserId, |
| 28 | ) -> Raw<AnySyncTimelineEvent> { |
| 29 | EventFactory::new() |
| 30 | .room(room_id) |
| 31 | .text_msg("hello") |
| 32 | .mentions(Mentions::with_user_ids([mentioned.to_owned()])) |
| 33 | .event_id(event_id) |
| 34 | .sender(sender) |
| 35 | .into_raw_sync() |
| 36 | } |
| 37 | |
| 38 | fn assert_mention_actions(actions: &[Action]) { |
| 39 | assert!( |
| 40 | actions |
| 41 | .iter() |
| 42 | .any(|action| matches!(action, Action::Notify)) |
| 43 | ); |
| 44 | assert!(actions.iter().any(Action::is_highlight)); |
| 45 | } |
| 46 | |
| 47 | async fn sync_room_with_mention( |
| 48 | server: &MatrixMockServer, |
| 49 | client: &matrix_sdk::Client, |
| 50 | room_id: &matrix_sdk::ruma::RoomId, |
| 51 | event_id: &matrix_sdk::ruma::EventId, |
| 52 | ) { |
| 53 | let user_id = client.user_id().unwrap(); |
| 54 | let factory = EventFactory::new().room(room_id); |
| 55 | let sender = owned_user_id!("@alice:localhost"); |
| 56 | let event = explicit_mention(room_id, event_id, &sender, user_id); |
| 57 | let rules = Ruleset::server_default(user_id); |
| 58 | |
| 59 | server |
| 60 | .mock_sync() |
| 61 | .ok_and_run(client, |builder| { |
| 62 | builder.add_global_account_data(factory.push_rules(rules)); |
| 63 | builder.add_joined_room( |
| 64 | JoinedRoomBuilder::new(room_id) |
| 65 | .add_state_event(factory.member(user_id)) |
| 66 | .add_timeline_event(event), |
| 67 | ); |
| 68 | }) |
| 69 | .await; |
| 70 | } |
| 71 | |
| 72 | async fn actions_for(room: &Room, event: &Raw<AnySyncTimelineEvent>) -> Option<Vec<Action>> { |
| 73 | room.event_push_actions(event).await.unwrap() |
| 74 | } |
| 75 | |
| 76 | #[tokio::test] |
| 77 | async fn joined_room_update_exposes_mention_actions_without_opening_a_ui_timeline() { |
| 78 | let server = MatrixMockServer::new().await; |
| 79 | let client = server.client_builder().build().await; |
| 80 | let room_id = room_id!("!mentions:localhost"); |
| 81 | let event_id = event_id!("$mention:localhost"); |
| 82 | let mut updates = client.subscribe_to_all_room_updates(); |
| 83 | |
| 84 | sync_room_with_mention(&server, &client, room_id, event_id).await; |
| 85 | |
| 86 | let room_updates = tokio::time::timeout(Duration::from_secs(2), updates.recv()) |
| 87 | .await |
| 88 | .unwrap() |
| 89 | .unwrap(); |
| 90 | let joined = room_updates |
| 91 | .joined |
| 92 | .get(room_id) |
| 93 | .expect("the joined room update was absent"); |
| 94 | let event = joined |
| 95 | .timeline |
| 96 | .events |
| 97 | .iter() |
| 98 | .find(|event| event.event_id().as_deref().map(|id| id.as_str()) == Some(event_id.as_str())) |
| 99 | .expect("the explicit mention was absent from the room update"); |
| 100 | let event_json: serde_json::Value = |
| 101 | serde_json::from_str(event.kind.raw().json().get()).unwrap(); |
| 102 | |
| 103 | assert_eq!( |
| 104 | event_json["content"]["m.mentions"]["user_ids"], |
| 105 | json!([client.user_id().unwrap().as_str()]) |
| 106 | ); |
| 107 | assert_mention_actions( |
| 108 | event |
| 109 | .push_actions() |
| 110 | .expect("push actions were not computed"), |
| 111 | ); |
| 112 | } |
| 113 | |
| 114 | #[tokio::test] |
| 115 | async fn event_cache_generic_update_finds_the_mention_without_opening_a_ui_timeline() { |
| 116 | let server = MatrixMockServer::new().await; |
| 117 | let client = server.client_builder().build().await; |
| 118 | let room_id = room_id!("!cached-mentions:localhost"); |
| 119 | let event_id = event_id!("$cached-mention:localhost"); |
| 120 | client.event_cache().subscribe().unwrap(); |
| 121 | let mut updates = client.event_cache().subscribe_to_room_generic_updates(); |
| 122 | |
| 123 | sync_room_with_mention(&server, &client, room_id, event_id).await; |
| 124 | |
| 125 | let update = tokio::time::timeout(Duration::from_secs(2), updates.recv()) |
| 126 | .await |
| 127 | .unwrap() |
| 128 | .unwrap(); |
| 129 | assert_eq!(update.room_id, room_id); |
| 130 | let room = client.get_room(room_id).unwrap(); |
| 131 | let (event_cache, _drop_handles) = room.event_cache().await.unwrap(); |
| 132 | let event = event_cache.find_event(event_id).await.unwrap().unwrap(); |
| 133 | assert_eq!( |
| 134 | event.event_id().as_deref().map(|id| id.as_str()), |
| 135 | Some(event_id.as_str()) |
| 136 | ); |
| 137 | assert_mention_actions( |
| 138 | event |
| 139 | .push_actions() |
| 140 | .expect("cached push actions were absent"), |
| 141 | ); |
| 142 | } |
| 143 | |
| 144 | #[tokio::test] |
| 145 | async fn ignored_sender_and_muted_room_actions_remain_sdk_push_rule_results() { |
| 146 | let server = MatrixMockServer::new().await; |
| 147 | let client = server.client_builder().build().await; |
| 148 | let ignored_room_id = room_id!("!ignored-sender:localhost"); |
| 149 | let muted_room_id = room_id!("!muted-room:localhost"); |
| 150 | let ignored_sender = owned_user_id!("@ignored:localhost"); |
| 151 | let own_user = client.user_id().unwrap(); |
| 152 | let factory = EventFactory::new(); |
| 153 | let ignored_event_id = event_id!("$ignored-mention:localhost"); |
| 154 | let muted_event_id = event_id!("$muted-message:localhost"); |
| 155 | let ignored_event = |
| 156 | explicit_mention(ignored_room_id, ignored_event_id, &ignored_sender, own_user); |
| 157 | let sender = owned_user_id!("@alice:localhost"); |
| 158 | let muted_event = explicit_mention(muted_room_id, muted_event_id, &sender, own_user); |
| 159 | let mut rules = Ruleset::server_default(own_user); |
| 160 | rules.override_.shift_insert( |
| 161 | 1, |
| 162 | NewConditionalPushRule::new( |
| 163 | "mute-room".to_owned(), |
| 164 | vec![PushCondition::EventMatch(EventMatchConditionData::new( |
| 165 | "room_id".to_owned(), |
| 166 | muted_room_id.to_string(), |
| 167 | ))], |
| 168 | Vec::new(), |
| 169 | ) |
| 170 | .into(), |
| 171 | ); |
| 172 | |
| 173 | server |
| 174 | .mock_sync() |
| 175 | .ok_and_run(&client, |builder| { |
| 176 | builder.add_global_account_data(factory.ignored_user_list([ignored_sender.clone()])); |
| 177 | builder.add_global_account_data(factory.push_rules(rules)); |
| 178 | builder.add_joined_room( |
| 179 | JoinedRoomBuilder::new(ignored_room_id) |
| 180 | .add_state_event(EventFactory::new().room(ignored_room_id).member(own_user)) |
| 181 | .add_timeline_event(ignored_event.clone()), |
| 182 | ); |
| 183 | builder.add_joined_room( |
| 184 | JoinedRoomBuilder::new(muted_room_id) |
| 185 | .add_state_event(EventFactory::new().room(muted_room_id).member(own_user)) |
| 186 | .add_timeline_event(muted_event.clone()), |
| 187 | ); |
| 188 | }) |
| 189 | .await; |
| 190 | |
| 191 | assert!(client.is_user_ignored(&ignored_sender).await); |
| 192 | let ignored_room = client.get_room(ignored_room_id).unwrap(); |
| 193 | let ignored_actions = actions_for(&ignored_room, &ignored_event) |
| 194 | .await |
| 195 | .expect("the ignored sender's push actions could not be computed"); |
| 196 | assert_mention_actions(&ignored_actions); |
| 197 | |
| 198 | let muted_room = client.get_room(muted_room_id).unwrap(); |
| 199 | let muted_actions = actions_for(&muted_room, &muted_event) |
| 200 | .await |
| 201 | .expect("the muted room's push actions could not be computed"); |
| 202 | assert!(muted_actions.is_empty()); |
| 203 | } |
| 204 | |
| 205 | #[tokio::test] |
| 206 | async fn notifications_v3_request_round_trips_opaque_pagination() { |
| 207 | let server = MatrixMockServer::new().await; |
| 208 | let client = server.client_builder().build().await; |
| 209 | let cursor = "opaque / cursor?x=1&y=2"; |
| 210 | let next_cursor = "next / cursor?x=3&y=4"; |
| 211 | |
| 212 | Mock::given(method("GET")) |
| 213 | .and(path("/_matrix/client/v3/notifications")) |
| 214 | .and(query_param("from", cursor)) |
| 215 | .and(query_param("limit", "8")) |
| 216 | .and(query_param("only", "highlight")) |
| 217 | .respond_with(ResponseTemplate::new(200).set_body_json(json!({ |
| 218 | "next_token": next_cursor, |
| 219 | "notifications": [], |
| 220 | }))) |
| 221 | .expect(1) |
| 222 | .mount(server.server()) |
| 223 | .await; |
| 224 | |
| 225 | let mut request = v3::Request::new(); |
| 226 | request.from = Some(cursor.to_owned()); |
| 227 | request.limit = Some(UInt::new(8).unwrap()); |
| 228 | request.only = Some("highlight".to_owned()); |
| 229 | let response = client.send(request).await.unwrap(); |
| 230 | |
| 231 | assert_eq!(response.next_token.as_deref(), Some(next_cursor)); |
| 232 | } |