mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2025-05-02 08:36:03 -04:00
Port the push rule classes to Rust. (#13768)
This commit is contained in:
parent
c802ef1411
commit
42d261c32f
14 changed files with 930 additions and 615 deletions
|
@ -1,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(())
|
||||
}
|
||||
|
|
335
rust/src/push/base_rules.rs
Normal file
335
rust/src/push/base_rules.rs
Normal file
|
@ -0,0 +1,335 @@
|
|||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Contains the definitions of the "base" push rules.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::KnownCondition;
|
||||
use crate::push::Action;
|
||||
use crate::push::Condition;
|
||||
use crate::push::EventMatchCondition;
|
||||
use crate::push::PushRule;
|
||||
use crate::push::SetTweak;
|
||||
use crate::push::TweakValue;
|
||||
|
||||
const HIGHLIGHT_ACTION: Action = Action::SetTweak(SetTweak {
|
||||
set_tweak: Cow::Borrowed("highlight"),
|
||||
value: None,
|
||||
other_keys: Value::Null,
|
||||
});
|
||||
|
||||
const HIGHLIGHT_FALSE_ACTION: Action = Action::SetTweak(SetTweak {
|
||||
set_tweak: Cow::Borrowed("highlight"),
|
||||
value: Some(TweakValue::Other(Value::Bool(false))),
|
||||
other_keys: Value::Null,
|
||||
});
|
||||
|
||||
const SOUND_ACTION: Action = Action::SetTweak(SetTweak {
|
||||
set_tweak: Cow::Borrowed("sound"),
|
||||
value: Some(TweakValue::String(Cow::Borrowed("default"))),
|
||||
other_keys: Value::Null,
|
||||
});
|
||||
|
||||
const RING_ACTION: Action = Action::SetTweak(SetTweak {
|
||||
set_tweak: Cow::Borrowed("sound"),
|
||||
value: Some(TweakValue::String(Cow::Borrowed("ring"))),
|
||||
other_keys: Value::Null,
|
||||
});
|
||||
|
||||
pub const BASE_PREPEND_OVERRIDE_RULES: &[PushRule] = &[PushRule {
|
||||
rule_id: Cow::Borrowed("global/override/.m.rule.master"),
|
||||
priority_class: 5,
|
||||
conditions: Cow::Borrowed(&[]),
|
||||
actions: Cow::Borrowed(&[Action::DontNotify]),
|
||||
default: true,
|
||||
default_enabled: false,
|
||||
}];
|
||||
|
||||
pub const BASE_APPEND_OVERRIDE_RULES: &[PushRule] = &[
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed("global/override/.m.rule.suppress_notices"),
|
||||
priority_class: 5,
|
||||
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::EventMatch(
|
||||
EventMatchCondition {
|
||||
key: Cow::Borrowed("content.msgtype"),
|
||||
pattern: Some(Cow::Borrowed("m.notice")),
|
||||
pattern_type: None,
|
||||
},
|
||||
))]),
|
||||
actions: Cow::Borrowed(&[Action::DontNotify]),
|
||||
default: true,
|
||||
default_enabled: true,
|
||||
},
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed("global/override/.m.rule.invite_for_me"),
|
||||
priority_class: 5,
|
||||
conditions: Cow::Borrowed(&[
|
||||
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||
key: Cow::Borrowed("type"),
|
||||
pattern: Some(Cow::Borrowed("m.room.member")),
|
||||
pattern_type: None,
|
||||
})),
|
||||
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||
key: Cow::Borrowed("content.membership"),
|
||||
pattern: Some(Cow::Borrowed("invite")),
|
||||
pattern_type: None,
|
||||
})),
|
||||
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||
key: Cow::Borrowed("state_key"),
|
||||
pattern: None,
|
||||
pattern_type: Some(Cow::Borrowed("user_id")),
|
||||
})),
|
||||
]),
|
||||
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_FALSE_ACTION, SOUND_ACTION]),
|
||||
default: true,
|
||||
default_enabled: true,
|
||||
},
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed("global/override/.m.rule.member_event"),
|
||||
priority_class: 5,
|
||||
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::EventMatch(
|
||||
EventMatchCondition {
|
||||
key: Cow::Borrowed("type"),
|
||||
pattern: Some(Cow::Borrowed("m.room.member")),
|
||||
pattern_type: None,
|
||||
},
|
||||
))]),
|
||||
actions: Cow::Borrowed(&[Action::DontNotify]),
|
||||
default: true,
|
||||
default_enabled: true,
|
||||
},
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed("global/override/.m.rule.contains_display_name"),
|
||||
priority_class: 5,
|
||||
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::ContainsDisplayName)]),
|
||||
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_ACTION, SOUND_ACTION]),
|
||||
default: true,
|
||||
default_enabled: true,
|
||||
},
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed("global/override/.m.rule.roomnotif"),
|
||||
priority_class: 5,
|
||||
conditions: Cow::Borrowed(&[
|
||||
Condition::Known(KnownCondition::SenderNotificationPermission {
|
||||
key: Cow::Borrowed("room"),
|
||||
}),
|
||||
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||
key: Cow::Borrowed("content.body"),
|
||||
pattern: Some(Cow::Borrowed("@room")),
|
||||
pattern_type: None,
|
||||
})),
|
||||
]),
|
||||
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_ACTION]),
|
||||
default: true,
|
||||
default_enabled: true,
|
||||
},
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed("global/override/.m.rule.tombstone"),
|
||||
priority_class: 5,
|
||||
conditions: Cow::Borrowed(&[
|
||||
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||
key: Cow::Borrowed("type"),
|
||||
pattern: Some(Cow::Borrowed("m.room.tombstone")),
|
||||
pattern_type: None,
|
||||
})),
|
||||
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||
key: Cow::Borrowed("state_key"),
|
||||
pattern: Some(Cow::Borrowed("")),
|
||||
pattern_type: None,
|
||||
})),
|
||||
]),
|
||||
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_ACTION]),
|
||||
default: true,
|
||||
default_enabled: true,
|
||||
},
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed("global/override/.m.rule.reaction"),
|
||||
priority_class: 5,
|
||||
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::EventMatch(
|
||||
EventMatchCondition {
|
||||
key: Cow::Borrowed("type"),
|
||||
pattern: Some(Cow::Borrowed("m.reaction")),
|
||||
pattern_type: None,
|
||||
},
|
||||
))]),
|
||||
actions: Cow::Borrowed(&[Action::DontNotify]),
|
||||
default: true,
|
||||
default_enabled: true,
|
||||
},
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed("global/override/.org.matrix.msc3786.rule.room.server_acl"),
|
||||
priority_class: 5,
|
||||
conditions: Cow::Borrowed(&[
|
||||
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||
key: Cow::Borrowed("type"),
|
||||
pattern: Some(Cow::Borrowed("m.room.server_acl")),
|
||||
pattern_type: None,
|
||||
})),
|
||||
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||
key: Cow::Borrowed("state_key"),
|
||||
pattern: Some(Cow::Borrowed("")),
|
||||
pattern_type: None,
|
||||
})),
|
||||
]),
|
||||
actions: Cow::Borrowed(&[]),
|
||||
default: true,
|
||||
default_enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
pub const BASE_APPEND_CONTENT_RULES: &[PushRule] = &[PushRule {
|
||||
rule_id: Cow::Borrowed("global/content/.m.rule.contains_user_name"),
|
||||
priority_class: 4,
|
||||
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::EventMatch(
|
||||
EventMatchCondition {
|
||||
key: Cow::Borrowed("content.body"),
|
||||
pattern: None,
|
||||
pattern_type: Some(Cow::Borrowed("user_localpart")),
|
||||
},
|
||||
))]),
|
||||
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_ACTION, SOUND_ACTION]),
|
||||
default: true,
|
||||
default_enabled: true,
|
||||
}];
|
||||
|
||||
pub const BASE_APPEND_UNDERRIDE_RULES: &[PushRule] = &[
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed("global/underride/.m.rule.call"),
|
||||
priority_class: 1,
|
||||
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::EventMatch(
|
||||
EventMatchCondition {
|
||||
key: Cow::Borrowed("type"),
|
||||
pattern: Some(Cow::Borrowed("m.call.invite")),
|
||||
pattern_type: None,
|
||||
},
|
||||
))]),
|
||||
actions: Cow::Borrowed(&[Action::Notify, RING_ACTION, HIGHLIGHT_FALSE_ACTION]),
|
||||
default: true,
|
||||
default_enabled: true,
|
||||
},
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed("global/underride/.m.rule.room_one_to_one"),
|
||||
priority_class: 1,
|
||||
conditions: Cow::Borrowed(&[
|
||||
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||
key: Cow::Borrowed("type"),
|
||||
pattern: Some(Cow::Borrowed("m.room.message")),
|
||||
pattern_type: None,
|
||||
})),
|
||||
Condition::Known(KnownCondition::RoomMemberCount {
|
||||
is: Some(Cow::Borrowed("2")),
|
||||
}),
|
||||
]),
|
||||
actions: Cow::Borrowed(&[Action::Notify, SOUND_ACTION, HIGHLIGHT_FALSE_ACTION]),
|
||||
default: true,
|
||||
default_enabled: true,
|
||||
},
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed("global/underride/.m.rule.encrypted_room_one_to_one"),
|
||||
priority_class: 1,
|
||||
conditions: Cow::Borrowed(&[
|
||||
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||
key: Cow::Borrowed("type"),
|
||||
pattern: Some(Cow::Borrowed("m.room.encrypted")),
|
||||
pattern_type: None,
|
||||
})),
|
||||
Condition::Known(KnownCondition::RoomMemberCount {
|
||||
is: Some(Cow::Borrowed("2")),
|
||||
}),
|
||||
]),
|
||||
actions: Cow::Borrowed(&[Action::Notify, SOUND_ACTION, HIGHLIGHT_FALSE_ACTION]),
|
||||
default: true,
|
||||
default_enabled: true,
|
||||
},
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed("global/underride/.org.matrix.msc3772.thread_reply"),
|
||||
priority_class: 1,
|
||||
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::RelationMatch {
|
||||
rel_type: Cow::Borrowed("m.thread"),
|
||||
sender: None,
|
||||
sender_type: Some(Cow::Borrowed("user_id")),
|
||||
})]),
|
||||
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_FALSE_ACTION]),
|
||||
default: true,
|
||||
default_enabled: true,
|
||||
},
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed("global/underride/.m.rule.message"),
|
||||
priority_class: 1,
|
||||
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::EventMatch(
|
||||
EventMatchCondition {
|
||||
key: Cow::Borrowed("type"),
|
||||
pattern: Some(Cow::Borrowed("m.room.message")),
|
||||
pattern_type: None,
|
||||
},
|
||||
))]),
|
||||
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_FALSE_ACTION]),
|
||||
default: true,
|
||||
default_enabled: true,
|
||||
},
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed("global/underride/.m.rule.encrypted"),
|
||||
priority_class: 1,
|
||||
conditions: Cow::Borrowed(&[Condition::Known(KnownCondition::EventMatch(
|
||||
EventMatchCondition {
|
||||
key: Cow::Borrowed("type"),
|
||||
pattern: Some(Cow::Borrowed("m.room.encrypted")),
|
||||
pattern_type: None,
|
||||
},
|
||||
))]),
|
||||
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_FALSE_ACTION]),
|
||||
default: true,
|
||||
default_enabled: true,
|
||||
},
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed("global/underride/.im.vector.jitsi"),
|
||||
priority_class: 1,
|
||||
conditions: Cow::Borrowed(&[
|
||||
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||
key: Cow::Borrowed("type"),
|
||||
pattern: Some(Cow::Borrowed("im.vector.modular.widgets")),
|
||||
pattern_type: None,
|
||||
})),
|
||||
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||
key: Cow::Borrowed("content.type"),
|
||||
pattern: Some(Cow::Borrowed("jitsi")),
|
||||
pattern_type: None,
|
||||
})),
|
||||
Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||
key: Cow::Borrowed("state_key"),
|
||||
pattern: Some(Cow::Borrowed("*")),
|
||||
pattern_type: None,
|
||||
})),
|
||||
]),
|
||||
actions: Cow::Borrowed(&[Action::Notify, HIGHLIGHT_FALSE_ACTION]),
|
||||
default: true,
|
||||
default_enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
lazy_static! {
|
||||
pub static ref BASE_RULES_BY_ID: HashMap<&'static str, &'static PushRule> =
|
||||
BASE_PREPEND_OVERRIDE_RULES
|
||||
.iter()
|
||||
.chain(BASE_APPEND_OVERRIDE_RULES.iter())
|
||||
.chain(BASE_APPEND_CONTENT_RULES.iter())
|
||||
.chain(BASE_APPEND_UNDERRIDE_RULES.iter())
|
||||
.map(|rule| { (&*rule.rule_id, rule) })
|
||||
.collect();
|
||||
}
|
502
rust/src/push/mod.rs
Normal file
502
rust/src/push/mod.rs
Normal file
|
@ -0,0 +1,502 @@
|
|||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! An implementation of Matrix push rules.
|
||||
//!
|
||||
//! The `Cow<_>` type is used extensively within this module to allow creating
|
||||
//! the base rules as constants (in Rust constants can't require explicit
|
||||
//! allocation atm).
|
||||
//!
|
||||
//! ---
|
||||
//!
|
||||
//! Push rules is the system used to determine which events trigger a push (and a
|
||||
//! bump in notification counts).
|
||||
//!
|
||||
//! This consists of a list of "push rules" for each user, where a push rule is a
|
||||
//! pair of "conditions" and "actions". When a user receives an event Synapse
|
||||
//! iterates over the list of push rules until it finds one where all the conditions
|
||||
//! match the event, at which point "actions" describe the outcome (e.g. notify,
|
||||
//! highlight, etc).
|
||||
//!
|
||||
//! Push rules are split up into 5 different "kinds" (aka "priority classes"), which
|
||||
//! are run in order:
|
||||
//! 1. Override — highest priority rules, e.g. always ignore notices
|
||||
//! 2. Content — content specific rules, e.g. @ notifications
|
||||
//! 3. Room — per room rules, e.g. enable/disable notifications for all messages
|
||||
//! in a room
|
||||
//! 4. Sender — per sender rules, e.g. never notify for messages from a given
|
||||
//! user
|
||||
//! 5. Underride — the lowest priority "default" rules, e.g. notify for every
|
||||
//! message.
|
||||
//!
|
||||
//! The set of "base rules" are the list of rules that every user has by default. A
|
||||
//! user can modify their copy of the push rules in one of three ways:
|
||||
//!
|
||||
//! 1. Adding a new push rule of a certain kind
|
||||
//! 2. Changing the actions of a base rule
|
||||
//! 3. Enabling/disabling a base rule.
|
||||
//!
|
||||
//! The base rules are split into whether they come before or after a particular
|
||||
//! kind, so the order of push rule evaluation would be: base rules for before
|
||||
//! "override" kind, user defined "override" rules, base rules after "override"
|
||||
//! kind, etc, etc.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
|
||||
use anyhow::{Context, Error};
|
||||
use log::warn;
|
||||
use pyo3::prelude::*;
|
||||
use pythonize::pythonize;
|
||||
use serde::de::Error as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
mod base_rules;
|
||||
|
||||
/// Called when registering modules with python.
|
||||
pub fn register_module(py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
||||
let child_module = PyModule::new(py, "push")?;
|
||||
child_module.add_class::<PushRule>()?;
|
||||
child_module.add_class::<PushRules>()?;
|
||||
child_module.add_class::<FilteredPushRules>()?;
|
||||
child_module.add_function(wrap_pyfunction!(get_base_rule_ids, m)?)?;
|
||||
|
||||
m.add_submodule(child_module)?;
|
||||
|
||||
// We need to manually add the module to sys.modules to make `from
|
||||
// synapse.synapse_rust import push` work.
|
||||
py.import("sys")?
|
||||
.getattr("modules")?
|
||||
.set_item("synapse.synapse_rust.push", child_module)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn get_base_rule_ids() -> HashSet<&'static str> {
|
||||
base_rules::BASE_RULES_BY_ID.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// A single push rule for a user.
|
||||
#[derive(Debug, Clone)]
|
||||
#[pyclass(frozen)]
|
||||
pub struct PushRule {
|
||||
/// A unique ID for this rule
|
||||
pub rule_id: Cow<'static, str>,
|
||||
/// The "kind" of push rule this is (see `PRIORITY_CLASS_MAP` in Python)
|
||||
#[pyo3(get)]
|
||||
pub priority_class: i32,
|
||||
/// The conditions that must all match for actions to be applied
|
||||
pub conditions: Cow<'static, [Condition]>,
|
||||
/// The actions to apply if all conditions are met
|
||||
pub actions: Cow<'static, [Action]>,
|
||||
/// Whether this is a base rule
|
||||
#[pyo3(get)]
|
||||
pub default: bool,
|
||||
/// Whether this is enabled by default
|
||||
#[pyo3(get)]
|
||||
pub default_enabled: bool,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PushRule {
|
||||
#[staticmethod]
|
||||
pub fn from_db(
|
||||
rule_id: String,
|
||||
priority_class: i32,
|
||||
conditions: &str,
|
||||
actions: &str,
|
||||
) -> Result<PushRule, Error> {
|
||||
let conditions = serde_json::from_str(conditions).context("parsing conditions")?;
|
||||
let actions = serde_json::from_str(actions).context("parsing actions")?;
|
||||
|
||||
Ok(PushRule {
|
||||
rule_id: Cow::Owned(rule_id),
|
||||
priority_class,
|
||||
conditions,
|
||||
actions,
|
||||
default: false,
|
||||
default_enabled: true,
|
||||
})
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn rule_id(&self) -> &str {
|
||||
&self.rule_id
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn actions(&self) -> Vec<Action> {
|
||||
self.actions.clone().into_owned()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn conditions(&self) -> Vec<Condition> {
|
||||
self.conditions.clone().into_owned()
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"<PushRule rule_id={}, conditions={:?}, actions={:?}>",
|
||||
self.rule_id, self.conditions, self.actions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The "action" Synapse should perform for a matching push rule.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Action {
|
||||
DontNotify,
|
||||
Notify,
|
||||
Coalesce,
|
||||
SetTweak(SetTweak),
|
||||
|
||||
// An unrecognized custom action.
|
||||
Unknown(Value),
|
||||
}
|
||||
|
||||
impl IntoPy<PyObject> for Action {
|
||||
fn into_py(self, py: Python<'_>) -> PyObject {
|
||||
// When we pass the `Action` struct to Python we want it to be converted
|
||||
// to a dict. We use `pythonize`, which converts the struct using the
|
||||
// `serde` serialization.
|
||||
pythonize(py, &self).expect("valid action")
|
||||
}
|
||||
}
|
||||
|
||||
/// The body of a `SetTweak` push action.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SetTweak {
|
||||
set_tweak: Cow<'static, str>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
value: Option<TweakValue>,
|
||||
|
||||
// This picks up any other fields that may have been added by clients.
|
||||
// These get added when we convert the `Action` to a python object.
|
||||
#[serde(flatten)]
|
||||
other_keys: Value,
|
||||
}
|
||||
|
||||
/// The value of a `set_tweak`.
|
||||
///
|
||||
/// We need this (rather than using `TweakValue` directly) so that we can use
|
||||
/// `&'static str` in the value when defining the constant base rules.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(untagged)]
|
||||
pub enum TweakValue {
|
||||
String(Cow<'static, str>),
|
||||
Other(Value),
|
||||
}
|
||||
|
||||
impl Serialize for Action {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
Action::DontNotify => serializer.serialize_str("dont_notify"),
|
||||
Action::Notify => serializer.serialize_str("notify"),
|
||||
Action::Coalesce => serializer.serialize_str("coalesce"),
|
||||
Action::SetTweak(tweak) => tweak.serialize(serializer),
|
||||
Action::Unknown(value) => value.serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple helper class for deserializing Action from JSON.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ActionDeserializeHelper {
|
||||
Str(String),
|
||||
SetTweak(SetTweak),
|
||||
Unknown(Value),
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Action {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let helper: ActionDeserializeHelper = Deserialize::deserialize(deserializer)?;
|
||||
match helper {
|
||||
ActionDeserializeHelper::Str(s) => match &*s {
|
||||
"dont_notify" => Ok(Action::DontNotify),
|
||||
"notify" => Ok(Action::Notify),
|
||||
"coalesce" => Ok(Action::Coalesce),
|
||||
_ => Err(D::Error::custom("unrecognized action")),
|
||||
},
|
||||
ActionDeserializeHelper::SetTweak(set_tweak) => Ok(Action::SetTweak(set_tweak)),
|
||||
ActionDeserializeHelper::Unknown(value) => Ok(Action::Unknown(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A condition used in push rules to match against an event.
|
||||
///
|
||||
/// We need this split as `serde` doesn't give us the ability to have a
|
||||
/// "catchall" variant in tagged enums.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum Condition {
|
||||
/// A recognized condition that we can match against
|
||||
Known(KnownCondition),
|
||||
/// An unrecognized condition that we ignore.
|
||||
Unknown(Value),
|
||||
}
|
||||
|
||||
/// The set of "known" conditions that we can handle.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum KnownCondition {
|
||||
EventMatch(EventMatchCondition),
|
||||
ContainsDisplayName,
|
||||
RoomMemberCount {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
is: Option<Cow<'static, str>>,
|
||||
},
|
||||
SenderNotificationPermission {
|
||||
key: Cow<'static, str>,
|
||||
},
|
||||
#[serde(rename = "org.matrix.msc3772.relation_match")]
|
||||
RelationMatch {
|
||||
rel_type: Cow<'static, str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
sender: Option<Cow<'static, str>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
sender_type: Option<Cow<'static, str>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl IntoPy<PyObject> for Condition {
|
||||
fn into_py(self, py: Python<'_>) -> PyObject {
|
||||
pythonize(py, &self).expect("valid condition")
|
||||
}
|
||||
}
|
||||
|
||||
/// The body of a [`Condition::EventMatch`]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct EventMatchCondition {
|
||||
key: Cow<'static, str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pattern: Option<Cow<'static, str>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pattern_type: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
/// The collection of push rules for a user.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[pyclass(frozen)]
|
||||
struct PushRules {
|
||||
/// Custom push rules that override a base rule.
|
||||
overridden_base_rules: HashMap<Cow<'static, str>, PushRule>,
|
||||
|
||||
/// Custom rules that come between the prepend/append override base rules.
|
||||
override_rules: Vec<PushRule>,
|
||||
/// Custom rules that come before the base content rules.
|
||||
content: Vec<PushRule>,
|
||||
/// Custom rules that come before the base room rules.
|
||||
room: Vec<PushRule>,
|
||||
/// Custom rules that come before the base sender rules.
|
||||
sender: Vec<PushRule>,
|
||||
/// Custom rules that come before the base underride rules.
|
||||
underride: Vec<PushRule>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PushRules {
|
||||
#[new]
|
||||
fn new(rules: Vec<PushRule>) -> PushRules {
|
||||
let mut push_rules: PushRules = Default::default();
|
||||
|
||||
for rule in rules {
|
||||
if let Some(&o) = base_rules::BASE_RULES_BY_ID.get(&*rule.rule_id) {
|
||||
push_rules.overridden_base_rules.insert(
|
||||
rule.rule_id.clone(),
|
||||
PushRule {
|
||||
actions: rule.actions.clone(),
|
||||
..o.clone()
|
||||
},
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
match rule.priority_class {
|
||||
5 => push_rules.override_rules.push(rule),
|
||||
4 => push_rules.content.push(rule),
|
||||
3 => push_rules.room.push(rule),
|
||||
2 => push_rules.sender.push(rule),
|
||||
1 => push_rules.underride.push(rule),
|
||||
_ => {
|
||||
warn!(
|
||||
"Unrecognized priority class for rule {}: {}",
|
||||
rule.rule_id, rule.priority_class
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
push_rules
|
||||
}
|
||||
|
||||
/// Returns the list of all rules, including base rules, in the order they
|
||||
/// should be executed in.
|
||||
fn rules(&self) -> Vec<PushRule> {
|
||||
self.iter().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl PushRules {
|
||||
/// Iterates over all the rules, including base rules, in the order they
|
||||
/// should be executed in.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &PushRule> {
|
||||
base_rules::BASE_PREPEND_OVERRIDE_RULES
|
||||
.iter()
|
||||
.chain(self.override_rules.iter())
|
||||
.chain(base_rules::BASE_APPEND_OVERRIDE_RULES.iter())
|
||||
.chain(self.content.iter())
|
||||
.chain(base_rules::BASE_APPEND_CONTENT_RULES.iter())
|
||||
.chain(self.room.iter())
|
||||
.chain(self.sender.iter())
|
||||
.chain(self.underride.iter())
|
||||
.chain(base_rules::BASE_APPEND_UNDERRIDE_RULES.iter())
|
||||
.map(|rule| {
|
||||
self.overridden_base_rules
|
||||
.get(&*rule.rule_id)
|
||||
.unwrap_or(rule)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around `PushRules` that checks the enabled state of rules and
|
||||
/// filters out disabled experimental rules.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[pyclass(frozen)]
|
||||
pub struct FilteredPushRules {
|
||||
push_rules: PushRules,
|
||||
enabled_map: BTreeMap<String, bool>,
|
||||
msc3786_enabled: bool,
|
||||
msc3772_enabled: bool,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl FilteredPushRules {
|
||||
#[new]
|
||||
fn py_new(
|
||||
push_rules: PushRules,
|
||||
enabled_map: BTreeMap<String, bool>,
|
||||
msc3786_enabled: bool,
|
||||
msc3772_enabled: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
push_rules,
|
||||
enabled_map,
|
||||
msc3786_enabled,
|
||||
msc3772_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the list of all rules and their enabled state, including base
|
||||
/// rules, in the order they should be executed in.
|
||||
fn rules(&self) -> Vec<(PushRule, bool)> {
|
||||
self.iter().map(|(r, e)| (r.clone(), e)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl FilteredPushRules {
|
||||
/// Iterates over all the rules and their enabled state, including base
|
||||
/// rules, in the order they should be executed in.
|
||||
fn iter(&self) -> impl Iterator<Item = (&PushRule, bool)> {
|
||||
self.push_rules
|
||||
.iter()
|
||||
.filter(|rule| {
|
||||
// Ignore disabled experimental push rules
|
||||
if !self.msc3786_enabled
|
||||
&& rule.rule_id == "global/override/.org.matrix.msc3786.rule.room.server_acl"
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.msc3772_enabled
|
||||
&& rule.rule_id == "global/underride/.org.matrix.msc3772.thread_reply"
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.map(|r| {
|
||||
let enabled = *self
|
||||
.enabled_map
|
||||
.get(&*r.rule_id)
|
||||
.unwrap_or(&r.default_enabled);
|
||||
(r, enabled)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_condition() {
|
||||
let condition = Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
|
||||
key: "content.body".into(),
|
||||
pattern: Some("coffee".into()),
|
||||
pattern_type: None,
|
||||
}));
|
||||
|
||||
let json = serde_json::to_string(&condition).unwrap();
|
||||
assert_eq!(
|
||||
json,
|
||||
r#"{"kind":"event_match","key":"content.body","pattern":"coffee"}"#
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_condition() {
|
||||
let json = r#"{"kind":"event_match","key":"content.body","pattern":"coffee"}"#;
|
||||
|
||||
let _: Condition = serde_json::from_str(json).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_custom_condition() {
|
||||
let json = r#"{"kind":"custom_tag"}"#;
|
||||
|
||||
let condition: Condition = serde_json::from_str(json).unwrap();
|
||||
assert!(matches!(condition, Condition::Unknown(_)));
|
||||
|
||||
let new_json = serde_json::to_string(&condition).unwrap();
|
||||
assert_eq!(json, new_json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_action() {
|
||||
let _: Action = serde_json::from_str(r#""notify""#).unwrap();
|
||||
let _: Action = serde_json::from_str(r#""dont_notify""#).unwrap();
|
||||
let _: Action = serde_json::from_str(r#""coalesce""#).unwrap();
|
||||
let _: Action = serde_json::from_str(r#"{"set_tweak": "highlight"}"#).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_action() {
|
||||
let json = r#"{"some_custom":"action_fields"}"#;
|
||||
|
||||
let action: Action = serde_json::from_str(json).unwrap();
|
||||
assert!(matches!(action, Action::Unknown(_)));
|
||||
|
||||
let new_json = serde_json::to_string(&action).unwrap();
|
||||
assert_eq!(json, new_json);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue