push: Add method to insert a user push rule in a Ruleset

This commit is contained in:
Kévin Commaille 2022-11-04 18:21:28 +01:00 committed by Kévin Commaille
parent a50f5f5cb2
commit f87714d73f
3 changed files with 178 additions and 5 deletions

View File

@ -35,7 +35,9 @@ Improvements:
* Stabilize support for event replacements (edits) * Stabilize support for event replacements (edits)
* Add support for read receipts for threads (MSC3771 / Matrix 1.4) * Add support for read receipts for threads (MSC3771 / Matrix 1.4)
* Add `push::PredefinedRuleId` and associated types as a list of predefined push rule IDs * Add `push::PredefinedRuleId` and associated types as a list of predefined push rule IDs
* Add `Ruleset::get` to access push rules. * Add convenience methods to `Ruleset`
* `Ruleset::get` to access a push rule
* `Ruleset::insert` to add or update user push rules
# 0.10.5 # 0.10.5

View File

@ -21,10 +21,10 @@ default = ["client", "server"]
client = [] client = []
server = [] server = []
api = ["dep:http", "dep:thiserror"] api = ["dep:http"]
canonical-json = [] canonical-json = []
compat = ["ruma-macros/compat", "ruma-identifiers-validation/compat"] compat = ["ruma-macros/compat", "ruma-identifiers-validation/compat"]
events = ["dep:thiserror"] events = []
js = ["dep:js-sys", "getrandom?/js", "uuid?/js"] js = ["dep:js-sys", "getrandom?/js", "uuid?/js"]
markdown = ["pulldown-cmark"] markdown = ["pulldown-cmark"]
rand = ["dep:rand", "dep:uuid"] rand = ["dep:rand", "dep:uuid"]
@ -35,7 +35,7 @@ unstable-msc2677 = []
unstable-msc2746 = [] unstable-msc2746 = []
unstable-msc2870 = [] unstable-msc2870 = []
unstable-msc3245 = ["unstable-msc3246"] unstable-msc3245 = ["unstable-msc3246"]
unstable-msc3246 = ["unstable-msc3551", "dep:thiserror"] unstable-msc3246 = ["unstable-msc3551"]
unstable-msc3381 = ["unstable-msc1767"] unstable-msc3381 = ["unstable-msc1767"]
unstable-msc3488 = ["unstable-msc1767"] unstable-msc3488 = ["unstable-msc1767"]
unstable-msc3551 = ["unstable-msc1767"] unstable-msc3551 = ["unstable-msc1767"]
@ -67,7 +67,7 @@ ruma-identifiers-validation = { version = "0.9.0", path = "../ruma-identifiers-v
ruma-macros = { version = "0.10.5", path = "../ruma-macros" } ruma-macros = { version = "0.10.5", path = "../ruma-macros" }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true, features = ["raw_value"] } serde_json = { workspace = true, features = ["raw_value"] }
thiserror = { workspace = true, optional = true } thiserror = { workspace = true }
tracing = { workspace = true, features = ["attributes"] } tracing = { workspace = true, features = ["attributes"] }
url = "2.2.2" url = "2.2.2"
uuid = { version = "1.0.0", optional = true, features = ["v4"] } uuid = { version = "1.0.0", optional = true, features = ["v4"] }

View File

@ -20,6 +20,7 @@ use indexmap::{Equivalent, IndexSet};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(feature = "unstable-unspecified")] #[cfg(feature = "unstable-unspecified")]
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use thiserror::Error;
use tracing::instrument; use tracing::instrument;
use crate::{ use crate::{
@ -85,6 +86,92 @@ impl Ruleset {
self.into_iter() self.into_iter()
} }
/// Inserts a user-defined rule in the rule set.
///
/// If a rule with the same kind and `rule_id` exists, it will be replaced.
///
/// If `after` or `before` is set, the rule will be moved relative to the rule with the given
/// ID. If both are set, the rule will become the next-most important rule with respect to
/// `before`. If neither are set, and the rule is newly inserted, it will become the rule with
/// the highest priority of its kind.
///
/// Returns an error if the parameters are invalid.
pub fn insert(
&mut self,
rule: NewPushRule,
after: Option<&str>,
before: Option<&str>,
) -> Result<(), InsertPushRuleError> {
let rule_id = rule.rule_id();
if rule_id.starts_with('.') {
return Err(InsertPushRuleError::ServerDefaultRuleId);
}
if rule_id.contains('/') {
return Err(InsertPushRuleError::InvalidRuleId);
}
if rule_id.contains('\\') {
return Err(InsertPushRuleError::InvalidRuleId);
}
if after.map_or(false, |s| s.starts_with('.')) {
return Err(InsertPushRuleError::RelativeToServerDefaultRule);
}
if before.map_or(false, |s| s.starts_with('.')) {
return Err(InsertPushRuleError::RelativeToServerDefaultRule);
}
match rule {
NewPushRule::Override(r) => {
let mut rule = ConditionalPushRule::from(r);
if let Some(prev_rule) = self.override_.get(rule.rule_id.as_str()) {
rule.enabled = prev_rule.enabled;
}
// `m.rule.master` should always be the rule with the highest priority, so we insert
// this one at most at the second place.
let default_position = 1;
insert_and_move_rule(&mut self.override_, rule, default_position, after, before)
}
NewPushRule::Underride(r) => {
let mut rule = ConditionalPushRule::from(r);
if let Some(prev_rule) = self.underride.get(rule.rule_id.as_str()) {
rule.enabled = prev_rule.enabled;
}
insert_and_move_rule(&mut self.underride, rule, 0, after, before)
}
NewPushRule::Content(r) => {
let mut rule = PatternedPushRule::from(r);
if let Some(prev_rule) = self.content.get(rule.rule_id.as_str()) {
rule.enabled = prev_rule.enabled;
}
insert_and_move_rule(&mut self.content, rule, 0, after, before)
}
NewPushRule::Room(r) => {
let mut rule = SimplePushRule::from(r);
if let Some(prev_rule) = self.room.get(rule.rule_id.as_str()) {
rule.enabled = prev_rule.enabled;
}
insert_and_move_rule(&mut self.room, rule, 0, after, before)
}
NewPushRule::Sender(r) => {
let mut rule = SimplePushRule::from(r);
if let Some(prev_rule) = self.sender.get(rule.rule_id.as_str()) {
rule.enabled = prev_rule.enabled;
}
insert_and_move_rule(&mut self.sender, rule, 0, after, before)
}
}
}
/// Get the rule from the given kind and with the given `rule_id` in the rule set. /// Get the rule from the given kind and with the given `rule_id` in the rule set.
pub fn get(&self, kind: RuleKind, rule_id: impl AsRef<str>) -> Option<AnyPushRuleRef<'_>> { pub fn get(&self, kind: RuleKind, rule_id: impl AsRef<str>) -> Option<AnyPushRuleRef<'_>> {
let rule_id = rule_id.as_ref(); let rule_id = rule_id.as_ref();
@ -545,6 +632,13 @@ impl NewSimplePushRule {
} }
} }
impl From<NewSimplePushRule> for SimplePushRule {
fn from(new_rule: NewSimplePushRule) -> Self {
let NewSimplePushRule { rule_id, actions } = new_rule;
Self { actions, default: false, enabled: true, rule_id }
}
}
/// A patterned push rule to update or create. /// A patterned push rule to update or create.
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
@ -567,6 +661,13 @@ impl NewPatternedPushRule {
} }
} }
impl From<NewPatternedPushRule> for PatternedPushRule {
fn from(new_rule: NewPatternedPushRule) -> Self {
let NewPatternedPushRule { rule_id, pattern, actions } = new_rule;
Self { actions, default: false, enabled: true, rule_id, pattern }
}
}
/// A conditional push rule to update or create. /// A conditional push rule to update or create.
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
@ -592,6 +693,76 @@ impl NewConditionalPushRule {
} }
} }
impl From<NewConditionalPushRule> for ConditionalPushRule {
fn from(new_rule: NewConditionalPushRule) -> Self {
let NewConditionalPushRule { rule_id, conditions, actions } = new_rule;
Self { actions, default: false, enabled: true, rule_id, conditions }
}
}
/// The error type returned when trying to insert a user-defined push rule into a `Ruleset`.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum InsertPushRuleError {
/// The rule ID starts with a dot (`.`), which is reserved for server-default rules.
#[error("rule IDs starting with a dot are reserved for server-default rules")]
ServerDefaultRuleId,
/// The rule ID contains an invalid character.
#[error("invalid rule ID")]
InvalidRuleId,
/// The rule is being placed relative to a server-default rule, which is forbidden.
#[error("can't place rule relative to server-default rule")]
RelativeToServerDefaultRule,
/// The `before` or `after` rule could not be found.
#[error("The before or after rule could not be found")]
UnknownRuleId,
/// `before` has a higher priority than `after`.
#[error("before has a higher priority than after")]
BeforeHigherThanAfter,
}
/// Insert the rule in the given indexset and move it to the given position.
pub fn insert_and_move_rule<T>(
set: &mut IndexSet<T>,
rule: T,
default_position: usize,
after: Option<&str>,
before: Option<&str>,
) -> Result<(), InsertPushRuleError>
where
T: Hash + Eq,
str: Equivalent<T>,
{
let (from, replaced) = set.replace_full(rule);
let mut to = default_position;
if let Some(rule_id) = after {
let idx = set.get_index_of(rule_id).ok_or(InsertPushRuleError::UnknownRuleId)?;
to = idx + 1;
}
if let Some(rule_id) = before {
let idx = set.get_index_of(rule_id).ok_or(InsertPushRuleError::UnknownRuleId)?;
if idx < to {
return Err(InsertPushRuleError::BeforeHigherThanAfter);
}
to = idx;
}
// Only move the item if it's new or if it was positioned.
if replaced.is_none() || after.is_some() || before.is_some() {
set.move_index(from, to);
}
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::BTreeMap; use std::collections::BTreeMap;