Implement MSC3966: Add a push rule condition to search for a value in an array. (#15045)
The `exact_event_property_contains` condition can be used to search for a value inside of an array.pull/15037/head
							parent
							
								
									157c571f3e
								
							
						
					
					
						commit
						119e0795a5
					
				| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					Experimental support for [MSC3966](https://github.com/matrix-org/matrix-spec-proposals/pull/3966): the `exact_event_property_contains` push rule condition.
 | 
				
			||||||
| 
						 | 
					@ -15,8 +15,8 @@
 | 
				
			||||||
#![feature(test)]
 | 
					#![feature(test)]
 | 
				
			||||||
use std::collections::BTreeSet;
 | 
					use std::collections::BTreeSet;
 | 
				
			||||||
use synapse::push::{
 | 
					use synapse::push::{
 | 
				
			||||||
    evaluator::PushRuleEvaluator, Condition, EventMatchCondition, FilteredPushRules, PushRules,
 | 
					    evaluator::PushRuleEvaluator, Condition, EventMatchCondition, FilteredPushRules, JsonValue,
 | 
				
			||||||
    SimpleJsonValue,
 | 
					    PushRules, SimpleJsonValue,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use test::Bencher;
 | 
					use test::Bencher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,15 +27,15 @@ fn bench_match_exact(b: &mut Bencher) {
 | 
				
			||||||
    let flattened_keys = [
 | 
					    let flattened_keys = [
 | 
				
			||||||
        (
 | 
					        (
 | 
				
			||||||
            "type".to_string(),
 | 
					            "type".to_string(),
 | 
				
			||||||
            SimpleJsonValue::Str("m.text".to_string()),
 | 
					            JsonValue::Value(SimpleJsonValue::Str("m.text".to_string())),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        (
 | 
					        (
 | 
				
			||||||
            "room_id".to_string(),
 | 
					            "room_id".to_string(),
 | 
				
			||||||
            SimpleJsonValue::Str("!room:server".to_string()),
 | 
					            JsonValue::Value(SimpleJsonValue::Str("!room:server".to_string())),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        (
 | 
					        (
 | 
				
			||||||
            "content.body".to_string(),
 | 
					            "content.body".to_string(),
 | 
				
			||||||
            SimpleJsonValue::Str("test message".to_string()),
 | 
					            JsonValue::Value(SimpleJsonValue::Str("test message".to_string())),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
    .into_iter()
 | 
					    .into_iter()
 | 
				
			||||||
| 
						 | 
					@ -54,6 +54,7 @@ fn bench_match_exact(b: &mut Bencher) {
 | 
				
			||||||
        vec![],
 | 
					        vec![],
 | 
				
			||||||
        false,
 | 
					        false,
 | 
				
			||||||
        false,
 | 
					        false,
 | 
				
			||||||
 | 
					        false,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .unwrap();
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -76,15 +77,15 @@ fn bench_match_word(b: &mut Bencher) {
 | 
				
			||||||
    let flattened_keys = [
 | 
					    let flattened_keys = [
 | 
				
			||||||
        (
 | 
					        (
 | 
				
			||||||
            "type".to_string(),
 | 
					            "type".to_string(),
 | 
				
			||||||
            SimpleJsonValue::Str("m.text".to_string()),
 | 
					            JsonValue::Value(SimpleJsonValue::Str("m.text".to_string())),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        (
 | 
					        (
 | 
				
			||||||
            "room_id".to_string(),
 | 
					            "room_id".to_string(),
 | 
				
			||||||
            SimpleJsonValue::Str("!room:server".to_string()),
 | 
					            JsonValue::Value(SimpleJsonValue::Str("!room:server".to_string())),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        (
 | 
					        (
 | 
				
			||||||
            "content.body".to_string(),
 | 
					            "content.body".to_string(),
 | 
				
			||||||
            SimpleJsonValue::Str("test message".to_string()),
 | 
					            JsonValue::Value(SimpleJsonValue::Str("test message".to_string())),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
    .into_iter()
 | 
					    .into_iter()
 | 
				
			||||||
| 
						 | 
					@ -103,6 +104,7 @@ fn bench_match_word(b: &mut Bencher) {
 | 
				
			||||||
        vec![],
 | 
					        vec![],
 | 
				
			||||||
        false,
 | 
					        false,
 | 
				
			||||||
        false,
 | 
					        false,
 | 
				
			||||||
 | 
					        false,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .unwrap();
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -125,15 +127,15 @@ fn bench_match_word_miss(b: &mut Bencher) {
 | 
				
			||||||
    let flattened_keys = [
 | 
					    let flattened_keys = [
 | 
				
			||||||
        (
 | 
					        (
 | 
				
			||||||
            "type".to_string(),
 | 
					            "type".to_string(),
 | 
				
			||||||
            SimpleJsonValue::Str("m.text".to_string()),
 | 
					            JsonValue::Value(SimpleJsonValue::Str("m.text".to_string())),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        (
 | 
					        (
 | 
				
			||||||
            "room_id".to_string(),
 | 
					            "room_id".to_string(),
 | 
				
			||||||
            SimpleJsonValue::Str("!room:server".to_string()),
 | 
					            JsonValue::Value(SimpleJsonValue::Str("!room:server".to_string())),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        (
 | 
					        (
 | 
				
			||||||
            "content.body".to_string(),
 | 
					            "content.body".to_string(),
 | 
				
			||||||
            SimpleJsonValue::Str("test message".to_string()),
 | 
					            JsonValue::Value(SimpleJsonValue::Str("test message".to_string())),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
    .into_iter()
 | 
					    .into_iter()
 | 
				
			||||||
| 
						 | 
					@ -152,6 +154,7 @@ fn bench_match_word_miss(b: &mut Bencher) {
 | 
				
			||||||
        vec![],
 | 
					        vec![],
 | 
				
			||||||
        false,
 | 
					        false,
 | 
				
			||||||
        false,
 | 
					        false,
 | 
				
			||||||
 | 
					        false,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .unwrap();
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -174,15 +177,15 @@ fn bench_eval_message(b: &mut Bencher) {
 | 
				
			||||||
    let flattened_keys = [
 | 
					    let flattened_keys = [
 | 
				
			||||||
        (
 | 
					        (
 | 
				
			||||||
            "type".to_string(),
 | 
					            "type".to_string(),
 | 
				
			||||||
            SimpleJsonValue::Str("m.text".to_string()),
 | 
					            JsonValue::Value(SimpleJsonValue::Str("m.text".to_string())),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        (
 | 
					        (
 | 
				
			||||||
            "room_id".to_string(),
 | 
					            "room_id".to_string(),
 | 
				
			||||||
            SimpleJsonValue::Str("!room:server".to_string()),
 | 
					            JsonValue::Value(SimpleJsonValue::Str("!room:server".to_string())),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        (
 | 
					        (
 | 
				
			||||||
            "content.body".to_string(),
 | 
					            "content.body".to_string(),
 | 
				
			||||||
            SimpleJsonValue::Str("test message".to_string()),
 | 
					            JsonValue::Value(SimpleJsonValue::Str("test message".to_string())),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
    .into_iter()
 | 
					    .into_iter()
 | 
				
			||||||
| 
						 | 
					@ -201,6 +204,7 @@ fn bench_eval_message(b: &mut Bencher) {
 | 
				
			||||||
        vec![],
 | 
					        vec![],
 | 
				
			||||||
        false,
 | 
					        false,
 | 
				
			||||||
        false,
 | 
					        false,
 | 
				
			||||||
 | 
					        false,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .unwrap();
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::collections::{BTreeMap, BTreeSet};
 | 
					use std::collections::{BTreeMap, BTreeSet};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::push::JsonValue;
 | 
				
			||||||
use anyhow::{Context, Error};
 | 
					use anyhow::{Context, Error};
 | 
				
			||||||
use lazy_static::lazy_static;
 | 
					use lazy_static::lazy_static;
 | 
				
			||||||
use log::warn;
 | 
					use log::warn;
 | 
				
			||||||
| 
						 | 
					@ -63,7 +64,7 @@ impl RoomVersionFeatures {
 | 
				
			||||||
pub struct PushRuleEvaluator {
 | 
					pub struct PushRuleEvaluator {
 | 
				
			||||||
    /// A mapping of "flattened" keys to simple JSON values in the event, e.g.
 | 
					    /// A mapping of "flattened" keys to simple JSON values in the event, e.g.
 | 
				
			||||||
    /// includes things like "type" and "content.msgtype".
 | 
					    /// includes things like "type" and "content.msgtype".
 | 
				
			||||||
    flattened_keys: BTreeMap<String, SimpleJsonValue>,
 | 
					    flattened_keys: BTreeMap<String, JsonValue>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// The "content.body", if any.
 | 
					    /// The "content.body", if any.
 | 
				
			||||||
    body: String,
 | 
					    body: String,
 | 
				
			||||||
| 
						 | 
					@ -87,7 +88,7 @@ pub struct PushRuleEvaluator {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// The related events, indexed by relation type. Flattened in the same manner as
 | 
					    /// The related events, indexed by relation type. Flattened in the same manner as
 | 
				
			||||||
    /// `flattened_keys`.
 | 
					    /// `flattened_keys`.
 | 
				
			||||||
    related_events_flattened: BTreeMap<String, BTreeMap<String, SimpleJsonValue>>,
 | 
					    related_events_flattened: BTreeMap<String, BTreeMap<String, JsonValue>>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// If msc3664, push rules for related events, is enabled.
 | 
					    /// If msc3664, push rules for related events, is enabled.
 | 
				
			||||||
    related_event_match_enabled: bool,
 | 
					    related_event_match_enabled: bool,
 | 
				
			||||||
| 
						 | 
					@ -101,6 +102,9 @@ pub struct PushRuleEvaluator {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// If MSC3758 (exact_event_match push rule condition) is enabled.
 | 
					    /// If MSC3758 (exact_event_match push rule condition) is enabled.
 | 
				
			||||||
    msc3758_exact_event_match: bool,
 | 
					    msc3758_exact_event_match: bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// If MSC3966 (exact_event_property_contains push rule condition) is enabled.
 | 
				
			||||||
 | 
					    msc3966_exact_event_property_contains: bool,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[pymethods]
 | 
					#[pymethods]
 | 
				
			||||||
| 
						 | 
					@ -109,21 +113,22 @@ impl PushRuleEvaluator {
 | 
				
			||||||
    #[allow(clippy::too_many_arguments)]
 | 
					    #[allow(clippy::too_many_arguments)]
 | 
				
			||||||
    #[new]
 | 
					    #[new]
 | 
				
			||||||
    pub fn py_new(
 | 
					    pub fn py_new(
 | 
				
			||||||
        flattened_keys: BTreeMap<String, SimpleJsonValue>,
 | 
					        flattened_keys: BTreeMap<String, JsonValue>,
 | 
				
			||||||
        has_mentions: bool,
 | 
					        has_mentions: bool,
 | 
				
			||||||
        user_mentions: BTreeSet<String>,
 | 
					        user_mentions: BTreeSet<String>,
 | 
				
			||||||
        room_mention: bool,
 | 
					        room_mention: bool,
 | 
				
			||||||
        room_member_count: u64,
 | 
					        room_member_count: u64,
 | 
				
			||||||
        sender_power_level: Option<i64>,
 | 
					        sender_power_level: Option<i64>,
 | 
				
			||||||
        notification_power_levels: BTreeMap<String, i64>,
 | 
					        notification_power_levels: BTreeMap<String, i64>,
 | 
				
			||||||
        related_events_flattened: BTreeMap<String, BTreeMap<String, SimpleJsonValue>>,
 | 
					        related_events_flattened: BTreeMap<String, BTreeMap<String, JsonValue>>,
 | 
				
			||||||
        related_event_match_enabled: bool,
 | 
					        related_event_match_enabled: bool,
 | 
				
			||||||
        room_version_feature_flags: Vec<String>,
 | 
					        room_version_feature_flags: Vec<String>,
 | 
				
			||||||
        msc3931_enabled: bool,
 | 
					        msc3931_enabled: bool,
 | 
				
			||||||
        msc3758_exact_event_match: bool,
 | 
					        msc3758_exact_event_match: bool,
 | 
				
			||||||
 | 
					        msc3966_exact_event_property_contains: bool,
 | 
				
			||||||
    ) -> Result<Self, Error> {
 | 
					    ) -> Result<Self, Error> {
 | 
				
			||||||
        let body = match flattened_keys.get("content.body") {
 | 
					        let body = match flattened_keys.get("content.body") {
 | 
				
			||||||
            Some(SimpleJsonValue::Str(s)) => s.clone(),
 | 
					            Some(JsonValue::Value(SimpleJsonValue::Str(s))) => s.clone(),
 | 
				
			||||||
            _ => String::new(),
 | 
					            _ => String::new(),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -141,6 +146,7 @@ impl PushRuleEvaluator {
 | 
				
			||||||
            room_version_feature_flags,
 | 
					            room_version_feature_flags,
 | 
				
			||||||
            msc3931_enabled,
 | 
					            msc3931_enabled,
 | 
				
			||||||
            msc3758_exact_event_match,
 | 
					            msc3758_exact_event_match,
 | 
				
			||||||
 | 
					            msc3966_exact_event_property_contains,
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -263,6 +269,9 @@ impl PushRuleEvaluator {
 | 
				
			||||||
            KnownCondition::RelatedEventMatch(event_match) => {
 | 
					            KnownCondition::RelatedEventMatch(event_match) => {
 | 
				
			||||||
                self.match_related_event_match(event_match, user_id)?
 | 
					                self.match_related_event_match(event_match, user_id)?
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            KnownCondition::ExactEventPropertyContains(exact_event_match) => {
 | 
				
			||||||
 | 
					                self.match_exact_event_property_contains(exact_event_match)?
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            KnownCondition::IsUserMention => {
 | 
					            KnownCondition::IsUserMention => {
 | 
				
			||||||
                if let Some(uid) = user_id {
 | 
					                if let Some(uid) = user_id {
 | 
				
			||||||
                    self.user_mentions.contains(uid)
 | 
					                    self.user_mentions.contains(uid)
 | 
				
			||||||
| 
						 | 
					@ -345,7 +354,7 @@ impl PushRuleEvaluator {
 | 
				
			||||||
            return Ok(false);
 | 
					            return Ok(false);
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let haystack = if let Some(SimpleJsonValue::Str(haystack)) =
 | 
					        let haystack = if let Some(JsonValue::Value(SimpleJsonValue::Str(haystack))) =
 | 
				
			||||||
            self.flattened_keys.get(&*event_match.key)
 | 
					            self.flattened_keys.get(&*event_match.key)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            haystack
 | 
					            haystack
 | 
				
			||||||
| 
						 | 
					@ -377,7 +386,9 @@ impl PushRuleEvaluator {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let value = &exact_event_match.value;
 | 
					        let value = &exact_event_match.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let haystack = if let Some(haystack) = self.flattened_keys.get(&*exact_event_match.key) {
 | 
					        let haystack = if let Some(JsonValue::Value(haystack)) =
 | 
				
			||||||
 | 
					            self.flattened_keys.get(&*exact_event_match.key)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
            haystack
 | 
					            haystack
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            return Ok(false);
 | 
					            return Ok(false);
 | 
				
			||||||
| 
						 | 
					@ -441,11 +452,12 @@ impl PushRuleEvaluator {
 | 
				
			||||||
            return Ok(false);
 | 
					            return Ok(false);
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let haystack = if let Some(SimpleJsonValue::Str(haystack)) = event.get(&**key) {
 | 
					        let haystack =
 | 
				
			||||||
            haystack
 | 
					            if let Some(JsonValue::Value(SimpleJsonValue::Str(haystack))) = event.get(&**key) {
 | 
				
			||||||
        } else {
 | 
					                haystack
 | 
				
			||||||
            return Ok(false);
 | 
					            } else {
 | 
				
			||||||
        };
 | 
					                return Ok(false);
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // For the content.body we match against "words", but for everything
 | 
					        // For the content.body we match against "words", but for everything
 | 
				
			||||||
        // else we match against the entire value.
 | 
					        // else we match against the entire value.
 | 
				
			||||||
| 
						 | 
					@ -459,6 +471,29 @@ impl PushRuleEvaluator {
 | 
				
			||||||
        compiled_pattern.is_match(haystack)
 | 
					        compiled_pattern.is_match(haystack)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Evaluates a `exact_event_property_contains` condition. (MSC3758)
 | 
				
			||||||
 | 
					    fn match_exact_event_property_contains(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        exact_event_match: &ExactEventMatchCondition,
 | 
				
			||||||
 | 
					    ) -> Result<bool, Error> {
 | 
				
			||||||
 | 
					        // First check if the feature is enabled.
 | 
				
			||||||
 | 
					        if !self.msc3966_exact_event_property_contains {
 | 
				
			||||||
 | 
					            return Ok(false);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let value = &exact_event_match.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let haystack = if let Some(JsonValue::Array(haystack)) =
 | 
				
			||||||
 | 
					            self.flattened_keys.get(&*exact_event_match.key)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            haystack
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            return Ok(false);
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(haystack.contains(&**value))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Match the member count against an 'is' condition
 | 
					    /// Match the member count against an 'is' condition
 | 
				
			||||||
    /// The `is` condition can be things like '>2', '==3' or even just '4'.
 | 
					    /// The `is` condition can be things like '>2', '==3' or even just '4'.
 | 
				
			||||||
    fn match_member_count(&self, is: &str) -> Result<bool, Error> {
 | 
					    fn match_member_count(&self, is: &str) -> Result<bool, Error> {
 | 
				
			||||||
| 
						 | 
					@ -488,7 +523,7 @@ fn push_rule_evaluator() {
 | 
				
			||||||
    let mut flattened_keys = BTreeMap::new();
 | 
					    let mut flattened_keys = BTreeMap::new();
 | 
				
			||||||
    flattened_keys.insert(
 | 
					    flattened_keys.insert(
 | 
				
			||||||
        "content.body".to_string(),
 | 
					        "content.body".to_string(),
 | 
				
			||||||
        SimpleJsonValue::Str("foo bar bob hello".to_string()),
 | 
					        JsonValue::Value(SimpleJsonValue::Str("foo bar bob hello".to_string())),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    let evaluator = PushRuleEvaluator::py_new(
 | 
					    let evaluator = PushRuleEvaluator::py_new(
 | 
				
			||||||
        flattened_keys,
 | 
					        flattened_keys,
 | 
				
			||||||
| 
						 | 
					@ -503,6 +538,7 @@ fn push_rule_evaluator() {
 | 
				
			||||||
        vec![],
 | 
					        vec![],
 | 
				
			||||||
        true,
 | 
					        true,
 | 
				
			||||||
        true,
 | 
					        true,
 | 
				
			||||||
 | 
					        true,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .unwrap();
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -519,7 +555,7 @@ fn test_requires_room_version_supports_condition() {
 | 
				
			||||||
    let mut flattened_keys = BTreeMap::new();
 | 
					    let mut flattened_keys = BTreeMap::new();
 | 
				
			||||||
    flattened_keys.insert(
 | 
					    flattened_keys.insert(
 | 
				
			||||||
        "content.body".to_string(),
 | 
					        "content.body".to_string(),
 | 
				
			||||||
        SimpleJsonValue::Str("foo bar bob hello".to_string()),
 | 
					        JsonValue::Value(SimpleJsonValue::Str("foo bar bob hello".to_string())),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    let flags = vec![RoomVersionFeatures::ExtensibleEvents.as_str().to_string()];
 | 
					    let flags = vec![RoomVersionFeatures::ExtensibleEvents.as_str().to_string()];
 | 
				
			||||||
    let evaluator = PushRuleEvaluator::py_new(
 | 
					    let evaluator = PushRuleEvaluator::py_new(
 | 
				
			||||||
| 
						 | 
					@ -535,6 +571,7 @@ fn test_requires_room_version_supports_condition() {
 | 
				
			||||||
        flags,
 | 
					        flags,
 | 
				
			||||||
        true,
 | 
					        true,
 | 
				
			||||||
        true,
 | 
					        true,
 | 
				
			||||||
 | 
					        true,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .unwrap();
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -58,7 +58,7 @@ use anyhow::{Context, Error};
 | 
				
			||||||
use log::warn;
 | 
					use log::warn;
 | 
				
			||||||
use pyo3::exceptions::PyTypeError;
 | 
					use pyo3::exceptions::PyTypeError;
 | 
				
			||||||
use pyo3::prelude::*;
 | 
					use pyo3::prelude::*;
 | 
				
			||||||
use pyo3::types::{PyBool, PyLong, PyString};
 | 
					use pyo3::types::{PyBool, PyList, PyLong, PyString};
 | 
				
			||||||
use pythonize::{depythonize, pythonize};
 | 
					use pythonize::{depythonize, pythonize};
 | 
				
			||||||
use serde::de::Error as _;
 | 
					use serde::de::Error as _;
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
| 
						 | 
					@ -280,6 +280,35 @@ impl<'source> FromPyObject<'source> for SimpleJsonValue {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A JSON values (list, string, int, boolean, or null).
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
 | 
				
			||||||
 | 
					#[serde(untagged)]
 | 
				
			||||||
 | 
					pub enum JsonValue {
 | 
				
			||||||
 | 
					    Array(Vec<SimpleJsonValue>),
 | 
				
			||||||
 | 
					    Value(SimpleJsonValue),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'source> FromPyObject<'source> for JsonValue {
 | 
				
			||||||
 | 
					    fn extract(ob: &'source PyAny) -> PyResult<Self> {
 | 
				
			||||||
 | 
					        if let Ok(l) = <PyList as pyo3::PyTryFrom>::try_from(ob) {
 | 
				
			||||||
 | 
					            match l.iter().map(SimpleJsonValue::extract).collect() {
 | 
				
			||||||
 | 
					                Ok(a) => Ok(JsonValue::Array(a)),
 | 
				
			||||||
 | 
					                Err(e) => Err(PyTypeError::new_err(format!(
 | 
				
			||||||
 | 
					                    "Can't convert to JsonValue::Array: {}",
 | 
				
			||||||
 | 
					                    e
 | 
				
			||||||
 | 
					                ))),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else if let Ok(v) = SimpleJsonValue::extract(ob) {
 | 
				
			||||||
 | 
					            Ok(JsonValue::Value(v))
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Err(PyTypeError::new_err(format!(
 | 
				
			||||||
 | 
					                "Can't convert from {} to JsonValue",
 | 
				
			||||||
 | 
					                ob.get_type().name()?
 | 
				
			||||||
 | 
					            )))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// A condition used in push rules to match against an event.
 | 
					/// A condition used in push rules to match against an event.
 | 
				
			||||||
///
 | 
					///
 | 
				
			||||||
/// We need this split as `serde` doesn't give us the ability to have a
 | 
					/// We need this split as `serde` doesn't give us the ability to have a
 | 
				
			||||||
| 
						 | 
					@ -303,6 +332,8 @@ pub enum KnownCondition {
 | 
				
			||||||
    ExactEventMatch(ExactEventMatchCondition),
 | 
					    ExactEventMatch(ExactEventMatchCondition),
 | 
				
			||||||
    #[serde(rename = "im.nheko.msc3664.related_event_match")]
 | 
					    #[serde(rename = "im.nheko.msc3664.related_event_match")]
 | 
				
			||||||
    RelatedEventMatch(RelatedEventMatchCondition),
 | 
					    RelatedEventMatch(RelatedEventMatchCondition),
 | 
				
			||||||
 | 
					    #[serde(rename = "org.matrix.msc3966.exact_event_property_contains")]
 | 
				
			||||||
 | 
					    ExactEventPropertyContains(ExactEventMatchCondition),
 | 
				
			||||||
    #[serde(rename = "org.matrix.msc3952.is_user_mention")]
 | 
					    #[serde(rename = "org.matrix.msc3952.is_user_mention")]
 | 
				
			||||||
    IsUserMention,
 | 
					    IsUserMention,
 | 
				
			||||||
    #[serde(rename = "org.matrix.msc3952.is_room_mention")]
 | 
					    #[serde(rename = "org.matrix.msc3952.is_room_mention")]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from typing import Any, Collection, Dict, Mapping, Optional, Sequence, Set, Tuple, Union
 | 
					from typing import Any, Collection, Dict, Mapping, Optional, Sequence, Set, Tuple, Union
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from synapse.types import JsonDict, SimpleJsonValue
 | 
					from synapse.types import JsonDict, JsonValue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PushRule:
 | 
					class PushRule:
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
| 
						 | 
					@ -56,18 +56,19 @@ def get_base_rule_ids() -> Collection[str]: ...
 | 
				
			||||||
class PushRuleEvaluator:
 | 
					class PushRuleEvaluator:
 | 
				
			||||||
    def __init__(
 | 
					    def __init__(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        flattened_keys: Mapping[str, SimpleJsonValue],
 | 
					        flattened_keys: Mapping[str, JsonValue],
 | 
				
			||||||
        has_mentions: bool,
 | 
					        has_mentions: bool,
 | 
				
			||||||
        user_mentions: Set[str],
 | 
					        user_mentions: Set[str],
 | 
				
			||||||
        room_mention: bool,
 | 
					        room_mention: bool,
 | 
				
			||||||
        room_member_count: int,
 | 
					        room_member_count: int,
 | 
				
			||||||
        sender_power_level: Optional[int],
 | 
					        sender_power_level: Optional[int],
 | 
				
			||||||
        notification_power_levels: Mapping[str, int],
 | 
					        notification_power_levels: Mapping[str, int],
 | 
				
			||||||
        related_events_flattened: Mapping[str, Mapping[str, SimpleJsonValue]],
 | 
					        related_events_flattened: Mapping[str, Mapping[str, JsonValue]],
 | 
				
			||||||
        related_event_match_enabled: bool,
 | 
					        related_event_match_enabled: bool,
 | 
				
			||||||
        room_version_feature_flags: Tuple[str, ...],
 | 
					        room_version_feature_flags: Tuple[str, ...],
 | 
				
			||||||
        msc3931_enabled: bool,
 | 
					        msc3931_enabled: bool,
 | 
				
			||||||
        msc3758_exact_event_match: bool,
 | 
					        msc3758_exact_event_match: bool,
 | 
				
			||||||
 | 
					        msc3966_exact_event_property_contains: bool,
 | 
				
			||||||
    ): ...
 | 
					    ): ...
 | 
				
			||||||
    def run(
 | 
					    def run(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -188,3 +188,8 @@ class ExperimentalConfig(Config):
 | 
				
			||||||
        self.msc3958_supress_edit_notifs = experimental.get(
 | 
					        self.msc3958_supress_edit_notifs = experimental.get(
 | 
				
			||||||
            "msc3958_supress_edit_notifs", False
 | 
					            "msc3958_supress_edit_notifs", False
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # MSC3966: exact_event_property_contains push rule condition.
 | 
				
			||||||
 | 
					        self.msc3966_exact_event_property_contains = experimental.get(
 | 
				
			||||||
 | 
					            "msc3966_exact_event_property_contains", False
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,7 +44,7 @@ from synapse.events.snapshot import EventContext
 | 
				
			||||||
from synapse.state import POWER_KEY
 | 
					from synapse.state import POWER_KEY
 | 
				
			||||||
from synapse.storage.databases.main.roommember import EventIdMembership
 | 
					from synapse.storage.databases.main.roommember import EventIdMembership
 | 
				
			||||||
from synapse.synapse_rust.push import FilteredPushRules, PushRuleEvaluator
 | 
					from synapse.synapse_rust.push import FilteredPushRules, PushRuleEvaluator
 | 
				
			||||||
from synapse.types import SimpleJsonValue
 | 
					from synapse.types import JsonValue
 | 
				
			||||||
from synapse.types.state import StateFilter
 | 
					from synapse.types.state import StateFilter
 | 
				
			||||||
from synapse.util.caches import register_cache
 | 
					from synapse.util.caches import register_cache
 | 
				
			||||||
from synapse.util.metrics import measure_func
 | 
					from synapse.util.metrics import measure_func
 | 
				
			||||||
| 
						 | 
					@ -259,13 +259,13 @@ class BulkPushRuleEvaluator:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def _related_events(
 | 
					    async def _related_events(
 | 
				
			||||||
        self, event: EventBase
 | 
					        self, event: EventBase
 | 
				
			||||||
    ) -> Dict[str, Dict[str, SimpleJsonValue]]:
 | 
					    ) -> Dict[str, Dict[str, JsonValue]]:
 | 
				
			||||||
        """Fetches the related events for 'event'. Sets the im.vector.is_falling_back key if the event is from a fallback relation
 | 
					        """Fetches the related events for 'event'. Sets the im.vector.is_falling_back key if the event is from a fallback relation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        Returns:
 | 
				
			||||||
            Mapping of relation type to flattened events.
 | 
					            Mapping of relation type to flattened events.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        related_events: Dict[str, Dict[str, SimpleJsonValue]] = {}
 | 
					        related_events: Dict[str, Dict[str, JsonValue]] = {}
 | 
				
			||||||
        if self._related_event_match_enabled:
 | 
					        if self._related_event_match_enabled:
 | 
				
			||||||
            related_event_id = event.content.get("m.relates_to", {}).get("event_id")
 | 
					            related_event_id = event.content.get("m.relates_to", {}).get("event_id")
 | 
				
			||||||
            relation_type = event.content.get("m.relates_to", {}).get("rel_type")
 | 
					            relation_type = event.content.get("m.relates_to", {}).get("rel_type")
 | 
				
			||||||
| 
						 | 
					@ -429,6 +429,7 @@ class BulkPushRuleEvaluator:
 | 
				
			||||||
            event.room_version.msc3931_push_features,
 | 
					            event.room_version.msc3931_push_features,
 | 
				
			||||||
            self.hs.config.experimental.msc1767_enabled,  # MSC3931 flag
 | 
					            self.hs.config.experimental.msc1767_enabled,  # MSC3931 flag
 | 
				
			||||||
            self.hs.config.experimental.msc3758_exact_event_match,
 | 
					            self.hs.config.experimental.msc3758_exact_event_match,
 | 
				
			||||||
 | 
					            self.hs.config.experimental.msc3966_exact_event_property_contains,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        users = rules_by_user.keys()
 | 
					        users = rules_by_user.keys()
 | 
				
			||||||
| 
						 | 
					@ -502,18 +503,22 @@ RulesByUser = Dict[str, List[Rule]]
 | 
				
			||||||
StateGroup = Union[object, int]
 | 
					StateGroup = Union[object, int]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _is_simple_value(value: Any) -> bool:
 | 
				
			||||||
 | 
					    return isinstance(value, (bool, str)) or type(value) is int or value is None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _flatten_dict(
 | 
					def _flatten_dict(
 | 
				
			||||||
    d: Union[EventBase, Mapping[str, Any]],
 | 
					    d: Union[EventBase, Mapping[str, Any]],
 | 
				
			||||||
    prefix: Optional[List[str]] = None,
 | 
					    prefix: Optional[List[str]] = None,
 | 
				
			||||||
    result: Optional[Dict[str, SimpleJsonValue]] = None,
 | 
					    result: Optional[Dict[str, JsonValue]] = None,
 | 
				
			||||||
    *,
 | 
					    *,
 | 
				
			||||||
    msc3783_escape_event_match_key: bool = False,
 | 
					    msc3783_escape_event_match_key: bool = False,
 | 
				
			||||||
) -> Dict[str, SimpleJsonValue]:
 | 
					) -> Dict[str, JsonValue]:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Given a JSON dictionary (or event) which might contain sub dictionaries,
 | 
					    Given a JSON dictionary (or event) which might contain sub dictionaries,
 | 
				
			||||||
    flatten it into a single layer dictionary by combining the keys & sub-keys.
 | 
					    flatten it into a single layer dictionary by combining the keys & sub-keys.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    String, integer, boolean, and null values are kept. All others are dropped.
 | 
					    String, integer, boolean, null or lists of those values are kept. All others are dropped.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Transforms:
 | 
					    Transforms:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -542,8 +547,10 @@ def _flatten_dict(
 | 
				
			||||||
            # nested fields.
 | 
					            # nested fields.
 | 
				
			||||||
            key = key.replace("\\", "\\\\").replace(".", "\\.")
 | 
					            key = key.replace("\\", "\\\\").replace(".", "\\.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if isinstance(value, (bool, str)) or type(value) is int or value is None:
 | 
					        if _is_simple_value(value):
 | 
				
			||||||
            result[".".join(prefix + [key])] = value
 | 
					            result[".".join(prefix + [key])] = value
 | 
				
			||||||
 | 
					        elif isinstance(value, (list, tuple)):
 | 
				
			||||||
 | 
					            result[".".join(prefix + [key])] = [v for v in value if _is_simple_value(v)]
 | 
				
			||||||
        elif isinstance(value, Mapping):
 | 
					        elif isinstance(value, Mapping):
 | 
				
			||||||
            # do not set `room_version` due to recursion considerations below
 | 
					            # do not set `room_version` due to recursion considerations below
 | 
				
			||||||
            _flatten_dict(
 | 
					            _flatten_dict(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -71,6 +71,7 @@ MutableStateMap = MutableMapping[StateKey, T]
 | 
				
			||||||
# JSON types. These could be made stronger, but will do for now.
 | 
					# JSON types. These could be made stronger, but will do for now.
 | 
				
			||||||
# A "simple" (canonical) JSON value.
 | 
					# A "simple" (canonical) JSON value.
 | 
				
			||||||
SimpleJsonValue = Optional[Union[str, int, bool]]
 | 
					SimpleJsonValue = Optional[Union[str, int, bool]]
 | 
				
			||||||
 | 
					JsonValue = Union[List[SimpleJsonValue], Tuple[SimpleJsonValue, ...], SimpleJsonValue]
 | 
				
			||||||
# A JSON-serialisable dict.
 | 
					# A JSON-serialisable dict.
 | 
				
			||||||
JsonDict = Dict[str, Any]
 | 
					JsonDict = Dict[str, Any]
 | 
				
			||||||
# A JSON-serialisable mapping; roughly speaking an immutable JSONDict.
 | 
					# A JSON-serialisable mapping; roughly speaking an immutable JSONDict.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,6 +32,7 @@ from synapse.storage.databases.main.appservice import _make_exclusive_regex
 | 
				
			||||||
from synapse.synapse_rust.push import PushRuleEvaluator
 | 
					from synapse.synapse_rust.push import PushRuleEvaluator
 | 
				
			||||||
from synapse.types import JsonDict, JsonMapping, UserID
 | 
					from synapse.types import JsonDict, JsonMapping, UserID
 | 
				
			||||||
from synapse.util import Clock
 | 
					from synapse.util import Clock
 | 
				
			||||||
 | 
					from synapse.util.frozenutils import freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from tests import unittest
 | 
					from tests import unittest
 | 
				
			||||||
from tests.test_utils.event_injection import create_event, inject_member_event
 | 
					from tests.test_utils.event_injection import create_event, inject_member_event
 | 
				
			||||||
| 
						 | 
					@ -57,17 +58,24 @@ class FlattenDictTestCase(unittest.TestCase):
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_non_string(self) -> None:
 | 
					    def test_non_string(self) -> None:
 | 
				
			||||||
        """Booleans, ints, and nulls should be kept while other items are dropped."""
 | 
					        """String, booleans, ints, nulls and list of those should be kept while other items are dropped."""
 | 
				
			||||||
        input: Dict[str, Any] = {
 | 
					        input: Dict[str, Any] = {
 | 
				
			||||||
            "woo": "woo",
 | 
					            "woo": "woo",
 | 
				
			||||||
            "foo": True,
 | 
					            "foo": True,
 | 
				
			||||||
            "bar": 1,
 | 
					            "bar": 1,
 | 
				
			||||||
            "baz": None,
 | 
					            "baz": None,
 | 
				
			||||||
            "fuzz": [],
 | 
					            "fuzz": ["woo", True, 1, None, [], {}],
 | 
				
			||||||
            "boo": {},
 | 
					            "boo": {},
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertEqual(
 | 
				
			||||||
            {"woo": "woo", "foo": True, "bar": 1, "baz": None}, _flatten_dict(input)
 | 
					            {
 | 
				
			||||||
 | 
					                "woo": "woo",
 | 
				
			||||||
 | 
					                "foo": True,
 | 
				
			||||||
 | 
					                "bar": 1,
 | 
				
			||||||
 | 
					                "baz": None,
 | 
				
			||||||
 | 
					                "fuzz": ["woo", True, 1, None],
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            _flatten_dict(input),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_event(self) -> None:
 | 
					    def test_event(self) -> None:
 | 
				
			||||||
| 
						 | 
					@ -117,6 +125,7 @@ class FlattenDictTestCase(unittest.TestCase):
 | 
				
			||||||
            "room_id": "!test:test",
 | 
					            "room_id": "!test:test",
 | 
				
			||||||
            "sender": "@alice:test",
 | 
					            "sender": "@alice:test",
 | 
				
			||||||
            "type": "m.room.message",
 | 
					            "type": "m.room.message",
 | 
				
			||||||
 | 
					            "content.org.matrix.msc1767.markup": [],
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        self.assertEqual(expected, _flatten_dict(event))
 | 
					        self.assertEqual(expected, _flatten_dict(event))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -128,6 +137,7 @@ class FlattenDictTestCase(unittest.TestCase):
 | 
				
			||||||
            "room_id": "!test:test",
 | 
					            "room_id": "!test:test",
 | 
				
			||||||
            "sender": "@alice:test",
 | 
					            "sender": "@alice:test",
 | 
				
			||||||
            "type": "m.room.message",
 | 
					            "type": "m.room.message",
 | 
				
			||||||
 | 
					            "content.org.matrix.msc1767.markup": [],
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        self.assertEqual(expected, _flatten_dict(event))
 | 
					        self.assertEqual(expected, _flatten_dict(event))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -169,6 +179,7 @@ class PushRuleEvaluatorTestCase(unittest.TestCase):
 | 
				
			||||||
            room_version_feature_flags=event.room_version.msc3931_push_features,
 | 
					            room_version_feature_flags=event.room_version.msc3931_push_features,
 | 
				
			||||||
            msc3931_enabled=True,
 | 
					            msc3931_enabled=True,
 | 
				
			||||||
            msc3758_exact_event_match=True,
 | 
					            msc3758_exact_event_match=True,
 | 
				
			||||||
 | 
					            msc3966_exact_event_property_contains=True,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_display_name(self) -> None:
 | 
					    def test_display_name(self) -> None:
 | 
				
			||||||
| 
						 | 
					@ -549,6 +560,42 @@ class PushRuleEvaluatorTestCase(unittest.TestCase):
 | 
				
			||||||
                "incorrect types should not match",
 | 
					                "incorrect types should not match",
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_exact_event_property_contains(self) -> None:
 | 
				
			||||||
 | 
					        """Check that exact_event_property_contains conditions work as expected."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        condition = {
 | 
				
			||||||
 | 
					            "kind": "org.matrix.msc3966.exact_event_property_contains",
 | 
				
			||||||
 | 
					            "key": "content.value",
 | 
				
			||||||
 | 
					            "value": "foobaz",
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        self._assert_matches(
 | 
				
			||||||
 | 
					            condition,
 | 
				
			||||||
 | 
					            {"value": ["foobaz"]},
 | 
				
			||||||
 | 
					            "exact value should match",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self._assert_matches(
 | 
				
			||||||
 | 
					            condition,
 | 
				
			||||||
 | 
					            {"value": ["foobaz", "bugz"]},
 | 
				
			||||||
 | 
					            "extra values should match",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self._assert_not_matches(
 | 
				
			||||||
 | 
					            condition,
 | 
				
			||||||
 | 
					            {"value": ["FoobaZ"]},
 | 
				
			||||||
 | 
					            "values should match and be case-sensitive",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self._assert_not_matches(
 | 
				
			||||||
 | 
					            condition,
 | 
				
			||||||
 | 
					            {"value": "foobaz"},
 | 
				
			||||||
 | 
					            "does not search in a string",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # it should work on frozendicts too
 | 
				
			||||||
 | 
					        self._assert_matches(
 | 
				
			||||||
 | 
					            condition,
 | 
				
			||||||
 | 
					            freeze({"value": ["foobaz"]}),
 | 
				
			||||||
 | 
					            "values should match on frozendicts",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_no_body(self) -> None:
 | 
					    def test_no_body(self) -> None:
 | 
				
			||||||
        """Not having a body shouldn't break the evaluator."""
 | 
					        """Not having a body shouldn't break the evaluator."""
 | 
				
			||||||
        evaluator = self._get_evaluator({})
 | 
					        evaluator = self._get_evaluator({})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue