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