matrix-sdk 0.16.0

A high level Matrix client-server library.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
use assert_matches2::assert_matches;
use matrix_sdk::{
    notification_settings::RoomNotificationMode,
    room::ThreadSubscription,
    test_utils::mocks::{MatrixMockServer, PushRuleIdSpec},
};
use matrix_sdk_test::{ALICE, JoinedRoomBuilder, async_test, event_factory::EventFactory};
use ruma::{event_id, owned_event_id, push::RuleKind, room_id};

#[async_test]
async fn test_subscribe_thread() {
    let server = MatrixMockServer::new().await;
    let client = server.client_builder().build().await;

    let room_id = room_id!("!test:example.org");
    let room = server.sync_joined_room(&client, room_id).await;

    let root_id = owned_event_id!("$root");

    server
        .mock_room_put_thread_subscription()
        .match_room_id(room_id.to_owned())
        .match_thread_id(root_id.clone())
        .ok()
        .mock_once()
        .mount()
        .await;

    // I can subscribe to a thread.
    room.subscribe_thread(root_id.clone(), Some(root_id.clone())).await.unwrap();

    server
        .mock_room_get_thread_subscription()
        .match_room_id(room_id.to_owned())
        .match_thread_id(root_id.clone())
        .ok(true)
        .mock_once()
        .mount()
        .await;

    // I can get the subscription for that same thread.
    let subscription = room.fetch_thread_subscription(root_id.clone()).await.unwrap().unwrap();
    assert_eq!(subscription, ThreadSubscription { automatic: true });

    // If I try to get a subscription for a thread event that's unknown, I get no
    // `ThreadSubscription`, not an error.
    let subscription =
        room.fetch_thread_subscription(owned_event_id!("$another_root")).await.unwrap();
    assert!(subscription.is_none());

    // I can also unsubscribe from a thread.
    server
        .mock_room_delete_thread_subscription()
        .match_room_id(room_id.to_owned())
        .match_thread_id(root_id.clone())
        .ok()
        .mock_once()
        .mount()
        .await;

    room.unsubscribe_thread(root_id.clone()).await.unwrap();

    // Now, if I retry to get the subscription for this thread, it doesn't exist
    // anymore.
    let subscription = room.fetch_thread_subscription(root_id.clone()).await.unwrap();
    assert_matches!(subscription, None);

    // Subscribing automatically to the thread may also return a `M_SKIPPED` error
    // that should be non-fatal.
    server
        .mock_room_put_thread_subscription()
        .match_room_id(room_id.to_owned())
        .match_thread_id(root_id.clone())
        .conflicting_unsubscription()
        .mock_once()
        .mount()
        .await;

    room.subscribe_thread(root_id.clone(), Some(root_id.clone())).await.unwrap();

    // And in this case, the thread is still unsubscribed.
    let subscription = room.fetch_thread_subscription(root_id).await.unwrap();
    assert_matches!(subscription, None);
}

#[async_test]
async fn test_subscribe_thread_if_needed() {
    let server = MatrixMockServer::new().await;
    let client = server.client_builder().build().await;

    let room_id = room_id!("!test:example.org");
    let room = server.sync_joined_room(&client, room_id).await;

    // If there's no prior subscription, the function `subscribe_thread_if_needed`
    // will automatically subscribe to the thread, whether the new subscription
    // is automatic or not.
    for (root_id, automatic) in [
        (owned_event_id!("$root"), None),
        (owned_event_id!("$woot"), Some(owned_event_id!("$woot"))),
    ] {
        server
            .mock_room_put_thread_subscription()
            .match_room_id(room_id.to_owned())
            .match_thread_id(root_id.clone())
            .ok()
            .mock_once()
            .mount()
            .await;

        room.subscribe_thread_if_needed(&root_id, automatic).await.unwrap();
    }

    // If there's a prior automatic subscription, the function
    // `subscribe_thread_if_needed` will only subscribe to the thread if the new
    // subscription is manual.
    {
        let root_id = owned_event_id!("$toot");

        server
            .mock_room_get_thread_subscription()
            .match_room_id(room_id.to_owned())
            .match_thread_id(root_id.clone())
            .ok(true)
            .mock_once()
            .mount()
            .await;

        server
            .mock_room_put_thread_subscription()
            .match_room_id(room_id.to_owned())
            .match_thread_id(root_id.clone())
            .ok()
            .mock_once()
            .mount()
            .await;

        room.subscribe_thread_if_needed(&root_id, None).await.unwrap();
    }

    // Otherwise, it will be a no-op.
    {
        let root_id = owned_event_id!("$foot");

        server
            .mock_room_get_thread_subscription()
            .match_room_id(room_id.to_owned())
            .match_thread_id(root_id.clone())
            .ok(true)
            .mock_once()
            .mount()
            .await;

        room.subscribe_thread_if_needed(&root_id, Some(owned_event_id!("$foot"))).await.unwrap();
    }

    // The function `subscribe_thread_if_needed` is a no-op if there's a prior
    // manual subscription, whether the new subscription is automatic or not.
    for (root_id, automatic) in [
        (owned_event_id!("$root"), None),
        (owned_event_id!("$woot"), Some(owned_event_id!("$woot"))),
    ] {
        server
            .mock_room_get_thread_subscription()
            .match_room_id(room_id.to_owned())
            .match_thread_id(root_id.clone())
            .ok(false)
            .mock_once()
            .mount()
            .await;

        // No-op! (The PUT endpoint hasn't been mocked, so this would result in a 404 if
        // it were trying to hit it.)
        room.subscribe_thread_if_needed(&root_id, automatic).await.unwrap();
    }
}

#[async_test]
async fn test_thread_push_rule_is_triggered_for_subscribed_threads() {
    // This test checks that the evaluation of push rules for threads will correctly
    // call `Room::fetch_thread_subscription` for threads.

    let server = MatrixMockServer::new().await;
    let client = server
        .client_builder()
        .on_builder(|builder| {
            builder.with_threading_support(matrix_sdk::ThreadingSupport::Enabled {
                with_subscriptions: true,
            })
        })
        .build()
        .await;

    let room_id = room_id!("!test:example.org");
    let room = server.sync_joined_room(&client, room_id).await;

    let thread_root_id = owned_event_id!("$root");
    let f = EventFactory::new().room(room_id).sender(*ALICE);

    // Make it so that the client has a member event for the current user.
    server
        .sync_room(
            &client,
            JoinedRoomBuilder::new(room_id).add_state_event(f.member(client.user_id().unwrap())),
        )
        .await;

    // Sanity check: we can get a push context.
    let push_context = room
        .push_context()
        .await
        .expect("getting a push context works")
        .expect("the push context should exist");

    // Mock the thread subscriptions endpoint so the user is subscribed to the
    // thread.
    server
        .mock_room_get_thread_subscription()
        .match_room_id(room_id.to_owned())
        .match_thread_id(thread_root_id.clone())
        .ok(true)
        .mock_once()
        .mount()
        .await;

    // Given an event in the thread I'm subscribed to, the push rule evaluation will
    // trigger the thread subscription endpoint,
    let event =
        f.text_msg("hello to you too!").in_thread(&thread_root_id, &thread_root_id).into_raw_sync();

    // And the event will trigger a notification.
    let actions = push_context.for_event(&event).await;
    assert!(actions.iter().any(|action| action.should_notify()));

    // But for a thread that I haven't subscribed to (i.e. the endpoint returns 404,
    // because it's not set up), no actions are returned.
    let another_thread_root_id = event_id!("$another_root");
    let event = f
        .text_msg("bonjour à vous également !")
        .in_thread(another_thread_root_id, another_thread_root_id)
        .into_raw_sync();

    let actions = push_context.for_event(&event).await;
    assert!(actions.is_empty());
}

#[async_test]
async fn test_thread_push_rules_and_notification_modes() {
    // This test checks that, given a combination of a global notification mode, and
    // a room notification mode, we do get notifications for thread events according
    // to the subscriptions.

    let server = MatrixMockServer::new().await;
    let client = server
        .client_builder()
        .on_builder(|builder| {
            builder.with_threading_support(matrix_sdk::ThreadingSupport::Enabled {
                with_subscriptions: true,
            })
        })
        .build()
        .await;

    let room_id = room_id!("!test:example.org");
    let f = EventFactory::new().room(room_id).sender(*ALICE);
    // Make it so that the client has a member event for the current user.
    let room = server
        .sync_room(
            &client,
            JoinedRoomBuilder::new(room_id).add_state_event(f.member(client.user_id().unwrap())),
        )
        .await;

    // Sanity check: we can get a push context.
    room.push_context()
        .await
        .expect("getting a push context works")
        .expect("the push context should exist");

    // Mock push rules endpoints, allowing any modification to any rule.
    server.mock_set_push_rules_actions(RuleKind::Underride, PushRuleIdSpec::Any).ok().mount().await;
    server.mock_set_push_rules_actions(RuleKind::Room, PushRuleIdSpec::Any).ok().mount().await;
    server.mock_set_push_rules(RuleKind::Underride, PushRuleIdSpec::Any).ok().mount().await;
    server.mock_set_push_rules(RuleKind::Room, PushRuleIdSpec::Any).ok().mount().await;
    server.mock_set_push_rules(RuleKind::Override, PushRuleIdSpec::Any).ok().mount().await;
    server.mock_delete_push_rules(RuleKind::Room, PushRuleIdSpec::Any).ok().mount().await;
    server.mock_delete_push_rules(RuleKind::Override, PushRuleIdSpec::Any).ok().mount().await;

    // Given a thread I'm subscribed to,
    let thread_root_id = owned_event_id!("$root");

    server
        .mock_room_get_thread_subscription()
        .match_room_id(room_id.to_owned())
        .match_thread_id(thread_root_id.clone())
        .ok(true)
        .mount()
        .await;

    // Given an event in the thread I'm subscribed to,
    let event =
        f.text_msg("hello to you too!").in_thread(&thread_root_id, &thread_root_id).into_raw_sync();

    // If global mode = AllMessages,
    let settings = client.notification_settings().await;

    let is_encrypted = false;
    let is_one_to_one = false;
    settings
        .set_default_room_notification_mode(
            is_encrypted.into(),
            is_one_to_one.into(),
            RoomNotificationMode::AllMessages,
        )
        .await
        .unwrap();

    // If room mode = AllMessages,
    settings.set_room_notification_mode(room_id, RoomNotificationMode::AllMessages).await.unwrap();
    // Ack the push rules change via sync, for it to be applied in the push context.
    let ruleset = settings.ruleset().await;
    server
        .mock_sync()
        .ok_and_run(&client, |builder| {
            builder.add_global_account_data(f.push_rules(ruleset));
        })
        .await;

    // The thread event will trigger a notification.
    let actions = room.push_context().await.unwrap().unwrap().traced_for_event(&event).await;
    assert!(actions.iter().any(|action| action.should_notify()));

    // If room mode = mentions and keywords only,
    settings
        .set_room_notification_mode(room_id, RoomNotificationMode::MentionsAndKeywordsOnly)
        .await
        .unwrap();
    let ruleset = settings.ruleset().await;
    server
        .mock_sync()
        .ok_and_run(&client, |builder| {
            builder.add_global_account_data(f.push_rules(ruleset));
        })
        .await;

    // The thread event will trigger a notification.
    let actions = room.push_context().await.unwrap().unwrap().traced_for_event(&event).await;
    assert!(actions.iter().any(|action| action.should_notify()));

    // If room mode = mute,
    settings.set_room_notification_mode(room_id, RoomNotificationMode::Mute).await.unwrap();
    let ruleset = settings.ruleset().await;
    server
        .mock_sync()
        .ok_and_run(&client, |builder| {
            builder.add_global_account_data(f.push_rules(ruleset));
        })
        .await;

    // The thread event will not trigger a notification, as the room has been muted.
    let actions = room.push_context().await.unwrap().unwrap().traced_for_event(&event).await;
    assert!(!actions.iter().any(|action| action.should_notify()));

    // Now, if global mode = mentions only,
    settings
        .set_default_room_notification_mode(
            is_encrypted.into(),
            is_one_to_one.into(),
            RoomNotificationMode::MentionsAndKeywordsOnly,
        )
        .await
        .unwrap();

    // If room mode = AllMessages,
    settings.set_room_notification_mode(room_id, RoomNotificationMode::AllMessages).await.unwrap();
    // Ack the push rules change via sync, for it to be applied in the push context.
    let ruleset = settings.ruleset().await;
    server
        .mock_sync()
        .ok_and_run(&client, |builder| {
            builder.add_global_account_data(f.push_rules(ruleset));
        })
        .await;

    // The thread event will trigger a notification.
    let actions = room.push_context().await.unwrap().unwrap().traced_for_event(&event).await;
    assert!(actions.iter().any(|action| action.should_notify()));

    // If room mode = Mentions only,
    settings
        .set_room_notification_mode(room_id, RoomNotificationMode::MentionsAndKeywordsOnly)
        .await
        .unwrap();
    // Ack the push rules change via sync, for it to be applied in the push context.
    let ruleset = settings.ruleset().await;
    server
        .mock_sync()
        .ok_and_run(&client, |builder| {
            builder.add_global_account_data(f.push_rules(ruleset));
        })
        .await;

    // The thread event will trigger a notification.
    let actions = room.push_context().await.unwrap().unwrap().traced_for_event(&event).await;
    assert!(actions.iter().any(|action| action.should_notify()));

    // If room mode = mute,
    settings.set_room_notification_mode(room_id, RoomNotificationMode::Mute).await.unwrap();
    // Ack the push rules change via sync, for it to be applied in the push context.
    let ruleset = settings.ruleset().await;
    server
        .mock_sync()
        .ok_and_run(&client, |builder| {
            builder.add_global_account_data(f.push_rules(ruleset));
        })
        .await;

    // The thread event will not trigger a notification, as the room has been muted.
    let actions = room.push_context().await.unwrap().unwrap().traced_for_event(&event).await;
    assert!(!actions.iter().any(|action| action.should_notify()));
}