Add push rules for MSC4075: MatrixRTC invites and notifications (#20227)

Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
This commit is contained in:
Johannes Marbach
2026-09-25 10:40:24 +00:00
committed by GitHub
co-authored by Andrew Morgan
parent 1c122cd825
commit b8bd6f93a1
9 changed files with 219 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
Add push rules for MSC4075: MatrixRTC invites and notifications.
+1
View File
@@ -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));
+171
View File
@@ -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![]);
}
}
+1
View File
@@ -637,6 +637,7 @@ fn test_requires_room_version_supports_condition() {
false,
false,
false,
false,
),
None,
None,
+12
View File
@@ -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"
+3
View File
@@ -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)
@@ -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,
)
+1
View File
@@ -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,
): ...
@@ -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
)