matrix_sdk/notification_settings/
rule_commands.rs

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