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

Commit 52700a0

Browse files
authored
Support the backwards compatibility features in MSC3952. (#14958)
If the feature is enabled and the event has a `m.mentions` property, skip processing of the legacy mentions rules.
1 parent 0a686d1 commit 52700a0

File tree

7 files changed

+184
-59
lines changed

7 files changed

+184
-59
lines changed

changelog.d/14958.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Experimental support for [MSC3952](https://github.com/matrix-org/matrix-spec-proposals/pull/3952): intentional mentions.

rust/benches/evaluator.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ fn bench_match_exact(b: &mut Bencher) {
3333

3434
let eval = PushRuleEvaluator::py_new(
3535
flattened_keys,
36+
false,
3637
BTreeSet::new(),
3738
false,
3839
10,
@@ -71,6 +72,7 @@ fn bench_match_word(b: &mut Bencher) {
7172

7273
let eval = PushRuleEvaluator::py_new(
7374
flattened_keys,
75+
false,
7476
BTreeSet::new(),
7577
false,
7678
10,
@@ -109,6 +111,7 @@ fn bench_match_word_miss(b: &mut Bencher) {
109111

110112
let eval = PushRuleEvaluator::py_new(
111113
flattened_keys,
114+
false,
112115
BTreeSet::new(),
113116
false,
114117
10,
@@ -147,6 +150,7 @@ fn bench_eval_message(b: &mut Bencher) {
147150

148151
let eval = PushRuleEvaluator::py_new(
149152
flattened_keys,
153+
false,
150154
BTreeSet::new(),
151155
false,
152156
10,

rust/src/push/evaluator.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ pub struct PushRuleEvaluator {
6868
/// The "content.body", if any.
6969
body: String,
7070

71+
/// True if the event has a mentions property and MSC3952 support is enabled.
72+
has_mentions: bool,
7173
/// The user mentions that were part of the message.
7274
user_mentions: BTreeSet<String>,
7375
/// True if the message is a room message.
@@ -105,6 +107,7 @@ impl PushRuleEvaluator {
105107
#[new]
106108
pub fn py_new(
107109
flattened_keys: BTreeMap<String, String>,
110+
has_mentions: bool,
108111
user_mentions: BTreeSet<String>,
109112
room_mention: bool,
110113
room_member_count: u64,
@@ -123,6 +126,7 @@ impl PushRuleEvaluator {
123126
Ok(PushRuleEvaluator {
124127
flattened_keys,
125128
body,
129+
has_mentions,
126130
user_mentions,
127131
room_mention,
128132
room_member_count,
@@ -155,6 +159,19 @@ impl PushRuleEvaluator {
155159
}
156160

157161
let rule_id = &push_rule.rule_id().to_string();
162+
163+
// For backwards-compatibility the legacy mention rules are disabled
164+
// if the event contains the 'm.mentions' property (and if the
165+
// experimental feature is enabled, both of these are represented
166+
// by the has_mentions flag).
167+
if self.has_mentions
168+
&& (rule_id == "global/override/.m.rule.contains_display_name"
169+
|| rule_id == "global/content/.m.rule.contains_user_name"
170+
|| rule_id == "global/override/.m.rule.roomnotif")
171+
{
172+
continue;
173+
}
174+
158175
let extev_flag = &RoomVersionFeatures::ExtensibleEvents.as_str().to_string();
159176
let supports_extensible_events = self.room_version_feature_flags.contains(extev_flag);
160177
let safe_from_rver_condition = SAFE_EXTENSIBLE_EVENTS_RULE_IDS.contains(rule_id);
@@ -441,6 +458,7 @@ fn push_rule_evaluator() {
441458
flattened_keys.insert("content.body".to_string(), "foo bar bob hello".to_string());
442459
let evaluator = PushRuleEvaluator::py_new(
443460
flattened_keys,
461+
false,
444462
BTreeSet::new(),
445463
false,
446464
10,
@@ -468,6 +486,7 @@ fn test_requires_room_version_supports_condition() {
468486
let flags = vec![RoomVersionFeatures::ExtensibleEvents.as_str().to_string()];
469487
let evaluator = PushRuleEvaluator::py_new(
470488
flattened_keys,
489+
false,
471490
BTreeSet::new(),
472491
false,
473492
10,

stubs/synapse/synapse_rust/push.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class PushRuleEvaluator:
5656
def __init__(
5757
self,
5858
flattened_keys: Mapping[str, str],
59+
has_mentions: bool,
5960
user_mentions: Set[str],
6061
room_mention: bool,
6162
room_member_count: int,

synapse/push/bulk_push_rule_evaluator.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ def __init__(self, hs: "HomeServer"):
119119
self.should_calculate_push_rules = self.hs.config.push.enable_push
120120

121121
self._related_event_match_enabled = self.hs.config.experimental.msc3664_enabled
122+
self._intentional_mentions_enabled = (
123+
self.hs.config.experimental.msc3952_intentional_mentions
124+
)
122125

123126
self.room_push_rule_cache_metrics = register_cache(
124127
"cache",
@@ -364,9 +367,12 @@ async def _action_for_event_by_user(
364367

365368
# Pull out any user and room mentions.
366369
mentions = event.content.get(EventContentFields.MSC3952_MENTIONS)
370+
has_mentions = self._intentional_mentions_enabled and isinstance(mentions, dict)
367371
user_mentions: Set[str] = set()
368372
room_mention = False
369-
if isinstance(mentions, dict):
373+
if has_mentions:
374+
# mypy seems to have lost the type even though it must be a dict here.
375+
assert isinstance(mentions, dict)
370376
# Remove out any non-string items and convert to a set.
371377
user_mentions_raw = mentions.get("user_ids")
372378
if isinstance(user_mentions_raw, list):
@@ -378,6 +384,7 @@ async def _action_for_event_by_user(
378384

379385
evaluator = PushRuleEvaluator(
380386
_flatten_dict(event, room_version=event.room_version),
387+
has_mentions,
381388
user_mentions,
382389
room_mention,
383390
room_member_count,

tests/push/test_bulk_push_rule_evaluator.py

Lines changed: 140 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from typing import Any
15+
from typing import Any, Optional
1616
from unittest.mock import patch
1717

1818
from parameterized import parameterized
@@ -25,7 +25,7 @@
2525
from synapse.rest import admin
2626
from synapse.rest.client import login, register, room
2727
from synapse.server import HomeServer
28-
from synapse.types import create_requester
28+
from synapse.types import JsonDict, create_requester
2929
from synapse.util import Clock
3030

3131
from tests.test_utils import simple_async_mock
@@ -196,77 +196,144 @@ def test_action_for_event_by_user_disabled_by_config(self) -> None:
196196
self.get_success(bulk_evaluator.action_for_events_by_user([(event, context)]))
197197
bulk_evaluator._action_for_event_by_user.assert_not_called()
198198

199-
@override_config({"experimental_features": {"msc3952_intentional_mentions": True}})
200-
def test_mentions(self) -> None:
201-
"""Test the behavior of an event which includes invalid mentions."""
202-
bulk_evaluator = BulkPushRuleEvaluator(self.hs)
203-
204-
sentinel = object()
205-
206-
def create_and_process(mentions: Any = sentinel) -> bool:
207-
"""Returns true iff the `mentions` trigger an event push action."""
208-
content = {}
209-
if mentions is not sentinel:
210-
content[EventContentFields.MSC3952_MENTIONS] = mentions
211-
212-
# Create a new message event which should cause a notification.
213-
event, context = self.get_success(
214-
self.event_creation_handler.create_event(
215-
self.requester,
216-
{
217-
"type": "test",
218-
"room_id": self.room_id,
219-
"content": content,
220-
"sender": f"@bob:{self.hs.hostname}",
221-
},
222-
)
199+
def _create_and_process(
200+
self, bulk_evaluator: BulkPushRuleEvaluator, content: Optional[JsonDict] = None
201+
) -> bool:
202+
"""Returns true iff the `mentions` trigger an event push action."""
203+
# Create a new message event which should cause a notification.
204+
event, context = self.get_success(
205+
self.event_creation_handler.create_event(
206+
self.requester,
207+
{
208+
"type": "test",
209+
"room_id": self.room_id,
210+
"content": content or {},
211+
"sender": f"@bob:{self.hs.hostname}",
212+
},
223213
)
214+
)
224215

225-
# Ensure no actions are generated!
226-
self.get_success(
227-
bulk_evaluator.action_for_events_by_user([(event, context)])
228-
)
216+
# Execute the push rule machinery.
217+
self.get_success(bulk_evaluator.action_for_events_by_user([(event, context)]))
229218

230-
# If any actions are generated for this event, return true.
231-
result = self.get_success(
232-
self.hs.get_datastores().main.db_pool.simple_select_list(
233-
table="event_push_actions_staging",
234-
keyvalues={"event_id": event.event_id},
235-
retcols=("*",),
236-
desc="get_event_push_actions_staging",
237-
)
219+
# If any actions are generated for this event, return true.
220+
result = self.get_success(
221+
self.hs.get_datastores().main.db_pool.simple_select_list(
222+
table="event_push_actions_staging",
223+
keyvalues={"event_id": event.event_id},
224+
retcols=("*",),
225+
desc="get_event_push_actions_staging",
238226
)
239-
return len(result) > 0
227+
)
228+
return len(result) > 0
229+
230+
@override_config({"experimental_features": {"msc3952_intentional_mentions": True}})
231+
def test_user_mentions(self) -> None:
232+
"""Test the behavior of an event which includes invalid user mentions."""
233+
bulk_evaluator = BulkPushRuleEvaluator(self.hs)
240234

241235
# Not including the mentions field should not notify.
242-
self.assertFalse(create_and_process())
236+
self.assertFalse(self._create_and_process(bulk_evaluator))
243237
# An empty mentions field should not notify.
244-
self.assertFalse(create_and_process({}))
238+
self.assertFalse(
239+
self._create_and_process(
240+
bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: {}}
241+
)
242+
)
245243

246244
# Non-dict mentions should be ignored.
247245
mentions: Any
248246
for mentions in (None, True, False, 1, "foo", []):
249-
self.assertFalse(create_and_process(mentions))
247+
self.assertFalse(
248+
self._create_and_process(
249+
bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: mentions}
250+
)
251+
)
250252

251253
# A non-list should be ignored.
252254
for mentions in (None, True, False, 1, "foo", {}):
253-
self.assertFalse(create_and_process({"user_ids": mentions}))
255+
self.assertFalse(
256+
self._create_and_process(
257+
bulk_evaluator,
258+
{EventContentFields.MSC3952_MENTIONS: {"user_ids": mentions}},
259+
)
260+
)
254261

255262
# The Matrix ID appearing anywhere in the list should notify.
256-
self.assertTrue(create_and_process({"user_ids": [self.alice]}))
257-
self.assertTrue(create_and_process({"user_ids": ["@another:test", self.alice]}))
263+
self.assertTrue(
264+
self._create_and_process(
265+
bulk_evaluator,
266+
{EventContentFields.MSC3952_MENTIONS: {"user_ids": [self.alice]}},
267+
)
268+
)
269+
self.assertTrue(
270+
self._create_and_process(
271+
bulk_evaluator,
272+
{
273+
EventContentFields.MSC3952_MENTIONS: {
274+
"user_ids": ["@another:test", self.alice]
275+
}
276+
},
277+
)
278+
)
258279

259280
# Duplicate user IDs should notify.
260-
self.assertTrue(create_and_process({"user_ids": [self.alice, self.alice]}))
281+
self.assertTrue(
282+
self._create_and_process(
283+
bulk_evaluator,
284+
{
285+
EventContentFields.MSC3952_MENTIONS: {
286+
"user_ids": [self.alice, self.alice]
287+
}
288+
},
289+
)
290+
)
261291

262292
# Invalid entries in the list are ignored.
263-
self.assertFalse(create_and_process({"user_ids": [None, True, False, {}, []]}))
293+
self.assertFalse(
294+
self._create_and_process(
295+
bulk_evaluator,
296+
{
297+
EventContentFields.MSC3952_MENTIONS: {
298+
"user_ids": [None, True, False, {}, []]
299+
}
300+
},
301+
)
302+
)
264303
self.assertTrue(
265-
create_and_process({"user_ids": [None, True, False, {}, [], self.alice]})
304+
self._create_and_process(
305+
bulk_evaluator,
306+
{
307+
EventContentFields.MSC3952_MENTIONS: {
308+
"user_ids": [None, True, False, {}, [], self.alice]
309+
}
310+
},
311+
)
266312
)
267313

314+
# The legacy push rule should not mention if the mentions field exists.
315+
self.assertFalse(
316+
self._create_and_process(
317+
bulk_evaluator,
318+
{
319+
"body": self.alice,
320+
"msgtype": "m.text",
321+
EventContentFields.MSC3952_MENTIONS: {},
322+
},
323+
)
324+
)
325+
326+
@override_config({"experimental_features": {"msc3952_intentional_mentions": True}})
327+
def test_room_mentions(self) -> None:
328+
"""Test the behavior of an event which includes invalid room mentions."""
329+
bulk_evaluator = BulkPushRuleEvaluator(self.hs)
330+
268331
# Room mentions from those without power should not notify.
269-
self.assertFalse(create_and_process({"room": True}))
332+
self.assertFalse(
333+
self._create_and_process(
334+
bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: {"room": True}}
335+
)
336+
)
270337

271338
# Room mentions from those with power should notify.
272339
self.helper.send_state(
@@ -276,8 +343,30 @@ def create_and_process(mentions: Any = sentinel) -> bool:
276343
self.token,
277344
state_key="",
278345
)
279-
self.assertTrue(create_and_process({"room": True}))
346+
self.assertTrue(
347+
self._create_and_process(
348+
bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: {"room": True}}
349+
)
350+
)
280351

281352
# Invalid data should not notify.
353+
mentions: Any
282354
for mentions in (None, False, 1, "foo", [], {}):
283-
self.assertFalse(create_and_process({"room": mentions}))
355+
self.assertFalse(
356+
self._create_and_process(
357+
bulk_evaluator,
358+
{EventContentFields.MSC3952_MENTIONS: {"room": mentions}},
359+
)
360+
)
361+
362+
# The legacy push rule should not mention if the mentions field exists.
363+
self.assertFalse(
364+
self._create_and_process(
365+
bulk_evaluator,
366+
{
367+
"body": "@room",
368+
"msgtype": "m.text",
369+
EventContentFields.MSC3952_MENTIONS: {},
370+
},
371+
)
372+
)

0 commit comments

Comments
 (0)