diff --git a/rust/src/push/evaluator.rs b/rust/src/push/evaluator.rs
index efe88ec76e..c901c0fbcc 100644
--- a/rust/src/push/evaluator.rs
+++ b/rust/src/push/evaluator.rs
@@ -12,10 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-use std::{
- borrow::Cow,
- collections::{BTreeMap, BTreeSet},
-};
+use std::collections::BTreeMap;
use anyhow::{Context, Error};
use lazy_static::lazy_static;
@@ -26,11 +23,39 @@ use regex::Regex;
use super::{
utils::{get_glob_matcher, get_localpart_from_id, GlobMatchType},
Action, Condition, EventMatchCondition, FilteredPushRules, KnownCondition,
+ RelatedEventMatchCondition,
};
lazy_static! {
/// Used to parse the `is` clause in the room member count condition.
static ref INEQUALITY_EXPR: Regex = Regex::new(r"^([=<>]*)([0-9]+)$").expect("valid regex");
+
+ /// Used to determine which MSC3931 room version feature flags are actually known to
+ /// the push evaluator.
+ static ref KNOWN_RVER_FLAGS: Vec<String> = vec![
+ RoomVersionFeatures::ExtensibleEvents.as_str().to_string(),
+ ];
+
+ /// The "safe" rule IDs which are not affected by MSC3932's behaviour (room versions which
+ /// declare Extensible Events support ultimately *disable* push rules which do not declare
+ /// *any* MSC3931 room_version_supports condition).
+ static ref SAFE_EXTENSIBLE_EVENTS_RULE_IDS: Vec<String> = vec![
+ "global/override/.m.rule.master".to_string(),
+ "global/override/.m.rule.roomnotif".to_string(),
+ "global/content/.m.rule.contains_user_name".to_string(),
+ ];
+}
+
+enum RoomVersionFeatures {
+ ExtensibleEvents,
+}
+
+impl RoomVersionFeatures {
+ fn as_str(&self) -> &'static str {
+ match self {
+ RoomVersionFeatures::ExtensibleEvents => "org.matrix.msc3932.extensible_events",
+ }
+ }
}
/// Allows running a set of push rules against a particular event.
@@ -49,29 +74,39 @@ pub struct PushRuleEvaluator {
/// The `notifications` section of the current power levels in the room.
notification_power_levels: BTreeMap<String, i64>,
- /// The relations related to the event as a mapping from relation type to
- /// set of sender/event type 2-tuples.
- relations: BTreeMap<String, BTreeSet<(String, String)>>,
-
- /// Is running "relation" conditions enabled?
- relation_match_enabled: bool,
-
/// The power level of the sender of the event, or None if event is an
/// outlier.
sender_power_level: Option<i64>,
+
+ /// The related events, indexed by relation type. Flattened in the same manner as
+ /// `flattened_keys`.
+ related_events_flattened: BTreeMap<String, BTreeMap<String, String>>,
+
+ /// If msc3664, push rules for related events, is enabled.
+ related_event_match_enabled: bool,
+
+ /// If MSC3931 is applicable, the feature flags for the room version.
+ room_version_feature_flags: Vec<String>,
+
+ /// If MSC3931 (room version feature flags) is enabled. Usually controlled by the same
+ /// flag as MSC1767 (extensible events core).
+ msc3931_enabled: bool,
}
#[pymethods]
impl PushRuleEvaluator {
/// Create a new `PushRuleEvaluator`. See struct docstring for details.
+ #[allow(clippy::too_many_arguments)]
#[new]
pub fn py_new(
flattened_keys: BTreeMap<String, String>,
room_member_count: u64,
sender_power_level: Option<i64>,
notification_power_levels: BTreeMap<String, i64>,
- relations: BTreeMap<String, BTreeSet<(String, String)>>,
- relation_match_enabled: bool,
+ related_events_flattened: BTreeMap<String, BTreeMap<String, String>>,
+ related_event_match_enabled: bool,
+ room_version_feature_flags: Vec<String>,
+ msc3931_enabled: bool,
) -> Result<Self, Error> {
let body = flattened_keys
.get("content.body")
@@ -83,9 +118,11 @@ impl PushRuleEvaluator {
body,
room_member_count,
notification_power_levels,
- relations,
- relation_match_enabled,
sender_power_level,
+ related_events_flattened,
+ related_event_match_enabled,
+ room_version_feature_flags,
+ msc3931_enabled,
})
}
@@ -108,7 +145,19 @@ impl PushRuleEvaluator {
continue;
}
+ let rule_id = &push_rule.rule_id().to_string();
+ let extev_flag = &RoomVersionFeatures::ExtensibleEvents.as_str().to_string();
+ let supports_extensible_events = self.room_version_feature_flags.contains(extev_flag);
+ let safe_from_rver_condition = SAFE_EXTENSIBLE_EVENTS_RULE_IDS.contains(rule_id);
+ let mut has_rver_condition = false;
+
for condition in push_rule.conditions.iter() {
+ has_rver_condition |= matches!(
+ condition,
+ // per MSC3932, we just need *any* room version condition to match
+ Condition::Known(KnownCondition::RoomVersionSupports { feature: _ }),
+ );
+
match self.match_condition(condition, user_id, display_name) {
Ok(true) => {}
Ok(false) => continue 'outer,
@@ -119,6 +168,13 @@ impl PushRuleEvaluator {
}
}
+ // MSC3932: Disable push rules in extensible event-supporting room versions if they
+ // don't describe *any* MSC3931 room version condition, unless the rule is on the
+ // safe list.
+ if !has_rver_condition && !safe_from_rver_condition && supports_extensible_events {
+ continue;
+ }
+
let actions = push_rule
.actions
.iter()
@@ -170,6 +226,9 @@ impl PushRuleEvaluator {
KnownCondition::EventMatch(event_match) => {
self.match_event_match(event_match, user_id)?
}
+ KnownCondition::RelatedEventMatch(event_match) => {
+ self.match_related_event_match(event_match, user_id)?
+ }
KnownCondition::ContainsDisplayName => {
if let Some(dn) = display_name {
if !dn.is_empty() {
@@ -203,95 +262,99 @@ impl PushRuleEvaluator {
false
}
}
- KnownCondition::RelationMatch {
- rel_type,
- event_type_pattern,
- sender,
- sender_type,
- } => {
- self.match_relations(rel_type, sender, sender_type, user_id, event_type_pattern)?
+ KnownCondition::RoomVersionSupports { feature } => {
+ if !self.msc3931_enabled {
+ false
+ } else {
+ let flag = feature.to_string();
+ KNOWN_RVER_FLAGS.contains(&flag)
+ && self.room_version_feature_flags.contains(&flag)
+ }
}
};
Ok(result)
}
- /// Evaluates a relation condition.
- fn match_relations(
+ /// Evaluates a `event_match` condition.
+ fn match_event_match(
&self,
- rel_type: &str,
- sender: &Option<Cow<str>>,
- sender_type: &Option<Cow<str>>,
+ event_match: &EventMatchCondition,
user_id: Option<&str>,
- event_type_pattern: &Option<Cow<str>>,
) -> Result<bool, Error> {
- // First check if relation matching is enabled...
- if !self.relation_match_enabled {
- return Ok(false);
- }
-
- // ... and if there are any relations to match against.
- let relations = if let Some(relations) = self.relations.get(rel_type) {
- relations
- } else {
- return Ok(false);
- };
-
- // Extract the sender pattern from the condition
- let sender_pattern = if let Some(sender) = sender {
- Some(sender.as_ref())
- } else if let Some(sender_type) = sender_type {
- if sender_type == "user_id" {
- if let Some(user_id) = user_id {
- Some(user_id)
- } else {
- return Ok(false);
- }
+ let pattern = if let Some(pattern) = &event_match.pattern {
+ pattern
+ } else if let Some(pattern_type) = &event_match.pattern_type {
+ // The `pattern_type` can either be "user_id" or "user_localpart",
+ // either way if we don't have a `user_id` then the condition can't
+ // match.
+ let user_id = if let Some(user_id) = user_id {
+ user_id
} else {
- warn!("Unrecognized sender_type: {sender_type}");
return Ok(false);
+ };
+
+ match &**pattern_type {
+ "user_id" => user_id,
+ "user_localpart" => get_localpart_from_id(user_id)?,
+ _ => return Ok(false),
}
} else {
- None
+ return Ok(false);
};
- let mut sender_compiled_pattern = if let Some(pattern) = sender_pattern {
- Some(get_glob_matcher(pattern, GlobMatchType::Whole)?)
+ let haystack = if let Some(haystack) = self.flattened_keys.get(&*event_match.key) {
+ haystack
} else {
- None
+ return Ok(false);
};
- let mut type_compiled_pattern = if let Some(pattern) = event_type_pattern {
- Some(get_glob_matcher(pattern, GlobMatchType::Whole)?)
+ // For the content.body we match against "words", but for everything
+ // else we match against the entire value.
+ let match_type = if event_match.key == "content.body" {
+ GlobMatchType::Word
} else {
- None
+ GlobMatchType::Whole
};
- for (relation_sender, event_type) in relations {
- if let Some(pattern) = &mut sender_compiled_pattern {
- if !pattern.is_match(relation_sender)? {
- continue;
- }
- }
-
- if let Some(pattern) = &mut type_compiled_pattern {
- if !pattern.is_match(event_type)? {
- continue;
- }
- }
-
- return Ok(true);
- }
-
- Ok(false)
+ let mut compiled_pattern = get_glob_matcher(pattern, match_type)?;
+ compiled_pattern.is_match(haystack)
}
- /// Evaluates a `event_match` condition.
- fn match_event_match(
+ /// Evaluates a `related_event_match` condition. (MSC3664)
+ fn match_related_event_match(
&self,
- event_match: &EventMatchCondition,
+ event_match: &RelatedEventMatchCondition,
user_id: Option<&str>,
) -> Result<bool, Error> {
+ // First check if related event matching is enabled...
+ if !self.related_event_match_enabled {
+ return Ok(false);
+ }
+
+ // get the related event, fail if there is none.
+ let event = if let Some(event) = self.related_events_flattened.get(&*event_match.rel_type) {
+ event
+ } else {
+ return Ok(false);
+ };
+
+ // If we are not matching fallbacks, don't match if our special key indicating this is a
+ // fallback relation is not present.
+ if !event_match.include_fallbacks.unwrap_or(false)
+ && event.contains_key("im.vector.is_falling_back")
+ {
+ return Ok(false);
+ }
+
+ // if we have no key, accept the event as matching, if it existed without matching any
+ // fields.
+ let key = if let Some(key) = &event_match.key {
+ key
+ } else {
+ return Ok(true);
+ };
+
let pattern = if let Some(pattern) = &event_match.pattern {
pattern
} else if let Some(pattern_type) = &event_match.pattern_type {
@@ -313,7 +376,7 @@ impl PushRuleEvaluator {
return Ok(false);
};
- let haystack = if let Some(haystack) = self.flattened_keys.get(&*event_match.key) {
+ let haystack = if let Some(haystack) = event.get(&**key) {
haystack
} else {
return Ok(false);
@@ -321,7 +384,7 @@ impl PushRuleEvaluator {
// For the content.body we match against "words", but for everything
// else we match against the entire value.
- let match_type = if event_match.key == "content.body" {
+ let match_type = if key == "content.body" {
GlobMatchType::Word
} else {
GlobMatchType::Whole
@@ -366,9 +429,63 @@ fn push_rule_evaluator() {
BTreeMap::new(),
BTreeMap::new(),
true,
+ vec![],
+ true,
)
.unwrap();
let result = evaluator.run(&FilteredPushRules::default(), None, Some("bob"));
assert_eq!(result.len(), 3);
}
+
+#[test]
+fn test_requires_room_version_supports_condition() {
+ use std::borrow::Cow;
+
+ use crate::push::{PushRule, PushRules};
+
+ let mut flattened_keys = BTreeMap::new();
+ flattened_keys.insert("content.body".to_string(), "foo bar bob hello".to_string());
+ let flags = vec![RoomVersionFeatures::ExtensibleEvents.as_str().to_string()];
+ let evaluator = PushRuleEvaluator::py_new(
+ flattened_keys,
+ 10,
+ Some(0),
+ BTreeMap::new(),
+ BTreeMap::new(),
+ false,
+ flags,
+ true,
+ )
+ .unwrap();
+
+ // first test: are the master and contains_user_name rules excluded from the "requires room
+ // version condition" check?
+ let mut result = evaluator.run(
+ &FilteredPushRules::default(),
+ Some("@bob:example.org"),
+ None,
+ );
+ assert_eq!(result.len(), 3);
+
+ // second test: if an appropriate push rule is in play, does it get handled?
+ let custom_rule = PushRule {
+ rule_id: Cow::from("global/underride/.org.example.extensible"),
+ priority_class: 1, // underride
+ conditions: Cow::from(vec![Condition::Known(
+ KnownCondition::RoomVersionSupports {
+ feature: Cow::from(RoomVersionFeatures::ExtensibleEvents.as_str().to_string()),
+ },
+ )]),
+ actions: Cow::from(vec![Action::Notify]),
+ default: false,
+ default_enabled: true,
+ };
+ let rules = PushRules::new(vec![custom_rule]);
+ result = evaluator.run(
+ &FilteredPushRules::py_new(rules, BTreeMap::new(), true, true),
+ None,
+ None,
+ );
+ assert_eq!(result.len(), 1);
+}
|