From f87714d73f0251a2edab86078bcc175f8c4bfd42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 4 Nov 2022 18:21:28 +0100 Subject: [PATCH] push: Add method to insert a user push rule in a Ruleset --- crates/ruma-common/CHANGELOG.md | 4 +- crates/ruma-common/Cargo.toml | 8 +- crates/ruma-common/src/push.rs | 171 ++++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 5 deletions(-) diff --git a/crates/ruma-common/CHANGELOG.md b/crates/ruma-common/CHANGELOG.md index 4e9a16eb..f09fee79 100644 --- a/crates/ruma-common/CHANGELOG.md +++ b/crates/ruma-common/CHANGELOG.md @@ -35,7 +35,9 @@ Improvements: * Stabilize support for event replacements (edits) * 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 `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 diff --git a/crates/ruma-common/Cargo.toml b/crates/ruma-common/Cargo.toml index 4e8686b5..313ccefb 100644 --- a/crates/ruma-common/Cargo.toml +++ b/crates/ruma-common/Cargo.toml @@ -21,10 +21,10 @@ default = ["client", "server"] client = [] server = [] -api = ["dep:http", "dep:thiserror"] +api = ["dep:http"] canonical-json = [] compat = ["ruma-macros/compat", "ruma-identifiers-validation/compat"] -events = ["dep:thiserror"] +events = [] js = ["dep:js-sys", "getrandom?/js", "uuid?/js"] markdown = ["pulldown-cmark"] rand = ["dep:rand", "dep:uuid"] @@ -35,7 +35,7 @@ unstable-msc2677 = [] unstable-msc2746 = [] unstable-msc2870 = [] unstable-msc3245 = ["unstable-msc3246"] -unstable-msc3246 = ["unstable-msc3551", "dep:thiserror"] +unstable-msc3246 = ["unstable-msc3551"] unstable-msc3381 = ["unstable-msc1767"] unstable-msc3488 = ["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" } serde = { workspace = true } serde_json = { workspace = true, features = ["raw_value"] } -thiserror = { workspace = true, optional = true } +thiserror = { workspace = true } tracing = { workspace = true, features = ["attributes"] } url = "2.2.2" uuid = { version = "1.0.0", optional = true, features = ["v4"] } diff --git a/crates/ruma-common/src/push.rs b/crates/ruma-common/src/push.rs index b3ec62ba..d059d537 100644 --- a/crates/ruma-common/src/push.rs +++ b/crates/ruma-common/src/push.rs @@ -20,6 +20,7 @@ use indexmap::{Equivalent, IndexSet}; use serde::{Deserialize, Serialize}; #[cfg(feature = "unstable-unspecified")] use serde_json::Value as JsonValue; +use thiserror::Error; use tracing::instrument; use crate::{ @@ -85,6 +86,92 @@ impl Ruleset { 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. pub fn get(&self, kind: RuleKind, rule_id: impl AsRef) -> Option> { let rule_id = rule_id.as_ref(); @@ -545,6 +632,13 @@ impl NewSimplePushRule { } } +impl From 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. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] @@ -567,6 +661,13 @@ impl NewPatternedPushRule { } } +impl From 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. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] @@ -592,6 +693,76 @@ impl NewConditionalPushRule { } } +impl From 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( + set: &mut IndexSet, + rule: T, + default_position: usize, + after: Option<&str>, + before: Option<&str>, +) -> Result<(), InsertPushRuleError> +where + T: Hash + Eq, + str: Equivalent, +{ + 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)] mod tests { use std::collections::BTreeMap;