diff --git a/changelog.d/14960.feature b/changelog.d/14960.feature
new file mode 100644
index 0000000000..b9bb331273
--- /dev/null
+++ b/changelog.d/14960.feature
@@ -0,0 +1 @@
+Experimental support to suppress notifications from message edits ([MSC3958](https://github.com/matrix-org/matrix-spec-proposals/pull/3958)).
diff --git a/rust/benches/evaluator.rs b/rust/benches/evaluator.rs
index 859d54961c..35f7a50bce 100644
--- a/rust/benches/evaluator.rs
+++ b/rust/benches/evaluator.rs
@@ -170,6 +170,7 @@ fn bench_eval_message(b: &mut Bencher) {
false,
false,
false,
+ false,
);
b.iter(|| eval.run(&rules, Some("bob"), Some("person")));
diff --git a/rust/src/push/base_rules.rs b/rust/src/push/base_rules.rs
index 49add4e951..e9af26dd4f 100644
--- a/rust/src/push/base_rules.rs
+++ b/rust/src/push/base_rules.rs
@@ -63,6 +63,23 @@ pub const BASE_PREPEND_OVERRIDE_RULES: &[PushRule] = &[PushRule {
}];
pub const BASE_APPEND_OVERRIDE_RULES: &[PushRule] = &[
+ // We don't want to notify on edits. Not only can this be confusing in real
+ // time (2 notifications, one message) but it's especially confusing
+ // if a bridge needs to edit a previously backfilled message.
+ PushRule {
+ rule_id: Cow::Borrowed("global/override/.com.beeper.suppress_edits"),
+ priority_class: 5,
+ conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::EventMatch(
+ EventMatchCondition {
+ key: Cow::Borrowed("content.m.relates_to.rel_type"),
+ pattern: Some(Cow::Borrowed("m.replace")),
+ pattern_type: None,
+ },
+ ))]),
+ actions: Cow::Borrowed(&[Action::DontNotify]),
+ default: true,
+ default_enabled: true,
+ },
PushRule {
rule_id: Cow::Borrowed("global/override/.m.rule.suppress_notices"),
priority_class: 5,
diff --git a/rust/src/push/evaluator.rs b/rust/src/push/evaluator.rs
index da6f704c0e..ec7a8c4453 100644
--- a/rust/src/push/evaluator.rs
+++ b/rust/src/push/evaluator.rs
@@ -523,7 +523,7 @@ fn test_requires_room_version_supports_condition() {
};
let rules = PushRules::new(vec![custom_rule]);
result = evaluator.run(
- &FilteredPushRules::py_new(rules, BTreeMap::new(), true, false, true, false),
+ &FilteredPushRules::py_new(rules, BTreeMap::new(), true, false, true, false, false),
None,
None,
);
diff --git a/rust/src/push/mod.rs b/rust/src/push/mod.rs
index 7e449f2433..3c4f876cab 100644
--- a/rust/src/push/mod.rs
+++ b/rust/src/push/mod.rs
@@ -419,6 +419,7 @@ pub struct FilteredPushRules {
msc3381_polls_enabled: bool,
msc3664_enabled: bool,
msc3952_intentional_mentions: bool,
+ msc3958_suppress_edits_enabled: bool,
}
#[pymethods]
@@ -431,6 +432,7 @@ impl FilteredPushRules {
msc3381_polls_enabled: bool,
msc3664_enabled: bool,
msc3952_intentional_mentions: bool,
+ msc3958_suppress_edits_enabled: bool,
) -> Self {
Self {
push_rules,
@@ -439,6 +441,7 @@ impl FilteredPushRules {
msc3381_polls_enabled,
msc3664_enabled,
msc3952_intentional_mentions,
+ msc3958_suppress_edits_enabled,
}
}
@@ -476,6 +479,11 @@ impl FilteredPushRules {
{
return false;
}
+ if !self.msc3958_suppress_edits_enabled
+ && rule.rule_id == "global/override/.com.beeper.suppress_edits"
+ {
+ return false;
+ }
true
})
diff --git a/stubs/synapse/synapse_rust/push.pyi b/stubs/synapse/synapse_rust/push.pyi
index c0af2af3df..754acab2f9 100644
--- a/stubs/synapse/synapse_rust/push.pyi
+++ b/stubs/synapse/synapse_rust/push.pyi
@@ -47,6 +47,7 @@ class FilteredPushRules:
msc3381_polls_enabled: bool,
msc3664_enabled: bool,
msc3952_intentional_mentions: bool,
+ msc3958_suppress_edits_enabled: bool,
): ...
def rules(self) -> Collection[Tuple[PushRule, bool]]: ...
diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py
index d2d0270ddd..53c0682dfd 100644
--- a/synapse/config/experimental.py
+++ b/synapse/config/experimental.py
@@ -173,3 +173,8 @@ class ExperimentalConfig(Config):
self.msc3952_intentional_mentions = experimental.get(
"msc3952_intentional_mentions", False
)
+
+ # MSC3959: Do not generate notifications for edits.
+ self.msc3958_supress_edit_notifs = experimental.get(
+ "msc3958_supress_edit_notifs", False
+ )
diff --git a/synapse/storage/databases/main/push_rule.py b/synapse/storage/databases/main/push_rule.py
index 466a1145b7..9b2bbe060d 100644
--- a/synapse/storage/databases/main/push_rule.py
+++ b/synapse/storage/databases/main/push_rule.py
@@ -90,6 +90,7 @@ def _load_rules(
msc3664_enabled=experimental_config.msc3664_enabled,
msc3381_polls_enabled=experimental_config.msc3381_polls_enabled,
msc3952_intentional_mentions=experimental_config.msc3952_intentional_mentions,
+ msc3958_suppress_edits_enabled=experimental_config.msc3958_supress_edit_notifs,
)
return filtered_rules
diff --git a/tests/push/test_bulk_push_rule_evaluator.py b/tests/push/test_bulk_push_rule_evaluator.py
index 3b2d082dcb..7567756135 100644
--- a/tests/push/test_bulk_push_rule_evaluator.py
+++ b/tests/push/test_bulk_push_rule_evaluator.py
@@ -19,7 +19,7 @@ from parameterized import parameterized
from twisted.test.proto_helpers import MemoryReactor
-from synapse.api.constants import EventContentFields
+from synapse.api.constants import EventContentFields, RelationTypes
from synapse.api.room_versions import RoomVersions
from synapse.push.bulk_push_rule_evaluator import BulkPushRuleEvaluator
from synapse.rest import admin
@@ -370,3 +370,43 @@ class TestBulkPushRuleEvaluator(HomeserverTestCase):
},
)
)
+
+ @override_config({"experimental_features": {"msc3958_supress_edit_notifs": True}})
+ def test_suppress_edits(self) -> None:
+ """Under the default push rules, event edits should not generate notifications."""
+ bulk_evaluator = BulkPushRuleEvaluator(self.hs)
+
+ # Create & persist an event to use as the parent of the relation.
+ event, context = self.get_success(
+ self.event_creation_handler.create_event(
+ self.requester,
+ {
+ "type": "m.room.message",
+ "room_id": self.room_id,
+ "content": {
+ "msgtype": "m.text",
+ "body": "helo",
+ },
+ "sender": self.alice,
+ },
+ )
+ )
+ self.get_success(
+ self.event_creation_handler.handle_new_client_event(
+ self.requester, events_and_context=[(event, context)]
+ )
+ )
+
+ # Room mentions from those without power should not notify.
+ self.assertFalse(
+ self._create_and_process(
+ bulk_evaluator,
+ {
+ "body": self.alice,
+ "m.relates_to": {
+ "rel_type": RelationTypes.REPLACE,
+ "event_id": event.event_id,
+ },
+ },
+ )
+ )
|