diff --git a/changelog.d/20227.feature b/changelog.d/20227.feature new file mode 100644 index 0000000000..e50010aed0 --- /dev/null +++ b/changelog.d/20227.feature @@ -0,0 +1 @@ +Add push rules for MSC4075: MatrixRTC invites and notifications. diff --git a/rust/benches/evaluator.rs b/rust/benches/evaluator.rs index 96169fd45d..e59a6d2f6a 100644 --- a/rust/benches/evaluator.rs +++ b/rust/benches/evaluator.rs @@ -215,6 +215,7 @@ fn bench_eval_message(b: &mut Bencher) { false, false, false, + false, ); b.iter(|| eval.run(&rules, Some("bob"), Some("person"), None)); diff --git a/rust/src/push/base_rules.rs b/rust/src/push/base_rules.rs index 47d5289006..af7e6c83fb 100644 --- a/rust/src/push/base_rules.rs +++ b/rust/src/push/base_rules.rs @@ -145,6 +145,45 @@ pub const BASE_APPEND_OVERRIDE_RULES: &[PushRule] = &[ default: true, default_enabled: true, }, + PushRule { + rule_id: Cow::Borrowed("global/override/.org.matrix.msc4075.rule.rtc.invite_for_me"), + priority_class: 5, + conditions: Cow::Borrowed(&[ + Condition::Known(KnownCondition::EventMatch(EventMatchCondition { + key: Cow::Borrowed("type"), + pattern: Cow::Borrowed("org.matrix.msc4075.rtc.notification"), + })), + Condition::Known(KnownCondition::ExactEventPropertyContainsType( + EventPropertyIsTypeCondition { + key: Cow::Borrowed(r"content.m\.mentions.user_ids"), + value_type: Cow::Borrowed(&EventMatchPatternType::UserId), + }, + )), + ]), + actions: Cow::Borrowed(&[Action::Notify, RING_ACTION]), + default: true, + default_enabled: true, + }, + PushRule { + rule_id: Cow::Borrowed("global/override/.org.matrix.msc4075.rule.rtc.invite_for_room"), + priority_class: 5, + conditions: Cow::Borrowed(&[ + Condition::Known(KnownCondition::EventMatch(EventMatchCondition { + key: Cow::Borrowed("type"), + pattern: Cow::Borrowed("org.matrix.msc4075.rtc.notification"), + })), + Condition::Known(KnownCondition::EventPropertyIs(EventPropertyIsCondition { + key: Cow::Borrowed(r"content.m\.mentions.room"), + value: Cow::Owned(SimpleJsonValue::Bool(true)), + })), + Condition::Known(KnownCondition::SenderNotificationPermission { + key: Cow::Borrowed("room"), + }), + ]), + actions: Cow::Borrowed(&[Action::Notify, RING_ACTION]), + default: true, + default_enabled: true, + }, PushRule { rule_id: Cow::Borrowed("global/override/.m.rule.is_user_mention"), priority_class: 5, @@ -734,3 +773,135 @@ lazy_static! { .map(|rule| { (&*rule.rule_id, rule) }) .collect(); } + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + use std::collections::BTreeMap; + + use super::RING_ACTION; + use crate::push::evaluator::PushRuleEvaluator; + use crate::push::{Action, FilteredPushRules, JsonValue, PushRules, SimpleJsonValue}; + + const RTC_NOTIFICATION_TYPE: &str = "org.matrix.msc4075.rtc.notification"; + const ALICE: &str = "@alice:example.org"; + const BOB: &str = "@bob:example.org"; + + /// The default push rules with no user-defined rules. + /// + /// The generic mention rules are disabled so that they cannot mask the + /// MSC4075 rules, which have identical actions and come immediately + /// before them in the override list. + fn push_rules(msc4075_enabled: bool) -> FilteredPushRules { + let mut enabled_map = BTreeMap::new(); + enabled_map.insert("global/override/.m.rule.is_user_mention".to_string(), false); + enabled_map.insert("global/override/.m.rule.is_room_mention".to_string(), false); + + FilteredPushRules::py_new( + PushRules::new(vec![]), + enabled_map, + false, // msc1767_enabled + false, // msc3381_polls_enabled + false, // msc3664_enabled + false, // msc4028_push_encrypted_events + msc4075_enabled, + false, // msc4210_enabled + false, // msc4306_enabled + ) + } + + /// Builds an evaluator for an event of the given type whose `m.mentions` + /// lists the given user IDs and optionally mentions the room, sent by a + /// user with the given power level. + fn build_evaluator( + event_type: &'static str, + mentioned_user_ids: &[&'static str], + mentions_room: bool, + sender_power_level: i64, + ) -> PushRuleEvaluator { + let mut flattened_keys = BTreeMap::new(); + flattened_keys.insert( + "type".to_string(), + JsonValue::Value(SimpleJsonValue::Str(Cow::Borrowed(event_type))), + ); + flattened_keys.insert( + r"content.m\.mentions.user_ids".to_string(), + JsonValue::Array( + mentioned_user_ids + .iter() + .map(|&user_id| SimpleJsonValue::Str(Cow::Borrowed(user_id))) + .collect(), + ), + ); + if mentions_room { + flattened_keys.insert( + r"content.m\.mentions.room".to_string(), + JsonValue::Value(SimpleJsonValue::Bool(true)), + ); + } + + PushRuleEvaluator::py_new( + flattened_keys, + true, + 10, + Some(sender_power_level), + BTreeMap::new(), + BTreeMap::new(), + false, + vec![], + false, + false, + false, + ) + .unwrap() + } + + #[test] + fn test_rtc_invite_for_me_notifies_mentioned_user() { + let evaluator = build_evaluator(RTC_NOTIFICATION_TYPE, &[ALICE], false, 0); + + let actions = evaluator.run(&push_rules(true), Some(ALICE), None, None); + assert_eq!(actions, vec![Action::Notify, RING_ACTION]); + } + + #[test] + fn test_rtc_invite_for_me_ignores_other_users() { + let evaluator = build_evaluator(RTC_NOTIFICATION_TYPE, &[BOB], false, 0); + + let actions = evaluator.run(&push_rules(true), Some(ALICE), None, None); + assert_eq!(actions, vec![]); + } + + #[test] + fn test_rtc_invite_for_room_notifies_with_permission() { + // The default `notifications.room` power level is 50. + let evaluator = build_evaluator(RTC_NOTIFICATION_TYPE, &[], true, 50); + + let actions = evaluator.run(&push_rules(true), Some(ALICE), None, None); + assert_eq!(actions, vec![Action::Notify, RING_ACTION]); + } + + #[test] + fn test_rtc_invite_for_room_requires_permission() { + let evaluator = build_evaluator(RTC_NOTIFICATION_TYPE, &[], true, 0); + + let actions = evaluator.run(&push_rules(true), Some(ALICE), None, None); + assert_eq!(actions, vec![]); + } + + #[test] + fn test_rtc_rules_ignore_other_event_types() { + let evaluator = build_evaluator("org.example.other", &[ALICE], true, 50); + + let actions = evaluator.run(&push_rules(true), Some(ALICE), None, None); + assert_eq!(actions, vec![]); + } + + #[test] + fn test_rtc_rules_are_omitted_when_msc4075_is_disabled() { + let evaluator = build_evaluator(RTC_NOTIFICATION_TYPE, &[ALICE], true, 50); + + let actions = evaluator.run(&push_rules(false), Some(ALICE), None, None); + assert_eq!(actions, vec![]); + } +} diff --git a/rust/src/push/evaluator.rs b/rust/src/push/evaluator.rs index 1cbca4c635..250d2890c0 100644 --- a/rust/src/push/evaluator.rs +++ b/rust/src/push/evaluator.rs @@ -637,6 +637,7 @@ fn test_requires_room_version_supports_condition() { false, false, false, + false, ), None, None, diff --git a/rust/src/push/mod.rs b/rust/src/push/mod.rs index 780d7a8cbd..868a57d00f 100644 --- a/rust/src/push/mod.rs +++ b/rust/src/push/mod.rs @@ -557,6 +557,7 @@ pub struct FilteredPushRules { msc3381_polls_enabled: bool, msc3664_enabled: bool, msc4028_push_encrypted_events: bool, + msc4075_enabled: bool, msc4210_enabled: bool, msc4306_enabled: bool, } @@ -572,6 +573,7 @@ impl FilteredPushRules { msc3381_polls_enabled: bool, msc3664_enabled: bool, msc4028_push_encrypted_events: bool, + msc4075_enabled: bool, msc4210_enabled: bool, msc4306_enabled: bool, ) -> Self { @@ -582,6 +584,7 @@ impl FilteredPushRules { msc3381_polls_enabled, msc3664_enabled, msc4028_push_encrypted_events, + msc4075_enabled, msc4210_enabled, msc4306_enabled, } @@ -626,6 +629,15 @@ impl FilteredPushRules { return false; } + if !self.msc4075_enabled + && (rule.rule_id + == "global/override/.org.matrix.msc4075.rule.rtc.invite_for_me" + || rule.rule_id + == "global/override/.org.matrix.msc4075.rule.rtc.invite_for_room") + { + return false; + } + if self.msc4210_enabled && (rule.rule_id == "global/override/.m.rule.contains_display_name" || rule.rule_id == "global/content/.m.rule.contains_user_name" diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 1c2f021322..c80b2740c3 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -191,6 +191,9 @@ class ExperimentalConfig(Config): "msc4069_profile_inhibit_propagation", False ) + # MSC4075: MatrixRTC invites and notifications + self.msc4075_enabled: bool = experimental.get("msc4075_enabled", False) + # MSC4108: Mechanism to allow OIDC sign in and E2EE set up via QR code - 2024 version: # See: https://github.com/element-hq/synapse/issues/19434 self.msc4108_enabled = experimental.get("msc4108_enabled", False) diff --git a/synapse/storage/databases/main/push_rule.py b/synapse/storage/databases/main/push_rule.py index 5086f4639b..b06e36149e 100644 --- a/synapse/storage/databases/main/push_rule.py +++ b/synapse/storage/databases/main/push_rule.py @@ -106,6 +106,7 @@ def _load_rules( msc3664_enabled=experimental_config.msc3664_enabled, msc3381_polls_enabled=experimental_config.msc3381_polls_enabled, msc4028_push_encrypted_events=experimental_config.msc4028_push_encrypted_events, + msc4075_enabled=experimental_config.msc4075_enabled, msc4210_enabled=experimental_config.msc4210_enabled, msc4306_enabled=experimental_config.msc4306_enabled, ) diff --git a/synapse/synapse_rust/push.pyi b/synapse/synapse_rust/push.pyi index ef0d5f94f4..eede2c5c37 100644 --- a/synapse/synapse_rust/push.pyi +++ b/synapse/synapse_rust/push.pyi @@ -48,6 +48,7 @@ class FilteredPushRules: msc3381_polls_enabled: bool, msc3664_enabled: bool, msc4028_push_encrypted_events: bool, + msc4075_enabled: bool, msc4210_enabled: bool, msc4306_enabled: bool, ): ... diff --git a/tests/push/test_bulk_push_rule_evaluator.py b/tests/push/test_bulk_push_rule_evaluator.py index 137bbe24b2..55c38c010b 100644 --- a/tests/push/test_bulk_push_rule_evaluator.py +++ b/tests/push/test_bulk_push_rule_evaluator.py @@ -650,3 +650,31 @@ class TestBulkPushRuleEvaluator(HomeserverTestCase): type="m.room.message", ) ) + + def _get_rule_ids(self) -> set[str]: + """Returns the IDs of all push rules that apply to Alice.""" + filtered_push_rules = self.get_success( + self.hs.get_datastores().main.get_push_rules_for_user(self.alice) + ) + return {rule.rule_id for rule, _ in filtered_push_rules.rules()} + + def test_msc4075_rules_omitted_when_disabled(self) -> None: + """The MSC4075 base rules should be filtered out unless the feature is enabled.""" + rule_ids = self._get_rule_ids() + self.assertNotIn( + "global/override/.org.matrix.msc4075.rule.rtc.invite_for_me", rule_ids + ) + self.assertNotIn( + "global/override/.org.matrix.msc4075.rule.rtc.invite_for_room", rule_ids + ) + + @override_config({"experimental_features": {"msc4075_enabled": True}}) + def test_msc4075_rules_present_when_enabled(self) -> None: + """The MSC4075 base rules should be included when the feature is enabled.""" + rule_ids = self._get_rule_ids() + self.assertIn( + "global/override/.org.matrix.msc4075.rule.rtc.invite_for_me", rule_ids + ) + self.assertIn( + "global/override/.org.matrix.msc4075.rule.rtc.invite_for_room", rule_ids + )