matrix_sdk/notification_settings/
rules.rs

1//! Ruleset utility struct
2
3use imbl::HashSet;
4use indexmap::IndexSet;
5use ruma::{
6    RoomId,
7    push::{
8        AnyPushRuleRef, PatternedPushRule, PredefinedContentRuleId, PredefinedOverrideRuleId,
9        PredefinedUnderrideRuleId, PushCondition, RuleKind, Ruleset,
10    },
11};
12
13use super::{RoomNotificationMode, command::Command, rule_commands::RuleCommands};
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                            && key == "room_id"
147                        {
148                            room_ids.insert(pattern.clone());
149                            break;
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            && rule.enabled()
177            && rule.triggers_notification()
178        {
179            return true;
180        }
181
182        #[allow(deprecated)]
183        if let Some(rule) =
184            self.ruleset.get(RuleKind::Content, PredefinedContentRuleId::ContainsUserName)
185            && rule.enabled()
186            && rule.triggers_notification()
187        {
188            return true;
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                Command::SetCustomPushRule { rule } => {
273                    _ = self.ruleset.insert(rule, None, None);
274                }
275            }
276        }
277    }
278}
279
280/// Gets the `PredefinedUnderrideRuleId` for rooms corresponding to the given
281/// criteria.
282///
283/// # Arguments
284///
285/// * `is_encrypted` - `Yes` if the room is encrypted
286/// * `is_one_to_one` - `Yes` if the room is a direct chat involving two people
287pub(crate) fn get_predefined_underride_room_rule_id(
288    is_encrypted: IsEncrypted,
289    is_one_to_one: IsOneToOne,
290) -> PredefinedUnderrideRuleId {
291    match (is_encrypted, is_one_to_one) {
292        (IsEncrypted::Yes, IsOneToOne::Yes) => PredefinedUnderrideRuleId::EncryptedRoomOneToOne,
293        (IsEncrypted::No, IsOneToOne::Yes) => PredefinedUnderrideRuleId::RoomOneToOne,
294        (IsEncrypted::Yes, IsOneToOne::No) => PredefinedUnderrideRuleId::Encrypted,
295        (IsEncrypted::No, IsOneToOne::No) => PredefinedUnderrideRuleId::Message,
296    }
297}
298
299/// Gets the `PredefinedUnderrideRuleId` for poll start events corresponding to
300/// the given criteria.
301///
302/// # Arguments
303///
304/// * `is_one_to_one` - `Yes` if the room is a direct chat involving two people
305pub(crate) fn get_predefined_underride_poll_start_rule_id(
306    is_one_to_one: IsOneToOne,
307) -> PredefinedUnderrideRuleId {
308    match is_one_to_one {
309        IsOneToOne::Yes => PredefinedUnderrideRuleId::PollStartOneToOne,
310        IsOneToOne::No => PredefinedUnderrideRuleId::PollStart,
311    }
312}
313
314#[cfg(test)]
315pub(crate) mod tests {
316    use imbl::HashSet;
317    use matrix_sdk_test::{
318        async_test,
319        notification_settings::{
320            build_ruleset, get_server_default_ruleset, server_default_ruleset_with_legacy_mentions,
321        },
322    };
323    use ruma::{
324        OwnedRoomId, RoomId,
325        push::{
326            Action, NewConditionalPushRule, NewPushRule, PredefinedContentRuleId,
327            PredefinedOverrideRuleId, PredefinedUnderrideRuleId, PushCondition, RuleKind,
328        },
329    };
330
331    use super::RuleCommands;
332    use crate::{
333        error::NotificationSettingsError,
334        notification_settings::{
335            IsEncrypted, IsOneToOne, RoomNotificationMode,
336            rules::{self, Rules},
337        },
338    };
339
340    fn get_test_room_id() -> OwnedRoomId {
341        RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap()
342    }
343
344    #[async_test]
345    async fn test_get_custom_rules_for_room() {
346        let room_id = get_test_room_id();
347
348        let rules = Rules::new(get_server_default_ruleset());
349        assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 0);
350
351        // Initialize with one rule.
352        let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
353        let rules = Rules::new(ruleset);
354        assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
355
356        // Insert a Room rule
357        let ruleset = build_ruleset(vec![
358            (RuleKind::Override, &room_id, false),
359            (RuleKind::Room, &room_id, false),
360        ]);
361        let rules = Rules::new(ruleset);
362        assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 2);
363    }
364
365    #[async_test]
366    async fn test_get_custom_rules_for_room_special_override_rule() {
367        let room_id = get_test_room_id();
368        let mut ruleset = get_server_default_ruleset();
369
370        // Insert an Override rule where the rule ID doesn't match the room id,
371        // but with a condition that matches
372        let new_rule = NewConditionalPushRule::new(
373            "custom_rule_id".to_owned(),
374            vec![PushCondition::EventMatch { key: "room_id".into(), pattern: room_id.to_string() }],
375            vec![Action::Notify],
376        );
377        ruleset.insert(NewPushRule::Override(new_rule), None, None).unwrap();
378
379        let rules = Rules::new(ruleset);
380        assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
381    }
382
383    #[async_test]
384    async fn test_get_user_defined_room_notification_mode() {
385        let room_id = get_test_room_id();
386        let rules = Rules::new(get_server_default_ruleset());
387        assert_eq!(rules.get_user_defined_room_notification_mode(&room_id), None);
388
389        // Initialize with an `Override` rule that doesn't notify
390        let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
391        let rules = Rules::new(ruleset);
392        assert_eq!(
393            rules.get_user_defined_room_notification_mode(&room_id),
394            Some(RoomNotificationMode::Mute)
395        );
396
397        // Initialize with a `Room` rule that doesn't notify
398        let ruleset = build_ruleset(vec![(RuleKind::Room, &room_id, false)]);
399        let rules = Rules::new(ruleset);
400        assert_eq!(
401            rules.get_user_defined_room_notification_mode(&room_id),
402            Some(RoomNotificationMode::MentionsAndKeywordsOnly)
403        );
404
405        // Initialize with a `Room` rule that doesn't notify
406        let ruleset = build_ruleset(vec![(RuleKind::Room, &room_id, true)]);
407        let rules = Rules::new(ruleset);
408        assert_eq!(
409            rules.get_user_defined_room_notification_mode(&room_id),
410            Some(RoomNotificationMode::AllMessages)
411        );
412
413        let room_id_a = RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap();
414        let room_id_b = RoomId::parse("!BBBbBBBBBbbBBbbbbb:matrix.org").unwrap();
415        let ruleset = build_ruleset(vec![
416            // A mute rule for room_id_a
417            (RuleKind::Override, &room_id_a, false),
418            // A notifying rule for room_id_b
419            (RuleKind::Override, &room_id_b, true),
420        ]);
421        let rules = Rules::new(ruleset);
422        let mode = rules.get_user_defined_room_notification_mode(&room_id_a);
423
424        // The mode should be Mute as there is an Override rule that doesn't notify,
425        // with a condition matching the room_id_a
426        assert_eq!(mode, Some(RoomNotificationMode::Mute));
427    }
428
429    #[async_test]
430    async fn test_get_predefined_underride_room_rule_id() {
431        assert_eq!(
432            rules::get_predefined_underride_room_rule_id(IsEncrypted::No, IsOneToOne::No),
433            PredefinedUnderrideRuleId::Message
434        );
435        assert_eq!(
436            rules::get_predefined_underride_room_rule_id(IsEncrypted::No, IsOneToOne::Yes),
437            PredefinedUnderrideRuleId::RoomOneToOne
438        );
439        assert_eq!(
440            rules::get_predefined_underride_room_rule_id(IsEncrypted::Yes, IsOneToOne::No),
441            PredefinedUnderrideRuleId::Encrypted
442        );
443        assert_eq!(
444            rules::get_predefined_underride_room_rule_id(IsEncrypted::Yes, IsOneToOne::Yes),
445            PredefinedUnderrideRuleId::EncryptedRoomOneToOne
446        );
447    }
448
449    #[async_test]
450    async fn test_get_predefined_underride_poll_start_rule_id() {
451        assert_eq!(
452            rules::get_predefined_underride_poll_start_rule_id(IsOneToOne::No),
453            PredefinedUnderrideRuleId::PollStart
454        );
455        assert_eq!(
456            rules::get_predefined_underride_poll_start_rule_id(IsOneToOne::Yes),
457            PredefinedUnderrideRuleId::PollStartOneToOne
458        );
459    }
460
461    #[async_test]
462    async fn test_get_default_room_notification_mode_mentions_and_keywords() {
463        let mut ruleset = get_server_default_ruleset();
464        // If the corresponding underride rule is disabled
465        ruleset
466            .set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, false)
467            .unwrap();
468
469        let rules = Rules::new(ruleset);
470        let mode = rules.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes);
471        // Then the mode should be `MentionsAndKeywordsOnly`
472        assert_eq!(mode, RoomNotificationMode::MentionsAndKeywordsOnly);
473    }
474
475    #[async_test]
476    async fn test_get_default_room_notification_mode_all_messages() {
477        let mut ruleset = get_server_default_ruleset();
478        // If the corresponding underride rule is enabled
479        ruleset
480            .set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, true)
481            .unwrap();
482
483        let rules = Rules::new(ruleset);
484        let mode = rules.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes);
485        // Then the mode should be `AllMessages`
486        assert_eq!(mode, RoomNotificationMode::AllMessages);
487    }
488
489    #[async_test]
490    async fn test_is_user_mention_enabled() {
491        // If `IsUserMention` is enable, then is_user_mention_enabled() should return
492        // `true` even if the deprecated rules are disabled
493        let mut ruleset = server_default_ruleset_with_legacy_mentions();
494        ruleset
495            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, true)
496            .unwrap();
497        #[allow(deprecated)]
498        ruleset
499            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName, false)
500            .unwrap();
501        #[allow(deprecated)]
502        ruleset
503            .set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, false)
504            .unwrap();
505
506        let rules = Rules::new(ruleset);
507        assert!(rules.is_user_mention_enabled());
508        // is_enabled() should also return `true` for
509        // PredefinedOverrideRuleId::IsUserMention
510        assert!(
511            rules
512                .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.as_str())
513                .unwrap()
514        );
515
516        // If `IsUserMention` is disabled, then is_user_mention_enabled() should return
517        // `false` even if the deprecated rules are enabled
518        let mut ruleset = server_default_ruleset_with_legacy_mentions();
519        ruleset
520            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, false)
521            .unwrap();
522        #[allow(deprecated)]
523        ruleset
524            .set_actions(
525                RuleKind::Override,
526                PredefinedOverrideRuleId::ContainsDisplayName,
527                vec![Action::Notify],
528            )
529            .unwrap();
530        #[allow(deprecated)]
531        ruleset
532            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName, true)
533            .unwrap();
534        #[allow(deprecated)]
535        ruleset
536            .set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, true)
537            .unwrap();
538
539        let rules = Rules::new(ruleset);
540        assert!(!rules.is_user_mention_enabled());
541        // is_enabled() should also return `false` for
542        // PredefinedOverrideRuleId::IsUserMention
543        assert!(
544            !rules
545                .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.as_str())
546                .unwrap()
547        );
548    }
549
550    #[async_test]
551    async fn test_is_room_mention_enabled() {
552        // If `IsRoomMention` is present and enabled then is_room_mention_enabled()
553        // should return `true` even if the deprecated rule is disabled
554        let mut ruleset = server_default_ruleset_with_legacy_mentions();
555        ruleset
556            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, true)
557            .unwrap();
558        #[allow(deprecated)]
559        ruleset
560            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, false)
561            .unwrap();
562
563        let rules = Rules::new(ruleset);
564        assert!(rules.is_room_mention_enabled());
565        // is_enabled() should also return `true` for
566        // PredefinedOverrideRuleId::IsRoomMention
567        assert!(
568            rules
569                .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention.as_str())
570                .unwrap()
571        );
572
573        // If `IsRoomMention` is present and disabled then is_room_mention_enabled()
574        // should return `false` even if the deprecated rule is enabled
575        let mut ruleset = server_default_ruleset_with_legacy_mentions();
576        ruleset
577            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, false)
578            .unwrap();
579        #[allow(deprecated)]
580        ruleset.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, true).unwrap();
581
582        let rules = Rules::new(ruleset);
583        assert!(!rules.is_room_mention_enabled());
584        // is_enabled() should also return `false` for
585        // PredefinedOverrideRuleId::IsRoomMention
586        assert!(
587            !rules
588                .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention.as_str())
589                .unwrap()
590        );
591    }
592
593    #[async_test]
594    async fn test_is_enabled_rule_not_found() {
595        let rules = Rules::new(get_server_default_ruleset());
596
597        assert_eq!(
598            rules.is_enabled(RuleKind::Override, "unknown_rule_id"),
599            Err(NotificationSettingsError::RuleNotFound("unknown_rule_id".to_owned()))
600        );
601    }
602
603    #[async_test]
604    async fn test_apply_delete_command() {
605        let room_id = get_test_room_id();
606        // Initialize with a custom rule
607        let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
608        let mut rules = Rules::new(ruleset);
609
610        // Build a `RuleCommands` deleting this rule
611        let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
612        rules_commands.delete_rule(RuleKind::Override, room_id.to_string()).unwrap();
613
614        rules.apply(rules_commands);
615
616        // The rule must have been removed from the updated rules
617        assert!(rules.get_custom_rules_for_room(&room_id).is_empty());
618    }
619
620    #[async_test]
621    async fn test_apply_set_command() {
622        let room_id = get_test_room_id();
623        let mut rules = Rules::new(get_server_default_ruleset());
624
625        // Build a `RuleCommands` inserting a rule
626        let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
627        rules_commands.insert_rule(RuleKind::Override, &room_id, false).unwrap();
628
629        rules.apply(rules_commands);
630
631        // The rule must have been removed from the updated rules
632        assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
633    }
634
635    #[async_test]
636    async fn test_apply_set_enabled_command() {
637        let mut rules = Rules::new(get_server_default_ruleset());
638
639        rules
640            .ruleset
641            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, true)
642            .unwrap();
643
644        // Build a `RuleCommands` disabling the rule
645        let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
646        rules_commands
647            .set_rule_enabled(
648                RuleKind::Override,
649                PredefinedOverrideRuleId::Reaction.as_str(),
650                false,
651            )
652            .unwrap();
653
654        rules.apply(rules_commands);
655
656        // The rule must have been disabled in the updated rules
657        assert!(
658            !rules
659                .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction.as_str())
660                .unwrap()
661        );
662    }
663
664    #[async_test]
665    async fn test_get_rooms_with_user_defined_rules() {
666        // Without user-defined rules
667        let rules = Rules::new(get_server_default_ruleset());
668        let room_ids = rules.get_rooms_with_user_defined_rules(None);
669        assert!(room_ids.is_empty());
670
671        // With one rule.
672        let room_id = RoomId::parse("!room_a:matrix.org").unwrap();
673        let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
674        let rules = Rules::new(ruleset);
675
676        let room_ids = rules.get_rooms_with_user_defined_rules(None);
677        assert_eq!(room_ids.len(), 1);
678
679        // With duplicates
680        let ruleset = build_ruleset(vec![
681            (RuleKind::Override, &room_id, false),
682            (RuleKind::Underride, &room_id, false),
683            (RuleKind::Room, &room_id, false),
684        ]);
685        let rules = Rules::new(ruleset);
686
687        let room_ids = rules.get_rooms_with_user_defined_rules(None);
688        assert_eq!(room_ids.len(), 1);
689        assert_eq!(room_ids[0], room_id.to_string());
690
691        // With multiple rules
692        let ruleset = build_ruleset(vec![
693            (RuleKind::Room, &RoomId::parse("!room_a:matrix.org").unwrap(), false),
694            (RuleKind::Room, &RoomId::parse("!room_b:matrix.org").unwrap(), false),
695            (RuleKind::Room, &RoomId::parse("!room_c:matrix.org").unwrap(), false),
696            (RuleKind::Override, &RoomId::parse("!room_d:matrix.org").unwrap(), false),
697            (RuleKind::Underride, &RoomId::parse("!room_e:matrix.org").unwrap(), false),
698        ]);
699        let rules = Rules::new(ruleset);
700
701        let room_ids = rules.get_rooms_with_user_defined_rules(None);
702        assert_eq!(room_ids.len(), 5);
703        let expected_set: HashSet<String> = vec![
704            "!room_a:matrix.org",
705            "!room_b:matrix.org",
706            "!room_c:matrix.org",
707            "!room_d:matrix.org",
708            "!room_e:matrix.org",
709        ]
710        .into_iter()
711        .collect();
712        assert!(expected_set.symmetric_difference(HashSet::from(room_ids)).is_empty());
713
714        // Only disabled rules
715        let room_ids = rules.get_rooms_with_user_defined_rules(Some(false));
716        assert_eq!(room_ids.len(), 0);
717
718        // Only enabled rules
719        let room_ids = rules.get_rooms_with_user_defined_rules(Some(true));
720        assert_eq!(room_ids.len(), 5);
721
722        let mut ruleset = build_ruleset(vec![
723            (RuleKind::Room, &RoomId::parse("!room_a:matrix.org").unwrap(), false),
724            (RuleKind::Room, &RoomId::parse("!room_b:matrix.org").unwrap(), false),
725            (RuleKind::Override, &RoomId::parse("!room_c:matrix.org").unwrap(), false),
726            (RuleKind::Underride, &RoomId::parse("!room_d:matrix.org").unwrap(), false),
727        ]);
728        ruleset.set_enabled(RuleKind::Room, "!room_b:matrix.org", false).unwrap();
729        ruleset.set_enabled(RuleKind::Override, "!room_c:matrix.org", false).unwrap();
730        let rules = Rules::new(ruleset);
731        // Only room_a and room_d rules are enabled
732        let room_ids = rules.get_rooms_with_user_defined_rules(Some(true));
733        assert_eq!(room_ids.len(), 2);
734        let expected_set: HashSet<String> =
735            vec!["!room_a:matrix.org", "!room_d:matrix.org"].into_iter().collect();
736        assert!(expected_set.symmetric_difference(HashSet::from(room_ids)).is_empty());
737    }
738}