ruma_common/
push.rs

1//! Common types for the [push notifications module][push].
2//!
3//! [push]: https://spec.matrix.org/latest/client-server-api/#push-notifications
4//!
5//! ## Understanding the types of this module
6//!
7//! Push rules are grouped in `RuleSet`s, and are grouped in five kinds (for
8//! more details about the different kind of rules, see the `Ruleset` documentation,
9//! or the specification). These five kinds are, by order of priority:
10//!
11//! - override rules
12//! - content rules
13//! - room rules
14//! - sender rules
15//! - underride rules
16
17use std::hash::{Hash, Hasher};
18
19use indexmap::{Equivalent, IndexSet};
20use serde::{Deserialize, Serialize};
21use thiserror::Error;
22use tracing::instrument;
23
24use crate::{
25    OwnedRoomId, OwnedUserId, PrivOwnedStr,
26    serde::{JsonObject, Raw, StringEnum},
27};
28
29mod action;
30mod condition;
31mod iter;
32mod predefined;
33
34#[cfg(feature = "unstable-msc3932")]
35pub use self::condition::RoomVersionFeature;
36pub use self::{
37    action::{Action, Tweak},
38    condition::{
39        _CustomPushCondition, ComparisonOperator, FlattenedJson, FlattenedJsonValue, PushCondition,
40        PushConditionPowerLevelsCtx, PushConditionRoomCtx, RoomMemberCountIs, ScalarJsonValue,
41    },
42    iter::{AnyPushRule, AnyPushRuleRef, RulesetIntoIter, RulesetIter},
43    predefined::{
44        PredefinedContentRuleId, PredefinedOverrideRuleId, PredefinedRuleId,
45        PredefinedUnderrideRuleId,
46    },
47};
48
49/// A push ruleset scopes a set of rules according to some criteria.
50///
51/// For example, some rules may only be applied for messages from a particular sender, a particular
52/// room, or by default. The push ruleset contains the entire set of scopes and rules.
53#[derive(Clone, Debug, Default, Deserialize, Serialize)]
54#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
55pub struct Ruleset {
56    /// These rules configure behavior for (unencrypted) messages that match certain patterns.
57    #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
58    pub content: IndexSet<PatternedPushRule>,
59
60    /// These rules are identical to override rules, but have a lower priority than `room` and
61    /// `sender` rules.
62    #[cfg(feature = "unstable-msc4306")]
63    #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
64    pub postcontent: IndexSet<ConditionalPushRule>,
65
66    /// These user-configured rules are given the highest priority.
67    ///
68    /// This field is named `override_` instead of `override` because the latter is a reserved
69    /// keyword in Rust.
70    #[serde(rename = "override", default, skip_serializing_if = "IndexSet::is_empty")]
71    pub override_: IndexSet<ConditionalPushRule>,
72
73    /// These rules change the behavior of all messages for a given room.
74    #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
75    pub room: IndexSet<SimplePushRule<OwnedRoomId>>,
76
77    /// These rules configure notification behavior for messages from a specific Matrix user ID.
78    #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
79    pub sender: IndexSet<SimplePushRule<OwnedUserId>>,
80
81    /// These rules are identical to override rules, but have a lower priority than `content`,
82    /// `room` and `sender` rules.
83    #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
84    pub underride: IndexSet<ConditionalPushRule>,
85}
86
87impl Ruleset {
88    /// Creates an empty `Ruleset`.
89    pub fn new() -> Self {
90        Default::default()
91    }
92
93    /// Creates a borrowing iterator over all push rules in this `Ruleset`.
94    ///
95    /// For an owning iterator, use `.into_iter()`.
96    pub fn iter(&self) -> RulesetIter<'_> {
97        self.into_iter()
98    }
99
100    /// Inserts a user-defined rule in the rule set.
101    ///
102    /// If a rule with the same kind and `rule_id` exists, it will be replaced.
103    ///
104    /// If `after` or `before` is set, the rule will be moved relative to the rule with the given
105    /// ID. If both are set, the rule will become the next-most important rule with respect to
106    /// `before`. If neither are set, and the rule is newly inserted, it will become the rule with
107    /// the highest priority of its kind.
108    ///
109    /// Returns an error if the parameters are invalid.
110    pub fn insert(
111        &mut self,
112        rule: NewPushRule,
113        after: Option<&str>,
114        before: Option<&str>,
115    ) -> Result<(), InsertPushRuleError> {
116        let rule_id = rule.rule_id();
117        if rule_id.starts_with('.') {
118            return Err(InsertPushRuleError::ServerDefaultRuleId);
119        }
120        if rule_id.contains('/') {
121            return Err(InsertPushRuleError::InvalidRuleId);
122        }
123        if rule_id.contains('\\') {
124            return Err(InsertPushRuleError::InvalidRuleId);
125        }
126        if after.is_some_and(|s| s.starts_with('.')) {
127            return Err(InsertPushRuleError::RelativeToServerDefaultRule);
128        }
129        if before.is_some_and(|s| s.starts_with('.')) {
130            return Err(InsertPushRuleError::RelativeToServerDefaultRule);
131        }
132
133        match rule {
134            NewPushRule::Override(r) => {
135                let mut rule = ConditionalPushRule::from(r);
136
137                if let Some(prev_rule) = self.override_.get(rule.rule_id.as_str()) {
138                    rule.enabled = prev_rule.enabled;
139                }
140
141                // `m.rule.master` should always be the rule with the highest priority, so we insert
142                // this one at most at the second place.
143                let default_position = 1;
144
145                insert_and_move_rule(&mut self.override_, rule, default_position, after, before)
146            }
147            #[cfg(feature = "unstable-msc4306")]
148            NewPushRule::PostContent(r) => {
149                let mut rule = ConditionalPushRule::from(r);
150
151                if let Some(prev_rule) = self.postcontent.get(rule.rule_id.as_str()) {
152                    rule.enabled = prev_rule.enabled;
153                }
154
155                insert_and_move_rule(&mut self.postcontent, rule, 0, after, before)
156            }
157            NewPushRule::Underride(r) => {
158                let mut rule = ConditionalPushRule::from(r);
159
160                if let Some(prev_rule) = self.underride.get(rule.rule_id.as_str()) {
161                    rule.enabled = prev_rule.enabled;
162                }
163
164                insert_and_move_rule(&mut self.underride, rule, 0, after, before)
165            }
166            NewPushRule::Content(r) => {
167                let mut rule = PatternedPushRule::from(r);
168
169                if let Some(prev_rule) = self.content.get(rule.rule_id.as_str()) {
170                    rule.enabled = prev_rule.enabled;
171                }
172
173                insert_and_move_rule(&mut self.content, rule, 0, after, before)
174            }
175            NewPushRule::Room(r) => {
176                let mut rule = SimplePushRule::from(r);
177
178                if let Some(prev_rule) = self.room.get(rule.rule_id.as_str()) {
179                    rule.enabled = prev_rule.enabled;
180                }
181
182                insert_and_move_rule(&mut self.room, rule, 0, after, before)
183            }
184            NewPushRule::Sender(r) => {
185                let mut rule = SimplePushRule::from(r);
186
187                if let Some(prev_rule) = self.sender.get(rule.rule_id.as_str()) {
188                    rule.enabled = prev_rule.enabled;
189                }
190
191                insert_and_move_rule(&mut self.sender, rule, 0, after, before)
192            }
193        }
194    }
195
196    /// Get the rule from the given kind and with the given `rule_id` in the rule set.
197    pub fn get(&self, kind: RuleKind, rule_id: impl AsRef<str>) -> Option<AnyPushRuleRef<'_>> {
198        let rule_id = rule_id.as_ref();
199
200        match kind {
201            RuleKind::Override => self.override_.get(rule_id).map(AnyPushRuleRef::Override),
202            RuleKind::Underride => self.underride.get(rule_id).map(AnyPushRuleRef::Underride),
203            RuleKind::Sender => self.sender.get(rule_id).map(AnyPushRuleRef::Sender),
204            RuleKind::Room => self.room.get(rule_id).map(AnyPushRuleRef::Room),
205            RuleKind::Content => self.content.get(rule_id).map(AnyPushRuleRef::Content),
206            #[cfg(feature = "unstable-msc4306")]
207            RuleKind::PostContent => self.postcontent.get(rule_id).map(AnyPushRuleRef::PostContent),
208            RuleKind::_Custom(_) => None,
209        }
210    }
211
212    /// Set whether the rule from the given kind and with the given `rule_id` in the rule set is
213    /// enabled.
214    ///
215    /// Returns an error if the rule can't be found.
216    pub fn set_enabled(
217        &mut self,
218        kind: RuleKind,
219        rule_id: impl AsRef<str>,
220        enabled: bool,
221    ) -> Result<(), RuleNotFoundError> {
222        let rule_id = rule_id.as_ref();
223
224        match kind {
225            RuleKind::Override => {
226                let mut rule = self.override_.get(rule_id).ok_or(RuleNotFoundError)?.clone();
227                rule.enabled = enabled;
228                self.override_.replace(rule);
229            }
230            RuleKind::Underride => {
231                let mut rule = self.underride.get(rule_id).ok_or(RuleNotFoundError)?.clone();
232                rule.enabled = enabled;
233                self.underride.replace(rule);
234            }
235            RuleKind::Sender => {
236                let mut rule = self.sender.get(rule_id).ok_or(RuleNotFoundError)?.clone();
237                rule.enabled = enabled;
238                self.sender.replace(rule);
239            }
240            RuleKind::Room => {
241                let mut rule = self.room.get(rule_id).ok_or(RuleNotFoundError)?.clone();
242                rule.enabled = enabled;
243                self.room.replace(rule);
244            }
245            RuleKind::Content => {
246                let mut rule = self.content.get(rule_id).ok_or(RuleNotFoundError)?.clone();
247                rule.enabled = enabled;
248                self.content.replace(rule);
249            }
250            #[cfg(feature = "unstable-msc4306")]
251            RuleKind::PostContent => {
252                let mut rule = self.postcontent.get(rule_id).ok_or(RuleNotFoundError)?.clone();
253                rule.enabled = enabled;
254                self.postcontent.replace(rule);
255            }
256            RuleKind::_Custom(_) => return Err(RuleNotFoundError),
257        }
258
259        Ok(())
260    }
261
262    /// Set the actions of the rule from the given kind and with the given `rule_id` in the rule
263    /// set.
264    ///
265    /// Returns an error if the rule can't be found.
266    pub fn set_actions(
267        &mut self,
268        kind: RuleKind,
269        rule_id: impl AsRef<str>,
270        actions: Vec<Action>,
271    ) -> Result<(), RuleNotFoundError> {
272        let rule_id = rule_id.as_ref();
273
274        match kind {
275            RuleKind::Override => {
276                let mut rule = self.override_.get(rule_id).ok_or(RuleNotFoundError)?.clone();
277                rule.actions = actions;
278                self.override_.replace(rule);
279            }
280            RuleKind::Underride => {
281                let mut rule = self.underride.get(rule_id).ok_or(RuleNotFoundError)?.clone();
282                rule.actions = actions;
283                self.underride.replace(rule);
284            }
285            RuleKind::Sender => {
286                let mut rule = self.sender.get(rule_id).ok_or(RuleNotFoundError)?.clone();
287                rule.actions = actions;
288                self.sender.replace(rule);
289            }
290            RuleKind::Room => {
291                let mut rule = self.room.get(rule_id).ok_or(RuleNotFoundError)?.clone();
292                rule.actions = actions;
293                self.room.replace(rule);
294            }
295            RuleKind::Content => {
296                let mut rule = self.content.get(rule_id).ok_or(RuleNotFoundError)?.clone();
297                rule.actions = actions;
298                self.content.replace(rule);
299            }
300            #[cfg(feature = "unstable-msc4306")]
301            RuleKind::PostContent => {
302                let mut rule = self.postcontent.get(rule_id).ok_or(RuleNotFoundError)?.clone();
303                rule.actions = actions;
304                self.postcontent.replace(rule);
305            }
306            RuleKind::_Custom(_) => return Err(RuleNotFoundError),
307        }
308
309        Ok(())
310    }
311
312    /// Get the first push rule that applies to this event, if any.
313    ///
314    /// # Arguments
315    ///
316    /// * `event` - The raw JSON of a room message event.
317    /// * `context` - The context of the message and room at the time of the event.
318    #[instrument(skip_all, fields(context.room_id = %context.room_id))]
319    pub async fn get_match<T>(
320        &self,
321        event: &Raw<T>,
322        context: &PushConditionRoomCtx,
323    ) -> Option<AnyPushRuleRef<'_>> {
324        let event = FlattenedJson::from_raw(event);
325
326        if event.get_str("sender").is_some_and(|sender| sender == context.user_id) {
327            // no need to look at the rules if the event was by the user themselves
328            return None;
329        }
330
331        for rule in self {
332            if rule.applies(&event, context).await {
333                return Some(rule);
334            }
335        }
336
337        None
338    }
339
340    /// Get the push actions that apply to this event.
341    ///
342    /// Returns an empty slice if no push rule applies.
343    ///
344    /// # Arguments
345    ///
346    /// * `event` - The raw JSON of a room message event.
347    /// * `context` - The context of the message and room at the time of the event.
348    #[instrument(skip_all, fields(context.room_id = %context.room_id))]
349    pub async fn get_actions<T>(
350        &self,
351        event: &Raw<T>,
352        context: &PushConditionRoomCtx,
353    ) -> &[Action] {
354        self.get_match(event, context).await.map(|rule| rule.actions()).unwrap_or(&[])
355    }
356
357    /// Removes a user-defined rule in the rule set.
358    ///
359    /// Returns an error if the parameters are invalid.
360    pub fn remove(
361        &mut self,
362        kind: RuleKind,
363        rule_id: impl AsRef<str>,
364    ) -> Result<(), RemovePushRuleError> {
365        let rule_id = rule_id.as_ref();
366
367        if let Some(rule) = self.get(kind.clone(), rule_id) {
368            if rule.is_server_default() {
369                return Err(RemovePushRuleError::ServerDefault);
370            }
371        } else {
372            return Err(RemovePushRuleError::NotFound);
373        }
374
375        match kind {
376            RuleKind::Override => {
377                self.override_.shift_remove(rule_id);
378            }
379            RuleKind::Underride => {
380                self.underride.shift_remove(rule_id);
381            }
382            RuleKind::Sender => {
383                self.sender.shift_remove(rule_id);
384            }
385            RuleKind::Room => {
386                self.room.shift_remove(rule_id);
387            }
388            RuleKind::Content => {
389                self.content.shift_remove(rule_id);
390            }
391            #[cfg(feature = "unstable-msc4306")]
392            RuleKind::PostContent => {
393                self.postcontent.shift_remove(rule_id);
394            }
395            // This has been handled in the `self.get` call earlier.
396            RuleKind::_Custom(_) => unreachable!(),
397        }
398
399        Ok(())
400    }
401}
402
403/// A push rule is a single rule that states under what conditions an event should be passed onto a
404/// push gateway and how the notification should be presented.
405///
406/// These rules are stored on the user's homeserver. They are manually configured by the user, who
407/// can create and view them via the Client/Server API.
408///
409/// To create an instance of this type, first create a `SimplePushRuleInit` and convert it via
410/// `SimplePushRule::from` / `.into()`.
411#[derive(Clone, Debug, Deserialize, Serialize)]
412#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
413pub struct SimplePushRule<T> {
414    /// Actions to determine if and how a notification is delivered for events matching this rule.
415    pub actions: Vec<Action>,
416
417    /// Whether this is a default rule, or has been set explicitly.
418    pub default: bool,
419
420    /// Whether the push rule is enabled or not.
421    pub enabled: bool,
422
423    /// The ID of this rule.
424    ///
425    /// This is generally the Matrix ID of the entity that it applies to.
426    pub rule_id: T,
427}
428
429/// Initial set of fields of `SimplePushRule`.
430///
431/// This struct will not be updated even if additional fields are added to `SimplePushRule` in a new
432/// (non-breaking) release of the Matrix specification.
433#[derive(Debug)]
434#[allow(clippy::exhaustive_structs)]
435pub struct SimplePushRuleInit<T> {
436    /// Actions to determine if and how a notification is delivered for events matching this rule.
437    pub actions: Vec<Action>,
438
439    /// Whether this is a default rule, or has been set explicitly.
440    pub default: bool,
441
442    /// Whether the push rule is enabled or not.
443    pub enabled: bool,
444
445    /// The ID of this rule.
446    ///
447    /// This is generally the Matrix ID of the entity that it applies to.
448    pub rule_id: T,
449}
450
451impl<T> From<SimplePushRuleInit<T>> for SimplePushRule<T> {
452    fn from(init: SimplePushRuleInit<T>) -> Self {
453        let SimplePushRuleInit { actions, default, enabled, rule_id } = init;
454        Self { actions, default, enabled, rule_id }
455    }
456}
457
458// The following trait are needed to be able to make
459// an IndexSet of the type
460
461impl<T> Hash for SimplePushRule<T>
462where
463    T: Hash,
464{
465    fn hash<H: Hasher>(&self, state: &mut H) {
466        self.rule_id.hash(state);
467    }
468}
469
470impl<T> PartialEq for SimplePushRule<T>
471where
472    T: PartialEq<T>,
473{
474    fn eq(&self, other: &Self) -> bool {
475        self.rule_id == other.rule_id
476    }
477}
478
479impl<T> Eq for SimplePushRule<T> where T: Eq {}
480
481impl<T> Equivalent<SimplePushRule<T>> for str
482where
483    T: AsRef<str>,
484{
485    fn equivalent(&self, key: &SimplePushRule<T>) -> bool {
486        self == key.rule_id.as_ref()
487    }
488}
489
490/// Like `SimplePushRule`, but with an additional `conditions` field.
491///
492/// Only applicable to underride and override rules.
493///
494/// To create an instance of this type, first create a `ConditionalPushRuleInit` and convert it via
495/// `ConditionalPushRule::from` / `.into()`.
496#[derive(Clone, Debug, Deserialize, Serialize)]
497#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
498pub struct ConditionalPushRule {
499    /// Actions to determine if and how a notification is delivered for events matching this rule.
500    pub actions: Vec<Action>,
501
502    /// Whether this is a default rule, or has been set explicitly.
503    pub default: bool,
504
505    /// Whether the push rule is enabled or not.
506    pub enabled: bool,
507
508    /// The ID of this rule.
509    pub rule_id: String,
510
511    /// The conditions that must hold true for an event in order for a rule to be applied to an
512    /// event.
513    ///
514    /// A rule with no conditions always matches.
515    #[serde(default)]
516    pub conditions: Vec<PushCondition>,
517}
518
519impl ConditionalPushRule {
520    /// Check if the push rule applies to the event.
521    ///
522    /// # Arguments
523    ///
524    /// * `event` - The flattened JSON representation of a room message event.
525    /// * `context` - The context of the room at the time of the event.
526    pub async fn applies(&self, event: &FlattenedJson, context: &PushConditionRoomCtx) -> bool {
527        if !self.enabled {
528            return false;
529        }
530
531        #[cfg(feature = "unstable-msc3932")]
532        {
533            // These 3 rules always apply.
534            #[allow(deprecated)]
535            if self.rule_id != PredefinedOverrideRuleId::Master.as_ref()
536                && self.rule_id != PredefinedOverrideRuleId::RoomNotif.as_ref()
537                && self.rule_id != PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
538            {
539                // Push rules which don't specify a `room_version_supports` condition are assumed
540                // to not support extensible events and are therefore expected to be treated as
541                // disabled when a room version does support extensible events.
542                let room_supports_ext_ev =
543                    context.supported_features.contains(&RoomVersionFeature::ExtensibleEvents);
544                let rule_has_room_version_supports = self.conditions.iter().any(|condition| {
545                    matches!(condition, PushCondition::RoomVersionSupports { .. })
546                });
547
548                if room_supports_ext_ev && !rule_has_room_version_supports {
549                    return false;
550                }
551            }
552        }
553
554        // The old mention rules are disabled when an m.mentions field is present.
555        #[allow(deprecated)]
556        if (self.rule_id == PredefinedOverrideRuleId::RoomNotif.as_ref()
557            || self.rule_id == PredefinedOverrideRuleId::ContainsDisplayName.as_ref())
558            && event.contains_mentions()
559        {
560            return false;
561        }
562
563        for cond in &self.conditions {
564            if !cond.applies(event, context).await {
565                return false;
566            }
567        }
568        true
569    }
570}
571
572/// Initial set of fields of `ConditionalPushRule`.
573///
574/// This struct will not be updated even if additional fields are added to `ConditionalPushRule` in
575/// a new (non-breaking) release of the Matrix specification.
576#[derive(Debug)]
577#[allow(clippy::exhaustive_structs)]
578pub struct ConditionalPushRuleInit {
579    /// Actions to determine if and how a notification is delivered for events matching this rule.
580    pub actions: Vec<Action>,
581
582    /// Whether this is a default rule, or has been set explicitly.
583    pub default: bool,
584
585    /// Whether the push rule is enabled or not.
586    pub enabled: bool,
587
588    /// The ID of this rule.
589    pub rule_id: String,
590
591    /// The conditions that must hold true for an event in order for a rule to be applied to an
592    /// event.
593    ///
594    /// A rule with no conditions always matches.
595    pub conditions: Vec<PushCondition>,
596}
597
598impl From<ConditionalPushRuleInit> for ConditionalPushRule {
599    fn from(init: ConditionalPushRuleInit) -> Self {
600        let ConditionalPushRuleInit { actions, default, enabled, rule_id, conditions } = init;
601        Self { actions, default, enabled, rule_id, conditions }
602    }
603}
604
605// The following trait are needed to be able to make
606// an IndexSet of the type
607
608impl Hash for ConditionalPushRule {
609    fn hash<H: Hasher>(&self, state: &mut H) {
610        self.rule_id.hash(state);
611    }
612}
613
614impl PartialEq for ConditionalPushRule {
615    fn eq(&self, other: &Self) -> bool {
616        self.rule_id == other.rule_id
617    }
618}
619
620impl Eq for ConditionalPushRule {}
621
622impl Equivalent<ConditionalPushRule> for str {
623    fn equivalent(&self, key: &ConditionalPushRule) -> bool {
624        self == key.rule_id
625    }
626}
627
628/// Like `SimplePushRule`, but with an additional `pattern` field.
629///
630/// Only applicable to content rules.
631///
632/// To create an instance of this type, first create a `PatternedPushRuleInit` and convert it via
633/// `PatternedPushRule::from` / `.into()`.
634#[derive(Clone, Debug, Deserialize, Serialize)]
635#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
636pub struct PatternedPushRule {
637    /// Actions to determine if and how a notification is delivered for events matching this rule.
638    pub actions: Vec<Action>,
639
640    /// Whether this is a default rule, or has been set explicitly.
641    pub default: bool,
642
643    /// Whether the push rule is enabled or not.
644    pub enabled: bool,
645
646    /// The ID of this rule.
647    pub rule_id: String,
648
649    /// The glob-style pattern to match against.
650    pub pattern: String,
651}
652
653impl PatternedPushRule {
654    /// Check if the push rule applies to the event.
655    ///
656    /// # Arguments
657    ///
658    /// * `event` - The flattened JSON representation of a room message event.
659    /// * `context` - The context of the room at the time of the event.
660    pub fn applies_to(
661        &self,
662        key: &str,
663        event: &FlattenedJson,
664        context: &PushConditionRoomCtx,
665    ) -> bool {
666        // The old mention rules are disabled when an m.mentions field is present.
667        #[allow(deprecated)]
668        if self.rule_id == PredefinedContentRuleId::ContainsUserName.as_ref()
669            && event.contains_mentions()
670        {
671            return false;
672        }
673
674        if event.get_str("sender").is_some_and(|sender| sender == context.user_id) {
675            return false;
676        }
677
678        self.enabled && condition::check_event_match(event, key, &self.pattern, context)
679    }
680}
681
682/// Initial set of fields of `PatternedPushRule`.
683///
684/// This struct will not be updated even if additional fields are added to `PatternedPushRule` in a
685/// new (non-breaking) release of the Matrix specification.
686#[derive(Debug)]
687#[allow(clippy::exhaustive_structs)]
688pub struct PatternedPushRuleInit {
689    /// Actions to determine if and how a notification is delivered for events matching this rule.
690    pub actions: Vec<Action>,
691
692    /// Whether this is a default rule, or has been set explicitly.
693    pub default: bool,
694
695    /// Whether the push rule is enabled or not.
696    pub enabled: bool,
697
698    /// The ID of this rule.
699    pub rule_id: String,
700
701    /// The glob-style pattern to match against.
702    pub pattern: String,
703}
704
705impl From<PatternedPushRuleInit> for PatternedPushRule {
706    fn from(init: PatternedPushRuleInit) -> Self {
707        let PatternedPushRuleInit { actions, default, enabled, rule_id, pattern } = init;
708        Self { actions, default, enabled, rule_id, pattern }
709    }
710}
711
712// The following trait are needed to be able to make
713// an IndexSet of the type
714
715impl Hash for PatternedPushRule {
716    fn hash<H: Hasher>(&self, state: &mut H) {
717        self.rule_id.hash(state);
718    }
719}
720
721impl PartialEq for PatternedPushRule {
722    fn eq(&self, other: &Self) -> bool {
723        self.rule_id == other.rule_id
724    }
725}
726
727impl Eq for PatternedPushRule {}
728
729impl Equivalent<PatternedPushRule> for str {
730    fn equivalent(&self, key: &PatternedPushRule) -> bool {
731        self == key.rule_id
732    }
733}
734
735/// Information for a pusher using the Push Gateway API.
736#[derive(Clone, Debug, Serialize, Deserialize)]
737#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
738pub struct HttpPusherData {
739    /// The URL to use to send notifications to.
740    ///
741    /// Required if the pusher's kind is http.
742    pub url: String,
743
744    /// The format to use when sending notifications to the Push Gateway.
745    #[serde(skip_serializing_if = "Option::is_none")]
746    pub format: Option<PushFormat>,
747
748    /// Custom data for the pusher.
749    #[serde(flatten, default, skip_serializing_if = "JsonObject::is_empty")]
750    pub data: JsonObject,
751}
752
753impl HttpPusherData {
754    /// Creates a new `HttpPusherData` with the given URL.
755    pub fn new(url: String) -> Self {
756        Self { url, format: None, data: JsonObject::default() }
757    }
758}
759
760/// A special format that the homeserver should use when sending notifications to a Push Gateway.
761/// Currently, only `event_id_only` is supported, see the [Push Gateway API][spec].
762///
763/// [spec]: https://spec.matrix.org/latest/push-gateway-api/#homeserver-behaviour
764#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
765#[derive(Clone, StringEnum)]
766#[ruma_enum(rename_all = "snake_case")]
767#[non_exhaustive]
768pub enum PushFormat {
769    /// Require the homeserver to only send a reduced set of fields in the push.
770    EventIdOnly,
771
772    #[doc(hidden)]
773    _Custom(PrivOwnedStr),
774}
775
776/// The kinds of push rules that are available.
777#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
778#[derive(Clone, StringEnum)]
779#[ruma_enum(rename_all = "snake_case")]
780#[non_exhaustive]
781pub enum RuleKind {
782    /// User-configured rules that override all other kinds.
783    Override,
784
785    /// Lowest priority user-defined rules.
786    Underride,
787
788    /// Sender-specific rules.
789    Sender,
790
791    /// Room-specific rules.
792    Room,
793
794    /// Content-specific rules.
795    Content,
796
797    /// Post-content specific rules.
798    #[cfg(feature = "unstable-msc4306")]
799    #[ruma_enum(rename = "io.element.msc4306.postcontent")]
800    PostContent,
801
802    #[doc(hidden)]
803    _Custom(PrivOwnedStr),
804}
805
806/// A push rule to update or create.
807#[derive(Clone, Debug)]
808#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
809pub enum NewPushRule {
810    /// Rules that override all other kinds.
811    Override(NewConditionalPushRule),
812
813    /// Content-specific rules.
814    Content(NewPatternedPushRule),
815
816    /// Post-content specific rules.
817    #[cfg(feature = "unstable-msc4306")]
818    PostContent(NewConditionalPushRule),
819
820    /// Room-specific rules.
821    Room(NewSimplePushRule<OwnedRoomId>),
822
823    /// Sender-specific rules.
824    Sender(NewSimplePushRule<OwnedUserId>),
825
826    /// Lowest priority rules.
827    Underride(NewConditionalPushRule),
828}
829
830impl NewPushRule {
831    /// The kind of this `NewPushRule`.
832    pub fn kind(&self) -> RuleKind {
833        match self {
834            NewPushRule::Override(_) => RuleKind::Override,
835            NewPushRule::Content(_) => RuleKind::Content,
836            #[cfg(feature = "unstable-msc4306")]
837            NewPushRule::PostContent(_) => RuleKind::PostContent,
838            NewPushRule::Room(_) => RuleKind::Room,
839            NewPushRule::Sender(_) => RuleKind::Sender,
840            NewPushRule::Underride(_) => RuleKind::Underride,
841        }
842    }
843
844    /// The ID of this `NewPushRule`.
845    pub fn rule_id(&self) -> &str {
846        match self {
847            NewPushRule::Override(r) => &r.rule_id,
848            NewPushRule::Content(r) => &r.rule_id,
849            #[cfg(feature = "unstable-msc4306")]
850            NewPushRule::PostContent(r) => &r.rule_id,
851            NewPushRule::Room(r) => r.rule_id.as_ref(),
852            NewPushRule::Sender(r) => r.rule_id.as_ref(),
853            NewPushRule::Underride(r) => &r.rule_id,
854        }
855    }
856}
857
858/// A simple push rule to update or create.
859#[derive(Clone, Debug, Deserialize, Serialize)]
860#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
861pub struct NewSimplePushRule<T> {
862    /// The ID of this rule.
863    ///
864    /// This is generally the Matrix ID of the entity that it applies to.
865    pub rule_id: T,
866
867    /// Actions to determine if and how a notification is delivered for events matching this
868    /// rule.
869    pub actions: Vec<Action>,
870}
871
872impl<T> NewSimplePushRule<T> {
873    /// Creates a `NewSimplePushRule` with the given ID and actions.
874    pub fn new(rule_id: T, actions: Vec<Action>) -> Self {
875        Self { rule_id, actions }
876    }
877}
878
879impl<T> From<NewSimplePushRule<T>> for SimplePushRule<T> {
880    fn from(new_rule: NewSimplePushRule<T>) -> Self {
881        let NewSimplePushRule { rule_id, actions } = new_rule;
882        Self { actions, default: false, enabled: true, rule_id }
883    }
884}
885
886/// A patterned push rule to update or create.
887#[derive(Clone, Debug, Deserialize, Serialize)]
888#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
889pub struct NewPatternedPushRule {
890    /// The ID of this rule.
891    pub rule_id: String,
892
893    /// The glob-style pattern to match against.
894    pub pattern: String,
895
896    /// Actions to determine if and how a notification is delivered for events matching this
897    /// rule.
898    pub actions: Vec<Action>,
899}
900
901impl NewPatternedPushRule {
902    /// Creates a `NewPatternedPushRule` with the given ID, pattern and actions.
903    pub fn new(rule_id: String, pattern: String, actions: Vec<Action>) -> Self {
904        Self { rule_id, pattern, actions }
905    }
906}
907
908impl From<NewPatternedPushRule> for PatternedPushRule {
909    fn from(new_rule: NewPatternedPushRule) -> Self {
910        let NewPatternedPushRule { rule_id, pattern, actions } = new_rule;
911        Self { actions, default: false, enabled: true, rule_id, pattern }
912    }
913}
914
915/// A conditional push rule to update or create.
916#[derive(Clone, Debug, Deserialize, Serialize)]
917#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
918pub struct NewConditionalPushRule {
919    /// The ID of this rule.
920    pub rule_id: String,
921
922    /// The conditions that must hold true for an event in order for a rule to be applied to an
923    /// event.
924    ///
925    /// A rule with no conditions always matches.
926    #[serde(default)]
927    pub conditions: Vec<PushCondition>,
928
929    /// Actions to determine if and how a notification is delivered for events matching this
930    /// rule.
931    pub actions: Vec<Action>,
932}
933
934impl NewConditionalPushRule {
935    /// Creates a `NewConditionalPushRule` with the given ID, conditions and actions.
936    pub fn new(rule_id: String, conditions: Vec<PushCondition>, actions: Vec<Action>) -> Self {
937        Self { rule_id, conditions, actions }
938    }
939}
940
941impl From<NewConditionalPushRule> for ConditionalPushRule {
942    fn from(new_rule: NewConditionalPushRule) -> Self {
943        let NewConditionalPushRule { rule_id, conditions, actions } = new_rule;
944        Self { actions, default: false, enabled: true, rule_id, conditions }
945    }
946}
947
948/// The error type returned when trying to insert a user-defined push rule into a `Ruleset`.
949#[derive(Debug, Error)]
950#[non_exhaustive]
951pub enum InsertPushRuleError {
952    /// The rule ID starts with a dot (`.`), which is reserved for server-default rules.
953    #[error("rule IDs starting with a dot are reserved for server-default rules")]
954    ServerDefaultRuleId,
955
956    /// The rule ID contains an invalid character.
957    #[error("invalid rule ID")]
958    InvalidRuleId,
959
960    /// The rule is being placed relative to a server-default rule, which is forbidden.
961    #[error("can't place rule relative to server-default rule")]
962    RelativeToServerDefaultRule,
963
964    /// The `before` or `after` rule could not be found.
965    #[error("The before or after rule could not be found")]
966    UnknownRuleId,
967
968    /// `before` has a higher priority than `after`.
969    #[error("before has a higher priority than after")]
970    BeforeHigherThanAfter,
971}
972
973/// The error type returned when trying modify a push rule that could not be found in a `Ruleset`.
974#[derive(Debug, Error)]
975#[non_exhaustive]
976#[error("The rule could not be found")]
977pub struct RuleNotFoundError;
978
979/// Insert the rule in the given indexset and move it to the given position.
980pub fn insert_and_move_rule<T>(
981    set: &mut IndexSet<T>,
982    rule: T,
983    default_position: usize,
984    after: Option<&str>,
985    before: Option<&str>,
986) -> Result<(), InsertPushRuleError>
987where
988    T: Hash + Eq,
989    str: Equivalent<T>,
990{
991    let (from, replaced) = set.replace_full(rule);
992
993    let mut to = default_position;
994
995    if let Some(rule_id) = after {
996        let idx = set.get_index_of(rule_id).ok_or(InsertPushRuleError::UnknownRuleId)?;
997        to = idx + 1;
998    }
999    if let Some(rule_id) = before {
1000        let idx = set.get_index_of(rule_id).ok_or(InsertPushRuleError::UnknownRuleId)?;
1001
1002        if idx < to {
1003            return Err(InsertPushRuleError::BeforeHigherThanAfter);
1004        }
1005
1006        to = idx;
1007    }
1008
1009    // Only move the item if it's new or if it was positioned.
1010    if replaced.is_none() || after.is_some() || before.is_some() {
1011        set.move_index(from, to);
1012    }
1013
1014    Ok(())
1015}
1016
1017/// The error type returned when trying to remove a user-defined push rule from a `Ruleset`.
1018#[derive(Debug, Error)]
1019#[non_exhaustive]
1020pub enum RemovePushRuleError {
1021    /// The rule is a server-default rules and they can't be removed.
1022    #[error("server-default rules cannot be removed")]
1023    ServerDefault,
1024
1025    /// The rule was not found.
1026    #[error("rule not found")]
1027    NotFound,
1028}
1029
1030#[cfg(test)]
1031mod tests {
1032    use std::{collections::BTreeMap, sync::LazyLock};
1033
1034    use assert_matches2::assert_matches;
1035    use js_int::{int, uint};
1036    use macro_rules_attribute::apply;
1037    use serde_json::{
1038        Value as JsonValue, from_value as from_json_value, json, to_value as to_json_value,
1039        value::RawValue as RawJsonValue,
1040    };
1041    use smol_macros::test;
1042
1043    use super::{
1044        AnyPushRule, ConditionalPushRule, PatternedPushRule, Ruleset, SimplePushRule,
1045        action::{Action, Tweak},
1046        condition::{
1047            PushCondition, PushConditionPowerLevelsCtx, PushConditionRoomCtx, RoomMemberCountIs,
1048        },
1049    };
1050    use crate::{
1051        owned_room_id, owned_user_id,
1052        power_levels::NotificationPowerLevels,
1053        push::{PredefinedContentRuleId, PredefinedOverrideRuleId},
1054        room_version_rules::{AuthorizationRules, RoomPowerLevelsRules},
1055        serde::Raw,
1056        user_id,
1057    };
1058
1059    fn example_ruleset() -> Ruleset {
1060        let mut set = Ruleset::new();
1061
1062        set.override_.insert(ConditionalPushRule {
1063            conditions: vec![PushCondition::EventMatch {
1064                key: "type".into(),
1065                pattern: "m.call.invite".into(),
1066            }],
1067            actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))],
1068            rule_id: ".m.rule.call".into(),
1069            enabled: true,
1070            default: true,
1071        });
1072
1073        set
1074    }
1075
1076    fn power_levels() -> PushConditionPowerLevelsCtx {
1077        PushConditionPowerLevelsCtx {
1078            users: BTreeMap::new(),
1079            users_default: int!(50),
1080            notifications: NotificationPowerLevels { room: int!(50) },
1081            rules: RoomPowerLevelsRules::new(&AuthorizationRules::V1, None),
1082        }
1083    }
1084
1085    static CONTEXT_ONE_TO_ONE: LazyLock<PushConditionRoomCtx> = LazyLock::new(|| {
1086        let mut ctx = PushConditionRoomCtx::new(
1087            owned_room_id!("!dm:server.name"),
1088            uint!(2),
1089            owned_user_id!("@jj:server.name"),
1090            "Jolly Jumper".into(),
1091        );
1092        ctx.power_levels = Some(power_levels());
1093        ctx
1094    });
1095
1096    static CONTEXT_PUBLIC_ROOM: LazyLock<PushConditionRoomCtx> = LazyLock::new(|| {
1097        let mut ctx = PushConditionRoomCtx::new(
1098            owned_room_id!("!far_west:server.name"),
1099            uint!(100),
1100            owned_user_id!("@jj:server.name"),
1101            "Jolly Jumper".into(),
1102        );
1103        ctx.power_levels = Some(power_levels());
1104        ctx
1105    });
1106
1107    #[test]
1108    fn iter() {
1109        let mut set = example_ruleset();
1110
1111        let added = set.override_.insert(ConditionalPushRule {
1112            conditions: vec![PushCondition::EventMatch {
1113                key: "room_id".into(),
1114                pattern: "!roomid:matrix.org".into(),
1115            }],
1116            actions: vec![],
1117            rule_id: "!roomid:matrix.org".into(),
1118            enabled: true,
1119            default: false,
1120        });
1121        assert!(added);
1122
1123        let added = set.override_.insert(ConditionalPushRule {
1124            conditions: vec![],
1125            actions: vec![],
1126            rule_id: ".m.rule.suppress_notices".into(),
1127            enabled: false,
1128            default: true,
1129        });
1130        assert!(added);
1131
1132        let mut iter = set.into_iter();
1133
1134        let rule_opt = iter.next();
1135        assert!(rule_opt.is_some());
1136        assert_matches!(
1137            rule_opt.unwrap(),
1138            AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1139        );
1140        assert_eq!(rule_id, ".m.rule.call");
1141
1142        let rule_opt = iter.next();
1143        assert!(rule_opt.is_some());
1144        assert_matches!(
1145            rule_opt.unwrap(),
1146            AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1147        );
1148        assert_eq!(rule_id, "!roomid:matrix.org");
1149
1150        let rule_opt = iter.next();
1151        assert!(rule_opt.is_some());
1152        assert_matches!(
1153            rule_opt.unwrap(),
1154            AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1155        );
1156        assert_eq!(rule_id, ".m.rule.suppress_notices");
1157
1158        assert_matches!(iter.next(), None);
1159    }
1160
1161    #[test]
1162    fn serialize_conditional_push_rule() {
1163        let rule = ConditionalPushRule {
1164            actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))],
1165            default: true,
1166            enabled: true,
1167            rule_id: ".m.rule.call".into(),
1168            conditions: vec![
1169                PushCondition::EventMatch { key: "type".into(), pattern: "m.call.invite".into() },
1170                #[allow(deprecated)]
1171                PushCondition::ContainsDisplayName,
1172                PushCondition::RoomMemberCount { is: RoomMemberCountIs::gt(uint!(2)) },
1173                PushCondition::SenderNotificationPermission { key: "room".into() },
1174            ],
1175        };
1176
1177        let rule_value: JsonValue = to_json_value(rule).unwrap();
1178        assert_eq!(
1179            rule_value,
1180            json!({
1181                "conditions": [
1182                    {
1183                        "kind": "event_match",
1184                        "key": "type",
1185                        "pattern": "m.call.invite"
1186                    },
1187                    {
1188                        "kind": "contains_display_name"
1189                    },
1190                    {
1191                        "kind": "room_member_count",
1192                        "is": ">2"
1193                    },
1194                    {
1195                        "kind": "sender_notification_permission",
1196                        "key": "room"
1197                    }
1198                ],
1199                "actions": [
1200                    "notify",
1201                    {
1202                        "set_tweak": "highlight"
1203                    }
1204                ],
1205                "rule_id": ".m.rule.call",
1206                "default": true,
1207                "enabled": true
1208            })
1209        );
1210    }
1211
1212    #[test]
1213    fn serialize_simple_push_rule() {
1214        let rule = SimplePushRule {
1215            actions: vec![Action::Notify],
1216            default: false,
1217            enabled: false,
1218            rule_id: owned_room_id!("!roomid:server.name"),
1219        };
1220
1221        let rule_value: JsonValue = to_json_value(rule).unwrap();
1222        assert_eq!(
1223            rule_value,
1224            json!({
1225                "actions": [
1226                    "notify"
1227                ],
1228                "rule_id": "!roomid:server.name",
1229                "default": false,
1230                "enabled": false
1231            })
1232        );
1233    }
1234
1235    #[test]
1236    fn serialize_patterned_push_rule() {
1237        let rule = PatternedPushRule {
1238            actions: vec![
1239                Action::Notify,
1240                Action::SetTweak(Tweak::Sound("default".into())),
1241                Action::SetTweak(Tweak::Custom {
1242                    name: "dance".into(),
1243                    value: RawJsonValue::from_string("true".into()).unwrap(),
1244                }),
1245            ],
1246            default: true,
1247            enabled: true,
1248            pattern: "user_id".into(),
1249            rule_id: ".m.rule.contains_user_name".into(),
1250        };
1251
1252        let rule_value: JsonValue = to_json_value(rule).unwrap();
1253        assert_eq!(
1254            rule_value,
1255            json!({
1256                "actions": [
1257                    "notify",
1258                    {
1259                        "set_tweak": "sound",
1260                        "value": "default"
1261                    },
1262                    {
1263                        "set_tweak": "dance",
1264                        "value": true
1265                    }
1266                ],
1267                "pattern": "user_id",
1268                "rule_id": ".m.rule.contains_user_name",
1269                "default": true,
1270                "enabled": true
1271            })
1272        );
1273    }
1274
1275    #[test]
1276    fn serialize_ruleset() {
1277        let mut set = example_ruleset();
1278
1279        set.override_.insert(ConditionalPushRule {
1280            conditions: vec![
1281                PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)) },
1282                PushCondition::EventMatch { key: "type".into(), pattern: "m.room.message".into() },
1283            ],
1284            actions: vec![
1285                Action::Notify,
1286                Action::SetTweak(Tweak::Sound("default".into())),
1287                Action::SetTweak(Tweak::Highlight(false)),
1288            ],
1289            rule_id: ".m.rule.room_one_to_one".into(),
1290            enabled: true,
1291            default: true,
1292        });
1293        set.content.insert(PatternedPushRule {
1294            actions: vec![
1295                Action::Notify,
1296                Action::SetTweak(Tweak::Sound("default".into())),
1297                Action::SetTweak(Tweak::Highlight(true)),
1298            ],
1299            rule_id: ".m.rule.contains_user_name".into(),
1300            pattern: "user_id".into(),
1301            enabled: true,
1302            default: true,
1303        });
1304
1305        let set_value: JsonValue = to_json_value(set).unwrap();
1306        assert_eq!(
1307            set_value,
1308            json!({
1309                "override": [
1310                    {
1311                        "actions": [
1312                            "notify",
1313                            {
1314                                "set_tweak": "highlight",
1315                            },
1316                        ],
1317                        "conditions": [
1318                            {
1319                                "kind": "event_match",
1320                                "key": "type",
1321                                "pattern": "m.call.invite"
1322                            },
1323                        ],
1324                        "rule_id": ".m.rule.call",
1325                        "default": true,
1326                        "enabled": true,
1327                    },
1328                    {
1329                        "conditions": [
1330                            {
1331                                "kind": "room_member_count",
1332                                "is": "2"
1333                            },
1334                            {
1335                                "kind": "event_match",
1336                                "key": "type",
1337                                "pattern": "m.room.message"
1338                            }
1339                        ],
1340                        "actions": [
1341                            "notify",
1342                            {
1343                                "set_tweak": "sound",
1344                                "value": "default"
1345                            },
1346                            {
1347                                "set_tweak": "highlight",
1348                                "value": false
1349                            }
1350                        ],
1351                        "rule_id": ".m.rule.room_one_to_one",
1352                        "default": true,
1353                        "enabled": true
1354                    },
1355                ],
1356                "content": [
1357                    {
1358                        "actions": [
1359                            "notify",
1360                            {
1361                                "set_tweak": "sound",
1362                                "value": "default"
1363                            },
1364                            {
1365                                "set_tweak": "highlight"
1366                            }
1367                        ],
1368                        "pattern": "user_id",
1369                        "rule_id": ".m.rule.contains_user_name",
1370                        "default": true,
1371                        "enabled": true
1372                    }
1373                ],
1374            })
1375        );
1376    }
1377
1378    #[test]
1379    fn deserialize_patterned_push_rule() {
1380        let rule = from_json_value::<PatternedPushRule>(json!({
1381            "actions": [
1382                "notify",
1383                {
1384                    "set_tweak": "sound",
1385                    "value": "default"
1386                },
1387                {
1388                    "set_tweak": "highlight",
1389                    "value": true
1390                }
1391            ],
1392            "pattern": "user_id",
1393            "rule_id": ".m.rule.contains_user_name",
1394            "default": true,
1395            "enabled": true
1396        }))
1397        .unwrap();
1398        assert!(rule.default);
1399        assert!(rule.enabled);
1400        assert_eq!(rule.pattern, "user_id");
1401        assert_eq!(rule.rule_id, ".m.rule.contains_user_name");
1402
1403        let mut iter = rule.actions.iter();
1404        assert_matches!(iter.next(), Some(Action::Notify));
1405        assert_matches!(iter.next(), Some(Action::SetTweak(Tweak::Sound(sound))));
1406        assert_eq!(sound, "default");
1407        assert_matches!(iter.next(), Some(Action::SetTweak(Tweak::Highlight(true))));
1408        assert_matches!(iter.next(), None);
1409    }
1410
1411    #[test]
1412    fn deserialize_ruleset() {
1413        let set: Ruleset = from_json_value(json!({
1414            "override": [
1415                {
1416                    "actions": [],
1417                    "conditions": [],
1418                    "rule_id": "!roomid:server.name",
1419                    "default": false,
1420                    "enabled": true
1421                },
1422                {
1423                    "actions": [],
1424                    "conditions": [],
1425                    "rule_id": ".m.rule.call",
1426                    "default": true,
1427                    "enabled": true
1428                },
1429            ],
1430            "underride": [
1431                {
1432                    "actions": [],
1433                    "conditions": [],
1434                    "rule_id": ".m.rule.room_one_to_one",
1435                    "default": true,
1436                    "enabled": true
1437                },
1438            ],
1439            "room": [
1440                {
1441                    "actions": [],
1442                    "rule_id": "!roomid:server.name",
1443                    "default": false,
1444                    "enabled": false
1445                }
1446            ],
1447            "sender": [],
1448            "content": [
1449                {
1450                    "actions": [],
1451                    "pattern": "user_id",
1452                    "rule_id": ".m.rule.contains_user_name",
1453                    "default": true,
1454                    "enabled": true
1455                },
1456                {
1457                    "actions": [],
1458                    "pattern": "ruma",
1459                    "rule_id": "ruma",
1460                    "default": false,
1461                    "enabled": true
1462                }
1463            ]
1464        }))
1465        .unwrap();
1466
1467        let mut iter = set.into_iter();
1468
1469        let rule_opt = iter.next();
1470        assert!(rule_opt.is_some());
1471        assert_matches!(
1472            rule_opt.unwrap(),
1473            AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1474        );
1475        assert_eq!(rule_id, "!roomid:server.name");
1476
1477        let rule_opt = iter.next();
1478        assert!(rule_opt.is_some());
1479        assert_matches!(
1480            rule_opt.unwrap(),
1481            AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
1482        );
1483        assert_eq!(rule_id, ".m.rule.call");
1484
1485        let rule_opt = iter.next();
1486        assert!(rule_opt.is_some());
1487        assert_matches!(rule_opt.unwrap(), AnyPushRule::Content(PatternedPushRule { rule_id, .. }));
1488        assert_eq!(rule_id, ".m.rule.contains_user_name");
1489
1490        let rule_opt = iter.next();
1491        assert!(rule_opt.is_some());
1492        assert_matches!(rule_opt.unwrap(), AnyPushRule::Content(PatternedPushRule { rule_id, .. }));
1493        assert_eq!(rule_id, "ruma");
1494
1495        let rule_opt = iter.next();
1496        assert!(rule_opt.is_some());
1497        assert_matches!(rule_opt.unwrap(), AnyPushRule::Room(SimplePushRule { rule_id, .. }));
1498        assert_eq!(rule_id, "!roomid:server.name");
1499
1500        let rule_opt = iter.next();
1501        assert!(rule_opt.is_some());
1502        assert_matches!(
1503            rule_opt.unwrap(),
1504            AnyPushRule::Underride(ConditionalPushRule { rule_id, .. })
1505        );
1506        assert_eq!(rule_id, ".m.rule.room_one_to_one");
1507
1508        assert_matches!(iter.next(), None);
1509    }
1510
1511    #[apply(test!)]
1512    async fn default_ruleset_applies() {
1513        let set = Ruleset::server_default(user_id!("@jj:server.name"));
1514
1515        let message = serde_json::from_str::<Raw<JsonValue>>(
1516            r#"{
1517                "type": "m.room.message"
1518            }"#,
1519        )
1520        .unwrap();
1521
1522        assert_matches!(
1523            set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1524            [
1525                Action::Notify,
1526                Action::SetTweak(Tweak::Sound(_)),
1527                Action::SetTweak(Tweak::Highlight(false))
1528            ]
1529        );
1530        assert_matches!(
1531            set.get_actions(&message, &CONTEXT_PUBLIC_ROOM).await,
1532            [Action::Notify, Action::SetTweak(Tweak::Highlight(false))]
1533        );
1534
1535        let user_mention = serde_json::from_str::<Raw<JsonValue>>(
1536            r#"{
1537                "type": "m.room.message",
1538                "content": {
1539                    "body": "Hi jolly_jumper!",
1540                    "m.mentions": {
1541                        "user_ids": ["@jj:server.name"]
1542                    }
1543                }
1544            }"#,
1545        )
1546        .unwrap();
1547
1548        assert_matches!(
1549            set.get_actions(&user_mention, &CONTEXT_ONE_TO_ONE).await,
1550            [
1551                Action::Notify,
1552                Action::SetTweak(Tweak::Sound(_)),
1553                Action::SetTweak(Tweak::Highlight(true)),
1554            ]
1555        );
1556        assert_matches!(
1557            set.get_actions(&user_mention, &CONTEXT_PUBLIC_ROOM).await,
1558            [
1559                Action::Notify,
1560                Action::SetTweak(Tweak::Sound(_)),
1561                Action::SetTweak(Tweak::Highlight(true)),
1562            ]
1563        );
1564
1565        let notice = serde_json::from_str::<Raw<JsonValue>>(
1566            r#"{
1567                "type": "m.room.message",
1568                "content": {
1569                    "msgtype": "m.notice"
1570                }
1571            }"#,
1572        )
1573        .unwrap();
1574        assert_matches!(set.get_actions(&notice, &CONTEXT_ONE_TO_ONE).await, []);
1575
1576        let room_mention = serde_json::from_str::<Raw<JsonValue>>(
1577            r#"{
1578                "type": "m.room.message",
1579                "sender": "@rantanplan:server.name",
1580                "content": {
1581                    "body": "@room Attention please!",
1582                    "msgtype": "m.text",
1583                    "m.mentions": {
1584                        "room": true
1585                    }
1586                }
1587            }"#,
1588        )
1589        .unwrap();
1590
1591        assert_matches!(
1592            set.get_actions(&room_mention, &CONTEXT_PUBLIC_ROOM).await,
1593            [Action::Notify, Action::SetTweak(Tweak::Highlight(true)),]
1594        );
1595
1596        let empty = serde_json::from_str::<Raw<JsonValue>>(r#"{}"#).unwrap();
1597        assert_matches!(set.get_actions(&empty, &CONTEXT_ONE_TO_ONE).await, []);
1598    }
1599
1600    #[apply(test!)]
1601    async fn custom_ruleset_applies() {
1602        let message = serde_json::from_str::<Raw<JsonValue>>(
1603            r#"{
1604                "sender": "@rantanplan:server.name",
1605                "type": "m.room.message",
1606                "content": {
1607                    "msgtype": "m.text",
1608                    "body": "Great joke!"
1609                }
1610            }"#,
1611        )
1612        .unwrap();
1613
1614        let mut set = Ruleset::new();
1615        let disabled = ConditionalPushRule {
1616            actions: vec![Action::Notify],
1617            default: false,
1618            enabled: false,
1619            rule_id: "disabled".into(),
1620            conditions: vec![PushCondition::RoomMemberCount {
1621                is: RoomMemberCountIs::from(uint!(2)),
1622            }],
1623        };
1624        set.underride.insert(disabled);
1625
1626        let test_set = set.clone();
1627        assert_matches!(test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await, []);
1628
1629        let no_conditions = ConditionalPushRule {
1630            actions: vec![Action::SetTweak(Tweak::Highlight(true))],
1631            default: false,
1632            enabled: true,
1633            rule_id: "no.conditions".into(),
1634            conditions: vec![],
1635        };
1636        set.underride.insert(no_conditions);
1637
1638        let test_set = set.clone();
1639        assert_matches!(
1640            test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1641            [Action::SetTweak(Tweak::Highlight(true))]
1642        );
1643
1644        let sender = SimplePushRule {
1645            actions: vec![Action::Notify],
1646            default: false,
1647            enabled: true,
1648            rule_id: owned_user_id!("@rantanplan:server.name"),
1649        };
1650        set.sender.insert(sender);
1651
1652        let test_set = set.clone();
1653        assert_matches!(
1654            test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1655            [Action::Notify]
1656        );
1657
1658        let room = SimplePushRule {
1659            actions: vec![Action::SetTweak(Tweak::Highlight(true))],
1660            default: false,
1661            enabled: true,
1662            rule_id: owned_room_id!("!dm:server.name"),
1663        };
1664        set.room.insert(room);
1665
1666        let test_set = set.clone();
1667        assert_matches!(
1668            test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1669            [Action::SetTweak(Tweak::Highlight(true))]
1670        );
1671
1672        let content = PatternedPushRule {
1673            actions: vec![Action::SetTweak(Tweak::Sound("content".into()))],
1674            default: false,
1675            enabled: true,
1676            rule_id: "content".into(),
1677            pattern: "joke".into(),
1678        };
1679        set.content.insert(content);
1680
1681        let test_set = set.clone();
1682        assert_matches!(
1683            test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1684            [Action::SetTweak(Tweak::Sound(sound))]
1685        );
1686        assert_eq!(sound, "content");
1687
1688        let three_conditions = ConditionalPushRule {
1689            actions: vec![Action::SetTweak(Tweak::Sound("three".into()))],
1690            default: false,
1691            enabled: true,
1692            rule_id: "three.conditions".into(),
1693            conditions: vec![
1694                PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)) },
1695                #[allow(deprecated)]
1696                PushCondition::ContainsDisplayName,
1697                PushCondition::EventMatch {
1698                    key: "room_id".into(),
1699                    pattern: "!dm:server.name".into(),
1700                },
1701            ],
1702        };
1703        set.override_.insert(three_conditions);
1704
1705        assert_matches!(
1706            set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1707            [Action::SetTweak(Tweak::Sound(sound))]
1708        );
1709        assert_eq!(sound, "content");
1710
1711        let new_message = serde_json::from_str::<Raw<JsonValue>>(
1712            r#"{
1713                "sender": "@rantanplan:server.name",
1714                "type": "m.room.message",
1715                "content": {
1716                    "msgtype": "m.text",
1717                    "body": "Tell me another one, Jolly Jumper!"
1718                }
1719            }"#,
1720        )
1721        .unwrap();
1722
1723        assert_matches!(
1724            set.get_actions(&new_message, &CONTEXT_ONE_TO_ONE).await,
1725            [Action::SetTweak(Tweak::Sound(sound))]
1726        );
1727        assert_eq!(sound, "three");
1728    }
1729
1730    #[apply(test!)]
1731    #[allow(deprecated)]
1732    async fn old_mentions_apply() {
1733        let mut set = Ruleset::new();
1734        set.content.insert(PatternedPushRule {
1735            rule_id: PredefinedContentRuleId::ContainsUserName.to_string(),
1736            enabled: true,
1737            default: true,
1738            pattern: "jolly_jumper".to_owned(),
1739            actions: vec![
1740                Action::Notify,
1741                Action::SetTweak(Tweak::Sound("default".into())),
1742                Action::SetTweak(Tweak::Highlight(true)),
1743            ],
1744        });
1745        set.override_.extend([
1746            ConditionalPushRule {
1747                actions: vec![
1748                    Action::Notify,
1749                    Action::SetTweak(Tweak::Sound("default".into())),
1750                    Action::SetTweak(Tweak::Highlight(true)),
1751                ],
1752                default: true,
1753                enabled: true,
1754                rule_id: PredefinedOverrideRuleId::ContainsDisplayName.to_string(),
1755                conditions: vec![PushCondition::ContainsDisplayName],
1756            },
1757            ConditionalPushRule {
1758                actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))],
1759                default: true,
1760                enabled: true,
1761                rule_id: PredefinedOverrideRuleId::RoomNotif.to_string(),
1762                conditions: vec![
1763                    PushCondition::EventMatch {
1764                        key: "content.body".into(),
1765                        pattern: "@room".into(),
1766                    },
1767                    PushCondition::SenderNotificationPermission { key: "room".into() },
1768                ],
1769            },
1770        ]);
1771
1772        let message = serde_json::from_str::<Raw<JsonValue>>(
1773            r#"{
1774                "content": {
1775                    "body": "jolly_jumper"
1776                },
1777                "type": "m.room.message"
1778            }"#,
1779        )
1780        .unwrap();
1781
1782        assert_eq!(
1783            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1784            PredefinedContentRuleId::ContainsUserName.as_ref()
1785        );
1786
1787        let message = serde_json::from_str::<Raw<JsonValue>>(
1788            r#"{
1789                "content": {
1790                    "body": "jolly_jumper",
1791                    "m.mentions": {}
1792                },
1793                "type": "m.room.message"
1794            }"#,
1795        )
1796        .unwrap();
1797
1798        assert_eq!(
1799            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.map(|rule| rule.rule_id()),
1800            None
1801        );
1802
1803        let message = serde_json::from_str::<Raw<JsonValue>>(
1804            r#"{
1805                "content": {
1806                    "body": "Jolly Jumper"
1807                },
1808                "type": "m.room.message"
1809            }"#,
1810        )
1811        .unwrap();
1812
1813        assert_eq!(
1814            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1815            PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
1816        );
1817
1818        let message = serde_json::from_str::<Raw<JsonValue>>(
1819            r#"{
1820                "content": {
1821                    "body": "Jolly Jumper",
1822                    "m.mentions": {}
1823                },
1824                "type": "m.room.message"
1825            }"#,
1826        )
1827        .unwrap();
1828
1829        assert_eq!(
1830            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.map(|rule| rule.rule_id()),
1831            None
1832        );
1833
1834        let message = serde_json::from_str::<Raw<JsonValue>>(
1835            r#"{
1836                "content": {
1837                    "body": "@room"
1838                },
1839                "sender": "@admin:server.name",
1840                "type": "m.room.message"
1841            }"#,
1842        )
1843        .unwrap();
1844
1845        assert_eq!(
1846            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1847            PredefinedOverrideRuleId::RoomNotif.as_ref()
1848        );
1849
1850        let message = serde_json::from_str::<Raw<JsonValue>>(
1851            r#"{
1852                "content": {
1853                    "body": "@room",
1854                    "m.mentions": {}
1855                },
1856                "sender": "@admin:server.name",
1857                "type": "m.room.message"
1858            }"#,
1859        )
1860        .unwrap();
1861
1862        assert_eq!(
1863            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.map(|rule| rule.rule_id()),
1864            None
1865        );
1866    }
1867
1868    #[apply(test!)]
1869    async fn intentional_mentions_apply() {
1870        let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
1871
1872        let message = serde_json::from_str::<Raw<JsonValue>>(
1873            r#"{
1874                "content": {
1875                    "body": "Hey jolly_jumper!",
1876                    "m.mentions": {
1877                        "user_ids": ["@jolly_jumper:server.name"]
1878                    }
1879                },
1880                "sender": "@admin:server.name",
1881                "type": "m.room.message"
1882            }"#,
1883        )
1884        .unwrap();
1885
1886        assert_eq!(
1887            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1888            PredefinedOverrideRuleId::IsUserMention.as_ref()
1889        );
1890
1891        let message = serde_json::from_str::<Raw<JsonValue>>(
1892            r#"{
1893                "content": {
1894                    "body": "Listen room!",
1895                    "m.mentions": {
1896                        "room": true
1897                    }
1898                },
1899                "sender": "@admin:server.name",
1900                "type": "m.room.message"
1901            }"#,
1902        )
1903        .unwrap();
1904
1905        assert_eq!(
1906            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1907            PredefinedOverrideRuleId::IsRoomMention.as_ref()
1908        );
1909    }
1910
1911    #[apply(test!)]
1912    async fn invite_for_me_applies() {
1913        let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
1914
1915        // `invite_state` usually doesn't include the power levels.
1916        let context = PushConditionRoomCtx::new(
1917            owned_room_id!("!far_west:server.name"),
1918            uint!(100),
1919            owned_user_id!("@jj:server.name"),
1920            "Jolly Jumper".into(),
1921        );
1922
1923        let message = serde_json::from_str::<Raw<JsonValue>>(
1924            r#"{
1925                "content": {
1926                    "membership": "invite"
1927                },
1928                "state_key": "@jolly_jumper:server.name",
1929                "sender": "@admin:server.name",
1930                "type": "m.room.member"
1931            }"#,
1932        )
1933        .unwrap();
1934
1935        assert_eq!(
1936            set.get_match(&message, &context).await.unwrap().rule_id(),
1937            PredefinedOverrideRuleId::InviteForMe.as_ref()
1938        );
1939    }
1940}