diff options
author | Erik Johnston <erik@matrix.org> | 2022-09-04 18:40:32 +0100 |
---|---|---|
committer | Erik Johnston <erik@matrix.org> | 2022-09-09 15:10:51 +0100 |
commit | 18b6ccadd2f9290573d64ccf54f61e3a5a547814 (patch) | |
tree | b04038ce9161bea10dd083dabea5183b113f541c | |
parent | Use an upsert for `receipts_graph`. (#13752) (diff) | |
download | synapse-18b6ccadd2f9290573d64ccf54f61e3a5a547814.tar.xz |
Implement the push evaluator in Rust
-rw-r--r-- | rust/Cargo.toml | 10 | ||||
-rw-r--r-- | rust/src/lib.rs | 8 | ||||
-rw-r--r-- | rust/src/push.rs | 768 | ||||
-rw-r--r-- | synapse/push/bulk_push_rule_evaluator.py | 44 | ||||
-rw-r--r-- | synapse/push/clientformat.py | 2 | ||||
-rw-r--r-- | synapse/storage/databases/main/push_rule.py | 13 |
6 files changed, 810 insertions, 35 deletions
diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 0a9760cafc..fd4bdfe8e7 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -18,4 +18,12 @@ 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" diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 142fc2ed93..2bd76c1aa8 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,5 +1,7 @@ use pyo3::prelude::*; +pub mod push; + /// Formats the sum of two numbers as string. #[pyfunction] #[pyo3(text_signature = "(a, b, /)")] @@ -9,8 +11,12 @@ 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)?)?; + push::register_module(py, m)?; + Ok(()) } diff --git a/rust/src/push.rs b/rust/src/push.rs new file mode 100644 index 0000000000..11df9c2657 --- /dev/null +++ b/rust/src/push.rs @@ -0,0 +1,768 @@ +use std::borrow::Cow; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; + +use anyhow::{Context, Error}; +use lazy_static::lazy_static; +use log::{debug, info}; +use pyo3::prelude::*; +use pythonize::pythonize; +use regex::Regex; +use serde::de::Error as _; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +lazy_static! { + static ref INEQUALITY_EXPR: Regex = Regex::new(r"^([=<>]*)([0-9]*)$").expect("valid regex"); +} + +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::<PushRuleEvaluator>()?; + child_module.add_class::<FilteredPushRules>()?; + m.add_submodule(child_module)?; + py.import("sys")? + .getattr("modules")? + .set_item("synapse.synapse_rust.push", child_module)?; + + Ok(()) +} + +#[derive(Debug, Clone)] +#[pyclass(frozen)] +pub struct PushRule { + pub rule_id: Cow<'static, str>, + #[pyo3(get)] + pub priority_class: i32, + pub conditions: Cow<'static, [Condition]>, + pub actions: Cow<'static, [Action]>, + #[pyo3(get)] + pub default: bool, + #[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 + ) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Action { + DontNotify, + Notify, + Coalesce, + SetTweak(SetTweak), +} + +impl IntoPy<PyObject> for Action { + fn into_py(self, py: Python<'_>) -> PyObject { + pythonize(py, &self).expect("valid action") + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SetTweak { + set_tweak: Cow<'static, str>, + value: Option<TweakValue>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[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), + } + } +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum ActionDeserializeHelper { + Str(String), + SetTweak(SetTweak), +} + +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)), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "kind")] +pub enum Condition { + 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, +} + +impl IntoPy<PyObject> for Condition { + fn into_py(self, py: Python<'_>) -> PyObject { + pythonize(py, &self).expect("valid condition") + } +} + +#[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>>, +} + +#[derive(Debug, Clone, Default)] +#[pyclass(frozen)] +struct PushRules { + overridden_base_rules: HashMap<Cow<'static, str>, PushRule>, + + override_rules: Vec<PushRule>, + content: Vec<PushRule>, + room: Vec<PushRule>, + sender: Vec<PushRule>, + 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_BY_ID.get(&*rule.rule_id) { + push_rules.overridden_base_rules.insert( + rule.rule_id.clone(), + PushRule { + actions: o.actions.clone(), + ..rule + }, + ); + + 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), + _ => { + todo!() + } // TODO: log + } + } + + push_rules + } + + fn rules(&self) -> Vec<PushRule> { + self.iter().cloned().collect() + } +} + +impl PushRules { + pub fn iter(&self) -> impl Iterator<Item = &PushRule> { + BASE_PREPEND_OVERRIDE_RULES + .iter() + .chain(self.override_rules.iter()) + .chain(BASE_APPEND_OVERRIDE_RULES.iter()) + .chain(self.content.iter()) + .chain(BASE_APPEND_CONTENT_RULES.iter()) + .chain(self.room.iter()) + .chain(self.sender.iter()) + .chain(self.underride.iter()) + .chain(BASE_APPEND_UNDERRIDE_RULES.iter()) + .map(|rule| { + self.overridden_base_rules + .get(&*rule.rule_id) + .unwrap_or(rule) + }) + } +} + +#[derive(Debug, Clone, Default)] +#[pyclass(frozen)] +pub struct FilteredPushRules { + push_rules: PushRules, + enabled_map: BTreeMap<String, bool>, +} + +#[pymethods] +impl FilteredPushRules { + #[new] + fn py_new(push_rules: PushRules, enabled_map: BTreeMap<String, bool>) -> Self { + Self { + push_rules, + enabled_map, + } + } + + fn rules(&self) -> Vec<(PushRule, bool)> { + self.iter().map(|(r, e)| (r.clone(), e)).collect() + } +} + +impl FilteredPushRules { + fn iter(&self) -> impl Iterator<Item = (&PushRule, bool)> { + self.push_rules.iter().map(|r| { + let enabled = *self + .enabled_map + .get(&*r.rule_id) + .unwrap_or(&r.default_enabled); + (r, enabled) + }) + } +} + +#[pyclass] +pub struct PushRuleEvaluator { + flattened_keys: BTreeMap<String, String>, + split_body: HashSet<String>, + room_member_count: u64, + power_levels: BTreeMap<String, BTreeMap<String, u64>>, + relations: BTreeMap<String, BTreeSet<(String, String)>>, + relation_match_enabled: bool, + sender_power_level: u64, +} + +#[pymethods] +impl PushRuleEvaluator { + #[new] + fn py_new( + flattened_keys: BTreeMap<String, String>, + room_member_count: u64, + sender_power_level: u64, + power_levels: BTreeMap<String, BTreeMap<String, u64>>, + ) -> Result<Self, Error> { + let split_body = flattened_keys + .get("content.body") + .map(|s| &**s) + .unwrap_or_default() + .split_whitespace() + .map(|s| s.to_owned()) + .collect(); + + // TODO + let relations = BTreeMap::new(); + let relation_match_enabled = false; + + Ok(PushRuleEvaluator { + flattened_keys, + split_body, + room_member_count, + power_levels, + relations, + relation_match_enabled, + sender_power_level, + }) + } + + fn run( + &self, + push_rules: &FilteredPushRules, + user_id: Option<&str>, + display_name: Option<&str>, + ) -> Vec<Action> { + let mut actions = Vec::new(); + 'outer: for (push_rule, enabled) in push_rules.iter() { + if !enabled { + continue; + } + + for condition in push_rule.conditions.iter() { + if !self.match_condition(condition, user_id, display_name) { + continue 'outer; + } + } + + actions.extend( + push_rule + .actions + .iter() + // .filter(|a| **a != Action::DontNotify) + .cloned(), + ); + + return actions; + } + + actions + } +} + +impl PushRuleEvaluator { + pub fn match_condition( + &self, + condition: &Condition, + user_id: Option<&str>, + display_name: Option<&str>, + ) -> bool { + let result = match condition { + Condition::EventMatch(event_match) => self + .match_event_match(event_match, user_id) + .unwrap_or(false), + Condition::ContainsDisplayName => { + if let Some(dn) = display_name { + self.split_body.contains(dn) + } else { + false + } + } + Condition::RoomMemberCount { is } => { + if let Some(is) = is { + self.match_member_count(is).unwrap_or(false) + } else { + false + } + } + Condition::SenderNotificationPermission { key } => { + let required_level = self + .power_levels + .get("notifications") + .and_then(|m| m.get(key.as_ref())) + .copied() + .unwrap_or(50); + + self.sender_power_level >= required_level + } + Condition::RelationMatch => { + // TODO + false + } + }; + + result + } + + fn match_event_match( + &self, + event_match: &EventMatchCondition, + user_id: Option<&str>, + ) -> Result<bool, Error> { + let pattern = if let Some(pattern) = &event_match.pattern { + pattern + } else if let Some(pattern_type) = &event_match.pattern_type { + let user_id = if let Some(user_id) = user_id { + user_id + } else { + return Ok(false); + }; + match &**pattern_type { + "user_id" => user_id, + "user_localpart" => user_id, // TODO + _ => return Ok(false), + } + } else { + return Ok(false); + }; + + let pattern = pattern.to_ascii_lowercase(); + + if event_match.key == "content.body" { + // TODO: Handle globs + Ok(self.split_body.contains(&pattern)) + } else if let Some(value) = self.flattened_keys.get(&*event_match.key) { + // TODO: Handle globs. + Ok(value.contains(&pattern)) + } else { + Ok(false) + } + } + + fn match_member_count(&self, is: &str) -> Result<bool, Error> { + let captures = INEQUALITY_EXPR.captures(is).context("bad is clause")?; + let ineq = captures.get(1).map(|m| m.as_str()).unwrap_or("=="); + let rhs: u64 = captures + .get(2) + .context("missing number")? + .as_str() + .parse()?; + + let matches = match ineq { + "" | "==" => self.room_member_count == rhs, + "<" => self.room_member_count < rhs, + ">" => self.room_member_count > rhs, + ">=" => self.room_member_count >= rhs, + "<=" => self.room_member_count <= rhs, + _ => false, + }; + + Ok(matches) + } +} + +const HIGHLIGHT_ACTION: Action = Action::SetTweak(SetTweak { + set_tweak: Cow::Borrowed("highlight"), + value: None, +}); + +const HIGHLIGHT_FALSE_ACTION: Action = Action::SetTweak(SetTweak { + set_tweak: Cow::Borrowed("highlight"), + value: Some(TweakValue::Other(Value::Bool(false))), +}); + +const SOUND_ACTION: Action = Action::SetTweak(SetTweak { + set_tweak: Cow::Borrowed("sound"), + value: Some(TweakValue::String(Cow::Borrowed("default"))), +}); + +const RING_ACTION: Action = Action::SetTweak(SetTweak { + set_tweak: Cow::Borrowed("sound"), + value: Some(TweakValue::String(Cow::Borrowed("ring"))), +}); + +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::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::EventMatch(EventMatchCondition { + key: Cow::Borrowed("type"), + pattern: Some(Cow::Borrowed("m.room.member")), + pattern_type: None, + }), + Condition::EventMatch(EventMatchCondition { + key: Cow::Borrowed("content.membership"), + pattern: Some(Cow::Borrowed("invite")), + pattern_type: None, + }), + Condition::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::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::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::SenderNotificationPermission { + key: Cow::Borrowed("room"), + }, + Condition::EventMatch(EventMatchCondition { + key: Cow::Borrowed("content.body"), + pattern: Some(Cow::Borrowed("@room")), + pattern_type: None, + }), + ]), + actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_ACTION, SOUND_ACTION]), + default: true, + default_enabled: true, + }, + PushRule { + rule_id: Cow::Borrowed("global/override/.m.rule.tombstone"), + priority_class: 5, + conditions: Cow::Borrowed(&[ + Condition::EventMatch(EventMatchCondition { + key: Cow::Borrowed("type"), + pattern: Some(Cow::Borrowed("m.room.tombstone")), + pattern_type: None, + }), + Condition::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::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, + }, + // TODO: org.matrix.msc3786.rule.room.server_acl +]; + +pub const BASE_APPEND_CONTENT_RULES: &[PushRule] = &[PushRule { + rule_id: Cow::Borrowed("global/override/.m.rule.contains_user_name"), + priority_class: 4, + conditions: Cow::Borrowed(&[Condition::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/override/.m.rule.call"), + priority_class: 1, + conditions: Cow::Borrowed(&[Condition::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/override/.m.rule.room_one_to_one"), + priority_class: 1, + conditions: Cow::Borrowed(&[ + Condition::EventMatch(EventMatchCondition { + key: Cow::Borrowed("type"), + pattern: Some(Cow::Borrowed("m.room.message")), + pattern_type: None, + }), + Condition::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/override/.m.rule.encrypted_room_one_to_one"), + priority_class: 1, + conditions: Cow::Borrowed(&[ + Condition::EventMatch(EventMatchCondition { + key: Cow::Borrowed("type"), + pattern: Some(Cow::Borrowed("m.room.encrypted")), + pattern_type: None, + }), + Condition::RoomMemberCount { + is: Some(Cow::Borrowed("2")), + }, + ]), + actions: Cow::Borrowed(&[Action::Notify, SOUND_ACTION, HIGHLIGHT_FALSE_ACTION]), + default: true, + default_enabled: true, + }, + // TODO: org.matrix.msc3772.thread_reply + PushRule { + rule_id: Cow::Borrowed("global/override/.m.rule.message"), + priority_class: 1, + conditions: Cow::Borrowed(&[Condition::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/override/.m.rule.encrypted"), + priority_class: 1, + conditions: Cow::Borrowed(&[Condition::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/override/.im.vector.jitsi"), + priority_class: 1, + conditions: Cow::Borrowed(&[ + Condition::EventMatch(EventMatchCondition { + key: Cow::Borrowed("type"), + pattern: Some(Cow::Borrowed("im.vector.modular.widgets")), + pattern_type: None, + }), + Condition::EventMatch(EventMatchCondition { + key: Cow::Borrowed("content.type"), + pattern: Some(Cow::Borrowed("jitsi")), + pattern_type: None, + }), + Condition::EventMatch(EventMatchCondition { + key: Cow::Borrowed("state_key"), + pattern: Some(Cow::Borrowed("*")), + pattern_type: None, + }), + ]), + actions: Cow::Borrowed(&[HIGHLIGHT_FALSE_ACTION]), + default: true, + default_enabled: true, + }, +]; + +lazy_static! { + 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(); +} + +#[test] +fn test_erialize_condition() { + let condition = Condition::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_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 push_rule_evaluator() { + let mut flattened_keys = BTreeMap::new(); + flattened_keys.insert("content.body".to_string(), "foo bar bob hello".to_string()); + let evaluator = PushRuleEvaluator::py_new(flattened_keys, 10, 0, BTreeMap::new()).unwrap(); + + let result = evaluator.run(&FilteredPushRules::default(), None, Some("bob")); + assert_eq!(result.len(), 3); +} diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index d1caf8a0f7..e150eaf111 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -34,16 +34,15 @@ from synapse.api.constants import EventTypes, Membership, RelationTypes from synapse.event_auth import auth_types_for_event, get_user_power_level from synapse.events import EventBase, relation_from_event from synapse.events.snapshot import EventContext +from synapse.push.push_rule_evaluator import _flatten_dict 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, PushRuleEvaluator 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: from synapse.server import HomeServer @@ -160,14 +159,14 @@ class BulkPushRuleEvaluator: rules_by_user = await self.store.bulk_get_push_rules(local_users) - logger.debug("Users in room: %s", local_users) + # logger.debug("Users in room: %s", local_users) - if logger.isEnabledFor(logging.DEBUG): - logger.debug( - "Returning push rules for %r %r", - event.room_id, - list(rules_by_user.keys()), - ) + # if logger.isEnabledFor(logging.DEBUG): + # logger.debug( + # "Returning push rules for %r %r", + # event.room_id, + # list(rules_by_user.keys()), + # ) return rules_by_user @@ -285,13 +284,16 @@ class BulkPushRuleEvaluator: event, itertools.chain(*rules_by_user.values()) ) - evaluator = PushRuleEvaluatorForEvent( - event, + logger.info("Flatten map: %s", _flatten_dict(event)) + logger.info("room_member_count: %s", room_member_count) + evaluator = PushRuleEvaluator( + _flatten_dict(event), room_member_count, sender_power_level, - power_levels, - relations, - self._relations_match_enabled, + # power_levels, + {}, # TODO + # relations, + # self._relations_match_enabled, ) users = rules_by_user.keys() @@ -333,17 +335,7 @@ class BulkPushRuleEvaluator: # current user, it'll be added to the dict later. actions_by_user[uid] = [] - for rule, enabled in rules: - if not enabled: - continue - - matches = evaluator.check_conditions(rule.conditions, uid, display_name) - if matches: - actions = [x for x in rule.actions if x != "dont_notify"] - if actions and "notify" in actions: - # Push rules say we should notify the user of this event - actions_by_user[uid] = actions - break + actions_by_user[uid] = evaluator.run(rules, uid, display_name) # Mark in the DB staging area the push actions for users who should be # notified for this event. (This will then get handled when we persist diff --git a/synapse/push/clientformat.py b/synapse/push/clientformat.py index 73618d9234..a293d4dbb6 100644 --- a/synapse/push/clientformat.py +++ b/synapse/push/clientformat.py @@ -34,7 +34,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] diff --git a/synapse/storage/databases/main/push_rule.py b/synapse/storage/databases/main/push_rule.py index 5079edd1e0..665e2ccf91 100644 --- a/synapse/storage/databases/main/push_rule.py +++ b/synapse/storage/databases/main/push_rule.py @@ -30,7 +30,6 @@ 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.database import ( @@ -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,19 @@ 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) + # TODO: Experimental config + filtered_rules = FilteredPushRules(push_rules, enabled_map) return filtered_rules |