mirror of
https://github.com/element-hq/synapse.git
synced 2024-12-14 11:57:44 +00:00
Port the push rule classes to Rust. (#13768)
This commit is contained in:
parent
c802ef1411
commit
42d261c32f
14 changed files with 930 additions and 615 deletions
1
.rustfmt.toml
Normal file
1
.rustfmt.toml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
group_imports = "StdExternalCrate"
|
1
changelog.d/13768.misc
Normal file
1
changelog.d/13768.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Port push rules to using Rust.
|
|
@ -18,7 +18,15 @@ crate-type = ["cdylib"]
|
||||||
name = "synapse.synapse_rust"
|
name = "synapse.synapse_rust"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pyo3 = { version = "0.16.5", features = ["extension-module", "macros", "abi3", "abi3-py37"] }
|
anyhow = "1.0.63"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
log = "0.4.17"
|
||||||
|
pyo3 = { version = "0.17.1", features = ["extension-module", "macros", "anyhow", "abi3", "abi3-py37"] }
|
||||||
|
pyo3-log = "0.7.0"
|
||||||
|
pythonize = "0.17.0"
|
||||||
|
regex = "1.6.0"
|
||||||
|
serde = { version = "1.0.144", features = ["derive"] }
|
||||||
|
serde_json = "1.0.85"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
blake2 = "0.10.4"
|
blake2 = "0.10.4"
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
pub mod push;
|
||||||
|
|
||||||
/// Returns the hash of all the rust source files at the time it was compiled.
|
/// Returns the hash of all the rust source files at the time it was compiled.
|
||||||
///
|
///
|
||||||
/// Used by python to detect if the rust library is outdated.
|
/// Used by python to detect if the rust library is outdated.
|
||||||
|
@ -17,8 +19,13 @@ fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
|
||||||
|
|
||||||
/// The entry point for defining the Python module.
|
/// The entry point for defining the Python module.
|
||||||
#[pymodule]
|
#[pymodule]
|
||||||
fn synapse_rust(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
fn synapse_rust(py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
||||||
|
pyo3_log::init();
|
||||||
|
|
||||||
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
|
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
|
||||||
m.add_function(wrap_pyfunction!(get_rust_file_digest, m)?)?;
|
m.add_function(wrap_pyfunction!(get_rust_file_digest, m)?)?;
|
||||||
|
|
||||||
|
push::register_module(py, m)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
335
rust/src/push/base_rules.rs
Normal file
335
rust/src/push/base_rules.rs
Normal file
|
@ -0,0 +1,335 @@
|
||||||
|
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Contains the definitions of the "base" push rules.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use super::KnownCondition;
|
||||||
|
use crate::push::Action;
|
||||||
|
use crate::push::Condition;
|
||||||
|
use crate::push::EventMatchCondition;
|
||||||
|
use crate::push::PushRule;
|
||||||
|
use crate::push::SetTweak;
|
||||||
|
use crate::push::TweakValue;
|
||||||
|
|
||||||
|
const HIGHLIGHT_ACTION: Action = Action::SetTweak(SetTweak {
|
||||||
|
set_tweak: Cow::Borrowed("highlight"),
|
||||||
|
value: None,
|
||||||
|
other_keys: Value::Null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const HIGHLIGHT_FALSE_ACTION: Action = Action::SetTweak(SetTweak {
|
||||||
|
set_tweak: Cow::Borrowed("highlight"),
|
||||||
|
value: Some(TweakValue::Other(Value::Bool(false))),
|
||||||
|
other_keys: Value::Null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const SOUND_ACTION: Action = Action::SetTweak(SetTweak {
|
||||||
|
set_tweak: Cow::Borrowed("sound"),
|
||||||
|
value: Some(TweakValue::String(Cow::Borrowed("default"))),
|
||||||
|
other_keys: Value::Null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const RING_ACTION: Action = Action::SetTweak(SetTweak {
|
||||||
|
set_tweak: Cow::Borrowed("sound"),
|
||||||
|
value: Some(TweakValue::String(Cow::Borrowed("ring"))),
|
||||||
|
other_keys: Value::Null,
|
||||||
|
});
|
||||||
|
|
||||||
|
pub const BASE_PREPEND_OVERRIDE_RULES: &[PushRule] = &[PushRule {
|
||||||
|
rule_id: Cow::Borrowed("global/override/.m.rule.master"),
|
||||||
|
priority_class: 5,
|
||||||
|
conditions: Cow::Borrowed(&[]),
|
||||||
|
actions: Cow::Borrowed(&[Action::DontNotify]),
|
||||||
|
default: true,
|
||||||
|
default_enabled: false,
|
||||||
|
}];
|
||||||
|
|
||||||
|
pub const BASE_APPEND_OVERRIDE_RULES: &[PushRule] = &[
|
||||||
|
PushRule {
|
||||||
|
rule_id: Cow::Borrowed("global/override/.m.rule.suppress_notices"),
|
||||||
|
priority_class: 5,
|
||||||
|
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::EventMatch(
|
||||||
|
EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("content.msgtype"),
|
||||||
|
pattern: Some(Cow::Borrowed("m.notice")),
|
||||||
|
pattern_type: None,
|
||||||
|
},
|
||||||
|
))]),
|
||||||
|
actions: Cow::Borrowed(&[Action::DontNotify]),
|
||||||
|
default: true,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
|
PushRule {
|
||||||
|
rule_id: Cow::Borrowed("global/override/.m.rule.invite_for_me"),
|
||||||
|
priority_class: 5,
|
||||||
|
conditions: Cow::Borrowed(&[
|
||||||
|
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("type"),
|
||||||
|
pattern: Some(Cow::Borrowed("m.room.member")),
|
||||||
|
pattern_type: None,
|
||||||
|
})),
|
||||||
|
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("content.membership"),
|
||||||
|
pattern: Some(Cow::Borrowed("invite")),
|
||||||
|
pattern_type: None,
|
||||||
|
})),
|
||||||
|
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("state_key"),
|
||||||
|
pattern: None,
|
||||||
|
pattern_type: Some(Cow::Borrowed("user_id")),
|
||||||
|
})),
|
||||||
|
]),
|
||||||
|
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,
|
||||||
|
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::EventMatch(
|
||||||
|
EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("type"),
|
||||||
|
pattern: Some(Cow::Borrowed("m.room.member")),
|
||||||
|
pattern_type: None,
|
||||||
|
},
|
||||||
|
))]),
|
||||||
|
actions: Cow::Borrowed(&[Action::DontNotify]),
|
||||||
|
default: true,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
|
PushRule {
|
||||||
|
rule_id: Cow::Borrowed("global/override/.m.rule.contains_display_name"),
|
||||||
|
priority_class: 5,
|
||||||
|
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::ContainsDisplayName)]),
|
||||||
|
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_ACTION, SOUND_ACTION]),
|
||||||
|
default: true,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
|
PushRule {
|
||||||
|
rule_id: Cow::Borrowed("global/override/.m.rule.roomnotif"),
|
||||||
|
priority_class: 5,
|
||||||
|
conditions: Cow::Borrowed(&[
|
||||||
|
Condition::Known(KnownCondition::SenderNotificationPermission {
|
||||||
|
key: Cow::Borrowed("room"),
|
||||||
|
}),
|
||||||
|
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("content.body"),
|
||||||
|
pattern: Some(Cow::Borrowed("@room")),
|
||||||
|
pattern_type: None,
|
||||||
|
})),
|
||||||
|
]),
|
||||||
|
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_ACTION]),
|
||||||
|
default: true,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
|
PushRule {
|
||||||
|
rule_id: Cow::Borrowed("global/override/.m.rule.tombstone"),
|
||||||
|
priority_class: 5,
|
||||||
|
conditions: Cow::Borrowed(&[
|
||||||
|
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("type"),
|
||||||
|
pattern: Some(Cow::Borrowed("m.room.tombstone")),
|
||||||
|
pattern_type: None,
|
||||||
|
})),
|
||||||
|
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("state_key"),
|
||||||
|
pattern: Some(Cow::Borrowed("")),
|
||||||
|
pattern_type: None,
|
||||||
|
})),
|
||||||
|
]),
|
||||||
|
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_ACTION]),
|
||||||
|
default: true,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
|
PushRule {
|
||||||
|
rule_id: Cow::Borrowed("global/override/.m.rule.reaction"),
|
||||||
|
priority_class: 5,
|
||||||
|
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::EventMatch(
|
||||||
|
EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("type"),
|
||||||
|
pattern: Some(Cow::Borrowed("m.reaction")),
|
||||||
|
pattern_type: None,
|
||||||
|
},
|
||||||
|
))]),
|
||||||
|
actions: Cow::Borrowed(&[Action::DontNotify]),
|
||||||
|
default: true,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
|
PushRule {
|
||||||
|
rule_id: Cow::Borrowed("global/override/.org.matrix.msc3786.rule.room.server_acl"),
|
||||||
|
priority_class: 5,
|
||||||
|
conditions: Cow::Borrowed(&[
|
||||||
|
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("type"),
|
||||||
|
pattern: Some(Cow::Borrowed("m.room.server_acl")),
|
||||||
|
pattern_type: None,
|
||||||
|
})),
|
||||||
|
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("state_key"),
|
||||||
|
pattern: Some(Cow::Borrowed("")),
|
||||||
|
pattern_type: None,
|
||||||
|
})),
|
||||||
|
]),
|
||||||
|
actions: Cow::Borrowed(&[]),
|
||||||
|
default: true,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const BASE_APPEND_CONTENT_RULES: &[PushRule] = &[PushRule {
|
||||||
|
rule_id: Cow::Borrowed("global/content/.m.rule.contains_user_name"),
|
||||||
|
priority_class: 4,
|
||||||
|
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::EventMatch(
|
||||||
|
EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("content.body"),
|
||||||
|
pattern: None,
|
||||||
|
pattern_type: Some(Cow::Borrowed("user_localpart")),
|
||||||
|
},
|
||||||
|
))]),
|
||||||
|
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_ACTION, SOUND_ACTION]),
|
||||||
|
default: true,
|
||||||
|
default_enabled: true,
|
||||||
|
}];
|
||||||
|
|
||||||
|
pub const BASE_APPEND_UNDERRIDE_RULES: &[PushRule] = &[
|
||||||
|
PushRule {
|
||||||
|
rule_id: Cow::Borrowed("global/underride/.m.rule.call"),
|
||||||
|
priority_class: 1,
|
||||||
|
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::EventMatch(
|
||||||
|
EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("type"),
|
||||||
|
pattern: Some(Cow::Borrowed("m.call.invite")),
|
||||||
|
pattern_type: None,
|
||||||
|
},
|
||||||
|
))]),
|
||||||
|
actions: Cow::Borrowed(&[Action::Notify, RING_ACTION, HIGHLIGHT_FALSE_ACTION]),
|
||||||
|
default: true,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
|
PushRule {
|
||||||
|
rule_id: Cow::Borrowed("global/underride/.m.rule.room_one_to_one"),
|
||||||
|
priority_class: 1,
|
||||||
|
conditions: Cow::Borrowed(&[
|
||||||
|
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("type"),
|
||||||
|
pattern: Some(Cow::Borrowed("m.room.message")),
|
||||||
|
pattern_type: None,
|
||||||
|
})),
|
||||||
|
Condition::Known(KnownCondition::RoomMemberCount {
|
||||||
|
is: Some(Cow::Borrowed("2")),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
actions: Cow::Borrowed(&[Action::Notify, SOUND_ACTION, HIGHLIGHT_FALSE_ACTION]),
|
||||||
|
default: true,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
|
PushRule {
|
||||||
|
rule_id: Cow::Borrowed("global/underride/.m.rule.encrypted_room_one_to_one"),
|
||||||
|
priority_class: 1,
|
||||||
|
conditions: Cow::Borrowed(&[
|
||||||
|
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("type"),
|
||||||
|
pattern: Some(Cow::Borrowed("m.room.encrypted")),
|
||||||
|
pattern_type: None,
|
||||||
|
})),
|
||||||
|
Condition::Known(KnownCondition::RoomMemberCount {
|
||||||
|
is: Some(Cow::Borrowed("2")),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
actions: Cow::Borrowed(&[Action::Notify, SOUND_ACTION, HIGHLIGHT_FALSE_ACTION]),
|
||||||
|
default: true,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
|
PushRule {
|
||||||
|
rule_id: Cow::Borrowed("global/underride/.org.matrix.msc3772.thread_reply"),
|
||||||
|
priority_class: 1,
|
||||||
|
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::RelationMatch {
|
||||||
|
rel_type: Cow::Borrowed("m.thread"),
|
||||||
|
sender: None,
|
||||||
|
sender_type: Some(Cow::Borrowed("user_id")),
|
||||||
|
})]),
|
||||||
|
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_FALSE_ACTION]),
|
||||||
|
default: true,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
|
PushRule {
|
||||||
|
rule_id: Cow::Borrowed("global/underride/.m.rule.message"),
|
||||||
|
priority_class: 1,
|
||||||
|
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::EventMatch(
|
||||||
|
EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("type"),
|
||||||
|
pattern: Some(Cow::Borrowed("m.room.message")),
|
||||||
|
pattern_type: None,
|
||||||
|
},
|
||||||
|
))]),
|
||||||
|
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_FALSE_ACTION]),
|
||||||
|
default: true,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
|
PushRule {
|
||||||
|
rule_id: Cow::Borrowed("global/underride/.m.rule.encrypted"),
|
||||||
|
priority_class: 1,
|
||||||
|
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::EventMatch(
|
||||||
|
EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("type"),
|
||||||
|
pattern: Some(Cow::Borrowed("m.room.encrypted")),
|
||||||
|
pattern_type: None,
|
||||||
|
},
|
||||||
|
))]),
|
||||||
|
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_FALSE_ACTION]),
|
||||||
|
default: true,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
|
PushRule {
|
||||||
|
rule_id: Cow::Borrowed("global/underride/.im.vector.jitsi"),
|
||||||
|
priority_class: 1,
|
||||||
|
conditions: Cow::Borrowed(&[
|
||||||
|
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("type"),
|
||||||
|
pattern: Some(Cow::Borrowed("im.vector.modular.widgets")),
|
||||||
|
pattern_type: None,
|
||||||
|
})),
|
||||||
|
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("content.type"),
|
||||||
|
pattern: Some(Cow::Borrowed("jitsi")),
|
||||||
|
pattern_type: None,
|
||||||
|
})),
|
||||||
|
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||||
|
key: Cow::Borrowed("state_key"),
|
||||||
|
pattern: Some(Cow::Borrowed("*")),
|
||||||
|
pattern_type: None,
|
||||||
|
})),
|
||||||
|
]),
|
||||||
|
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_FALSE_ACTION]),
|
||||||
|
default: true,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref BASE_RULES_BY_ID: HashMap<&'static str, &'static PushRule> =
|
||||||
|
BASE_PREPEND_OVERRIDE_RULES
|
||||||
|
.iter()
|
||||||
|
.chain(BASE_APPEND_OVERRIDE_RULES.iter())
|
||||||
|
.chain(BASE_APPEND_CONTENT_RULES.iter())
|
||||||
|
.chain(BASE_APPEND_UNDERRIDE_RULES.iter())
|
||||||
|
.map(|rule| { (&*rule.rule_id, rule) })
|
||||||
|
.collect();
|
||||||
|
}
|
502
rust/src/push/mod.rs
Normal file
502
rust/src/push/mod.rs
Normal file
|
@ -0,0 +1,502 @@
|
||||||
|
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! An implementation of Matrix push rules.
|
||||||
|
//!
|
||||||
|
//! The `Cow<_>` type is used extensively within this module to allow creating
|
||||||
|
//! the base rules as constants (in Rust constants can't require explicit
|
||||||
|
//! allocation atm).
|
||||||
|
//!
|
||||||
|
//! ---
|
||||||
|
//!
|
||||||
|
//! Push rules is the system used to determine which events trigger a push (and a
|
||||||
|
//! bump in notification counts).
|
||||||
|
//!
|
||||||
|
//! This consists of a list of "push rules" for each user, where a push rule is a
|
||||||
|
//! pair of "conditions" and "actions". When a user receives an event Synapse
|
||||||
|
//! iterates over the list of push rules until it finds one where all the conditions
|
||||||
|
//! match the event, at which point "actions" describe the outcome (e.g. notify,
|
||||||
|
//! highlight, etc).
|
||||||
|
//!
|
||||||
|
//! Push rules are split up into 5 different "kinds" (aka "priority classes"), which
|
||||||
|
//! are run in order:
|
||||||
|
//! 1. Override — highest priority rules, e.g. always ignore notices
|
||||||
|
//! 2. Content — content specific rules, e.g. @ notifications
|
||||||
|
//! 3. Room — per room rules, e.g. enable/disable notifications for all messages
|
||||||
|
//! in a room
|
||||||
|
//! 4. Sender — per sender rules, e.g. never notify for messages from a given
|
||||||
|
//! user
|
||||||
|
//! 5. Underride — the lowest priority "default" rules, e.g. notify for every
|
||||||
|
//! message.
|
||||||
|
//!
|
||||||
|
//! The set of "base rules" are the list of rules that every user has by default. A
|
||||||
|
//! user can modify their copy of the push rules in one of three ways:
|
||||||
|
//!
|
||||||
|
//! 1. Adding a new push rule of a certain kind
|
||||||
|
//! 2. Changing the actions of a base rule
|
||||||
|
//! 3. Enabling/disabling a base rule.
|
||||||
|
//!
|
||||||
|
//! The base rules are split into whether they come before or after a particular
|
||||||
|
//! kind, so the order of push rule evaluation would be: base rules for before
|
||||||
|
//! "override" kind, user defined "override" rules, base rules after "override"
|
||||||
|
//! kind, etc, etc.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
|
|
||||||
|
use anyhow::{Context, Error};
|
||||||
|
use log::warn;
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
use pythonize::pythonize;
|
||||||
|
use serde::de::Error as _;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
mod base_rules;
|
||||||
|
|
||||||
|
/// Called when registering modules with python.
|
||||||
|
pub fn register_module(py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
||||||
|
let child_module = PyModule::new(py, "push")?;
|
||||||
|
child_module.add_class::<PushRule>()?;
|
||||||
|
child_module.add_class::<PushRules>()?;
|
||||||
|
child_module.add_class::<FilteredPushRules>()?;
|
||||||
|
child_module.add_function(wrap_pyfunction!(get_base_rule_ids, m)?)?;
|
||||||
|
|
||||||
|
m.add_submodule(child_module)?;
|
||||||
|
|
||||||
|
// We need to manually add the module to sys.modules to make `from
|
||||||
|
// synapse.synapse_rust import push` work.
|
||||||
|
py.import("sys")?
|
||||||
|
.getattr("modules")?
|
||||||
|
.set_item("synapse.synapse_rust.push", child_module)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pyfunction]
|
||||||
|
fn get_base_rule_ids() -> HashSet<&'static str> {
|
||||||
|
base_rules::BASE_RULES_BY_ID.keys().copied().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single push rule for a user.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[pyclass(frozen)]
|
||||||
|
pub struct PushRule {
|
||||||
|
/// A unique ID for this rule
|
||||||
|
pub rule_id: Cow<'static, str>,
|
||||||
|
/// The "kind" of push rule this is (see `PRIORITY_CLASS_MAP` in Python)
|
||||||
|
#[pyo3(get)]
|
||||||
|
pub priority_class: i32,
|
||||||
|
/// The conditions that must all match for actions to be applied
|
||||||
|
pub conditions: Cow<'static, [Condition]>,
|
||||||
|
/// The actions to apply if all conditions are met
|
||||||
|
pub actions: Cow<'static, [Action]>,
|
||||||
|
/// Whether this is a base rule
|
||||||
|
#[pyo3(get)]
|
||||||
|
pub default: bool,
|
||||||
|
/// Whether this is enabled by default
|
||||||
|
#[pyo3(get)]
|
||||||
|
pub default_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl PushRule {
|
||||||
|
#[staticmethod]
|
||||||
|
pub fn from_db(
|
||||||
|
rule_id: String,
|
||||||
|
priority_class: i32,
|
||||||
|
conditions: &str,
|
||||||
|
actions: &str,
|
||||||
|
) -> Result<PushRule, Error> {
|
||||||
|
let conditions = serde_json::from_str(conditions).context("parsing conditions")?;
|
||||||
|
let actions = serde_json::from_str(actions).context("parsing actions")?;
|
||||||
|
|
||||||
|
Ok(PushRule {
|
||||||
|
rule_id: Cow::Owned(rule_id),
|
||||||
|
priority_class,
|
||||||
|
conditions,
|
||||||
|
actions,
|
||||||
|
default: false,
|
||||||
|
default_enabled: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn rule_id(&self) -> &str {
|
||||||
|
&self.rule_id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn actions(&self) -> Vec<Action> {
|
||||||
|
self.actions.clone().into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn conditions(&self) -> Vec<Condition> {
|
||||||
|
self.conditions.clone().into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn __repr__(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"<PushRule rule_id={}, conditions={:?}, actions={:?}>",
|
||||||
|
self.rule_id, self.conditions, self.actions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The "action" Synapse should perform for a matching push rule.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Action {
|
||||||
|
DontNotify,
|
||||||
|
Notify,
|
||||||
|
Coalesce,
|
||||||
|
SetTweak(SetTweak),
|
||||||
|
|
||||||
|
// An unrecognized custom action.
|
||||||
|
Unknown(Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoPy<PyObject> for Action {
|
||||||
|
fn into_py(self, py: Python<'_>) -> PyObject {
|
||||||
|
// When we pass the `Action` struct to Python we want it to be converted
|
||||||
|
// to a dict. We use `pythonize`, which converts the struct using the
|
||||||
|
// `serde` serialization.
|
||||||
|
pythonize(py, &self).expect("valid action")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The body of a `SetTweak` push action.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SetTweak {
|
||||||
|
set_tweak: Cow<'static, str>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
value: Option<TweakValue>,
|
||||||
|
|
||||||
|
// This picks up any other fields that may have been added by clients.
|
||||||
|
// These get added when we convert the `Action` to a python object.
|
||||||
|
#[serde(flatten)]
|
||||||
|
other_keys: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value of a `set_tweak`.
|
||||||
|
///
|
||||||
|
/// We need this (rather than using `TweakValue` directly) so that we can use
|
||||||
|
/// `&'static str` in the value when defining the constant base rules.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum TweakValue {
|
||||||
|
String(Cow<'static, str>),
|
||||||
|
Other(Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Action {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Action::DontNotify => serializer.serialize_str("dont_notify"),
|
||||||
|
Action::Notify => serializer.serialize_str("notify"),
|
||||||
|
Action::Coalesce => serializer.serialize_str("coalesce"),
|
||||||
|
Action::SetTweak(tweak) => tweak.serialize(serializer),
|
||||||
|
Action::Unknown(value) => value.serialize(serializer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple helper class for deserializing Action from JSON.
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum ActionDeserializeHelper {
|
||||||
|
Str(String),
|
||||||
|
SetTweak(SetTweak),
|
||||||
|
Unknown(Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Action {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let helper: ActionDeserializeHelper = Deserialize::deserialize(deserializer)?;
|
||||||
|
match helper {
|
||||||
|
ActionDeserializeHelper::Str(s) => match &*s {
|
||||||
|
"dont_notify" => Ok(Action::DontNotify),
|
||||||
|
"notify" => Ok(Action::Notify),
|
||||||
|
"coalesce" => Ok(Action::Coalesce),
|
||||||
|
_ => Err(D::Error::custom("unrecognized action")),
|
||||||
|
},
|
||||||
|
ActionDeserializeHelper::SetTweak(set_tweak) => Ok(Action::SetTweak(set_tweak)),
|
||||||
|
ActionDeserializeHelper::Unknown(value) => Ok(Action::Unknown(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A condition used in push rules to match against an event.
|
||||||
|
///
|
||||||
|
/// We need this split as `serde` doesn't give us the ability to have a
|
||||||
|
/// "catchall" variant in tagged enums.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum Condition {
|
||||||
|
/// A recognized condition that we can match against
|
||||||
|
Known(KnownCondition),
|
||||||
|
/// An unrecognized condition that we ignore.
|
||||||
|
Unknown(Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The set of "known" conditions that we can handle.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[serde(tag = "kind")]
|
||||||
|
pub enum KnownCondition {
|
||||||
|
EventMatch(EventMatchCondition),
|
||||||
|
ContainsDisplayName,
|
||||||
|
RoomMemberCount {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
is: Option<Cow<'static, str>>,
|
||||||
|
},
|
||||||
|
SenderNotificationPermission {
|
||||||
|
key: Cow<'static, str>,
|
||||||
|
},
|
||||||
|
#[serde(rename = "org.matrix.msc3772.relation_match")]
|
||||||
|
RelationMatch {
|
||||||
|
rel_type: Cow<'static, str>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
sender: Option<Cow<'static, str>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
sender_type: Option<Cow<'static, str>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoPy<PyObject> for Condition {
|
||||||
|
fn into_py(self, py: Python<'_>) -> PyObject {
|
||||||
|
pythonize(py, &self).expect("valid condition")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The body of a [`Condition::EventMatch`]
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct EventMatchCondition {
|
||||||
|
key: Cow<'static, str>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pattern: Option<Cow<'static, str>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pattern_type: Option<Cow<'static, str>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The collection of push rules for a user.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
#[pyclass(frozen)]
|
||||||
|
struct PushRules {
|
||||||
|
/// Custom push rules that override a base rule.
|
||||||
|
overridden_base_rules: HashMap<Cow<'static, str>, PushRule>,
|
||||||
|
|
||||||
|
/// Custom rules that come between the prepend/append override base rules.
|
||||||
|
override_rules: Vec<PushRule>,
|
||||||
|
/// Custom rules that come before the base content rules.
|
||||||
|
content: Vec<PushRule>,
|
||||||
|
/// Custom rules that come before the base room rules.
|
||||||
|
room: Vec<PushRule>,
|
||||||
|
/// Custom rules that come before the base sender rules.
|
||||||
|
sender: Vec<PushRule>,
|
||||||
|
/// Custom rules that come before the base underride rules.
|
||||||
|
underride: Vec<PushRule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl PushRules {
|
||||||
|
#[new]
|
||||||
|
fn new(rules: Vec<PushRule>) -> PushRules {
|
||||||
|
let mut push_rules: PushRules = Default::default();
|
||||||
|
|
||||||
|
for rule in rules {
|
||||||
|
if let Some(&o) = base_rules::BASE_RULES_BY_ID.get(&*rule.rule_id) {
|
||||||
|
push_rules.overridden_base_rules.insert(
|
||||||
|
rule.rule_id.clone(),
|
||||||
|
PushRule {
|
||||||
|
actions: rule.actions.clone(),
|
||||||
|
..o.clone()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match rule.priority_class {
|
||||||
|
5 => push_rules.override_rules.push(rule),
|
||||||
|
4 => push_rules.content.push(rule),
|
||||||
|
3 => push_rules.room.push(rule),
|
||||||
|
2 => push_rules.sender.push(rule),
|
||||||
|
1 => push_rules.underride.push(rule),
|
||||||
|
_ => {
|
||||||
|
warn!(
|
||||||
|
"Unrecognized priority class for rule {}: {}",
|
||||||
|
rule.rule_id, rule.priority_class
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
push_rules
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the list of all rules, including base rules, in the order they
|
||||||
|
/// should be executed in.
|
||||||
|
fn rules(&self) -> Vec<PushRule> {
|
||||||
|
self.iter().cloned().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PushRules {
|
||||||
|
/// Iterates over all the rules, including base rules, in the order they
|
||||||
|
/// should be executed in.
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &PushRule> {
|
||||||
|
base_rules::BASE_PREPEND_OVERRIDE_RULES
|
||||||
|
.iter()
|
||||||
|
.chain(self.override_rules.iter())
|
||||||
|
.chain(base_rules::BASE_APPEND_OVERRIDE_RULES.iter())
|
||||||
|
.chain(self.content.iter())
|
||||||
|
.chain(base_rules::BASE_APPEND_CONTENT_RULES.iter())
|
||||||
|
.chain(self.room.iter())
|
||||||
|
.chain(self.sender.iter())
|
||||||
|
.chain(self.underride.iter())
|
||||||
|
.chain(base_rules::BASE_APPEND_UNDERRIDE_RULES.iter())
|
||||||
|
.map(|rule| {
|
||||||
|
self.overridden_base_rules
|
||||||
|
.get(&*rule.rule_id)
|
||||||
|
.unwrap_or(rule)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper around `PushRules` that checks the enabled state of rules and
|
||||||
|
/// filters out disabled experimental rules.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
#[pyclass(frozen)]
|
||||||
|
pub struct FilteredPushRules {
|
||||||
|
push_rules: PushRules,
|
||||||
|
enabled_map: BTreeMap<String, bool>,
|
||||||
|
msc3786_enabled: bool,
|
||||||
|
msc3772_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl FilteredPushRules {
|
||||||
|
#[new]
|
||||||
|
fn py_new(
|
||||||
|
push_rules: PushRules,
|
||||||
|
enabled_map: BTreeMap<String, bool>,
|
||||||
|
msc3786_enabled: bool,
|
||||||
|
msc3772_enabled: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
push_rules,
|
||||||
|
enabled_map,
|
||||||
|
msc3786_enabled,
|
||||||
|
msc3772_enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the list of all rules and their enabled state, including base
|
||||||
|
/// rules, in the order they should be executed in.
|
||||||
|
fn rules(&self) -> Vec<(PushRule, bool)> {
|
||||||
|
self.iter().map(|(r, e)| (r.clone(), e)).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilteredPushRules {
|
||||||
|
/// Iterates over all the rules and their enabled state, including base
|
||||||
|
/// rules, in the order they should be executed in.
|
||||||
|
fn iter(&self) -> impl Iterator<Item = (&PushRule, bool)> {
|
||||||
|
self.push_rules
|
||||||
|
.iter()
|
||||||
|
.filter(|rule| {
|
||||||
|
// Ignore disabled experimental push rules
|
||||||
|
if !self.msc3786_enabled
|
||||||
|
&& rule.rule_id == "global/override/.org.matrix.msc3786.rule.room.server_acl"
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.msc3772_enabled
|
||||||
|
&& rule.rule_id == "global/underride/.org.matrix.msc3772.thread_reply"
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
})
|
||||||
|
.map(|r| {
|
||||||
|
let enabled = *self
|
||||||
|
.enabled_map
|
||||||
|
.get(&*r.rule_id)
|
||||||
|
.unwrap_or(&r.default_enabled);
|
||||||
|
(r, enabled)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_condition() {
|
||||||
|
let condition = Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||||
|
key: "content.body".into(),
|
||||||
|
pattern: Some("coffee".into()),
|
||||||
|
pattern_type: None,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&condition).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
json,
|
||||||
|
r#"{"kind":"event_match","key":"content.body","pattern":"coffee"}"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_condition() {
|
||||||
|
let json = r#"{"kind":"event_match","key":"content.body","pattern":"coffee"}"#;
|
||||||
|
|
||||||
|
let _: Condition = serde_json::from_str(json).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_custom_condition() {
|
||||||
|
let json = r#"{"kind":"custom_tag"}"#;
|
||||||
|
|
||||||
|
let condition: Condition = serde_json::from_str(json).unwrap();
|
||||||
|
assert!(matches!(condition, Condition::Unknown(_)));
|
||||||
|
|
||||||
|
let new_json = serde_json::to_string(&condition).unwrap();
|
||||||
|
assert_eq!(json, new_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_action() {
|
||||||
|
let _: Action = serde_json::from_str(r#""notify""#).unwrap();
|
||||||
|
let _: Action = serde_json::from_str(r#""dont_notify""#).unwrap();
|
||||||
|
let _: Action = serde_json::from_str(r#""coalesce""#).unwrap();
|
||||||
|
let _: Action = serde_json::from_str(r#"{"set_tweak": "highlight"}"#).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_custom_action() {
|
||||||
|
let json = r#"{"some_custom":"action_fields"}"#;
|
||||||
|
|
||||||
|
let action: Action = serde_json::from_str(json).unwrap();
|
||||||
|
assert!(matches!(action, Action::Unknown(_)));
|
||||||
|
|
||||||
|
let new_json = serde_json::to_string(&action).unwrap();
|
||||||
|
assert_eq!(json, new_json);
|
||||||
|
}
|
37
stubs/synapse/synapse_rust/push.pyi
Normal file
37
stubs/synapse/synapse_rust/push.pyi
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
from typing import Any, Collection, Dict, Mapping, Sequence, Tuple, Union
|
||||||
|
|
||||||
|
from synapse.types import JsonDict
|
||||||
|
|
||||||
|
class PushRule:
|
||||||
|
@property
|
||||||
|
def rule_id(self) -> str: ...
|
||||||
|
@property
|
||||||
|
def priority_class(self) -> int: ...
|
||||||
|
@property
|
||||||
|
def conditions(self) -> Sequence[Mapping[str, str]]: ...
|
||||||
|
@property
|
||||||
|
def actions(self) -> Sequence[Union[Mapping[str, Any], str]]: ...
|
||||||
|
@property
|
||||||
|
def default(self) -> bool: ...
|
||||||
|
@property
|
||||||
|
def default_enabled(self) -> bool: ...
|
||||||
|
@staticmethod
|
||||||
|
def from_db(
|
||||||
|
rule_id: str, priority_class: int, conditions: str, actions: str
|
||||||
|
) -> "PushRule": ...
|
||||||
|
|
||||||
|
class PushRules:
|
||||||
|
def __init__(self, rules: Collection[PushRule]): ...
|
||||||
|
def rules(self) -> Collection[PushRule]: ...
|
||||||
|
|
||||||
|
class FilteredPushRules:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
push_rules: PushRules,
|
||||||
|
enabled_map: Dict[str, bool],
|
||||||
|
msc3786_enabled: bool,
|
||||||
|
msc3772_enabled: bool,
|
||||||
|
): ...
|
||||||
|
def rules(self) -> Collection[Tuple[PushRule, bool]]: ...
|
||||||
|
|
||||||
|
def get_base_rule_ids() -> Collection[str]: ...
|
|
@ -16,14 +16,17 @@ from typing import TYPE_CHECKING, List, Optional, Union
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
from synapse.api.errors import SynapseError, UnrecognizedRequestError
|
from synapse.api.errors import SynapseError, UnrecognizedRequestError
|
||||||
from synapse.push.baserules import BASE_RULE_IDS
|
|
||||||
from synapse.storage.push_rule import RuleNotFoundException
|
from synapse.storage.push_rule import RuleNotFoundException
|
||||||
|
from synapse.synapse_rust.push import get_base_rule_ids
|
||||||
from synapse.types import JsonDict
|
from synapse.types import JsonDict
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
|
||||||
|
|
||||||
|
BASE_RULE_IDS = get_base_rule_ids()
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||||
class RuleSpec:
|
class RuleSpec:
|
||||||
scope: str
|
scope: str
|
||||||
|
|
|
@ -1,583 +0,0 @@
|
||||||
# Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
# Copyright 2017 New Vector Ltd
|
|
||||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Push rules is the system used to determine which events trigger a push (and a
|
|
||||||
bump in notification counts).
|
|
||||||
|
|
||||||
This consists of a list of "push rules" for each user, where a push rule is a
|
|
||||||
pair of "conditions" and "actions". When a user receives an event Synapse
|
|
||||||
iterates over the list of push rules until it finds one where all the conditions
|
|
||||||
match the event, at which point "actions" describe the outcome (e.g. notify,
|
|
||||||
highlight, etc).
|
|
||||||
|
|
||||||
Push rules are split up into 5 different "kinds" (aka "priority classes"), which
|
|
||||||
are run in order:
|
|
||||||
1. Override — highest priority rules, e.g. always ignore notices
|
|
||||||
2. Content — content specific rules, e.g. @ notifications
|
|
||||||
3. Room — per room rules, e.g. enable/disable notifications for all messages
|
|
||||||
in a room
|
|
||||||
4. Sender — per sender rules, e.g. never notify for messages from a given
|
|
||||||
user
|
|
||||||
5. Underride — the lowest priority "default" rules, e.g. notify for every
|
|
||||||
message.
|
|
||||||
|
|
||||||
The set of "base rules" are the list of rules that every user has by default. A
|
|
||||||
user can modify their copy of the push rules in one of three ways:
|
|
||||||
|
|
||||||
1. Adding a new push rule of a certain kind
|
|
||||||
2. Changing the actions of a base rule
|
|
||||||
3. Enabling/disabling a base rule.
|
|
||||||
|
|
||||||
The base rules are split into whether they come before or after a particular
|
|
||||||
kind, so the order of push rule evaluation would be: base rules for before
|
|
||||||
"override" kind, user defined "override" rules, base rules after "override"
|
|
||||||
kind, etc, etc.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import itertools
|
|
||||||
import logging
|
|
||||||
from typing import Dict, Iterator, List, Mapping, Sequence, Tuple, Union
|
|
||||||
|
|
||||||
import attr
|
|
||||||
|
|
||||||
from synapse.config.experimental import ExperimentalConfig
|
|
||||||
from synapse.push.rulekinds import PRIORITY_CLASS_MAP
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s(auto_attribs=True, slots=True, frozen=True)
|
|
||||||
class PushRule:
|
|
||||||
"""A push rule
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
rule_id: a unique ID for this rule
|
|
||||||
priority_class: what "kind" of push rule this is (see
|
|
||||||
`PRIORITY_CLASS_MAP` for mapping between int and kind)
|
|
||||||
conditions: the sequence of conditions that all need to match
|
|
||||||
actions: the actions to apply if all conditions are met
|
|
||||||
default: is this a base rule?
|
|
||||||
default_enabled: is this enabled by default?
|
|
||||||
"""
|
|
||||||
|
|
||||||
rule_id: str
|
|
||||||
priority_class: int
|
|
||||||
conditions: Sequence[Mapping[str, str]]
|
|
||||||
actions: Sequence[Union[str, Mapping]]
|
|
||||||
default: bool = False
|
|
||||||
default_enabled: bool = True
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s(auto_attribs=True, slots=True, frozen=True, weakref_slot=False)
|
|
||||||
class PushRules:
|
|
||||||
"""A collection of push rules for an account.
|
|
||||||
|
|
||||||
Can be iterated over, producing push rules in priority order.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# A mapping from rule ID to push rule that overrides a base rule. These will
|
|
||||||
# be returned instead of the base rule.
|
|
||||||
overriden_base_rules: Dict[str, PushRule] = attr.Factory(dict)
|
|
||||||
|
|
||||||
# The following stores the custom push rules at each priority class.
|
|
||||||
#
|
|
||||||
# We keep these separate (rather than combining into one big list) to avoid
|
|
||||||
# copying the base rules around all the time.
|
|
||||||
override: List[PushRule] = attr.Factory(list)
|
|
||||||
content: List[PushRule] = attr.Factory(list)
|
|
||||||
room: List[PushRule] = attr.Factory(list)
|
|
||||||
sender: List[PushRule] = attr.Factory(list)
|
|
||||||
underride: List[PushRule] = attr.Factory(list)
|
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[PushRule]:
|
|
||||||
# When iterating over the push rules we need to return the base rules
|
|
||||||
# interspersed at the correct spots.
|
|
||||||
for rule in itertools.chain(
|
|
||||||
BASE_PREPEND_OVERRIDE_RULES,
|
|
||||||
self.override,
|
|
||||||
BASE_APPEND_OVERRIDE_RULES,
|
|
||||||
self.content,
|
|
||||||
BASE_APPEND_CONTENT_RULES,
|
|
||||||
self.room,
|
|
||||||
self.sender,
|
|
||||||
self.underride,
|
|
||||||
BASE_APPEND_UNDERRIDE_RULES,
|
|
||||||
):
|
|
||||||
# Check if a base rule has been overriden by a custom rule. If so
|
|
||||||
# return that instead.
|
|
||||||
override_rule = self.overriden_base_rules.get(rule.rule_id)
|
|
||||||
if override_rule:
|
|
||||||
yield override_rule
|
|
||||||
else:
|
|
||||||
yield rule
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
# The length is mostly used by caches to get a sense of "size" / amount
|
|
||||||
# of memory this object is using, so we only count the number of custom
|
|
||||||
# rules.
|
|
||||||
return (
|
|
||||||
len(self.overriden_base_rules)
|
|
||||||
+ len(self.override)
|
|
||||||
+ len(self.content)
|
|
||||||
+ len(self.room)
|
|
||||||
+ len(self.sender)
|
|
||||||
+ len(self.underride)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s(auto_attribs=True, slots=True, frozen=True, weakref_slot=False)
|
|
||||||
class FilteredPushRules:
|
|
||||||
"""A wrapper around `PushRules` that filters out disabled experimental push
|
|
||||||
rules, and includes the "enabled" state for each rule when iterated over.
|
|
||||||
"""
|
|
||||||
|
|
||||||
push_rules: PushRules
|
|
||||||
enabled_map: Dict[str, bool]
|
|
||||||
experimental_config: ExperimentalConfig
|
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[Tuple[PushRule, bool]]:
|
|
||||||
for rule in self.push_rules:
|
|
||||||
if not _is_experimental_rule_enabled(
|
|
||||||
rule.rule_id, self.experimental_config
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
enabled = self.enabled_map.get(rule.rule_id, rule.default_enabled)
|
|
||||||
|
|
||||||
yield rule, enabled
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self.push_rules)
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_EMPTY_PUSH_RULES = PushRules()
|
|
||||||
|
|
||||||
|
|
||||||
def compile_push_rules(rawrules: List[PushRule]) -> PushRules:
|
|
||||||
"""Given a set of custom push rules return a `PushRules` instance (which
|
|
||||||
includes the base rules).
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not rawrules:
|
|
||||||
# Fast path to avoid allocating empty lists when there are no custom
|
|
||||||
# rules for the user.
|
|
||||||
return DEFAULT_EMPTY_PUSH_RULES
|
|
||||||
|
|
||||||
rules = PushRules()
|
|
||||||
|
|
||||||
for rule in rawrules:
|
|
||||||
# We need to decide which bucket each custom push rule goes into.
|
|
||||||
|
|
||||||
# If it has the same ID as a base rule then it overrides that...
|
|
||||||
overriden_base_rule = BASE_RULES_BY_ID.get(rule.rule_id)
|
|
||||||
if overriden_base_rule:
|
|
||||||
rules.overriden_base_rules[rule.rule_id] = attr.evolve(
|
|
||||||
overriden_base_rule, actions=rule.actions
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# ... otherwise it gets added to the appropriate priority class bucket
|
|
||||||
collection: List[PushRule]
|
|
||||||
if rule.priority_class == 5:
|
|
||||||
collection = rules.override
|
|
||||||
elif rule.priority_class == 4:
|
|
||||||
collection = rules.content
|
|
||||||
elif rule.priority_class == 3:
|
|
||||||
collection = rules.room
|
|
||||||
elif rule.priority_class == 2:
|
|
||||||
collection = rules.sender
|
|
||||||
elif rule.priority_class == 1:
|
|
||||||
collection = rules.underride
|
|
||||||
elif rule.priority_class <= 0:
|
|
||||||
logger.info(
|
|
||||||
"Got rule with priority class less than zero, but doesn't override a base rule: %s",
|
|
||||||
rule,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
# We log and continue here so as not to break event sending
|
|
||||||
logger.error("Unknown priority class: %", rule.priority_class)
|
|
||||||
continue
|
|
||||||
|
|
||||||
collection.append(rule)
|
|
||||||
|
|
||||||
return rules
|
|
||||||
|
|
||||||
|
|
||||||
def _is_experimental_rule_enabled(
|
|
||||||
rule_id: str, experimental_config: ExperimentalConfig
|
|
||||||
) -> bool:
|
|
||||||
"""Used by `FilteredPushRules` to filter out experimental rules when they
|
|
||||||
have not been enabled.
|
|
||||||
"""
|
|
||||||
if (
|
|
||||||
rule_id == "global/override/.org.matrix.msc3786.rule.room.server_acl"
|
|
||||||
and not experimental_config.msc3786_enabled
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
if (
|
|
||||||
rule_id == "global/underride/.org.matrix.msc3772.thread_reply"
|
|
||||||
and not experimental_config.msc3772_enabled
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
BASE_APPEND_CONTENT_RULES = [
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["content"],
|
|
||||||
rule_id="global/content/.m.rule.contains_user_name",
|
|
||||||
conditions=[
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "content.body",
|
|
||||||
# Match the localpart of the requester's MXID.
|
|
||||||
"pattern_type": "user_localpart",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
actions=[
|
|
||||||
"notify",
|
|
||||||
{"set_tweak": "sound", "value": "default"},
|
|
||||||
{"set_tweak": "highlight"},
|
|
||||||
],
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
BASE_PREPEND_OVERRIDE_RULES = [
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["override"],
|
|
||||||
rule_id="global/override/.m.rule.master",
|
|
||||||
default_enabled=False,
|
|
||||||
conditions=[],
|
|
||||||
actions=["dont_notify"],
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
BASE_APPEND_OVERRIDE_RULES = [
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["override"],
|
|
||||||
rule_id="global/override/.m.rule.suppress_notices",
|
|
||||||
conditions=[
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "content.msgtype",
|
|
||||||
"pattern": "m.notice",
|
|
||||||
"_cache_key": "_suppress_notices",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
actions=["dont_notify"],
|
|
||||||
),
|
|
||||||
# NB. .m.rule.invite_for_me must be higher prio than .m.rule.member_event
|
|
||||||
# otherwise invites will be matched by .m.rule.member_event
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["override"],
|
|
||||||
rule_id="global/override/.m.rule.invite_for_me",
|
|
||||||
conditions=[
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "type",
|
|
||||||
"pattern": "m.room.member",
|
|
||||||
"_cache_key": "_member",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "content.membership",
|
|
||||||
"pattern": "invite",
|
|
||||||
"_cache_key": "_invite_member",
|
|
||||||
},
|
|
||||||
# Match the requester's MXID.
|
|
||||||
{"kind": "event_match", "key": "state_key", "pattern_type": "user_id"},
|
|
||||||
],
|
|
||||||
actions=[
|
|
||||||
"notify",
|
|
||||||
{"set_tweak": "sound", "value": "default"},
|
|
||||||
{"set_tweak": "highlight", "value": False},
|
|
||||||
],
|
|
||||||
),
|
|
||||||
# Will we sometimes want to know about people joining and leaving?
|
|
||||||
# Perhaps: if so, this could be expanded upon. Seems the most usual case
|
|
||||||
# is that we don't though. We add this override rule so that even if
|
|
||||||
# the room rule is set to notify, we don't get notifications about
|
|
||||||
# join/leave/avatar/displayname events.
|
|
||||||
# See also: https://matrix.org/jira/browse/SYN-607
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["override"],
|
|
||||||
rule_id="global/override/.m.rule.member_event",
|
|
||||||
conditions=[
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "type",
|
|
||||||
"pattern": "m.room.member",
|
|
||||||
"_cache_key": "_member",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
actions=["dont_notify"],
|
|
||||||
),
|
|
||||||
# This was changed from underride to override so it's closer in priority
|
|
||||||
# to the content rules where the user name highlight rule lives. This
|
|
||||||
# way a room rule is lower priority than both but a custom override rule
|
|
||||||
# is higher priority than both.
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["override"],
|
|
||||||
rule_id="global/override/.m.rule.contains_display_name",
|
|
||||||
conditions=[{"kind": "contains_display_name"}],
|
|
||||||
actions=[
|
|
||||||
"notify",
|
|
||||||
{"set_tweak": "sound", "value": "default"},
|
|
||||||
{"set_tweak": "highlight"},
|
|
||||||
],
|
|
||||||
),
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["override"],
|
|
||||||
rule_id="global/override/.m.rule.roomnotif",
|
|
||||||
conditions=[
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "content.body",
|
|
||||||
"pattern": "@room",
|
|
||||||
"_cache_key": "_roomnotif_content",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "sender_notification_permission",
|
|
||||||
"key": "room",
|
|
||||||
"_cache_key": "_roomnotif_pl",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
actions=["notify", {"set_tweak": "highlight", "value": True}],
|
|
||||||
),
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["override"],
|
|
||||||
rule_id="global/override/.m.rule.tombstone",
|
|
||||||
conditions=[
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "type",
|
|
||||||
"pattern": "m.room.tombstone",
|
|
||||||
"_cache_key": "_tombstone",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "state_key",
|
|
||||||
"pattern": "",
|
|
||||||
"_cache_key": "_tombstone_statekey",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
actions=["notify", {"set_tweak": "highlight", "value": True}],
|
|
||||||
),
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["override"],
|
|
||||||
rule_id="global/override/.m.rule.reaction",
|
|
||||||
conditions=[
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "type",
|
|
||||||
"pattern": "m.reaction",
|
|
||||||
"_cache_key": "_reaction",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
actions=["dont_notify"],
|
|
||||||
),
|
|
||||||
# XXX: This is an experimental rule that is only enabled if msc3786_enabled
|
|
||||||
# is enabled, if it is not the rule gets filtered out in _load_rules() in
|
|
||||||
# PushRulesWorkerStore
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["override"],
|
|
||||||
rule_id="global/override/.org.matrix.msc3786.rule.room.server_acl",
|
|
||||||
conditions=[
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "type",
|
|
||||||
"pattern": "m.room.server_acl",
|
|
||||||
"_cache_key": "_room_server_acl",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "state_key",
|
|
||||||
"pattern": "",
|
|
||||||
"_cache_key": "_room_server_acl_state_key",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
actions=[],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
BASE_APPEND_UNDERRIDE_RULES = [
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["underride"],
|
|
||||||
rule_id="global/underride/.m.rule.call",
|
|
||||||
conditions=[
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "type",
|
|
||||||
"pattern": "m.call.invite",
|
|
||||||
"_cache_key": "_call",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
actions=[
|
|
||||||
"notify",
|
|
||||||
{"set_tweak": "sound", "value": "ring"},
|
|
||||||
{"set_tweak": "highlight", "value": False},
|
|
||||||
],
|
|
||||||
),
|
|
||||||
# XXX: once m.direct is standardised everywhere, we should use it to detect
|
|
||||||
# a DM from the user's perspective rather than this heuristic.
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["underride"],
|
|
||||||
rule_id="global/underride/.m.rule.room_one_to_one",
|
|
||||||
conditions=[
|
|
||||||
{"kind": "room_member_count", "is": "2", "_cache_key": "member_count"},
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "type",
|
|
||||||
"pattern": "m.room.message",
|
|
||||||
"_cache_key": "_message",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
actions=[
|
|
||||||
"notify",
|
|
||||||
{"set_tweak": "sound", "value": "default"},
|
|
||||||
{"set_tweak": "highlight", "value": False},
|
|
||||||
],
|
|
||||||
),
|
|
||||||
# XXX: this is going to fire for events which aren't m.room.messages
|
|
||||||
# but are encrypted (e.g. m.call.*)...
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["underride"],
|
|
||||||
rule_id="global/underride/.m.rule.encrypted_room_one_to_one",
|
|
||||||
conditions=[
|
|
||||||
{"kind": "room_member_count", "is": "2", "_cache_key": "member_count"},
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "type",
|
|
||||||
"pattern": "m.room.encrypted",
|
|
||||||
"_cache_key": "_encrypted",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
actions=[
|
|
||||||
"notify",
|
|
||||||
{"set_tweak": "sound", "value": "default"},
|
|
||||||
{"set_tweak": "highlight", "value": False},
|
|
||||||
],
|
|
||||||
),
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["underride"],
|
|
||||||
rule_id="global/underride/.org.matrix.msc3772.thread_reply",
|
|
||||||
conditions=[
|
|
||||||
{
|
|
||||||
"kind": "org.matrix.msc3772.relation_match",
|
|
||||||
"rel_type": "m.thread",
|
|
||||||
# Match the requester's MXID.
|
|
||||||
"sender_type": "user_id",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
actions=["notify", {"set_tweak": "highlight", "value": False}],
|
|
||||||
),
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["underride"],
|
|
||||||
rule_id="global/underride/.m.rule.message",
|
|
||||||
conditions=[
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "type",
|
|
||||||
"pattern": "m.room.message",
|
|
||||||
"_cache_key": "_message",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
actions=["notify", {"set_tweak": "highlight", "value": False}],
|
|
||||||
),
|
|
||||||
# XXX: this is going to fire for events which aren't m.room.messages
|
|
||||||
# but are encrypted (e.g. m.call.*)...
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["underride"],
|
|
||||||
rule_id="global/underride/.m.rule.encrypted",
|
|
||||||
conditions=[
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "type",
|
|
||||||
"pattern": "m.room.encrypted",
|
|
||||||
"_cache_key": "_encrypted",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
actions=["notify", {"set_tweak": "highlight", "value": False}],
|
|
||||||
),
|
|
||||||
PushRule(
|
|
||||||
default=True,
|
|
||||||
priority_class=PRIORITY_CLASS_MAP["underride"],
|
|
||||||
rule_id="global/underride/.im.vector.jitsi",
|
|
||||||
conditions=[
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "type",
|
|
||||||
"pattern": "im.vector.modular.widgets",
|
|
||||||
"_cache_key": "_type_modular_widgets",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "content.type",
|
|
||||||
"pattern": "jitsi",
|
|
||||||
"_cache_key": "_content_type_jitsi",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "event_match",
|
|
||||||
"key": "state_key",
|
|
||||||
"pattern": "*",
|
|
||||||
"_cache_key": "_is_state_event",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
actions=["notify", {"set_tweak": "highlight", "value": False}],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
BASE_RULE_IDS = set()
|
|
||||||
|
|
||||||
BASE_RULES_BY_ID: Dict[str, PushRule] = {}
|
|
||||||
|
|
||||||
for r in BASE_APPEND_CONTENT_RULES:
|
|
||||||
BASE_RULE_IDS.add(r.rule_id)
|
|
||||||
BASE_RULES_BY_ID[r.rule_id] = r
|
|
||||||
|
|
||||||
for r in BASE_PREPEND_OVERRIDE_RULES:
|
|
||||||
BASE_RULE_IDS.add(r.rule_id)
|
|
||||||
BASE_RULES_BY_ID[r.rule_id] = r
|
|
||||||
|
|
||||||
for r in BASE_APPEND_OVERRIDE_RULES:
|
|
||||||
BASE_RULE_IDS.add(r.rule_id)
|
|
||||||
BASE_RULES_BY_ID[r.rule_id] = r
|
|
||||||
|
|
||||||
for r in BASE_APPEND_UNDERRIDE_RULES:
|
|
||||||
BASE_RULE_IDS.add(r.rule_id)
|
|
||||||
BASE_RULES_BY_ID[r.rule_id] = r
|
|
|
@ -37,11 +37,11 @@ from synapse.events.snapshot import EventContext
|
||||||
from synapse.state import POWER_KEY
|
from synapse.state import POWER_KEY
|
||||||
from synapse.storage.databases.main.roommember import EventIdMembership
|
from synapse.storage.databases.main.roommember import EventIdMembership
|
||||||
from synapse.storage.state import StateFilter
|
from synapse.storage.state import StateFilter
|
||||||
|
from synapse.synapse_rust.push import FilteredPushRules, PushRule
|
||||||
from synapse.util.caches import register_cache
|
from synapse.util.caches import register_cache
|
||||||
from synapse.util.metrics import measure_func
|
from synapse.util.metrics import measure_func
|
||||||
from synapse.visibility import filter_event_for_clients_with_state
|
from synapse.visibility import filter_event_for_clients_with_state
|
||||||
|
|
||||||
from .baserules import FilteredPushRules, PushRule
|
|
||||||
from .push_rule_evaluator import PushRuleEvaluatorForEvent
|
from .push_rule_evaluator import PushRuleEvaluatorForEvent
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -280,7 +280,8 @@ class BulkPushRuleEvaluator:
|
||||||
thread_id = "main"
|
thread_id = "main"
|
||||||
if relation:
|
if relation:
|
||||||
relations = await self._get_mutual_relations(
|
relations = await self._get_mutual_relations(
|
||||||
relation.parent_id, itertools.chain(*rules_by_user.values())
|
relation.parent_id,
|
||||||
|
itertools.chain(*(r.rules() for r in rules_by_user.values())),
|
||||||
)
|
)
|
||||||
if relation.rel_type == RelationTypes.THREAD:
|
if relation.rel_type == RelationTypes.THREAD:
|
||||||
thread_id = relation.parent_id
|
thread_id = relation.parent_id
|
||||||
|
@ -333,7 +334,7 @@ class BulkPushRuleEvaluator:
|
||||||
# current user, it'll be added to the dict later.
|
# current user, it'll be added to the dict later.
|
||||||
actions_by_user[uid] = []
|
actions_by_user[uid] = []
|
||||||
|
|
||||||
for rule, enabled in rules:
|
for rule, enabled in rules.rules():
|
||||||
if not enabled:
|
if not enabled:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
@ -16,10 +16,9 @@ import copy
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from synapse.push.rulekinds import PRIORITY_CLASS_INVERSE_MAP, PRIORITY_CLASS_MAP
|
from synapse.push.rulekinds import PRIORITY_CLASS_INVERSE_MAP, PRIORITY_CLASS_MAP
|
||||||
|
from synapse.synapse_rust.push import FilteredPushRules, PushRule
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID
|
||||||
|
|
||||||
from .baserules import FilteredPushRules, PushRule
|
|
||||||
|
|
||||||
|
|
||||||
def format_push_rules_for_user(
|
def format_push_rules_for_user(
|
||||||
user: UserID, ruleslist: FilteredPushRules
|
user: UserID, ruleslist: FilteredPushRules
|
||||||
|
@ -34,7 +33,7 @@ def format_push_rules_for_user(
|
||||||
|
|
||||||
rules["global"] = _add_empty_priority_class_arrays(rules["global"])
|
rules["global"] = _add_empty_priority_class_arrays(rules["global"])
|
||||||
|
|
||||||
for r, enabled in ruleslist:
|
for r, enabled in ruleslist.rules():
|
||||||
template_name = _priority_class_to_template_name(r.priority_class)
|
template_name = _priority_class_to_template_name(r.priority_class)
|
||||||
|
|
||||||
rulearray = rules["global"][template_name]
|
rulearray = rules["global"][template_name]
|
||||||
|
|
|
@ -30,9 +30,8 @@ from typing import (
|
||||||
|
|
||||||
from synapse.api.errors import StoreError
|
from synapse.api.errors import StoreError
|
||||||
from synapse.config.homeserver import ExperimentalConfig
|
from synapse.config.homeserver import ExperimentalConfig
|
||||||
from synapse.push.baserules import FilteredPushRules, PushRule, compile_push_rules
|
|
||||||
from synapse.replication.slave.storage._slaved_id_tracker import SlavedIdTracker
|
from synapse.replication.slave.storage._slaved_id_tracker import SlavedIdTracker
|
||||||
from synapse.storage._base import SQLBaseStore, db_to_json
|
from synapse.storage._base import SQLBaseStore
|
||||||
from synapse.storage.database import (
|
from synapse.storage.database import (
|
||||||
DatabasePool,
|
DatabasePool,
|
||||||
LoggingDatabaseConnection,
|
LoggingDatabaseConnection,
|
||||||
|
@ -51,6 +50,7 @@ from synapse.storage.util.id_generators import (
|
||||||
IdGenerator,
|
IdGenerator,
|
||||||
StreamIdGenerator,
|
StreamIdGenerator,
|
||||||
)
|
)
|
||||||
|
from synapse.synapse_rust.push import FilteredPushRules, PushRule, PushRules
|
||||||
from synapse.types import JsonDict
|
from synapse.types import JsonDict
|
||||||
from synapse.util import json_encoder
|
from synapse.util import json_encoder
|
||||||
from synapse.util.caches.descriptors import cached, cachedList
|
from synapse.util.caches.descriptors import cached, cachedList
|
||||||
|
@ -72,18 +72,25 @@ def _load_rules(
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ruleslist = [
|
ruleslist = [
|
||||||
PushRule(
|
PushRule.from_db(
|
||||||
rule_id=rawrule["rule_id"],
|
rule_id=rawrule["rule_id"],
|
||||||
priority_class=rawrule["priority_class"],
|
priority_class=rawrule["priority_class"],
|
||||||
conditions=db_to_json(rawrule["conditions"]),
|
conditions=rawrule["conditions"],
|
||||||
actions=db_to_json(rawrule["actions"]),
|
actions=rawrule["actions"],
|
||||||
)
|
)
|
||||||
for rawrule in rawrules
|
for rawrule in rawrules
|
||||||
]
|
]
|
||||||
|
|
||||||
push_rules = compile_push_rules(ruleslist)
|
push_rules = PushRules(
|
||||||
|
ruleslist,
|
||||||
|
)
|
||||||
|
|
||||||
filtered_rules = FilteredPushRules(push_rules, enabled_map, experimental_config)
|
filtered_rules = FilteredPushRules(
|
||||||
|
push_rules,
|
||||||
|
enabled_map,
|
||||||
|
msc3786_enabled=experimental_config.msc3786_enabled,
|
||||||
|
msc3772_enabled=experimental_config.msc3772_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
return filtered_rules
|
return filtered_rules
|
||||||
|
|
||||||
|
@ -845,7 +852,7 @@ class PushRuleStore(PushRulesWorkerStore):
|
||||||
user_push_rules = await self.get_push_rules_for_user(user_id)
|
user_push_rules = await self.get_push_rules_for_user(user_id)
|
||||||
|
|
||||||
# Get rules relating to the old room and copy them to the new room
|
# Get rules relating to the old room and copy them to the new room
|
||||||
for rule, enabled in user_push_rules:
|
for rule, enabled in user_push_rules.rules():
|
||||||
if not enabled:
|
if not enabled:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,11 @@
|
||||||
from twisted.test.proto_helpers import MemoryReactor
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
from synapse.api.constants import AccountDataTypes
|
from synapse.api.constants import AccountDataTypes
|
||||||
from synapse.push.baserules import PushRule
|
|
||||||
from synapse.push.rulekinds import PRIORITY_CLASS_MAP
|
from synapse.push.rulekinds import PRIORITY_CLASS_MAP
|
||||||
from synapse.rest import admin
|
from synapse.rest import admin
|
||||||
from synapse.rest.client import account, login
|
from synapse.rest.client import account, login
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
from synapse.synapse_rust.push import PushRule
|
||||||
from synapse.util import Clock
|
from synapse.util import Clock
|
||||||
|
|
||||||
from tests.unittest import HomeserverTestCase
|
from tests.unittest import HomeserverTestCase
|
||||||
|
@ -161,20 +161,15 @@ class DeactivateAccountTestCase(HomeserverTestCase):
|
||||||
self._store.get_push_rules_for_user(self.user)
|
self._store.get_push_rules_for_user(self.user)
|
||||||
)
|
)
|
||||||
# Filter out default rules; we don't care
|
# Filter out default rules; we don't care
|
||||||
push_rules = [r for r, _ in filtered_push_rules if self._is_custom_rule(r)]
|
push_rules = [
|
||||||
|
r for r, _ in filtered_push_rules.rules() if self._is_custom_rule(r)
|
||||||
|
]
|
||||||
# Check our rule made it
|
# Check our rule made it
|
||||||
self.assertEqual(
|
self.assertEqual(len(push_rules), 1)
|
||||||
push_rules,
|
self.assertEqual(push_rules[0].rule_id, "personal.override.rule1")
|
||||||
[
|
self.assertEqual(push_rules[0].priority_class, 5)
|
||||||
PushRule(
|
self.assertEqual(push_rules[0].conditions, [])
|
||||||
rule_id="personal.override.rule1",
|
self.assertEqual(push_rules[0].actions, [])
|
||||||
priority_class=5,
|
|
||||||
conditions=[],
|
|
||||||
actions=[],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
push_rules,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Request the deactivation of our account
|
# Request the deactivation of our account
|
||||||
self._deactivate_my_account()
|
self._deactivate_my_account()
|
||||||
|
@ -183,7 +178,9 @@ class DeactivateAccountTestCase(HomeserverTestCase):
|
||||||
self._store.get_push_rules_for_user(self.user)
|
self._store.get_push_rules_for_user(self.user)
|
||||||
)
|
)
|
||||||
# Filter out default rules; we don't care
|
# Filter out default rules; we don't care
|
||||||
push_rules = [r for r, _ in filtered_push_rules if self._is_custom_rule(r)]
|
push_rules = [
|
||||||
|
r for r, _ in filtered_push_rules.rules() if self._is_custom_rule(r)
|
||||||
|
]
|
||||||
# Check our rule no longer exists
|
# Check our rule no longer exists
|
||||||
self.assertEqual(push_rules, [], push_rules)
|
self.assertEqual(push_rules, [], push_rules)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue