@@ -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" | |||
[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] | |||
blake2 = "0.10.4" | |||
@@ -1,5 +1,7 @@ | |||
use pyo3::prelude::*; | |||
pub mod push; | |||
/// 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. | |||
@@ -17,8 +19,13 @@ fn sum_as_string(a: usize, b: usize) -> PyResult<String> { | |||
/// The entry point for defining the Python module. | |||
#[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!(get_rust_file_digest, m)?)?; | |||
push::register_module(py, m)?; | |||
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 | |||
from synapse.api.errors import SynapseError, UnrecognizedRequestError | |||
from synapse.push.baserules import BASE_RULE_IDS | |||
from synapse.storage.push_rule import RuleNotFoundException | |||
from synapse.synapse_rust.push import get_base_rule_ids | |||
from synapse.types import JsonDict | |||
if TYPE_CHECKING: | |||
from synapse.server import HomeServer | |||
BASE_RULE_IDS = get_base_rule_ids() | |||
@attr.s(slots=True, frozen=True, auto_attribs=True) | |||
class RuleSpec: | |||
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.storage.databases.main.roommember import EventIdMembership | |||
from synapse.storage.state import StateFilter | |||
from synapse.synapse_rust.push import FilteredPushRules, PushRule | |||
from synapse.util.caches import register_cache | |||
from synapse.util.metrics import measure_func | |||
from synapse.visibility import filter_event_for_clients_with_state | |||
from .baserules import FilteredPushRules, PushRule | |||
from .push_rule_evaluator import PushRuleEvaluatorForEvent | |||
if TYPE_CHECKING: | |||
@@ -280,7 +280,8 @@ class BulkPushRuleEvaluator: | |||
thread_id = "main" | |||
if relation: | |||
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: | |||
thread_id = relation.parent_id | |||
@@ -333,7 +334,7 @@ class BulkPushRuleEvaluator: | |||
# current user, it'll be added to the dict later. | |||
actions_by_user[uid] = [] | |||
for rule, enabled in rules: | |||
for rule, enabled in rules.rules(): | |||
if not enabled: | |||
continue | |||
@@ -16,10 +16,9 @@ import copy | |||
from typing import Any, Dict, List, Optional | |||
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 .baserules import FilteredPushRules, PushRule | |||
def format_push_rules_for_user( | |||
user: UserID, ruleslist: FilteredPushRules | |||
@@ -34,7 +33,7 @@ def format_push_rules_for_user( | |||
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) | |||
rulearray = rules["global"][template_name] | |||
@@ -30,9 +30,8 @@ from typing import ( | |||
from synapse.api.errors import StoreError | |||
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.storage._base import SQLBaseStore, db_to_json | |||
from synapse.storage._base import SQLBaseStore | |||
from synapse.storage.database import ( | |||
DatabasePool, | |||
LoggingDatabaseConnection, | |||
@@ -51,6 +50,7 @@ from synapse.storage.util.id_generators import ( | |||
IdGenerator, | |||
StreamIdGenerator, | |||
) | |||
from synapse.synapse_rust.push import FilteredPushRules, PushRule, PushRules | |||
from synapse.types import JsonDict | |||
from synapse.util import json_encoder | |||
from synapse.util.caches.descriptors import cached, cachedList | |||
@@ -72,18 +72,25 @@ def _load_rules( | |||
""" | |||
ruleslist = [ | |||
PushRule( | |||
PushRule.from_db( | |||
rule_id=rawrule["rule_id"], | |||
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 | |||
] | |||
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 | |||
@@ -845,7 +852,7 @@ class PushRuleStore(PushRulesWorkerStore): | |||
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 | |||
for rule, enabled in user_push_rules: | |||
for rule, enabled in user_push_rules.rules(): | |||
if not enabled: | |||
continue | |||
@@ -15,11 +15,11 @@ | |||
from twisted.test.proto_helpers import MemoryReactor | |||
from synapse.api.constants import AccountDataTypes | |||
from synapse.push.baserules import PushRule | |||
from synapse.push.rulekinds import PRIORITY_CLASS_MAP | |||
from synapse.rest import admin | |||
from synapse.rest.client import account, login | |||
from synapse.server import HomeServer | |||
from synapse.synapse_rust.push import PushRule | |||
from synapse.util import Clock | |||
from tests.unittest import HomeserverTestCase | |||
@@ -161,20 +161,15 @@ class DeactivateAccountTestCase(HomeserverTestCase): | |||
self._store.get_push_rules_for_user(self.user) | |||
) | |||
# 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 | |||
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 | |||
self._deactivate_my_account() | |||
@@ -183,7 +178,9 @@ class DeactivateAccountTestCase(HomeserverTestCase): | |||
self._store.get_push_rules_for_user(self.user) | |||
) | |||
# 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 | |||
self.assertEqual(push_rules, [], push_rules) | |||