matrix_sdk/notification_settings/
rules.rs

1//! Ruleset utility struct
2
3use imbl::HashSet;
4use indexmap::IndexSet;
5use ruma::{
6    push::{
7        AnyPushRuleRef, PatternedPushRule, PredefinedContentRuleId, PredefinedOverrideRuleId,
8        PredefinedUnderrideRuleId, PushCondition, RuleKind, Ruleset,
9    },
10    RoomId,
11};
12
13use super::{command::Command, rule_commands::RuleCommands, RoomNotificationMode};
14use crate::{
15    error::NotificationSettingsError,
16    notification_settings::{IsEncrypted, IsOneToOne},
17};
18
19#[derive(Clone, Debug)]
20pub(crate) struct Rules {
21    pub ruleset: Ruleset,
22}
23
24impl Rules {
25    pub(crate) fn new(ruleset: Ruleset) -> Self {
26        Rules { ruleset }
27    }
28
29    /// Gets all user defined rules matching a given `room_id`.
30    pub(crate) fn get_custom_rules_for_room(&self, room_id: &RoomId) -> Vec<(RuleKind, String)> {
31        let mut custom_rules = vec![];
32
33        // add any `Override` rules matching this `room_id`
34        for rule in &self.ruleset.override_ {
35            // if the rule_id is the room_id
36            if &rule.rule_id == room_id || rule.conditions.iter().any(|x| matches!(
37                x,
38                PushCondition::EventMatch { key, pattern } if key == "room_id" && pattern == room_id
39            )) {
40                // the rule contains a condition matching this `room_id`
41                custom_rules.push((RuleKind::Override, rule.rule_id.clone()));
42            }
43        }
44
45        // add any `Room` rules matching this `room_id`
46        if let Some(rule) = self.ruleset.get(RuleKind::Room, room_id) {
47            custom_rules.push((RuleKind::Room, rule.rule_id().to_owned()));
48        }
49
50        // add any `Underride` rules matching this `room_id`
51        for rule in &self.ruleset.underride {
52            // if the rule_id is the room_id
53            if &rule.rule_id == room_id || rule.conditions.iter().any(|x| matches!(
54                x,
55                PushCondition::EventMatch { key, pattern } if key == "room_id" && pattern == room_id
56            )) {
57                // the rule contains a condition matching this `room_id`
58                custom_rules.push((RuleKind::Underride, rule.rule_id.clone()));
59            }
60        }
61
62        custom_rules
63    }
64
65    /// Gets the user defined notification mode for a room.
66    pub(crate) fn get_user_defined_room_notification_mode(
67        &self,
68        room_id: &RoomId,
69    ) -> Option<RoomNotificationMode> {
70        // Search for an enabled `Override` rule
71        if self.ruleset.override_.iter().any(|x| {
72            // enabled
73            x.enabled &&
74            // with a condition of type `EventMatch` for this `room_id`
75            // (checking on x.rule_id is not sufficient here as more than one override rule may have a condition matching on `room_id`)
76            x.conditions.iter().any(|x| matches!(
77                x,
78                PushCondition::EventMatch { key, pattern } if key == "room_id" && pattern == room_id
79            )) &&
80            // and without a Notify action
81            !x.actions.iter().any(|x| x.should_notify())
82        }) {
83            return Some(RoomNotificationMode::Mute);
84        }
85
86        // Search for an enabled `Room` rule where `rule_id` is the `room_id`
87        if let Some(rule) = self.ruleset.get(RuleKind::Room, room_id) {
88            // if this rule contains a `Notify` action
89            if rule.triggers_notification() {
90                return Some(RoomNotificationMode::AllMessages);
91            }
92            return Some(RoomNotificationMode::MentionsAndKeywordsOnly);
93        }
94
95        // There is no custom rule matching this `room_id`
96        None
97    }
98
99    /// Gets the default notification mode for a room.
100    ///
101    /// # Arguments
102    ///
103    /// * `is_encrypted` - `Yes` if the room is encrypted
104    /// * `is_one_to_one` - `Yes` if the room is a direct chat involving two
105    ///   people
106    pub(crate) fn get_default_room_notification_mode(
107        &self,
108        is_encrypted: IsEncrypted,
109        is_one_to_one: IsOneToOne,
110    ) -> RoomNotificationMode {
111        // get the correct default rule ID based on `is_encrypted` and `is_one_to_one`
112        let predefined_rule_id = get_predefined_underride_room_rule_id(is_encrypted, is_one_to_one);
113        let rule_id = predefined_rule_id.as_str();
114
115        // If there is an `Underride` rule that should trigger a notification, the mode
116        // is `AllMessages`
117        if self
118            .ruleset
119            .get(RuleKind::Underride, rule_id)
120            .is_some_and(|r| r.enabled() && r.triggers_notification())
121        {
122            RoomNotificationMode::AllMessages
123        } else {
124            // Otherwise, the mode is `MentionsAndKeywordsOnly`
125            RoomNotificationMode::MentionsAndKeywordsOnly
126        }
127    }
128
129    /// Get all room IDs for which a user-defined rule exists.
130    pub(crate) fn get_rooms_with_user_defined_rules(&self, enabled: Option<bool>) -> Vec<String> {
131        let test_if_enabled = enabled.is_some();
132        let must_be_enabled = enabled.unwrap_or(false);
133
134        let mut room_ids = HashSet::new();
135        for rule in &self.ruleset {
136            if rule.is_server_default() {
137                continue;
138            }
139            if test_if_enabled && rule.enabled() != must_be_enabled {
140                continue;
141            }
142            match rule {
143                AnyPushRuleRef::Override(r) | AnyPushRuleRef::Underride(r) => {
144                    for condition in &r.conditions {
145                        if let PushCondition::EventMatch { key, pattern } = condition {
146                            if key == "room_id" {
147                                room_ids.insert(pattern.clone());
148                                break;
149                            }
150                        }
151                    }
152                }
153                AnyPushRuleRef::Room(r) => {
154                    room_ids.insert(r.rule_id.to_string());
155                }
156                _ => {}
157            }
158        }
159        Vec::from_iter(room_ids)
160    }
161
162    /// Get whether the `IsUserMention` rule is enabled.
163    fn is_user_mention_enabled(&self) -> bool {
164        // Search for an `Override` rule `IsUserMention` (MSC3952).
165        // This is a new push rule that may not yet be present.
166        if let Some(rule) =
167            self.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
168        {
169            return rule.enabled();
170        }
171
172        // Fallback to deprecated rules for compatibility.
173        #[allow(deprecated)]
174        if let Some(rule) =
175            self.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName)
176        {
177            if rule.enabled() && rule.triggers_notification() {
178                return true;
179            }
180        }
181
182        #[allow(deprecated)]
183        if let Some(rule) =
184            self.ruleset.get(RuleKind::Content, PredefinedContentRuleId::ContainsUserName)
185        {
186            if rule.enabled() && rule.triggers_notification() {
187                return true;
188            }
189        }
190
191        false
192    }
193
194    /// Get whether the `IsRoomMention` rule is enabled.
195    fn is_room_mention_enabled(&self) -> bool {
196        // Search for an `Override` rule `IsRoomMention` (MSC3952).
197        // This is a new push rule that may not yet be present.
198        if let Some(rule) =
199            self.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
200        {
201            return rule.enabled();
202        }
203
204        // Fallback to deprecated rule for compatibility
205        #[allow(deprecated)]
206        self.ruleset
207            .get(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif)
208            .is_some_and(|r| r.enabled() && r.triggers_notification())
209    }
210
211    /// Get whether the given ruleset contains some enabled keywords rules.
212    pub(crate) fn contains_keyword_rules(&self) -> bool {
213        // Search for a user defined `Content` rule.
214        self.ruleset.content.iter().any(|r| !r.default && r.enabled)
215    }
216
217    /// The keywords which have enabled rules.
218    pub(crate) fn enabled_keywords(&self) -> IndexSet<String> {
219        self.ruleset
220            .content
221            .iter()
222            .filter(|r| !r.default && r.enabled)
223            .map(|r| r.pattern.clone())
224            .collect()
225    }
226
227    /// The rules for a keyword, if any.
228    pub(crate) fn keyword_rules(&self, keyword: &str) -> Vec<&PatternedPushRule> {
229        self.ruleset.content.iter().filter(|r| !r.default && r.pattern == keyword).collect()
230    }
231
232    /// Get whether a rule is enabled.
233    pub(crate) fn is_enabled(
234        &self,
235        kind: RuleKind,
236        rule_id: &str,
237    ) -> Result<bool, NotificationSettingsError> {
238        if rule_id == PredefinedOverrideRuleId::IsRoomMention.as_str() {
239            Ok(self.is_room_mention_enabled())
240        } else if rule_id == PredefinedOverrideRuleId::IsUserMention.as_str() {
241            Ok(self.is_user_mention_enabled())
242        } else if let Some(rule) = self.ruleset.get(kind, rule_id) {
243            Ok(rule.enabled())
244        } else {
245            Err(NotificationSettingsError::RuleNotFound(rule_id.to_owned()))
246        }
247    }
248
249    /// Apply a group of commands to the managed ruleset.
250    ///
251    /// The command may silently fail because the ruleset may have changed
252    /// between the time the command was created and the time it is applied.
253    pub(crate) fn apply(&mut self, commands: RuleCommands) {
254        for command in commands.commands {
255            match command {
256                Command::DeletePushRule { kind, rule_id } => {
257                    _ = self.ruleset.remove(kind, rule_id);
258                }
259                Command::SetRoomPushRule { .. }
260                | Command::SetOverridePushRule { .. }
261                | Command::SetKeywordPushRule { .. } => {
262                    if let Ok(push_rule) = command.to_push_rule() {
263                        _ = self.ruleset.insert(push_rule, None, None);
264                    }
265                }
266                Command::SetPushRuleEnabled { kind, rule_id, enabled } => {
267                    _ = self.ruleset.set_enabled(kind, rule_id, enabled);
268                }
269                Command::SetPushRuleActions { kind, rule_id, actions } => {
270                    _ = self.ruleset.set_actions(kind, rule_id, actions);
271                }
272            }
273        }
274    }
275}
276
277/// Gets the `PredefinedUnderrideRuleId` for rooms corresponding to the given
278/// criteria.
279///
280/// # Arguments
281///
282/// * `is_encrypted` - `Yes` if the room is encrypted
283/// * `is_one_to_one` - `Yes` if the room is a direct chat involving two people
284pub(crate) fn get_predefined_underride_room_rule_id(
285    is_encrypted: IsEncrypted,
286    is_one_to_one: IsOneToOne,
287) -> PredefinedUnderrideRuleId {
288    match (is_encrypted, is_one_to_one) {
289        (IsEncrypted::Yes, IsOneToOne::Yes) => PredefinedUnderrideRuleId::EncryptedRoomOneToOne,
290        (IsEncrypted::No, IsOneToOne::Yes) => PredefinedUnderrideRuleId::RoomOneToOne,
291        (IsEncrypted::Yes, IsOneToOne::No) => PredefinedUnderrideRuleId::Encrypted,
292        (IsEncrypted::No, IsOneToOne::No) => PredefinedUnderrideRuleId::Message,
293    }
294}
295
296/// Gets the `PredefinedUnderrideRuleId` for poll start events corresponding to
297/// the given criteria.
298///
299/// # Arguments
300///
301/// * `is_one_to_one` - `Yes` if the room is a direct chat involving two people
302pub(crate) fn get_predefined_underride_poll_start_rule_id(
303    is_one_to_one: IsOneToOne,
304) -> PredefinedUnderrideRuleId {
305    match is_one_to_one {
306        IsOneToOne::Yes => PredefinedUnderrideRuleId::PollStartOneToOne,
307        IsOneToOne::No => PredefinedUnderrideRuleId::PollStart,
308    }
309}
310
311#[cfg(test)]
312pub(crate) mod tests {
313    use imbl::HashSet;
314    use matrix_sdk_test::{
315        async_test,
316        notification_settings::{build_ruleset, get_server_default_ruleset},
317    };
318    use ruma::{
319        push::{
320            Action, NewConditionalPushRule, NewPushRule, PredefinedContentRuleId,
321            PredefinedOverrideRuleId, PredefinedUnderrideRuleId, PushCondition, RuleKind,
322        },
323        OwnedRoomId, RoomId,
324    };
325
326    use super::RuleCommands;
327    use crate::{
328        error::NotificationSettingsError,
329        notification_settings::{
330            rules::{self, Rules},
331            IsEncrypted, IsOneToOne, RoomNotificationMode,
332        },
333    };
334
335    fn get_test_room_id() -> OwnedRoomId {
336        RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap()
337    }
338
339    #[async_test]
340    async fn test_get_custom_rules_for_room() {
341        let room_id = get_test_room_id();
342
343        let rules = Rules::new(get_server_default_ruleset());
344        assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 0);
345
346        // Initialize with one rule.
347        let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
348        let rules = Rules::new(ruleset);
349        assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
350
351        // Insert a Room rule
352        let ruleset = build_ruleset(vec![
353            (RuleKind::Override, &room_id, false),
354            (RuleKind::Room, &room_id, false),
355        ]);
356        let rules = Rules::new(ruleset);
357        assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 2);
358    }
359
360    #[async_test]
361    async fn test_get_custom_rules_for_room_special_override_rule() {
362        let room_id = get_test_room_id();
363        let mut ruleset = get_server_default_ruleset();
364
365        // Insert an Override rule where the rule ID doesn't match the room id,
366        // but with a condition that matches
367        let new_rule = NewConditionalPushRule::new(
368            "custom_rule_id".to_owned(),
369            vec![PushCondition::EventMatch { key: "room_id".into(), pattern: room_id.to_string() }],
370            vec![Action::Notify],
371        );
372        ruleset.insert(NewPushRule::Override(new_rule), None, None).unwrap();
373
374        let rules = Rules::new(ruleset);
375        assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
376    }
377
378    #[async_test]
379    async fn test_get_user_defined_room_notification_mode() {
380        let room_id = get_test_room_id();
381        let rules = Rules::new(get_server_default_ruleset());
382        assert_eq!(rules.get_user_defined_room_notification_mode(&room_id), None);
383
384        // Initialize with an `Override` rule that doesn't notify
385        let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
386        let rules = Rules::new(ruleset);
387        assert_eq!(
388            rules.get_user_defined_room_notification_mode(&room_id),
389            Some(RoomNotificationMode::Mute)
390        );
391
392        // Initialize with a `Room` rule that doesn't notify
393        let ruleset = build_ruleset(vec![(RuleKind::Room, &room_id, false)]);
394        let rules = Rules::new(ruleset);
395        assert_eq!(
396            rules.get_user_defined_room_notification_mode(&room_id),
397            Some(RoomNotificationMode::MentionsAndKeywordsOnly)
398        );
399
400        // Initialize with a `Room` rule that doesn't notify
401        let ruleset = build_ruleset(vec![(RuleKind::Room, &room_id, true)]);
402        let rules = Rules::new(ruleset);
403        assert_eq!(
404            rules.get_user_defined_room_notification_mode(&room_id),
405            Some(RoomNotificationMode::AllMessages)
406        );
407
408        let room_id_a = RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap();
409        let room_id_b = RoomId::parse("!BBBbBBBBBbbBBbbbbb:matrix.org").unwrap();
410        let ruleset = build_ruleset(vec![
411            // A mute rule for room_id_a
412            (RuleKind::Override, &room_id_a, false),
413            // A notifying rule for room_id_b
414            (RuleKind::Override, &room_id_b, true),
415        ]);
416        let rules = Rules::new(ruleset);
417        let mode = rules.get_user_defined_room_notification_mode(&room_id_a);
418
419        // The mode should be Mute as there is an Override rule that doesn't notify,
420        // with a condition matching the room_id_a
421        assert_eq!(mode, Some(RoomNotificationMode::Mute));
422    }
423
424    #[async_test]
425    async fn test_get_predefined_underride_room_rule_id() {
426        assert_eq!(
427            rules::get_predefined_underride_room_rule_id(IsEncrypted::No, IsOneToOne::No),
428            PredefinedUnderrideRuleId::Message
429        );
430        assert_eq!(
431            rules::get_predefined_underride_room_rule_id(IsEncrypted::No, IsOneToOne::Yes),
432            PredefinedUnderrideRuleId::RoomOneToOne
433        );
434        assert_eq!(
435            rules::get_predefined_underride_room_rule_id(IsEncrypted::Yes, IsOneToOne::No),
436            PredefinedUnderrideRuleId::Encrypted
437        );
438        assert_eq!(
439            rules::get_predefined_underride_room_rule_id(IsEncrypted::Yes, IsOneToOne::Yes),
440            PredefinedUnderrideRuleId::EncryptedRoomOneToOne
441        );
442    }
443
444    #[async_test]
445    async fn test_get_predefined_underride_poll_start_rule_id() {
446        assert_eq!(
447            rules::get_predefined_underride_poll_start_rule_id(IsOneToOne::No),
448            PredefinedUnderrideRuleId::PollStart
449        );
450        assert_eq!(
451            rules::get_predefined_underride_poll_start_rule_id(IsOneToOne::Yes),
452            PredefinedUnderrideRuleId::PollStartOneToOne
453        );
454    }
455
456    #[async_test]
457    async fn test_get_default_room_notification_mode_mentions_and_keywords() {
458        let mut ruleset = get_server_default_ruleset();
459        // If the corresponding underride rule is disabled
460        ruleset
461            .set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, false)
462            .unwrap();
463
464        let rules = Rules::new(ruleset);
465        let mode = rules.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes);
466        // Then the mode should be `MentionsAndKeywordsOnly`
467        assert_eq!(mode, RoomNotificationMode::MentionsAndKeywordsOnly);
468    }
469
470    #[async_test]
471    async fn test_get_default_room_notification_mode_all_messages() {
472        let mut ruleset = get_server_default_ruleset();
473        // If the corresponding underride rule is enabled
474        ruleset
475            .set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, true)
476            .unwrap();
477
478        let rules = Rules::new(ruleset);
479        let mode = rules.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes);
480        // Then the mode should be `AllMessages`
481        assert_eq!(mode, RoomNotificationMode::AllMessages);
482    }
483
484    #[async_test]
485    async fn test_is_user_mention_enabled() {
486        // If `IsUserMention` is enable, then is_user_mention_enabled() should return
487        // `true` even if the deprecated rules are disabled
488        let mut ruleset = get_server_default_ruleset();
489        ruleset
490            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, true)
491            .unwrap();
492        #[allow(deprecated)]
493        ruleset
494            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName, false)
495            .unwrap();
496        #[allow(deprecated)]
497        ruleset
498            .set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, false)
499            .unwrap();
500
501        let rules = Rules::new(ruleset);
502        assert!(rules.is_user_mention_enabled());
503        // is_enabled() should also return `true` for
504        // PredefinedOverrideRuleId::IsUserMention
505        assert!(rules
506            .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.as_str())
507            .unwrap());
508
509        // If `IsUserMention` is disabled, then is_user_mention_enabled() should return
510        // `false` even if the deprecated rules are enabled
511        let mut ruleset = get_server_default_ruleset();
512        ruleset
513            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, false)
514            .unwrap();
515        #[allow(deprecated)]
516        ruleset
517            .set_actions(
518                RuleKind::Override,
519                PredefinedOverrideRuleId::ContainsDisplayName,
520                vec![Action::Notify],
521            )
522            .unwrap();
523        #[allow(deprecated)]
524        ruleset
525            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName, true)
526            .unwrap();
527        #[allow(deprecated)]
528        ruleset
529            .set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, true)
530            .unwrap();
531
532        let rules = Rules::new(ruleset);
533        assert!(!rules.is_user_mention_enabled());
534        // is_enabled() should also return `false` for
535        // PredefinedOverrideRuleId::IsUserMention
536        assert!(!rules
537            .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.as_str())
538            .unwrap());
539    }
540
541    #[async_test]
542    async fn test_is_room_mention_enabled() {
543        // If `IsRoomMention` is present and enabled then is_room_mention_enabled()
544        // should return `true` even if the deprecated rule is disabled
545        let mut ruleset = get_server_default_ruleset();
546        ruleset
547            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, true)
548            .unwrap();
549        #[allow(deprecated)]
550        ruleset
551            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, false)
552            .unwrap();
553
554        let rules = Rules::new(ruleset);
555        assert!(rules.is_room_mention_enabled());
556        // is_enabled() should also return `true` for
557        // PredefinedOverrideRuleId::IsRoomMention
558        assert!(rules
559            .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention.as_str())
560            .unwrap());
561
562        // If `IsRoomMention` is present and disabled then is_room_mention_enabled()
563        // should return `false` even if the deprecated rule is enabled
564        let mut ruleset = get_server_default_ruleset();
565        ruleset
566            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, false)
567            .unwrap();
568        #[allow(deprecated)]
569        ruleset.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, true).unwrap();
570
571        let rules = Rules::new(ruleset);
572        assert!(!rules.is_room_mention_enabled());
573        // is_enabled() should also return `false` for
574        // PredefinedOverrideRuleId::IsRoomMention
575        assert!(!rules
576            .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention.as_str())
577            .unwrap());
578    }
579
580    #[async_test]
581    async fn test_is_enabled_rule_not_found() {
582        let rules = Rules::new(get_server_default_ruleset());
583
584        assert_eq!(
585            rules.is_enabled(RuleKind::Override, "unknown_rule_id"),
586            Err(NotificationSettingsError::RuleNotFound("unknown_rule_id".to_owned()))
587        );
588    }
589
590    #[async_test]
591    async fn test_apply_delete_command() {
592        let room_id = get_test_room_id();
593        // Initialize with a custom rule
594        let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
595        let mut rules = Rules::new(ruleset);
596
597        // Build a `RuleCommands` deleting this rule
598        let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
599        rules_commands.delete_rule(RuleKind::Override, room_id.to_string()).unwrap();
600
601        rules.apply(rules_commands);
602
603        // The rule must have been removed from the updated rules
604        assert!(rules.get_custom_rules_for_room(&room_id).is_empty());
605    }
606
607    #[async_test]
608    async fn test_apply_set_command() {
609        let room_id = get_test_room_id();
610        let mut rules = Rules::new(get_server_default_ruleset());
611
612        // Build a `RuleCommands` inserting a rule
613        let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
614        rules_commands.insert_rule(RuleKind::Override, &room_id, false).unwrap();
615
616        rules.apply(rules_commands);
617
618        // The rule must have been removed from the updated rules
619        assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
620    }
621
622    #[async_test]
623    async fn test_apply_set_enabled_command() {
624        let mut rules = Rules::new(get_server_default_ruleset());
625
626        rules
627            .ruleset
628            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, true)
629            .unwrap();
630
631        // Build a `RuleCommands` disabling the rule
632        let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
633        rules_commands
634            .set_rule_enabled(
635                RuleKind::Override,
636                PredefinedOverrideRuleId::Reaction.as_str(),
637                false,
638            )
639            .unwrap();
640
641        rules.apply(rules_commands);
642
643        // The rule must have been disabled in the updated rules
644        assert!(!rules
645            .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction.as_str())
646            .unwrap());
647    }
648
649    #[async_test]
650    async fn test_get_rooms_with_user_defined_rules() {
651        // Without user-defined rules
652        let rules = Rules::new(get_server_default_ruleset());
653        let room_ids = rules.get_rooms_with_user_defined_rules(None);
654        assert!(room_ids.is_empty());
655
656        // With one rule.
657        let room_id = RoomId::parse("!room_a:matrix.org").unwrap();
658        let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
659        let rules = Rules::new(ruleset);
660
661        let room_ids = rules.get_rooms_with_user_defined_rules(None);
662        assert_eq!(room_ids.len(), 1);
663
664        // With duplicates
665        let ruleset = build_ruleset(vec![
666            (RuleKind::Override, &room_id, false),
667            (RuleKind::Underride, &room_id, false),
668            (RuleKind::Room, &room_id, false),
669        ]);
670        let rules = Rules::new(ruleset);
671
672        let room_ids = rules.get_rooms_with_user_defined_rules(None);
673        assert_eq!(room_ids.len(), 1);
674        assert_eq!(room_ids[0], room_id.to_string());
675
676        // With multiple rules
677        let ruleset = build_ruleset(vec![
678            (RuleKind::Room, &RoomId::parse("!room_a:matrix.org").unwrap(), false),
679            (RuleKind::Room, &RoomId::parse("!room_b:matrix.org").unwrap(), false),
680            (RuleKind::Room, &RoomId::parse("!room_c:matrix.org").unwrap(), false),
681            (RuleKind::Override, &RoomId::parse("!room_d:matrix.org").unwrap(), false),
682            (RuleKind::Underride, &RoomId::parse("!room_e:matrix.org").unwrap(), false),
683        ]);
684        let rules = Rules::new(ruleset);
685
686        let room_ids = rules.get_rooms_with_user_defined_rules(None);
687        assert_eq!(room_ids.len(), 5);
688        let expected_set: HashSet<String> = vec![
689            "!room_a:matrix.org",
690            "!room_b:matrix.org",
691            "!room_c:matrix.org",
692            "!room_d:matrix.org",
693            "!room_e:matrix.org",
694        ]
695        .into_iter()
696        .collect();
697        assert!(expected_set.symmetric_difference(HashSet::from(room_ids)).is_empty());
698
699        // Only disabled rules
700        let room_ids = rules.get_rooms_with_user_defined_rules(Some(false));
701        assert_eq!(room_ids.len(), 0);
702
703        // Only enabled rules
704        let room_ids = rules.get_rooms_with_user_defined_rules(Some(true));
705        assert_eq!(room_ids.len(), 5);
706
707        let mut ruleset = build_ruleset(vec![
708            (RuleKind::Room, &RoomId::parse("!room_a:matrix.org").unwrap(), false),
709            (RuleKind::Room, &RoomId::parse("!room_b:matrix.org").unwrap(), false),
710            (RuleKind::Override, &RoomId::parse("!room_c:matrix.org").unwrap(), false),
711            (RuleKind::Underride, &RoomId::parse("!room_d:matrix.org").unwrap(), false),
712        ]);
713        ruleset.set_enabled(RuleKind::Room, "!room_b:matrix.org", false).unwrap();
714        ruleset.set_enabled(RuleKind::Override, "!room_c:matrix.org", false).unwrap();
715        let rules = Rules::new(ruleset);
716        // Only room_a and room_d rules are enabled
717        let room_ids = rules.get_rooms_with_user_defined_rules(Some(true));
718        assert_eq!(room_ids.len(), 2);
719        let expected_set: HashSet<String> =
720            vec!["!room_a:matrix.org", "!room_d:matrix.org"].into_iter().collect();
721        assert!(expected_set.symmetric_difference(HashSet::from(room_ids)).is_empty());
722    }
723}