matrix_sdk/notification_settings/
rule_commands.rs

1use ruma::{
2    RoomId,
3    push::{
4        Action, NewPushRule, PredefinedContentRuleId, PredefinedOverrideRuleId,
5        RemovePushRuleError, RuleKind, Ruleset,
6    },
7};
8
9use super::command::Command;
10use crate::NotificationSettingsError;
11
12/// A `RuleCommand` allows to generate a list of `Command` needed to modify a
13/// `Ruleset`
14#[derive(Clone, Debug)]
15pub(crate) struct RuleCommands {
16    pub(crate) commands: Vec<Command>,
17    pub(crate) rules: Ruleset,
18}
19
20impl RuleCommands {
21    pub(crate) fn new(rules: Ruleset) -> Self {
22        RuleCommands { commands: vec![], rules }
23    }
24
25    /// Insert a new rule
26    pub(crate) fn insert_rule(
27        &mut self,
28        kind: RuleKind,
29        room_id: &RoomId,
30        notify: bool,
31    ) -> Result<(), NotificationSettingsError> {
32        let command = match kind {
33            RuleKind::Room => Command::SetRoomPushRule { room_id: room_id.to_owned(), notify },
34            RuleKind::Override => Command::SetOverridePushRule {
35                rule_id: room_id.to_string(),
36                room_id: room_id.to_owned(),
37                notify,
38            },
39            _ => {
40                return Err(NotificationSettingsError::InvalidParameter(
41                    "cannot insert a rule for this kind.".to_owned(),
42                ));
43            }
44        };
45
46        self.rules.insert(command.to_push_rule()?, None, None)?;
47        self.commands.push(command);
48
49        Ok(())
50    }
51
52    /// Insert a new rule for a keyword.
53    pub(crate) fn insert_keyword_rule(
54        &mut self,
55        keyword: String,
56    ) -> Result<(), NotificationSettingsError> {
57        let command = Command::SetKeywordPushRule { keyword };
58
59        self.rules.insert(command.to_push_rule()?, None, None)?;
60        self.commands.push(command);
61
62        Ok(())
63    }
64
65    /// Insert a new custom rule
66    pub(crate) fn insert_custom_rule(
67        &mut self,
68        rule: NewPushRule,
69    ) -> Result<(), NotificationSettingsError> {
70        let command = Command::SetCustomPushRule { rule: rule.clone() };
71
72        self.rules.insert(rule, None, None)?;
73        self.commands.push(command);
74
75        Ok(())
76    }
77
78    /// Delete a rule
79    pub(crate) fn delete_rule(
80        &mut self,
81        kind: RuleKind,
82        rule_id: String,
83    ) -> Result<(), RemovePushRuleError> {
84        self.rules.remove(kind.clone(), &rule_id)?;
85        self.commands.push(Command::DeletePushRule { kind, rule_id });
86
87        Ok(())
88    }
89
90    /// Enable or disable the given rule.
91    ///
92    /// Will return an error if the rule does not exist.
93    fn set_enabled_internal(
94        &mut self,
95        kind: RuleKind,
96        rule_id: &str,
97        enabled: bool,
98    ) -> Result<(), NotificationSettingsError> {
99        self.rules
100            .set_enabled(kind.clone(), rule_id, enabled)
101            .map_err(|_| NotificationSettingsError::RuleNotFound(rule_id.to_owned()))?;
102        self.commands.push(Command::SetPushRuleEnabled {
103            kind,
104            rule_id: rule_id.to_owned(),
105            enabled,
106        });
107        Ok(())
108    }
109
110    /// Set whether a rule is enabled
111    pub(crate) fn set_rule_enabled(
112        &mut self,
113        kind: RuleKind,
114        rule_id: &str,
115        enabled: bool,
116    ) -> Result<(), NotificationSettingsError> {
117        if rule_id == PredefinedOverrideRuleId::IsRoomMention.as_str() {
118            // Handle specific case for `PredefinedOverrideRuleId::IsRoomMention`
119            self.set_room_mention_enabled(enabled)
120        } else if rule_id == PredefinedOverrideRuleId::IsUserMention.as_str() {
121            // Handle specific case for `PredefinedOverrideRuleId::IsUserMention`
122            self.set_user_mention_enabled(enabled)
123        } else {
124            self.set_enabled_internal(kind, rule_id, enabled)
125        }
126    }
127
128    /// Set whether `IsUserMention` is enabled
129    fn set_user_mention_enabled(&mut self, enabled: bool) -> Result<(), NotificationSettingsError> {
130        // Add a command for the `IsUserMention` `Override` rule (MSC3952).
131        // This is a new push rule that may not yet be present.
132        self.set_enabled_internal(
133            RuleKind::Override,
134            PredefinedOverrideRuleId::IsUserMention.as_str(),
135            enabled,
136        )?;
137
138        // For compatibility purpose, we still need to add commands for
139        // `ContainsUserName` and `ContainsDisplayName` (removed rules).
140        #[allow(deprecated)]
141        {
142            // `ContainsUserName`
143            if let Err(err) = self.set_enabled_internal(
144                RuleKind::Content,
145                PredefinedContentRuleId::ContainsUserName.as_str(),
146                enabled,
147            ) {
148                // This rule has been removed from the spec, so it's fine if it wasn't found.
149                if !err.is_rule_not_found() {
150                    return Err(err);
151                }
152            }
153
154            // `ContainsDisplayName`
155            if let Err(err) = self.set_enabled_internal(
156                RuleKind::Override,
157                PredefinedOverrideRuleId::ContainsDisplayName.as_str(),
158                enabled,
159            ) {
160                // This rule has been removed from the spec, so it's fine if it wasn't found.
161                if !err.is_rule_not_found() {
162                    return Err(err);
163                }
164            }
165        }
166
167        Ok(())
168    }
169
170    /// Set whether `IsRoomMention` is enabled
171    fn set_room_mention_enabled(&mut self, enabled: bool) -> Result<(), NotificationSettingsError> {
172        // Sets the `IsRoomMention` `Override` rule (MSC3952).
173        // This is a new push rule that may not yet be present.
174        self.set_enabled_internal(
175            RuleKind::Override,
176            PredefinedOverrideRuleId::IsRoomMention.as_str(),
177            enabled,
178        )?;
179
180        // For compatibility purpose, we still need to set `RoomNotif` (removed
181        // rule).
182        #[allow(deprecated)]
183        if let Err(err) = self.set_enabled_internal(
184            RuleKind::Override,
185            PredefinedOverrideRuleId::RoomNotif.as_str(),
186            enabled,
187        ) {
188            // This rule has been removed from the spec, so it's fine if it wasn't found.
189            if !err.is_rule_not_found() {
190                return Err(err);
191            }
192        }
193
194        Ok(())
195    }
196
197    /// Set the actions of the rule from the given kind and with the given
198    /// `rule_id`
199    pub(crate) fn set_rule_actions(
200        &mut self,
201        kind: RuleKind,
202        rule_id: &str,
203        actions: Vec<Action>,
204    ) -> Result<(), NotificationSettingsError> {
205        self.rules
206            .set_actions(kind.clone(), rule_id, actions.clone())
207            .map_err(|_| NotificationSettingsError::RuleNotFound(rule_id.to_owned()))?;
208        self.commands.push(Command::SetPushRuleActions {
209            kind,
210            rule_id: rule_id.to_owned(),
211            actions,
212        });
213        Ok(())
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use assert_matches::assert_matches;
220    use matrix_sdk_test::{
221        async_test,
222        notification_settings::{
223            get_server_default_ruleset, server_default_ruleset_with_legacy_mentions,
224        },
225    };
226    use ruma::{
227        OwnedRoomId, RoomId,
228        push::{
229            Action, NewPushRule, NewSimplePushRule, PredefinedContentRuleId,
230            PredefinedOverrideRuleId, PredefinedUnderrideRuleId, RemovePushRuleError, RuleKind,
231            Tweak,
232        },
233    };
234
235    use super::RuleCommands;
236    use crate::{error::NotificationSettingsError, notification_settings::command::Command};
237
238    fn get_test_room_id() -> OwnedRoomId {
239        RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap()
240    }
241
242    #[async_test]
243    async fn test_insert_rule_room() {
244        let room_id = get_test_room_id();
245        let mut rule_commands = RuleCommands::new(get_server_default_ruleset());
246        rule_commands.insert_rule(RuleKind::Room, &room_id, true).unwrap();
247
248        // A rule must have been inserted in the ruleset.
249        assert!(rule_commands.rules.get(RuleKind::Room, &room_id).is_some());
250
251        // Exactly one command must have been created.
252        assert_eq!(rule_commands.commands.len(), 1);
253        assert_matches!(&rule_commands.commands[0],
254            Command::SetRoomPushRule { room_id: command_room_id, notify } => {
255                assert_eq!(command_room_id, &room_id);
256                assert!(notify);
257            }
258        );
259    }
260
261    #[async_test]
262    async fn test_insert_rule_override() {
263        let room_id = get_test_room_id();
264        let mut rule_commands = RuleCommands::new(get_server_default_ruleset());
265        rule_commands.insert_rule(RuleKind::Override, &room_id, true).unwrap();
266
267        // A rule must have been inserted in the ruleset.
268        assert!(rule_commands.rules.get(RuleKind::Override, &room_id).is_some());
269
270        // Exactly one command must have been created.
271        assert_eq!(rule_commands.commands.len(), 1);
272        assert_matches!(&rule_commands.commands[0],
273            Command::SetOverridePushRule {room_id: command_room_id, rule_id, notify } => {
274                assert_eq!(command_room_id, &room_id);
275                assert_eq!(rule_id, room_id.as_str());
276                assert!(notify);
277            }
278        );
279    }
280
281    #[async_test]
282    async fn test_insert_rule_unsupported() {
283        let room_id = get_test_room_id();
284        let mut rule_commands = RuleCommands::new(get_server_default_ruleset());
285
286        assert_matches!(
287            rule_commands.insert_rule(RuleKind::Underride, &room_id, true),
288            Err(NotificationSettingsError::InvalidParameter(_)) => {}
289        );
290
291        assert_matches!(
292            rule_commands.insert_rule(RuleKind::Content, &room_id, true),
293            Err(NotificationSettingsError::InvalidParameter(_)) => {}
294        );
295
296        assert_matches!(
297            rule_commands.insert_rule(RuleKind::Sender, &room_id, true),
298            Err(NotificationSettingsError::InvalidParameter(_)) => {}
299        );
300    }
301
302    #[async_test]
303    async fn test_delete_rule() {
304        let room_id = get_test_room_id();
305        let mut ruleset = get_server_default_ruleset();
306
307        let new_rule = NewSimplePushRule::new(room_id.to_owned(), vec![]);
308        ruleset.insert(NewPushRule::Room(new_rule), None, None).unwrap();
309
310        let mut rule_commands = RuleCommands::new(ruleset);
311
312        // Delete must succeed.
313        rule_commands.delete_rule(RuleKind::Room, room_id.to_string()).unwrap();
314
315        // The ruleset must have been updated.
316        assert!(rule_commands.rules.get(RuleKind::Room, &room_id).is_none());
317
318        // Exactly one command must have been created.
319        assert_eq!(rule_commands.commands.len(), 1);
320        assert_matches!(&rule_commands.commands[0],
321            Command::DeletePushRule { kind, rule_id } => {
322                assert_eq!(kind, &RuleKind::Room);
323                assert_eq!(rule_id, room_id.as_str());
324            }
325        );
326    }
327
328    #[async_test]
329    async fn test_delete_rule_errors() {
330        let room_id = get_test_room_id();
331        let ruleset = get_server_default_ruleset();
332
333        let mut rule_commands = RuleCommands::new(ruleset);
334
335        // Deletion should fail if an attempt is made to delete a rule that does not
336        // exist.
337        assert_matches!(
338            rule_commands.delete_rule(RuleKind::Room, room_id.to_string()),
339            Err(RemovePushRuleError::NotFound) => {}
340        );
341
342        // Deletion should fail if an attempt is made to delete a default server rule.
343        assert_matches!(
344            rule_commands.delete_rule(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.to_string()),
345            Err(RemovePushRuleError::ServerDefault) => {}
346        );
347
348        assert!(rule_commands.commands.is_empty());
349    }
350
351    #[async_test]
352    async fn test_set_rule_enabled() {
353        let mut ruleset = get_server_default_ruleset();
354
355        // Initialize with `Reaction` rule disabled.
356        ruleset.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, false).unwrap();
357
358        let mut rule_commands = RuleCommands::new(ruleset);
359        rule_commands
360            .set_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction.as_str(), true)
361            .unwrap();
362
363        // The ruleset must have been updated
364        let rule = rule_commands
365            .rules
366            .get(RuleKind::Override, PredefinedOverrideRuleId::Reaction.as_str())
367            .unwrap();
368        assert!(rule.enabled());
369
370        // Exactly one command must have been created.
371        assert_eq!(rule_commands.commands.len(), 1);
372        assert_matches!(&rule_commands.commands[0],
373            Command::SetPushRuleEnabled { kind, rule_id, enabled } => {
374                assert_eq!(kind, &RuleKind::Override);
375                assert_eq!(rule_id, PredefinedOverrideRuleId::Reaction.as_str());
376                assert!(enabled);
377            }
378        );
379    }
380
381    #[async_test]
382    async fn test_set_rule_enabled_not_found() {
383        let ruleset = get_server_default_ruleset();
384        let mut rule_commands = RuleCommands::new(ruleset);
385        assert_eq!(
386            rule_commands.set_rule_enabled(RuleKind::Room, "unknown_rule_id", true),
387            Err(NotificationSettingsError::RuleNotFound("unknown_rule_id".to_owned()))
388        );
389    }
390
391    #[async_test]
392    async fn test_set_rule_enabled_user_mention() {
393        let mut ruleset = server_default_ruleset_with_legacy_mentions();
394        let mut rule_commands = RuleCommands::new(ruleset.clone());
395
396        ruleset
397            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, false)
398            .unwrap();
399
400        #[allow(deprecated)]
401        {
402            ruleset
403                .set_enabled(
404                    RuleKind::Override,
405                    PredefinedOverrideRuleId::ContainsDisplayName,
406                    false,
407                )
408                .unwrap();
409            ruleset
410                .set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, false)
411                .unwrap();
412        }
413
414        // Enable the user mention rule.
415        rule_commands
416            .set_rule_enabled(
417                RuleKind::Override,
418                PredefinedOverrideRuleId::IsUserMention.as_str(),
419                true,
420            )
421            .unwrap();
422
423        // The ruleset must have been updated.
424        assert!(
425            rule_commands
426                .rules
427                .get(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
428                .unwrap()
429                .enabled()
430        );
431        #[allow(deprecated)]
432        {
433            assert!(
434                rule_commands
435                    .rules
436                    .get(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName)
437                    .unwrap()
438                    .enabled()
439            );
440            assert!(
441                rule_commands
442                    .rules
443                    .get(RuleKind::Content, PredefinedContentRuleId::ContainsUserName)
444                    .unwrap()
445                    .enabled()
446            );
447        }
448
449        // Three commands are expected.
450        assert_eq!(rule_commands.commands.len(), 3);
451
452        assert_matches!(&rule_commands.commands[0],
453            Command::SetPushRuleEnabled { kind, rule_id, enabled } => {
454                assert_eq!(kind, &RuleKind::Override);
455                assert_eq!(rule_id, PredefinedOverrideRuleId::IsUserMention.as_str());
456                assert!(enabled);
457            }
458        );
459
460        #[allow(deprecated)]
461        {
462            assert_matches!(&rule_commands.commands[1],
463                Command::SetPushRuleEnabled { kind, rule_id, enabled } => {
464                    assert_eq!(kind, &RuleKind::Content);
465                    assert_eq!(rule_id, PredefinedContentRuleId::ContainsUserName.as_str());
466                    assert!(enabled);
467                }
468            );
469
470            assert_matches!(&rule_commands.commands[2],
471                Command::SetPushRuleEnabled { kind, rule_id, enabled } => {
472                    assert_eq!(kind, &RuleKind::Override);
473                    assert_eq!(rule_id, PredefinedOverrideRuleId::ContainsDisplayName.as_str());
474                    assert!(enabled);
475                }
476            );
477        }
478    }
479
480    #[async_test]
481    async fn test_set_rule_enabled_room_mention() {
482        let mut ruleset = server_default_ruleset_with_legacy_mentions();
483        let mut rule_commands = RuleCommands::new(ruleset.clone());
484
485        ruleset
486            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, false)
487            .unwrap();
488
489        #[allow(deprecated)]
490        {
491            ruleset
492                .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, false)
493                .unwrap();
494        }
495
496        rule_commands
497            .set_rule_enabled(
498                RuleKind::Override,
499                PredefinedOverrideRuleId::IsRoomMention.as_str(),
500                true,
501            )
502            .unwrap();
503
504        // The ruleset must have been updated.
505        assert!(
506            rule_commands
507                .rules
508                .get(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
509                .unwrap()
510                .enabled()
511        );
512        #[allow(deprecated)]
513        {
514            assert!(
515                rule_commands
516                    .rules
517                    .get(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif)
518                    .unwrap()
519                    .enabled()
520            );
521        }
522
523        // Two commands are expected.
524        assert_eq!(rule_commands.commands.len(), 2);
525
526        assert_matches!(&rule_commands.commands[0],
527            Command::SetPushRuleEnabled {  kind, rule_id, enabled } => {
528                assert_eq!(kind, &RuleKind::Override);
529                assert_eq!(rule_id, PredefinedOverrideRuleId::IsRoomMention.as_str());
530                assert!(enabled);
531            }
532        );
533
534        #[allow(deprecated)]
535        {
536            assert_matches!(&rule_commands.commands[1],
537                Command::SetPushRuleEnabled { kind, rule_id, enabled } => {
538                    assert_eq!(kind, &RuleKind::Override);
539                    assert_eq!(rule_id, PredefinedOverrideRuleId::RoomNotif.as_str());
540                    assert!(enabled);
541                }
542            );
543        }
544    }
545
546    #[async_test]
547    async fn test_set_rule_actions() {
548        let mut ruleset = get_server_default_ruleset();
549        let mut rule_commands = RuleCommands::new(ruleset.clone());
550
551        // Starting with an empty action list for `PredefinedUnderrideRuleId::Message`.
552        ruleset
553            .set_actions(RuleKind::Underride, PredefinedUnderrideRuleId::Message, vec![])
554            .unwrap();
555
556        // After setting a list of actions
557        rule_commands
558            .set_rule_actions(
559                RuleKind::Underride,
560                PredefinedUnderrideRuleId::Message.as_str(),
561                vec![Action::Notify, Action::SetTweak(Tweak::Sound("default".into()))],
562            )
563            .unwrap();
564
565        // The ruleset must have been updated
566        let actions = rule_commands
567            .rules
568            .get(RuleKind::Underride, PredefinedUnderrideRuleId::Message)
569            .unwrap()
570            .actions();
571        assert_eq!(actions.len(), 2);
572
573        // and a `SetPushRuleActions` command must have been added
574        assert_eq!(rule_commands.commands.len(), 1);
575        assert_matches!(&rule_commands.commands[0],
576            Command::SetPushRuleActions { kind, rule_id, actions } => {
577                assert_eq!(kind, &RuleKind::Underride);
578                assert_eq!(rule_id, PredefinedUnderrideRuleId::Message.as_str());
579                assert_eq!(actions.len(), 2);
580                assert_matches!(&actions[0], Action::Notify);
581                assert_matches!(&actions[1], Action::SetTweak(Tweak::Sound(sound)) => {
582                    assert_eq!(sound, "default");
583                });
584            }
585        );
586    }
587}