Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 119e079

Browse files
authored
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.
1 parent 157c571 commit 119e079

File tree

9 files changed

+176
-42
lines changed

9 files changed

+176
-42
lines changed

changelog.d/15045.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Experimental support for [MSC3966](https://github.com/matrix-org/matrix-spec-proposals/pull/3966): the `exact_event_property_contains` push rule condition.

rust/benches/evaluator.rs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
#![feature(test)]
1616
use std::collections::BTreeSet;
1717
use synapse::push::{
18-
evaluator::PushRuleEvaluator, Condition, EventMatchCondition, FilteredPushRules, PushRules,
19-
SimpleJsonValue,
18+
evaluator::PushRuleEvaluator, Condition, EventMatchCondition, FilteredPushRules, JsonValue,
19+
PushRules, SimpleJsonValue,
2020
};
2121
use test::Bencher;
2222

@@ -27,15 +27,15 @@ fn bench_match_exact(b: &mut Bencher) {
2727
let flattened_keys = [
2828
(
2929
"type".to_string(),
30-
SimpleJsonValue::Str("m.text".to_string()),
30+
JsonValue::Value(SimpleJsonValue::Str("m.text".to_string())),
3131
),
3232
(
3333
"room_id".to_string(),
34-
SimpleJsonValue::Str("!room:server".to_string()),
34+
JsonValue::Value(SimpleJsonValue::Str("!room:server".to_string())),
3535
),
3636
(
3737
"content.body".to_string(),
38-
SimpleJsonValue::Str("test message".to_string()),
38+
JsonValue::Value(SimpleJsonValue::Str("test message".to_string())),
3939
),
4040
]
4141
.into_iter()
@@ -54,6 +54,7 @@ fn bench_match_exact(b: &mut Bencher) {
5454
vec![],
5555
false,
5656
false,
57+
false,
5758
)
5859
.unwrap();
5960

@@ -76,15 +77,15 @@ fn bench_match_word(b: &mut Bencher) {
7677
let flattened_keys = [
7778
(
7879
"type".to_string(),
79-
SimpleJsonValue::Str("m.text".to_string()),
80+
JsonValue::Value(SimpleJsonValue::Str("m.text".to_string())),
8081
),
8182
(
8283
"room_id".to_string(),
83-
SimpleJsonValue::Str("!room:server".to_string()),
84+
JsonValue::Value(SimpleJsonValue::Str("!room:server".to_string())),
8485
),
8586
(
8687
"content.body".to_string(),
87-
SimpleJsonValue::Str("test message".to_string()),
88+
JsonValue::Value(SimpleJsonValue::Str("test message".to_string())),
8889
),
8990
]
9091
.into_iter()
@@ -103,6 +104,7 @@ fn bench_match_word(b: &mut Bencher) {
103104
vec![],
104105
false,
105106
false,
107+
false,
106108
)
107109
.unwrap();
108110

@@ -125,15 +127,15 @@ fn bench_match_word_miss(b: &mut Bencher) {
125127
let flattened_keys = [
126128
(
127129
"type".to_string(),
128-
SimpleJsonValue::Str("m.text".to_string()),
130+
JsonValue::Value(SimpleJsonValue::Str("m.text".to_string())),
129131
),
130132
(
131133
"room_id".to_string(),
132-
SimpleJsonValue::Str("!room:server".to_string()),
134+
JsonValue::Value(SimpleJsonValue::Str("!room:server".to_string())),
133135
),
134136
(
135137
"content.body".to_string(),
136-
SimpleJsonValue::Str("test message".to_string()),
138+
JsonValue::Value(SimpleJsonValue::Str("test message".to_string())),
137139
),
138140
]
139141
.into_iter()
@@ -152,6 +154,7 @@ fn bench_match_word_miss(b: &mut Bencher) {
152154
vec![],
153155
false,
154156
false,
157+
false,
155158
)
156159
.unwrap();
157160

@@ -174,15 +177,15 @@ fn bench_eval_message(b: &mut Bencher) {
174177
let flattened_keys = [
175178
(
176179
"type".to_string(),
177-
SimpleJsonValue::Str("m.text".to_string()),
180+
JsonValue::Value(SimpleJsonValue::Str("m.text".to_string())),
178181
),
179182
(
180183
"room_id".to_string(),
181-
SimpleJsonValue::Str("!room:server".to_string()),
184+
JsonValue::Value(SimpleJsonValue::Str("!room:server".to_string())),
182185
),
183186
(
184187
"content.body".to_string(),
185-
SimpleJsonValue::Str("test message".to_string()),
188+
JsonValue::Value(SimpleJsonValue::Str("test message".to_string())),
186189
),
187190
]
188191
.into_iter()
@@ -201,6 +204,7 @@ fn bench_eval_message(b: &mut Bencher) {
201204
vec![],
202205
false,
203206
false,
207+
false,
204208
)
205209
.unwrap();
206210

rust/src/push/evaluator.rs

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
use std::collections::{BTreeMap, BTreeSet};
1616

17+
use crate::push::JsonValue;
1718
use anyhow::{Context, Error};
1819
use lazy_static::lazy_static;
1920
use log::warn;
@@ -63,7 +64,7 @@ impl RoomVersionFeatures {
6364
pub struct PushRuleEvaluator {
6465
/// A mapping of "flattened" keys to simple JSON values in the event, e.g.
6566
/// includes things like "type" and "content.msgtype".
66-
flattened_keys: BTreeMap<String, SimpleJsonValue>,
67+
flattened_keys: BTreeMap<String, JsonValue>,
6768

6869
/// The "content.body", if any.
6970
body: String,
@@ -87,7 +88,7 @@ pub struct PushRuleEvaluator {
8788

8889
/// The related events, indexed by relation type. Flattened in the same manner as
8990
/// `flattened_keys`.
90-
related_events_flattened: BTreeMap<String, BTreeMap<String, SimpleJsonValue>>,
91+
related_events_flattened: BTreeMap<String, BTreeMap<String, JsonValue>>,
9192

9293
/// If msc3664, push rules for related events, is enabled.
9394
related_event_match_enabled: bool,
@@ -101,6 +102,9 @@ pub struct PushRuleEvaluator {
101102

102103
/// If MSC3758 (exact_event_match push rule condition) is enabled.
103104
msc3758_exact_event_match: bool,
105+
106+
/// If MSC3966 (exact_event_property_contains push rule condition) is enabled.
107+
msc3966_exact_event_property_contains: bool,
104108
}
105109

106110
#[pymethods]
@@ -109,21 +113,22 @@ impl PushRuleEvaluator {
109113
#[allow(clippy::too_many_arguments)]
110114
#[new]
111115
pub fn py_new(
112-
flattened_keys: BTreeMap<String, SimpleJsonValue>,
116+
flattened_keys: BTreeMap<String, JsonValue>,
113117
has_mentions: bool,
114118
user_mentions: BTreeSet<String>,
115119
room_mention: bool,
116120
room_member_count: u64,
117121
sender_power_level: Option<i64>,
118122
notification_power_levels: BTreeMap<String, i64>,
119-
related_events_flattened: BTreeMap<String, BTreeMap<String, SimpleJsonValue>>,
123+
related_events_flattened: BTreeMap<String, BTreeMap<String, JsonValue>>,
120124
related_event_match_enabled: bool,
121125
room_version_feature_flags: Vec<String>,
122126
msc3931_enabled: bool,
123127
msc3758_exact_event_match: bool,
128+
msc3966_exact_event_property_contains: bool,
124129
) -> Result<Self, Error> {
125130
let body = match flattened_keys.get("content.body") {
126-
Some(SimpleJsonValue::Str(s)) => s.clone(),
131+
Some(JsonValue::Value(SimpleJsonValue::Str(s))) => s.clone(),
127132
_ => String::new(),
128133
};
129134

@@ -141,6 +146,7 @@ impl PushRuleEvaluator {
141146
room_version_feature_flags,
142147
msc3931_enabled,
143148
msc3758_exact_event_match,
149+
msc3966_exact_event_property_contains,
144150
})
145151
}
146152

@@ -263,6 +269,9 @@ impl PushRuleEvaluator {
263269
KnownCondition::RelatedEventMatch(event_match) => {
264270
self.match_related_event_match(event_match, user_id)?
265271
}
272+
KnownCondition::ExactEventPropertyContains(exact_event_match) => {
273+
self.match_exact_event_property_contains(exact_event_match)?
274+
}
266275
KnownCondition::IsUserMention => {
267276
if let Some(uid) = user_id {
268277
self.user_mentions.contains(uid)
@@ -345,7 +354,7 @@ impl PushRuleEvaluator {
345354
return Ok(false);
346355
};
347356

348-
let haystack = if let Some(SimpleJsonValue::Str(haystack)) =
357+
let haystack = if let Some(JsonValue::Value(SimpleJsonValue::Str(haystack))) =
349358
self.flattened_keys.get(&*event_match.key)
350359
{
351360
haystack
@@ -377,7 +386,9 @@ impl PushRuleEvaluator {
377386

378387
let value = &exact_event_match.value;
379388

380-
let haystack = if let Some(haystack) = self.flattened_keys.get(&*exact_event_match.key) {
389+
let haystack = if let Some(JsonValue::Value(haystack)) =
390+
self.flattened_keys.get(&*exact_event_match.key)
391+
{
381392
haystack
382393
} else {
383394
return Ok(false);
@@ -441,11 +452,12 @@ impl PushRuleEvaluator {
441452
return Ok(false);
442453
};
443454

444-
let haystack = if let Some(SimpleJsonValue::Str(haystack)) = event.get(&**key) {
445-
haystack
446-
} else {
447-
return Ok(false);
448-
};
455+
let haystack =
456+
if let Some(JsonValue::Value(SimpleJsonValue::Str(haystack))) = event.get(&**key) {
457+
haystack
458+
} else {
459+
return Ok(false);
460+
};
449461

450462
// For the content.body we match against "words", but for everything
451463
// else we match against the entire value.
@@ -459,6 +471,29 @@ impl PushRuleEvaluator {
459471
compiled_pattern.is_match(haystack)
460472
}
461473

474+
/// Evaluates a `exact_event_property_contains` condition. (MSC3758)
475+
fn match_exact_event_property_contains(
476+
&self,
477+
exact_event_match: &ExactEventMatchCondition,
478+
) -> Result<bool, Error> {
479+
// First check if the feature is enabled.
480+
if !self.msc3966_exact_event_property_contains {
481+
return Ok(false);
482+
}
483+
484+
let value = &exact_event_match.value;
485+
486+
let haystack = if let Some(JsonValue::Array(haystack)) =
487+
self.flattened_keys.get(&*exact_event_match.key)
488+
{
489+
haystack
490+
} else {
491+
return Ok(false);
492+
};
493+
494+
Ok(haystack.contains(&**value))
495+
}
496+
462497
/// Match the member count against an 'is' condition
463498
/// The `is` condition can be things like '>2', '==3' or even just '4'.
464499
fn match_member_count(&self, is: &str) -> Result<bool, Error> {
@@ -488,7 +523,7 @@ fn push_rule_evaluator() {
488523
let mut flattened_keys = BTreeMap::new();
489524
flattened_keys.insert(
490525
"content.body".to_string(),
491-
SimpleJsonValue::Str("foo bar bob hello".to_string()),
526+
JsonValue::Value(SimpleJsonValue::Str("foo bar bob hello".to_string())),
492527
);
493528
let evaluator = PushRuleEvaluator::py_new(
494529
flattened_keys,
@@ -503,6 +538,7 @@ fn push_rule_evaluator() {
503538
vec![],
504539
true,
505540
true,
541+
true,
506542
)
507543
.unwrap();
508544

@@ -519,7 +555,7 @@ fn test_requires_room_version_supports_condition() {
519555
let mut flattened_keys = BTreeMap::new();
520556
flattened_keys.insert(
521557
"content.body".to_string(),
522-
SimpleJsonValue::Str("foo bar bob hello".to_string()),
558+
JsonValue::Value(SimpleJsonValue::Str("foo bar bob hello".to_string())),
523559
);
524560
let flags = vec![RoomVersionFeatures::ExtensibleEvents.as_str().to_string()];
525561
let evaluator = PushRuleEvaluator::py_new(
@@ -535,6 +571,7 @@ fn test_requires_room_version_supports_condition() {
535571
flags,
536572
true,
537573
true,
574+
true,
538575
)
539576
.unwrap();
540577

rust/src/push/mod.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ use anyhow::{Context, Error};
5858
use log::warn;
5959
use pyo3::exceptions::PyTypeError;
6060
use pyo3::prelude::*;
61-
use pyo3::types::{PyBool, PyLong, PyString};
61+
use pyo3::types::{PyBool, PyList, PyLong, PyString};
6262
use pythonize::{depythonize, pythonize};
6363
use serde::de::Error as _;
6464
use serde::{Deserialize, Serialize};
@@ -280,6 +280,35 @@ impl<'source> FromPyObject<'source> for SimpleJsonValue {
280280
}
281281
}
282282

283+
/// A JSON values (list, string, int, boolean, or null).
284+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
285+
#[serde(untagged)]
286+
pub enum JsonValue {
287+
Array(Vec<SimpleJsonValue>),
288+
Value(SimpleJsonValue),
289+
}
290+
291+
impl<'source> FromPyObject<'source> for JsonValue {
292+
fn extract(ob: &'source PyAny) -> PyResult<Self> {
293+
if let Ok(l) = <PyList as pyo3::PyTryFrom>::try_from(ob) {
294+
match l.iter().map(SimpleJsonValue::extract).collect() {
295+
Ok(a) => Ok(JsonValue::Array(a)),
296+
Err(e) => Err(PyTypeError::new_err(format!(
297+
"Can't convert to JsonValue::Array: {}",
298+
e
299+
))),
300+
}
301+
} else if let Ok(v) = SimpleJsonValue::extract(ob) {
302+
Ok(JsonValue::Value(v))
303+
} else {
304+
Err(PyTypeError::new_err(format!(
305+
"Can't convert from {} to JsonValue",
306+
ob.get_type().name()?
307+
)))
308+
}
309+
}
310+
}
311+
283312
/// A condition used in push rules to match against an event.
284313
///
285314
/// We need this split as `serde` doesn't give us the ability to have a
@@ -303,6 +332,8 @@ pub enum KnownCondition {
303332
ExactEventMatch(ExactEventMatchCondition),
304333
#[serde(rename = "im.nheko.msc3664.related_event_match")]
305334
RelatedEventMatch(RelatedEventMatchCondition),
335+
#[serde(rename = "org.matrix.msc3966.exact_event_property_contains")]
336+
ExactEventPropertyContains(ExactEventMatchCondition),
306337
#[serde(rename = "org.matrix.msc3952.is_user_mention")]
307338
IsUserMention,
308339
#[serde(rename = "org.matrix.msc3952.is_room_mention")]

stubs/synapse/synapse_rust/push.pyi

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from typing import Any, Collection, Dict, Mapping, Optional, Sequence, Set, Tuple, Union
1616

17-
from synapse.types import JsonDict, SimpleJsonValue
17+
from synapse.types import JsonDict, JsonValue
1818

1919
class PushRule:
2020
@property
@@ -56,18 +56,19 @@ def get_base_rule_ids() -> Collection[str]: ...
5656
class PushRuleEvaluator:
5757
def __init__(
5858
self,
59-
flattened_keys: Mapping[str, SimpleJsonValue],
59+
flattened_keys: Mapping[str, JsonValue],
6060
has_mentions: bool,
6161
user_mentions: Set[str],
6262
room_mention: bool,
6363
room_member_count: int,
6464
sender_power_level: Optional[int],
6565
notification_power_levels: Mapping[str, int],
66-
related_events_flattened: Mapping[str, Mapping[str, SimpleJsonValue]],
66+
related_events_flattened: Mapping[str, Mapping[str, JsonValue]],
6767
related_event_match_enabled: bool,
6868
room_version_feature_flags: Tuple[str, ...],
6969
msc3931_enabled: bool,
7070
msc3758_exact_event_match: bool,
71+
msc3966_exact_event_property_contains: bool,
7172
): ...
7273
def run(
7374
self,

synapse/config/experimental.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,8 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
188188
self.msc3958_supress_edit_notifs = experimental.get(
189189
"msc3958_supress_edit_notifs", False
190190
)
191+
192+
# MSC3966: exact_event_property_contains push rule condition.
193+
self.msc3966_exact_event_property_contains = experimental.get(
194+
"msc3966_exact_event_property_contains", False
195+
)

0 commit comments

Comments
 (0)