Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/19955.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Push relevant users if there's a knock on a room (MSC4506).
34 changes: 27 additions & 7 deletions rust/benches/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ fn bench_match_exact(b: &mut Bencher) {
Some(0),
Default::default(),
Default::default(),
Default::default(),
true,
vec![],
false,
Expand All @@ -72,10 +73,15 @@ fn bench_match_exact(b: &mut Bencher) {
},
));

let matched = eval.match_condition(&condition, None, None, None).unwrap();
let matched = eval
.match_condition(&condition, None, None, None, None)
.unwrap();
assert!(matched, "Didn't match");

b.iter(|| eval.match_condition(&condition, None, None, None).unwrap());
b.iter(|| {
eval.match_condition(&condition, None, None, None, None)
.unwrap()
});
}

#[bench]
Expand Down Expand Up @@ -104,6 +110,7 @@ fn bench_match_word(b: &mut Bencher) {
Some(0),
Default::default(),
Default::default(),
Default::default(),
true,
vec![],
false,
Expand All @@ -119,10 +126,15 @@ fn bench_match_word(b: &mut Bencher) {
},
));

let matched = eval.match_condition(&condition, None, None, None).unwrap();
let matched = eval
.match_condition(&condition, None, None, None, None)
.unwrap();
assert!(matched, "Didn't match");

b.iter(|| eval.match_condition(&condition, None, None, None).unwrap());
b.iter(|| {
eval.match_condition(&condition, None, None, None, None)
.unwrap()
});
}

#[bench]
Expand Down Expand Up @@ -151,6 +163,7 @@ fn bench_match_word_miss(b: &mut Bencher) {
Some(0),
Default::default(),
Default::default(),
Default::default(),
true,
vec![],
false,
Expand All @@ -166,10 +179,15 @@ fn bench_match_word_miss(b: &mut Bencher) {
},
));

let matched = eval.match_condition(&condition, None, None, None).unwrap();
let matched = eval
.match_condition(&condition, None, None, None, None)
.unwrap();
assert!(!matched, "Didn't match");

b.iter(|| eval.match_condition(&condition, None, None, None).unwrap());
b.iter(|| {
eval.match_condition(&condition, None, None, None, None)
.unwrap()
});
}

#[bench]
Expand Down Expand Up @@ -198,6 +216,7 @@ fn bench_eval_message(b: &mut Bencher) {
Some(0),
Default::default(),
Default::default(),
Default::default(),
true,
vec![],
false,
Expand All @@ -215,7 +234,8 @@ fn bench_eval_message(b: &mut Bencher) {
false,
false,
false,
false,
);

b.iter(|| eval.run(&rules, Some("bob"), Some("person"), None));
b.iter(|| eval.run(&rules, Some("bob"), Some("person"), None, None));
}
24 changes: 24 additions & 0 deletions rust/src/push/base_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,30 @@ pub const BASE_APPEND_OVERRIDE_RULES: &[PushRule] = &[
default: true,
default_enabled: true,
},
// MSC4506: notify the members of a room who are able to act on a knock
// (i.e. those with a power level sufficient to invite the knocker). Must
// come before `.m.rule.member_event`, which suppresses all other member
// events.
PushRule {
rule_id: Cow::Borrowed("global/override/.org.matrix.msc4506.rule.knock"),
priority_class: 5,
conditions: Cow::Borrowed(&[
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
key: Cow::Borrowed("type"),
pattern: Cow::Borrowed("m.room.member"),
})),
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
key: Cow::Borrowed("content.membership"),
pattern: Cow::Borrowed("knock"),
})),
Condition::Known(KnownCondition::RecipientPermission {
key: Cow::Borrowed("invite"),
}),
]),
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_FALSE_ACTION, SOUND_ACTION]),
default: true,
default_enabled: true,
},
PushRule {
rule_id: Cow::Borrowed("global/override/.m.rule.member_event"),
priority_class: 5,
Expand Down
98 changes: 95 additions & 3 deletions rust/src/push/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ pub struct PushRuleEvaluator {
/// outlier.
sender_power_level: Option<i64>,

/// MSC4506: the power level required to perform each of the room's
/// power-levels actions (e.g. "invite", "kick"), for the
/// `recipient_permission` condition. Includes the spec defaults for
/// actions absent from the `m.room.power_levels` content.
action_power_levels: BTreeMap<String, i64>,

/// The related events, indexed by relation type. Flattened in the same manner as
/// `flattened_keys`.
related_events_flattened: BTreeMap<String, BTreeMap<String, JsonValue>>,
Expand Down Expand Up @@ -124,6 +130,7 @@ impl PushRuleEvaluator {
room_member_count,
sender_power_level,
notification_power_levels,
action_power_levels,
related_events_flattened,
related_event_match_enabled,
room_version_feature_flags,
Expand All @@ -137,6 +144,7 @@ impl PushRuleEvaluator {
room_member_count: u64,
sender_power_level: Option<i64>,
notification_power_levels: BTreeMap<String, i64>,
action_power_levels: BTreeMap<String, i64>,
related_events_flattened: BTreeMap<String, BTreeMap<String, JsonValue>>,
related_event_match_enabled: bool,
room_version_feature_flags: Vec<String>,
Expand All @@ -156,6 +164,7 @@ impl PushRuleEvaluator {
room_member_count,
notification_power_levels,
sender_power_level,
action_power_levels,
related_events_flattened,
related_event_match_enabled,
room_version_feature_flags,
Expand All @@ -179,13 +188,17 @@ impl PushRuleEvaluator {
/// - `None` if the event is not in a thread, or if MSC4306 is disabled.
/// - `Some(true)` if the event is in a thread and the user has a subscription for that thread
/// - `Some(false)` if the event is in a thread and the user does NOT have a subscription for that thread
#[pyo3(signature = (push_rules, user_id=None, display_name=None, msc4306_thread_subscription_state=None))]
///
/// recipient_power_level: the power level of the user the rules are being
/// evaluated for, used by the MSC4506 `recipient_permission` condition.
#[pyo3(signature = (push_rules, user_id=None, display_name=None, msc4306_thread_subscription_state=None, recipient_power_level=None))]
pub fn run(
&self,
push_rules: &FilteredPushRules,
user_id: Option<&str>,
display_name: Option<&str>,
msc4306_thread_subscription_state: Option<bool>,
recipient_power_level: Option<i64>,
) -> Vec<Action> {
'outer: for (push_rule, enabled) in push_rules.iter() {
if !enabled {
Expand Down Expand Up @@ -222,6 +235,7 @@ impl PushRuleEvaluator {
user_id,
display_name,
msc4306_thread_subscription_state,
recipient_power_level,
) {
Ok(true) => {}
Ok(false) => continue 'outer,
Expand Down Expand Up @@ -255,19 +269,21 @@ impl PushRuleEvaluator {
}

/// Check if the given condition matches.
#[pyo3(signature = (condition, user_id=None, display_name=None, msc4306_thread_subscription_state=None))]
#[pyo3(signature = (condition, user_id=None, display_name=None, msc4306_thread_subscription_state=None, recipient_power_level=None))]
fn matches(
&self,
condition: Condition,
user_id: Option<&str>,
display_name: Option<&str>,
msc4306_thread_subscription_state: Option<bool>,
recipient_power_level: Option<i64>,
) -> bool {
match self.match_condition(
&condition,
user_id,
display_name,
msc4306_thread_subscription_state,
recipient_power_level,
) {
Ok(true) => true,
Ok(false) => false,
Expand All @@ -287,6 +303,7 @@ impl PushRuleEvaluator {
user_id: Option<&str>,
display_name: Option<&str>,
msc4306_thread_subscription_state: Option<bool>,
recipient_power_level: Option<i64>,
) -> Result<bool, Error> {
let known_condition = match condition {
Condition::Known(known) => known,
Expand Down Expand Up @@ -409,6 +426,18 @@ impl PushRuleEvaluator {
false
}
}
KnownCondition::RecipientPermission { key } => {
if let Some(recipient_power_level) = recipient_power_level {
if let Some(required_level) = self.action_power_levels.get(key.as_ref()) {
recipient_power_level >= *required_level
} else {
// Unknown action keys never match.
false
}
} else {
false
}
}
KnownCondition::RoomVersionSupports { feature } => {
if !self.msc3931_enabled {
false
Expand Down Expand Up @@ -564,6 +593,7 @@ fn push_rule_evaluator() {
Some(0),
BTreeMap::new(),
BTreeMap::new(),
BTreeMap::new(),
true,
vec![],
true,
Expand All @@ -572,10 +602,68 @@ fn push_rule_evaluator() {
)
.unwrap();

let result = evaluator.run(&FilteredPushRules::default(), None, Some("bob"), None);
let result = evaluator.run(&FilteredPushRules::default(), None, Some("bob"), None, None);
assert_eq!(result.len(), 3);
}

#[test]
fn test_recipient_permission_condition() {
let mut action_power_levels = BTreeMap::new();
action_power_levels.insert("invite".to_string(), 50);

let evaluator = PushRuleEvaluator::py_new(
BTreeMap::new(),
false,
10,
Some(0),
BTreeMap::new(),
action_power_levels,
BTreeMap::new(),
true,
vec![],
true,
false,
false,
)
.unwrap();

let condition = Condition::Known(KnownCondition::RecipientPermission {
key: Cow::Borrowed("invite"),
});

// A recipient at or above the required level matches.
assert!(evaluator
.match_condition(&condition, Some("@bob:example.org"), None, None, Some(50))
.unwrap());
assert!(evaluator
.match_condition(&condition, Some("@bob:example.org"), None, None, Some(100))
.unwrap());

// A recipient below the required level does not match.
assert!(!evaluator
.match_condition(&condition, Some("@bob:example.org"), None, None, Some(0))
.unwrap());

// No recipient power level provided: never matches.
assert!(!evaluator
.match_condition(&condition, Some("@bob:example.org"), None, None, None)
.unwrap());

// An action key we don't have a level for never matches.
let unknown_key = Condition::Known(KnownCondition::RecipientPermission {
key: Cow::Borrowed("frobnicate"),
});
assert!(!evaluator
.match_condition(
&unknown_key,
Some("@bob:example.org"),
None,
None,
Some(100)
)
.unwrap());
}

#[test]
fn test_requires_room_version_supports_condition() {
use std::borrow::Cow;
Expand All @@ -595,6 +683,7 @@ fn test_requires_room_version_supports_condition() {
Some(0),
BTreeMap::new(),
BTreeMap::new(),
BTreeMap::new(),
false,
flags,
true,
Expand All @@ -610,6 +699,7 @@ fn test_requires_room_version_supports_condition() {
Some("@bob:example.org"),
None,
None,
None,
);
assert_eq!(result.len(), 3);

Expand Down Expand Up @@ -637,10 +727,12 @@ fn test_requires_room_version_supports_condition() {
false,
false,
false,
false,
),
None,
None,
None,
None,
);
assert_eq!(result.len(), 1);
}
15 changes: 15 additions & 0 deletions rust/src/push/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,13 @@ pub enum KnownCondition {
SenderNotificationPermission {
key: Cow<'static, str>,
},
// MSC4506 (knock push rules): matches if the user the rules are being
// evaluated for has a power level at least that required to perform the
// action named by `key` (e.g. "invite").
#[serde(rename = "org.matrix.msc4506.recipient_permission")]
RecipientPermission {
key: Cow<'static, str>,
},
#[serde(rename = "org.matrix.msc3931.room_version_supports")]
RoomVersionSupports {
feature: Cow<'static, str>,
Expand Down Expand Up @@ -559,6 +566,7 @@ pub struct FilteredPushRules {
msc4028_push_encrypted_events: bool,
msc4210_enabled: bool,
msc4306_enabled: bool,
msc4506_enabled: bool,
}

#[pymethods]
Expand All @@ -574,6 +582,7 @@ impl FilteredPushRules {
msc4028_push_encrypted_events: bool,
msc4210_enabled: bool,
msc4306_enabled: bool,
msc4506_enabled: bool,
) -> Self {
Self {
push_rules,
Expand All @@ -584,6 +593,7 @@ impl FilteredPushRules {
msc4028_push_encrypted_events,
msc4210_enabled,
msc4306_enabled,
msc4506_enabled,
}
}

Expand Down Expand Up @@ -620,6 +630,11 @@ impl FilteredPushRules {
return false;
}

if !self.msc4506_enabled && rule.rule_id.contains("/.org.matrix.msc4506.rule.knock")
{
return false;
}

if !self.msc4028_push_encrypted_events
&& rule.rule_id == "global/override/.org.matrix.msc4028.encrypted_event"
{
Expand Down
Loading
Loading