@@ -0,0 +1 @@ | |||||
group_imports = "StdExternalCrate" |
@@ -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(()) | ||||
} | } |
@@ -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(); | |||||
} |
@@ -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); | |||||
} |
@@ -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"]), | |||||
actions=db_to_json(rawrule["actions"]), | |||||
conditions=rawrule["conditions"], | |||||
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( | |||||
push_rules, | |||||
[ | |||||
PushRule( | |||||
rule_id="personal.override.rule1", | |||||
priority_class=5, | |||||
conditions=[], | |||||
actions=[], | |||||
) | |||||
], | |||||
push_rules, | |||||
) | |||||
self.assertEqual(len(push_rules), 1) | |||||
self.assertEqual(push_rules[0].rule_id, "personal.override.rule1") | |||||
self.assertEqual(push_rules[0].priority_class, 5) | |||||
self.assertEqual(push_rules[0].conditions, []) | |||||
self.assertEqual(push_rules[0].actions, []) | |||||
# 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) | ||||