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