1865 lines
60 KiB
Rust
1865 lines
60 KiB
Rust
//! Common types for the [push notifications module][push].
|
|
//!
|
|
//! [push]: https://spec.matrix.org/latest/client-server-api/#push-notifications
|
|
//!
|
|
//! ## Understanding the types of this module
|
|
//!
|
|
//! Push rules are grouped in `RuleSet`s, and are grouped in five kinds (for
|
|
//! more details about the different kind of rules, see the `Ruleset` documentation,
|
|
//! or the specification). These five kinds are, by order of priority:
|
|
//!
|
|
//! - override rules
|
|
//! - content rules
|
|
//! - room rules
|
|
//! - sender rules
|
|
//! - underride rules
|
|
|
|
use std::hash::{Hash, Hasher};
|
|
|
|
use indexmap::{Equivalent, IndexSet};
|
|
use serde::{Deserialize, Serialize};
|
|
use thiserror::Error;
|
|
use tracing::instrument;
|
|
|
|
use crate::{
|
|
serde::{JsonObject, Raw, StringEnum},
|
|
OwnedRoomId, OwnedUserId, PrivOwnedStr,
|
|
};
|
|
|
|
mod action;
|
|
mod condition;
|
|
mod iter;
|
|
mod predefined;
|
|
|
|
#[cfg(feature = "unstable-msc3932")]
|
|
pub use self::condition::RoomVersionFeature;
|
|
pub use self::{
|
|
action::{Action, Tweak},
|
|
condition::{
|
|
ComparisonOperator, FlattenedJson, FlattenedJsonValue, PushCondition,
|
|
PushConditionPowerLevelsCtx, PushConditionRoomCtx, RoomMemberCountIs, ScalarJsonValue,
|
|
_CustomPushCondition,
|
|
},
|
|
iter::{AnyPushRule, AnyPushRuleRef, RulesetIntoIter, RulesetIter},
|
|
predefined::{
|
|
PredefinedContentRuleId, PredefinedOverrideRuleId, PredefinedRuleId,
|
|
PredefinedUnderrideRuleId,
|
|
},
|
|
};
|
|
|
|
/// A push ruleset scopes a set of rules according to some criteria.
|
|
///
|
|
/// For example, some rules may only be applied for messages from a particular sender, a particular
|
|
/// room, or by default. The push ruleset contains the entire set of scopes and rules.
|
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
pub struct Ruleset {
|
|
/// These rules configure behavior for (unencrypted) messages that match certain patterns.
|
|
#[serde(default)]
|
|
pub content: IndexSet<PatternedPushRule>,
|
|
|
|
/// These user-configured rules are given the highest priority.
|
|
///
|
|
/// This field is named `override_` instead of `override` because the latter is a reserved
|
|
/// keyword in Rust.
|
|
#[serde(rename = "override", default)]
|
|
pub override_: IndexSet<ConditionalPushRule>,
|
|
|
|
/// These rules change the behavior of all messages for a given room.
|
|
#[serde(default)]
|
|
pub room: IndexSet<SimplePushRule<OwnedRoomId>>,
|
|
|
|
/// These rules configure notification behavior for messages from a specific Matrix user ID.
|
|
#[serde(default)]
|
|
pub sender: IndexSet<SimplePushRule<OwnedUserId>>,
|
|
|
|
/// These rules are identical to override rules, but have a lower priority than `content`,
|
|
/// `room` and `sender` rules.
|
|
#[serde(default)]
|
|
pub underride: IndexSet<ConditionalPushRule>,
|
|
}
|
|
|
|
impl Ruleset {
|
|
/// Creates an empty `Ruleset`.
|
|
pub fn new() -> Self {
|
|
Default::default()
|
|
}
|
|
|
|
/// Creates a borrowing iterator over all push rules in this `Ruleset`.
|
|
///
|
|
/// For an owning iterator, use `.into_iter()`.
|
|
pub fn iter(&self) -> RulesetIter<'_> {
|
|
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.is_some_and(|s| s.starts_with('.')) {
|
|
return Err(InsertPushRuleError::RelativeToServerDefaultRule);
|
|
}
|
|
if before.is_some_and(|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<str>) -> Option<AnyPushRuleRef<'_>> {
|
|
let rule_id = rule_id.as_ref();
|
|
|
|
match kind {
|
|
RuleKind::Override => self.override_.get(rule_id).map(AnyPushRuleRef::Override),
|
|
RuleKind::Underride => self.underride.get(rule_id).map(AnyPushRuleRef::Underride),
|
|
RuleKind::Sender => self.sender.get(rule_id).map(AnyPushRuleRef::Sender),
|
|
RuleKind::Room => self.room.get(rule_id).map(AnyPushRuleRef::Room),
|
|
RuleKind::Content => self.content.get(rule_id).map(AnyPushRuleRef::Content),
|
|
RuleKind::_Custom(_) => None,
|
|
}
|
|
}
|
|
|
|
/// Set whether the rule from the given kind and with the given `rule_id` in the rule set is
|
|
/// enabled.
|
|
///
|
|
/// Returns an error if the rule can't be found.
|
|
pub fn set_enabled(
|
|
&mut self,
|
|
kind: RuleKind,
|
|
rule_id: impl AsRef<str>,
|
|
enabled: bool,
|
|
) -> Result<(), RuleNotFoundError> {
|
|
let rule_id = rule_id.as_ref();
|
|
|
|
match kind {
|
|
RuleKind::Override => {
|
|
let mut rule = self.override_.get(rule_id).ok_or(RuleNotFoundError)?.clone();
|
|
rule.enabled = enabled;
|
|
self.override_.replace(rule);
|
|
}
|
|
RuleKind::Underride => {
|
|
let mut rule = self.underride.get(rule_id).ok_or(RuleNotFoundError)?.clone();
|
|
rule.enabled = enabled;
|
|
self.underride.replace(rule);
|
|
}
|
|
RuleKind::Sender => {
|
|
let mut rule = self.sender.get(rule_id).ok_or(RuleNotFoundError)?.clone();
|
|
rule.enabled = enabled;
|
|
self.sender.replace(rule);
|
|
}
|
|
RuleKind::Room => {
|
|
let mut rule = self.room.get(rule_id).ok_or(RuleNotFoundError)?.clone();
|
|
rule.enabled = enabled;
|
|
self.room.replace(rule);
|
|
}
|
|
RuleKind::Content => {
|
|
let mut rule = self.content.get(rule_id).ok_or(RuleNotFoundError)?.clone();
|
|
rule.enabled = enabled;
|
|
self.content.replace(rule);
|
|
}
|
|
RuleKind::_Custom(_) => return Err(RuleNotFoundError),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Set the actions of the rule from the given kind and with the given `rule_id` in the rule
|
|
/// set.
|
|
///
|
|
/// Returns an error if the rule can't be found.
|
|
pub fn set_actions(
|
|
&mut self,
|
|
kind: RuleKind,
|
|
rule_id: impl AsRef<str>,
|
|
actions: Vec<Action>,
|
|
) -> Result<(), RuleNotFoundError> {
|
|
let rule_id = rule_id.as_ref();
|
|
|
|
match kind {
|
|
RuleKind::Override => {
|
|
let mut rule = self.override_.get(rule_id).ok_or(RuleNotFoundError)?.clone();
|
|
rule.actions = actions;
|
|
self.override_.replace(rule);
|
|
}
|
|
RuleKind::Underride => {
|
|
let mut rule = self.underride.get(rule_id).ok_or(RuleNotFoundError)?.clone();
|
|
rule.actions = actions;
|
|
self.underride.replace(rule);
|
|
}
|
|
RuleKind::Sender => {
|
|
let mut rule = self.sender.get(rule_id).ok_or(RuleNotFoundError)?.clone();
|
|
rule.actions = actions;
|
|
self.sender.replace(rule);
|
|
}
|
|
RuleKind::Room => {
|
|
let mut rule = self.room.get(rule_id).ok_or(RuleNotFoundError)?.clone();
|
|
rule.actions = actions;
|
|
self.room.replace(rule);
|
|
}
|
|
RuleKind::Content => {
|
|
let mut rule = self.content.get(rule_id).ok_or(RuleNotFoundError)?.clone();
|
|
rule.actions = actions;
|
|
self.content.replace(rule);
|
|
}
|
|
RuleKind::_Custom(_) => return Err(RuleNotFoundError),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get the first push rule that applies to this event, if any.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `event` - The raw JSON of a room message event.
|
|
/// * `context` - The context of the message and room at the time of the event.
|
|
#[instrument(level = "debug", skip_all, fields(context.room_id = %context.room_id))]
|
|
pub fn get_match<T>(
|
|
&self,
|
|
event: &Raw<T>,
|
|
context: &PushConditionRoomCtx,
|
|
) -> Option<AnyPushRuleRef<'_>> {
|
|
let event = FlattenedJson::from_raw(event);
|
|
|
|
if event.get_str("sender").is_some_and(|sender| sender == context.user_id) {
|
|
// no need to look at the rules if the event was by the user themselves
|
|
None
|
|
} else {
|
|
self.iter().find(|rule| rule.applies(&event, context))
|
|
}
|
|
}
|
|
|
|
/// Get the push actions that apply to this event.
|
|
///
|
|
/// Returns an empty slice if no push rule applies.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `event` - The raw JSON of a room message event.
|
|
/// * `context` - The context of the message and room at the time of the event.
|
|
#[instrument(level = "debug", skip_all, fields(context.room_id = %context.room_id))]
|
|
pub fn get_actions<T>(&self, event: &Raw<T>, context: &PushConditionRoomCtx) -> &[Action] {
|
|
self.get_match(event, context).map(|rule| rule.actions()).unwrap_or(&[])
|
|
}
|
|
|
|
/// Removes a user-defined rule in the rule set.
|
|
///
|
|
/// Returns an error if the parameters are invalid.
|
|
pub fn remove(
|
|
&mut self,
|
|
kind: RuleKind,
|
|
rule_id: impl AsRef<str>,
|
|
) -> Result<(), RemovePushRuleError> {
|
|
let rule_id = rule_id.as_ref();
|
|
|
|
if let Some(rule) = self.get(kind.clone(), rule_id) {
|
|
if rule.is_server_default() {
|
|
return Err(RemovePushRuleError::ServerDefault);
|
|
}
|
|
} else {
|
|
return Err(RemovePushRuleError::NotFound);
|
|
}
|
|
|
|
match kind {
|
|
RuleKind::Override => {
|
|
self.override_.shift_remove(rule_id);
|
|
}
|
|
RuleKind::Underride => {
|
|
self.underride.shift_remove(rule_id);
|
|
}
|
|
RuleKind::Sender => {
|
|
self.sender.shift_remove(rule_id);
|
|
}
|
|
RuleKind::Room => {
|
|
self.room.shift_remove(rule_id);
|
|
}
|
|
RuleKind::Content => {
|
|
self.content.shift_remove(rule_id);
|
|
}
|
|
// This has been handled in the `self.get` call earlier.
|
|
RuleKind::_Custom(_) => unreachable!(),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// A push rule is a single rule that states under what conditions an event should be passed onto a
|
|
/// push gateway and how the notification should be presented.
|
|
///
|
|
/// These rules are stored on the user's homeserver. They are manually configured by the user, who
|
|
/// can create and view them via the Client/Server API.
|
|
///
|
|
/// To create an instance of this type, first create a `SimplePushRuleInit` and convert it via
|
|
/// `SimplePushRule::from` / `.into()`.
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
pub struct SimplePushRule<T> {
|
|
/// Actions to determine if and how a notification is delivered for events matching this rule.
|
|
pub actions: Vec<Action>,
|
|
|
|
/// Whether this is a default rule, or has been set explicitly.
|
|
pub default: bool,
|
|
|
|
/// Whether the push rule is enabled or not.
|
|
pub enabled: bool,
|
|
|
|
/// The ID of this rule.
|
|
///
|
|
/// This is generally the Matrix ID of the entity that it applies to.
|
|
pub rule_id: T,
|
|
}
|
|
|
|
/// Initial set of fields of `SimplePushRule`.
|
|
///
|
|
/// This struct will not be updated even if additional fields are added to `SimplePushRule` in a new
|
|
/// (non-breaking) release of the Matrix specification.
|
|
#[derive(Debug)]
|
|
#[allow(clippy::exhaustive_structs)]
|
|
pub struct SimplePushRuleInit<T> {
|
|
/// Actions to determine if and how a notification is delivered for events matching this rule.
|
|
pub actions: Vec<Action>,
|
|
|
|
/// Whether this is a default rule, or has been set explicitly.
|
|
pub default: bool,
|
|
|
|
/// Whether the push rule is enabled or not.
|
|
pub enabled: bool,
|
|
|
|
/// The ID of this rule.
|
|
///
|
|
/// This is generally the Matrix ID of the entity that it applies to.
|
|
pub rule_id: T,
|
|
}
|
|
|
|
impl<T> From<SimplePushRuleInit<T>> for SimplePushRule<T> {
|
|
fn from(init: SimplePushRuleInit<T>) -> Self {
|
|
let SimplePushRuleInit { actions, default, enabled, rule_id } = init;
|
|
Self { actions, default, enabled, rule_id }
|
|
}
|
|
}
|
|
|
|
// The following trait are needed to be able to make
|
|
// an IndexSet of the type
|
|
|
|
impl<T> Hash for SimplePushRule<T>
|
|
where
|
|
T: Hash,
|
|
{
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
self.rule_id.hash(state);
|
|
}
|
|
}
|
|
|
|
impl<T> PartialEq for SimplePushRule<T>
|
|
where
|
|
T: PartialEq<T>,
|
|
{
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.rule_id == other.rule_id
|
|
}
|
|
}
|
|
|
|
impl<T> Eq for SimplePushRule<T> where T: Eq {}
|
|
|
|
impl<T> Equivalent<SimplePushRule<T>> for str
|
|
where
|
|
T: AsRef<str>,
|
|
{
|
|
fn equivalent(&self, key: &SimplePushRule<T>) -> bool {
|
|
self == key.rule_id.as_ref()
|
|
}
|
|
}
|
|
|
|
/// Like `SimplePushRule`, but with an additional `conditions` field.
|
|
///
|
|
/// Only applicable to underride and override rules.
|
|
///
|
|
/// To create an instance of this type, first create a `ConditionalPushRuleInit` and convert it via
|
|
/// `ConditionalPushRule::from` / `.into()`.
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
pub struct ConditionalPushRule {
|
|
/// Actions to determine if and how a notification is delivered for events matching this rule.
|
|
pub actions: Vec<Action>,
|
|
|
|
/// Whether this is a default rule, or has been set explicitly.
|
|
pub default: bool,
|
|
|
|
/// Whether the push rule is enabled or not.
|
|
pub enabled: bool,
|
|
|
|
/// The ID of this rule.
|
|
pub rule_id: String,
|
|
|
|
/// The conditions that must hold true for an event in order for a rule to be applied to an
|
|
/// event.
|
|
///
|
|
/// A rule with no conditions always matches.
|
|
#[serde(default)]
|
|
pub conditions: Vec<PushCondition>,
|
|
}
|
|
|
|
impl ConditionalPushRule {
|
|
/// Check if the push rule applies to the event.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `event` - The flattened JSON representation of a room message event.
|
|
/// * `context` - The context of the room at the time of the event.
|
|
pub fn applies(&self, event: &FlattenedJson, context: &PushConditionRoomCtx) -> bool {
|
|
if !self.enabled {
|
|
return false;
|
|
}
|
|
|
|
// The old mention rules are disabled when an m.mentions field is present.
|
|
//
|
|
// MSC4210 always disables the legacy rules.
|
|
#[allow(deprecated)]
|
|
if (self.rule_id == PredefinedOverrideRuleId::RoomNotif.as_ref()
|
|
|| self.rule_id == PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
|
|
|| self.rule_id == PredefinedContentRuleId::ContainsUserName.as_ref())
|
|
&& (event.contains_mentions() || cfg!(feature = "unstable-msc4210"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
#[cfg(feature = "unstable-msc3932")]
|
|
{
|
|
// These 3 rules always apply.
|
|
#[allow(deprecated)]
|
|
if self.rule_id != PredefinedOverrideRuleId::Master.as_ref()
|
|
&& self.rule_id != PredefinedOverrideRuleId::RoomNotif.as_ref()
|
|
&& self.rule_id != PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
|
|
{
|
|
// Push rules which don't specify a `room_version_supports` condition are assumed
|
|
// to not support extensible events and are therefore expected to be treated as
|
|
// disabled when a room version does support extensible events.
|
|
let room_supports_ext_ev =
|
|
context.supported_features.contains(&RoomVersionFeature::ExtensibleEvents);
|
|
let rule_has_room_version_supports = self.conditions.iter().any(|condition| {
|
|
matches!(condition, PushCondition::RoomVersionSupports { .. })
|
|
});
|
|
|
|
if room_supports_ext_ev && !rule_has_room_version_supports {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
self.conditions.iter().all(|cond| cond.applies(event, context))
|
|
}
|
|
}
|
|
|
|
/// Initial set of fields of `ConditionalPushRule`.
|
|
///
|
|
/// This struct will not be updated even if additional fields are added to `ConditionalPushRule` in
|
|
/// a new (non-breaking) release of the Matrix specification.
|
|
#[derive(Debug)]
|
|
#[allow(clippy::exhaustive_structs)]
|
|
pub struct ConditionalPushRuleInit {
|
|
/// Actions to determine if and how a notification is delivered for events matching this rule.
|
|
pub actions: Vec<Action>,
|
|
|
|
/// Whether this is a default rule, or has been set explicitly.
|
|
pub default: bool,
|
|
|
|
/// Whether the push rule is enabled or not.
|
|
pub enabled: bool,
|
|
|
|
/// The ID of this rule.
|
|
pub rule_id: String,
|
|
|
|
/// The conditions that must hold true for an event in order for a rule to be applied to an
|
|
/// event.
|
|
///
|
|
/// A rule with no conditions always matches.
|
|
pub conditions: Vec<PushCondition>,
|
|
}
|
|
|
|
impl From<ConditionalPushRuleInit> for ConditionalPushRule {
|
|
fn from(init: ConditionalPushRuleInit) -> Self {
|
|
let ConditionalPushRuleInit { actions, default, enabled, rule_id, conditions } = init;
|
|
Self { actions, default, enabled, rule_id, conditions }
|
|
}
|
|
}
|
|
|
|
// The following trait are needed to be able to make
|
|
// an IndexSet of the type
|
|
|
|
impl Hash for ConditionalPushRule {
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
self.rule_id.hash(state);
|
|
}
|
|
}
|
|
|
|
impl PartialEq for ConditionalPushRule {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.rule_id == other.rule_id
|
|
}
|
|
}
|
|
|
|
impl Eq for ConditionalPushRule {}
|
|
|
|
impl Equivalent<ConditionalPushRule> for str {
|
|
fn equivalent(&self, key: &ConditionalPushRule) -> bool {
|
|
self == key.rule_id
|
|
}
|
|
}
|
|
|
|
/// Like `SimplePushRule`, but with an additional `pattern` field.
|
|
///
|
|
/// Only applicable to content rules.
|
|
///
|
|
/// To create an instance of this type, first create a `PatternedPushRuleInit` and convert it via
|
|
/// `PatternedPushRule::from` / `.into()`.
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
pub struct PatternedPushRule {
|
|
/// Actions to determine if and how a notification is delivered for events matching this rule.
|
|
pub actions: Vec<Action>,
|
|
|
|
/// Whether this is a default rule, or has been set explicitly.
|
|
pub default: bool,
|
|
|
|
/// Whether the push rule is enabled or not.
|
|
pub enabled: bool,
|
|
|
|
/// The ID of this rule.
|
|
pub rule_id: String,
|
|
|
|
/// The glob-style pattern to match against.
|
|
pub pattern: String,
|
|
}
|
|
|
|
impl PatternedPushRule {
|
|
/// Check if the push rule applies to the event.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `event` - The flattened JSON representation of a room message event.
|
|
/// * `context` - The context of the room at the time of the event.
|
|
pub fn applies_to(
|
|
&self,
|
|
key: &str,
|
|
event: &FlattenedJson,
|
|
context: &PushConditionRoomCtx,
|
|
) -> bool {
|
|
// The old mention rules are disabled when an m.mentions field is present.
|
|
#[allow(deprecated)]
|
|
if self.rule_id == PredefinedContentRuleId::ContainsUserName.as_ref()
|
|
&& event.contains_mentions()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if event.get_str("sender").is_some_and(|sender| sender == context.user_id) {
|
|
return false;
|
|
}
|
|
|
|
self.enabled && condition::check_event_match(event, key, &self.pattern, context)
|
|
}
|
|
}
|
|
|
|
/// Initial set of fields of `PatterenedPushRule`.
|
|
///
|
|
/// This struct will not be updated even if additional fields are added to `PatterenedPushRule` in a
|
|
/// new (non-breaking) release of the Matrix specification.
|
|
#[derive(Debug)]
|
|
#[allow(clippy::exhaustive_structs)]
|
|
pub struct PatternedPushRuleInit {
|
|
/// Actions to determine if and how a notification is delivered for events matching this rule.
|
|
pub actions: Vec<Action>,
|
|
|
|
/// Whether this is a default rule, or has been set explicitly.
|
|
pub default: bool,
|
|
|
|
/// Whether the push rule is enabled or not.
|
|
pub enabled: bool,
|
|
|
|
/// The ID of this rule.
|
|
pub rule_id: String,
|
|
|
|
/// The glob-style pattern to match against.
|
|
pub pattern: String,
|
|
}
|
|
|
|
impl From<PatternedPushRuleInit> for PatternedPushRule {
|
|
fn from(init: PatternedPushRuleInit) -> Self {
|
|
let PatternedPushRuleInit { actions, default, enabled, rule_id, pattern } = init;
|
|
Self { actions, default, enabled, rule_id, pattern }
|
|
}
|
|
}
|
|
|
|
// The following trait are needed to be able to make
|
|
// an IndexSet of the type
|
|
|
|
impl Hash for PatternedPushRule {
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
self.rule_id.hash(state);
|
|
}
|
|
}
|
|
|
|
impl PartialEq for PatternedPushRule {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.rule_id == other.rule_id
|
|
}
|
|
}
|
|
|
|
impl Eq for PatternedPushRule {}
|
|
|
|
impl Equivalent<PatternedPushRule> for str {
|
|
fn equivalent(&self, key: &PatternedPushRule) -> bool {
|
|
self == key.rule_id
|
|
}
|
|
}
|
|
|
|
/// Information for a pusher using the Push Gateway API.
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
pub struct HttpPusherData {
|
|
/// The URL to use to send notifications to.
|
|
///
|
|
/// Required if the pusher's kind is http.
|
|
pub url: String,
|
|
|
|
/// The format to use when sending notifications to the Push Gateway.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub format: Option<PushFormat>,
|
|
|
|
/// Custom data for the pusher.
|
|
#[serde(flatten, default, skip_serializing_if = "JsonObject::is_empty")]
|
|
pub data: JsonObject,
|
|
}
|
|
|
|
impl HttpPusherData {
|
|
/// Creates a new `HttpPusherData` with the given URL.
|
|
pub fn new(url: String) -> Self {
|
|
Self { url, format: None, data: JsonObject::default() }
|
|
}
|
|
}
|
|
|
|
/// A special format that the homeserver should use when sending notifications to a Push Gateway.
|
|
/// Currently, only `event_id_only` is supported, see the [Push Gateway API][spec].
|
|
///
|
|
/// [spec]: https://spec.matrix.org/latest/push-gateway-api/#homeserver-behaviour
|
|
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
|
#[derive(Clone, PartialEq, Eq, StringEnum)]
|
|
#[ruma_enum(rename_all = "snake_case")]
|
|
#[non_exhaustive]
|
|
pub enum PushFormat {
|
|
/// Require the homeserver to only send a reduced set of fields in the push.
|
|
EventIdOnly,
|
|
|
|
#[doc(hidden)]
|
|
_Custom(PrivOwnedStr),
|
|
}
|
|
|
|
/// The kinds of push rules that are available.
|
|
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
|
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, StringEnum)]
|
|
#[ruma_enum(rename_all = "snake_case")]
|
|
#[non_exhaustive]
|
|
pub enum RuleKind {
|
|
/// User-configured rules that override all other kinds.
|
|
Override,
|
|
|
|
/// Lowest priority user-defined rules.
|
|
Underride,
|
|
|
|
/// Sender-specific rules.
|
|
Sender,
|
|
|
|
/// Room-specific rules.
|
|
Room,
|
|
|
|
/// Content-specific rules.
|
|
Content,
|
|
|
|
#[doc(hidden)]
|
|
_Custom(PrivOwnedStr),
|
|
}
|
|
|
|
/// A push rule to update or create.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
pub enum NewPushRule {
|
|
/// Rules that override all other kinds.
|
|
Override(NewConditionalPushRule),
|
|
|
|
/// Content-specific rules.
|
|
Content(NewPatternedPushRule),
|
|
|
|
/// Room-specific rules.
|
|
Room(NewSimplePushRule<OwnedRoomId>),
|
|
|
|
/// Sender-specific rules.
|
|
Sender(NewSimplePushRule<OwnedUserId>),
|
|
|
|
/// Lowest priority rules.
|
|
Underride(NewConditionalPushRule),
|
|
}
|
|
|
|
impl NewPushRule {
|
|
/// The kind of this `NewPushRule`.
|
|
pub fn kind(&self) -> RuleKind {
|
|
match self {
|
|
NewPushRule::Override(_) => RuleKind::Override,
|
|
NewPushRule::Content(_) => RuleKind::Content,
|
|
NewPushRule::Room(_) => RuleKind::Room,
|
|
NewPushRule::Sender(_) => RuleKind::Sender,
|
|
NewPushRule::Underride(_) => RuleKind::Underride,
|
|
}
|
|
}
|
|
|
|
/// The ID of this `NewPushRule`.
|
|
pub fn rule_id(&self) -> &str {
|
|
match self {
|
|
NewPushRule::Override(r) => &r.rule_id,
|
|
NewPushRule::Content(r) => &r.rule_id,
|
|
NewPushRule::Room(r) => r.rule_id.as_ref(),
|
|
NewPushRule::Sender(r) => r.rule_id.as_ref(),
|
|
NewPushRule::Underride(r) => &r.rule_id,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A simple push rule to update or create.
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
pub struct NewSimplePushRule<T> {
|
|
/// The ID of this rule.
|
|
///
|
|
/// This is generally the Matrix ID of the entity that it applies to.
|
|
pub rule_id: T,
|
|
|
|
/// Actions to determine if and how a notification is delivered for events matching this
|
|
/// rule.
|
|
pub actions: Vec<Action>,
|
|
}
|
|
|
|
impl<T> NewSimplePushRule<T> {
|
|
/// Creates a `NewSimplePushRule` with the given ID and actions.
|
|
pub fn new(rule_id: T, actions: Vec<Action>) -> Self {
|
|
Self { rule_id, actions }
|
|
}
|
|
}
|
|
|
|
impl<T> From<NewSimplePushRule<T>> for SimplePushRule<T> {
|
|
fn from(new_rule: NewSimplePushRule<T>) -> 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)]
|
|
pub struct NewPatternedPushRule {
|
|
/// The ID of this rule.
|
|
pub rule_id: String,
|
|
|
|
/// The glob-style pattern to match against.
|
|
pub pattern: String,
|
|
|
|
/// Actions to determine if and how a notification is delivered for events matching this
|
|
/// rule.
|
|
pub actions: Vec<Action>,
|
|
}
|
|
|
|
impl NewPatternedPushRule {
|
|
/// Creates a `NewPatternedPushRule` with the given ID, pattern and actions.
|
|
pub fn new(rule_id: String, pattern: String, actions: Vec<Action>) -> Self {
|
|
Self { rule_id, pattern, actions }
|
|
}
|
|
}
|
|
|
|
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.
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
pub struct NewConditionalPushRule {
|
|
/// The ID of this rule.
|
|
pub rule_id: String,
|
|
|
|
/// The conditions that must hold true for an event in order for a rule to be applied to an
|
|
/// event.
|
|
///
|
|
/// A rule with no conditions always matches.
|
|
#[serde(default)]
|
|
pub conditions: Vec<PushCondition>,
|
|
|
|
/// Actions to determine if and how a notification is delivered for events matching this
|
|
/// rule.
|
|
pub actions: Vec<Action>,
|
|
}
|
|
|
|
impl NewConditionalPushRule {
|
|
/// Creates a `NewConditionalPushRule` with the given ID, conditions and actions.
|
|
pub fn new(rule_id: String, conditions: Vec<PushCondition>, actions: Vec<Action>) -> Self {
|
|
Self { rule_id, conditions, actions }
|
|
}
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
/// The error type returned when trying modify a push rule that could not be found in a `Ruleset`.
|
|
#[derive(Debug, Error)]
|
|
#[non_exhaustive]
|
|
#[error("The rule could not be found")]
|
|
pub struct RuleNotFoundError;
|
|
|
|
/// 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(())
|
|
}
|
|
|
|
/// The error type returned when trying to remove a user-defined push rule from a `Ruleset`.
|
|
#[derive(Debug, Error)]
|
|
#[non_exhaustive]
|
|
pub enum RemovePushRuleError {
|
|
/// The rule is a server-default rules and they can't be removed.
|
|
#[error("server-default rules cannot be removed")]
|
|
ServerDefault,
|
|
|
|
/// The rule was not found.
|
|
#[error("rule not found")]
|
|
NotFound,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::collections::BTreeMap;
|
|
|
|
use assert_matches2::assert_matches;
|
|
use js_int::{int, uint};
|
|
use serde_json::{
|
|
from_value as from_json_value, json, to_value as to_json_value,
|
|
value::RawValue as RawJsonValue, Value as JsonValue,
|
|
};
|
|
|
|
use super::{
|
|
action::{Action, Tweak},
|
|
condition::{
|
|
PushCondition, PushConditionPowerLevelsCtx, PushConditionRoomCtx, RoomMemberCountIs,
|
|
},
|
|
AnyPushRule, ConditionalPushRule, PatternedPushRule, Ruleset, SimplePushRule,
|
|
};
|
|
use crate::{
|
|
owned_room_id, owned_user_id,
|
|
power_levels::NotificationPowerLevels,
|
|
push::{PredefinedContentRuleId, PredefinedOverrideRuleId},
|
|
serde::Raw,
|
|
user_id,
|
|
};
|
|
|
|
fn example_ruleset() -> Ruleset {
|
|
let mut set = Ruleset::new();
|
|
|
|
set.override_.insert(ConditionalPushRule {
|
|
conditions: vec![PushCondition::EventMatch {
|
|
key: "type".into(),
|
|
pattern: "m.call.invite".into(),
|
|
}],
|
|
actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))],
|
|
rule_id: ".m.rule.call".into(),
|
|
enabled: true,
|
|
default: true,
|
|
});
|
|
|
|
set
|
|
}
|
|
|
|
fn power_levels() -> PushConditionPowerLevelsCtx {
|
|
PushConditionPowerLevelsCtx {
|
|
users: BTreeMap::new(),
|
|
users_default: int!(50),
|
|
notifications: NotificationPowerLevels { room: int!(50) },
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn iter() {
|
|
let mut set = example_ruleset();
|
|
|
|
let added = set.override_.insert(ConditionalPushRule {
|
|
conditions: vec![PushCondition::EventMatch {
|
|
key: "room_id".into(),
|
|
pattern: "!roomid:matrix.org".into(),
|
|
}],
|
|
actions: vec![],
|
|
rule_id: "!roomid:matrix.org".into(),
|
|
enabled: true,
|
|
default: false,
|
|
});
|
|
assert!(added);
|
|
|
|
let added = set.override_.insert(ConditionalPushRule {
|
|
conditions: vec![],
|
|
actions: vec![],
|
|
rule_id: ".m.rule.suppress_notices".into(),
|
|
enabled: false,
|
|
default: true,
|
|
});
|
|
assert!(added);
|
|
|
|
let mut iter = set.into_iter();
|
|
|
|
let rule_opt = iter.next();
|
|
assert!(rule_opt.is_some());
|
|
assert_matches!(
|
|
rule_opt.unwrap(),
|
|
AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
|
|
);
|
|
assert_eq!(rule_id, ".m.rule.call");
|
|
|
|
let rule_opt = iter.next();
|
|
assert!(rule_opt.is_some());
|
|
assert_matches!(
|
|
rule_opt.unwrap(),
|
|
AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
|
|
);
|
|
assert_eq!(rule_id, "!roomid:matrix.org");
|
|
|
|
let rule_opt = iter.next();
|
|
assert!(rule_opt.is_some());
|
|
assert_matches!(
|
|
rule_opt.unwrap(),
|
|
AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
|
|
);
|
|
assert_eq!(rule_id, ".m.rule.suppress_notices");
|
|
|
|
assert_matches!(iter.next(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_conditional_push_rule() {
|
|
let rule = ConditionalPushRule {
|
|
actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))],
|
|
default: true,
|
|
enabled: true,
|
|
rule_id: ".m.rule.call".into(),
|
|
conditions: vec![
|
|
PushCondition::EventMatch { key: "type".into(), pattern: "m.call.invite".into() },
|
|
PushCondition::ContainsDisplayName,
|
|
PushCondition::RoomMemberCount { is: RoomMemberCountIs::gt(uint!(2)) },
|
|
PushCondition::SenderNotificationPermission { key: "room".into() },
|
|
],
|
|
};
|
|
|
|
let rule_value: JsonValue = to_json_value(rule).unwrap();
|
|
assert_eq!(
|
|
rule_value,
|
|
json!({
|
|
"conditions": [
|
|
{
|
|
"kind": "event_match",
|
|
"key": "type",
|
|
"pattern": "m.call.invite"
|
|
},
|
|
{
|
|
"kind": "contains_display_name"
|
|
},
|
|
{
|
|
"kind": "room_member_count",
|
|
"is": ">2"
|
|
},
|
|
{
|
|
"kind": "sender_notification_permission",
|
|
"key": "room"
|
|
}
|
|
],
|
|
"actions": [
|
|
"notify",
|
|
{
|
|
"set_tweak": "highlight"
|
|
}
|
|
],
|
|
"rule_id": ".m.rule.call",
|
|
"default": true,
|
|
"enabled": true
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_simple_push_rule() {
|
|
let rule = SimplePushRule {
|
|
actions: vec![Action::Notify],
|
|
default: false,
|
|
enabled: false,
|
|
rule_id: owned_room_id!("!roomid:server.name"),
|
|
};
|
|
|
|
let rule_value: JsonValue = to_json_value(rule).unwrap();
|
|
assert_eq!(
|
|
rule_value,
|
|
json!({
|
|
"actions": [
|
|
"notify"
|
|
],
|
|
"rule_id": "!roomid:server.name",
|
|
"default": false,
|
|
"enabled": false
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_patterned_push_rule() {
|
|
let rule = PatternedPushRule {
|
|
actions: vec![
|
|
Action::Notify,
|
|
Action::SetTweak(Tweak::Sound("default".into())),
|
|
Action::SetTweak(Tweak::Custom {
|
|
name: "dance".into(),
|
|
value: RawJsonValue::from_string("true".into()).unwrap(),
|
|
}),
|
|
],
|
|
default: true,
|
|
enabled: true,
|
|
pattern: "user_id".into(),
|
|
rule_id: ".m.rule.contains_user_name".into(),
|
|
};
|
|
|
|
let rule_value: JsonValue = to_json_value(rule).unwrap();
|
|
assert_eq!(
|
|
rule_value,
|
|
json!({
|
|
"actions": [
|
|
"notify",
|
|
{
|
|
"set_tweak": "sound",
|
|
"value": "default"
|
|
},
|
|
{
|
|
"set_tweak": "dance",
|
|
"value": true
|
|
}
|
|
],
|
|
"pattern": "user_id",
|
|
"rule_id": ".m.rule.contains_user_name",
|
|
"default": true,
|
|
"enabled": true
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_ruleset() {
|
|
let mut set = example_ruleset();
|
|
|
|
set.override_.insert(ConditionalPushRule {
|
|
conditions: vec![
|
|
PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)) },
|
|
PushCondition::EventMatch { key: "type".into(), pattern: "m.room.message".into() },
|
|
],
|
|
actions: vec![
|
|
Action::Notify,
|
|
Action::SetTweak(Tweak::Sound("default".into())),
|
|
Action::SetTweak(Tweak::Highlight(false)),
|
|
],
|
|
rule_id: ".m.rule.room_one_to_one".into(),
|
|
enabled: true,
|
|
default: true,
|
|
});
|
|
set.content.insert(PatternedPushRule {
|
|
actions: vec![
|
|
Action::Notify,
|
|
Action::SetTweak(Tweak::Sound("default".into())),
|
|
Action::SetTweak(Tweak::Highlight(true)),
|
|
],
|
|
rule_id: ".m.rule.contains_user_name".into(),
|
|
pattern: "user_id".into(),
|
|
enabled: true,
|
|
default: true,
|
|
});
|
|
|
|
let set_value: JsonValue = to_json_value(set).unwrap();
|
|
assert_eq!(
|
|
set_value,
|
|
json!({
|
|
"override": [
|
|
{
|
|
"actions": [
|
|
"notify",
|
|
{
|
|
"set_tweak": "highlight",
|
|
},
|
|
],
|
|
"conditions": [
|
|
{
|
|
"kind": "event_match",
|
|
"key": "type",
|
|
"pattern": "m.call.invite"
|
|
},
|
|
],
|
|
"rule_id": ".m.rule.call",
|
|
"default": true,
|
|
"enabled": true,
|
|
},
|
|
{
|
|
"conditions": [
|
|
{
|
|
"kind": "room_member_count",
|
|
"is": "2"
|
|
},
|
|
{
|
|
"kind": "event_match",
|
|
"key": "type",
|
|
"pattern": "m.room.message"
|
|
}
|
|
],
|
|
"actions": [
|
|
"notify",
|
|
{
|
|
"set_tweak": "sound",
|
|
"value": "default"
|
|
},
|
|
{
|
|
"set_tweak": "highlight",
|
|
"value": false
|
|
}
|
|
],
|
|
"rule_id": ".m.rule.room_one_to_one",
|
|
"default": true,
|
|
"enabled": true
|
|
},
|
|
],
|
|
"content": [
|
|
{
|
|
"actions": [
|
|
"notify",
|
|
{
|
|
"set_tweak": "sound",
|
|
"value": "default"
|
|
},
|
|
{
|
|
"set_tweak": "highlight"
|
|
}
|
|
],
|
|
"pattern": "user_id",
|
|
"rule_id": ".m.rule.contains_user_name",
|
|
"default": true,
|
|
"enabled": true
|
|
}
|
|
],
|
|
"room": [],
|
|
"sender": [],
|
|
"underride": [],
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn deserialize_patterned_push_rule() {
|
|
let rule = from_json_value::<PatternedPushRule>(json!({
|
|
"actions": [
|
|
"notify",
|
|
{
|
|
"set_tweak": "sound",
|
|
"value": "default"
|
|
},
|
|
{
|
|
"set_tweak": "highlight",
|
|
"value": true
|
|
}
|
|
],
|
|
"pattern": "user_id",
|
|
"rule_id": ".m.rule.contains_user_name",
|
|
"default": true,
|
|
"enabled": true
|
|
}))
|
|
.unwrap();
|
|
assert!(rule.default);
|
|
assert!(rule.enabled);
|
|
assert_eq!(rule.pattern, "user_id");
|
|
assert_eq!(rule.rule_id, ".m.rule.contains_user_name");
|
|
|
|
let mut iter = rule.actions.iter();
|
|
assert_matches!(iter.next(), Some(Action::Notify));
|
|
assert_matches!(iter.next(), Some(Action::SetTweak(Tweak::Sound(sound))));
|
|
assert_eq!(sound, "default");
|
|
assert_matches!(iter.next(), Some(Action::SetTweak(Tweak::Highlight(true))));
|
|
assert_matches!(iter.next(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn deserialize_ruleset() {
|
|
let set: Ruleset = from_json_value(json!({
|
|
"override": [
|
|
{
|
|
"actions": [],
|
|
"conditions": [],
|
|
"rule_id": "!roomid:server.name",
|
|
"default": false,
|
|
"enabled": true
|
|
},
|
|
{
|
|
"actions": [],
|
|
"conditions": [],
|
|
"rule_id": ".m.rule.call",
|
|
"default": true,
|
|
"enabled": true
|
|
},
|
|
],
|
|
"underride": [
|
|
{
|
|
"actions": [],
|
|
"conditions": [],
|
|
"rule_id": ".m.rule.room_one_to_one",
|
|
"default": true,
|
|
"enabled": true
|
|
},
|
|
],
|
|
"room": [
|
|
{
|
|
"actions": [],
|
|
"rule_id": "!roomid:server.name",
|
|
"default": false,
|
|
"enabled": false
|
|
}
|
|
],
|
|
"sender": [],
|
|
"content": [
|
|
{
|
|
"actions": [],
|
|
"pattern": "user_id",
|
|
"rule_id": ".m.rule.contains_user_name",
|
|
"default": true,
|
|
"enabled": true
|
|
},
|
|
{
|
|
"actions": [],
|
|
"pattern": "ruma",
|
|
"rule_id": "ruma",
|
|
"default": false,
|
|
"enabled": true
|
|
}
|
|
]
|
|
}))
|
|
.unwrap();
|
|
|
|
let mut iter = set.into_iter();
|
|
|
|
let rule_opt = iter.next();
|
|
assert!(rule_opt.is_some());
|
|
assert_matches!(
|
|
rule_opt.unwrap(),
|
|
AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
|
|
);
|
|
assert_eq!(rule_id, "!roomid:server.name");
|
|
|
|
let rule_opt = iter.next();
|
|
assert!(rule_opt.is_some());
|
|
assert_matches!(
|
|
rule_opt.unwrap(),
|
|
AnyPushRule::Override(ConditionalPushRule { rule_id, .. })
|
|
);
|
|
assert_eq!(rule_id, ".m.rule.call");
|
|
|
|
let rule_opt = iter.next();
|
|
assert!(rule_opt.is_some());
|
|
assert_matches!(rule_opt.unwrap(), AnyPushRule::Content(PatternedPushRule { rule_id, .. }));
|
|
assert_eq!(rule_id, ".m.rule.contains_user_name");
|
|
|
|
let rule_opt = iter.next();
|
|
assert!(rule_opt.is_some());
|
|
assert_matches!(rule_opt.unwrap(), AnyPushRule::Content(PatternedPushRule { rule_id, .. }));
|
|
assert_eq!(rule_id, "ruma");
|
|
|
|
let rule_opt = iter.next();
|
|
assert!(rule_opt.is_some());
|
|
assert_matches!(rule_opt.unwrap(), AnyPushRule::Room(SimplePushRule { rule_id, .. }));
|
|
assert_eq!(rule_id, "!roomid:server.name");
|
|
|
|
let rule_opt = iter.next();
|
|
assert!(rule_opt.is_some());
|
|
assert_matches!(
|
|
rule_opt.unwrap(),
|
|
AnyPushRule::Underride(ConditionalPushRule { rule_id, .. })
|
|
);
|
|
assert_eq!(rule_id, ".m.rule.room_one_to_one");
|
|
|
|
assert_matches!(iter.next(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn default_ruleset_applies() {
|
|
let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
|
|
|
|
let context_one_to_one = &PushConditionRoomCtx {
|
|
room_id: owned_room_id!("!dm:server.name"),
|
|
member_count: uint!(2),
|
|
user_id: owned_user_id!("@jj:server.name"),
|
|
user_display_name: "Jolly Jumper".into(),
|
|
power_levels: Some(power_levels()),
|
|
#[cfg(feature = "unstable-msc3931")]
|
|
supported_features: Default::default(),
|
|
};
|
|
|
|
let context_public_room = &PushConditionRoomCtx {
|
|
room_id: owned_room_id!("!far_west:server.name"),
|
|
member_count: uint!(100),
|
|
user_id: owned_user_id!("@jj:server.name"),
|
|
user_display_name: "Jolly Jumper".into(),
|
|
power_levels: Some(power_levels()),
|
|
#[cfg(feature = "unstable-msc3931")]
|
|
supported_features: Default::default(),
|
|
};
|
|
|
|
let message = serde_json::from_str::<Raw<JsonValue>>(
|
|
r#"{
|
|
"type": "m.room.message"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_matches!(
|
|
set.get_actions(&message, context_one_to_one),
|
|
[
|
|
Action::Notify,
|
|
Action::SetTweak(Tweak::Sound(_)),
|
|
Action::SetTweak(Tweak::Highlight(false))
|
|
]
|
|
);
|
|
assert_matches!(
|
|
set.get_actions(&message, context_public_room),
|
|
[Action::Notify, Action::SetTweak(Tweak::Highlight(false))]
|
|
);
|
|
|
|
let user_name = serde_json::from_str::<Raw<JsonValue>>(
|
|
r#"{
|
|
"type": "m.room.message",
|
|
"content": {
|
|
"body": "Hi jolly_jumper!"
|
|
}
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_matches!(
|
|
set.get_actions(&user_name, context_one_to_one),
|
|
[
|
|
Action::Notify,
|
|
Action::SetTweak(Tweak::Sound(_)),
|
|
Action::SetTweak(Tweak::Highlight(true)),
|
|
]
|
|
);
|
|
assert_matches!(
|
|
set.get_actions(&user_name, context_public_room),
|
|
[
|
|
Action::Notify,
|
|
Action::SetTweak(Tweak::Sound(_)),
|
|
Action::SetTweak(Tweak::Highlight(true)),
|
|
]
|
|
);
|
|
|
|
let notice = serde_json::from_str::<Raw<JsonValue>>(
|
|
r#"{
|
|
"type": "m.room.message",
|
|
"content": {
|
|
"msgtype": "m.notice"
|
|
}
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
assert_matches!(set.get_actions(¬ice, context_one_to_one), []);
|
|
|
|
let at_room = serde_json::from_str::<Raw<JsonValue>>(
|
|
r#"{
|
|
"type": "m.room.message",
|
|
"sender": "@rantanplan:server.name",
|
|
"content": {
|
|
"body": "@room Attention please!",
|
|
"msgtype": "m.text"
|
|
}
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_matches!(
|
|
set.get_actions(&at_room, context_public_room),
|
|
[Action::Notify, Action::SetTweak(Tweak::Highlight(true)),]
|
|
);
|
|
|
|
let empty = serde_json::from_str::<Raw<JsonValue>>(r#"{}"#).unwrap();
|
|
assert_matches!(set.get_actions(&empty, context_one_to_one), []);
|
|
}
|
|
|
|
#[test]
|
|
fn custom_ruleset_applies() {
|
|
let context_one_to_one = &PushConditionRoomCtx {
|
|
room_id: owned_room_id!("!dm:server.name"),
|
|
member_count: uint!(2),
|
|
user_id: owned_user_id!("@jj:server.name"),
|
|
user_display_name: "Jolly Jumper".into(),
|
|
power_levels: Some(power_levels()),
|
|
#[cfg(feature = "unstable-msc3931")]
|
|
supported_features: Default::default(),
|
|
};
|
|
|
|
let message = serde_json::from_str::<Raw<JsonValue>>(
|
|
r#"{
|
|
"sender": "@rantanplan:server.name",
|
|
"type": "m.room.message",
|
|
"content": {
|
|
"msgtype": "m.text",
|
|
"body": "Great joke!"
|
|
}
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let mut set = Ruleset::new();
|
|
let disabled = ConditionalPushRule {
|
|
actions: vec![Action::Notify],
|
|
default: false,
|
|
enabled: false,
|
|
rule_id: "disabled".into(),
|
|
conditions: vec![PushCondition::RoomMemberCount {
|
|
is: RoomMemberCountIs::from(uint!(2)),
|
|
}],
|
|
};
|
|
set.underride.insert(disabled);
|
|
|
|
let test_set = set.clone();
|
|
assert_matches!(test_set.get_actions(&message, context_one_to_one), []);
|
|
|
|
let no_conditions = ConditionalPushRule {
|
|
actions: vec![Action::SetTweak(Tweak::Highlight(true))],
|
|
default: false,
|
|
enabled: true,
|
|
rule_id: "no.conditions".into(),
|
|
conditions: vec![],
|
|
};
|
|
set.underride.insert(no_conditions);
|
|
|
|
let test_set = set.clone();
|
|
assert_matches!(
|
|
test_set.get_actions(&message, context_one_to_one),
|
|
[Action::SetTweak(Tweak::Highlight(true))]
|
|
);
|
|
|
|
let sender = SimplePushRule {
|
|
actions: vec![Action::Notify],
|
|
default: false,
|
|
enabled: true,
|
|
rule_id: owned_user_id!("@rantanplan:server.name"),
|
|
};
|
|
set.sender.insert(sender);
|
|
|
|
let test_set = set.clone();
|
|
assert_matches!(test_set.get_actions(&message, context_one_to_one), [Action::Notify]);
|
|
|
|
let room = SimplePushRule {
|
|
actions: vec![Action::SetTweak(Tweak::Highlight(true))],
|
|
default: false,
|
|
enabled: true,
|
|
rule_id: owned_room_id!("!dm:server.name"),
|
|
};
|
|
set.room.insert(room);
|
|
|
|
let test_set = set.clone();
|
|
assert_matches!(
|
|
test_set.get_actions(&message, context_one_to_one),
|
|
[Action::SetTweak(Tweak::Highlight(true))]
|
|
);
|
|
|
|
let content = PatternedPushRule {
|
|
actions: vec![Action::SetTweak(Tweak::Sound("content".into()))],
|
|
default: false,
|
|
enabled: true,
|
|
rule_id: "content".into(),
|
|
pattern: "joke".into(),
|
|
};
|
|
set.content.insert(content);
|
|
|
|
let test_set = set.clone();
|
|
assert_matches!(
|
|
test_set.get_actions(&message, context_one_to_one),
|
|
[Action::SetTweak(Tweak::Sound(sound))]
|
|
);
|
|
assert_eq!(sound, "content");
|
|
|
|
let three_conditions = ConditionalPushRule {
|
|
actions: vec![Action::SetTweak(Tweak::Sound("three".into()))],
|
|
default: false,
|
|
enabled: true,
|
|
rule_id: "three.conditions".into(),
|
|
conditions: vec![
|
|
PushCondition::RoomMemberCount { is: RoomMemberCountIs::from(uint!(2)) },
|
|
PushCondition::ContainsDisplayName,
|
|
PushCondition::EventMatch {
|
|
key: "room_id".into(),
|
|
pattern: "!dm:server.name".into(),
|
|
},
|
|
],
|
|
};
|
|
set.override_.insert(three_conditions);
|
|
|
|
assert_matches!(
|
|
set.get_actions(&message, context_one_to_one),
|
|
[Action::SetTweak(Tweak::Sound(sound))]
|
|
);
|
|
assert_eq!(sound, "content");
|
|
|
|
let new_message = serde_json::from_str::<Raw<JsonValue>>(
|
|
r#"{
|
|
"sender": "@rantanplan:server.name",
|
|
"type": "m.room.message",
|
|
"content": {
|
|
"msgtype": "m.text",
|
|
"body": "Tell me another one, Jolly Jumper!"
|
|
}
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_matches!(
|
|
set.get_actions(&new_message, context_one_to_one),
|
|
[Action::SetTweak(Tweak::Sound(sound))]
|
|
);
|
|
assert_eq!(sound, "three");
|
|
}
|
|
|
|
#[test]
|
|
#[allow(deprecated)]
|
|
fn old_mentions_apply() {
|
|
let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
|
|
|
|
let context = &PushConditionRoomCtx {
|
|
room_id: owned_room_id!("!far_west:server.name"),
|
|
member_count: uint!(100),
|
|
user_id: owned_user_id!("@jj:server.name"),
|
|
user_display_name: "Jolly Jumper".into(),
|
|
power_levels: Some(power_levels()),
|
|
#[cfg(feature = "unstable-msc3931")]
|
|
supported_features: Default::default(),
|
|
};
|
|
|
|
let message = serde_json::from_str::<Raw<JsonValue>>(
|
|
r#"{
|
|
"content": {
|
|
"body": "jolly_jumper"
|
|
},
|
|
"type": "m.room.message"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
set.get_match(&message, context).unwrap().rule_id(),
|
|
PredefinedContentRuleId::ContainsUserName.as_ref()
|
|
);
|
|
|
|
let message = serde_json::from_str::<Raw<JsonValue>>(
|
|
r#"{
|
|
"content": {
|
|
"body": "jolly_jumper",
|
|
"m.mentions": {}
|
|
},
|
|
"type": "m.room.message"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_ne!(
|
|
set.get_match(&message, context).unwrap().rule_id(),
|
|
PredefinedContentRuleId::ContainsUserName.as_ref()
|
|
);
|
|
|
|
let message = serde_json::from_str::<Raw<JsonValue>>(
|
|
r#"{
|
|
"content": {
|
|
"body": "Jolly Jumper"
|
|
},
|
|
"type": "m.room.message"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
set.get_match(&message, context).unwrap().rule_id(),
|
|
PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
|
|
);
|
|
|
|
let message = serde_json::from_str::<Raw<JsonValue>>(
|
|
r#"{
|
|
"content": {
|
|
"body": "Jolly Jumper",
|
|
"m.mentions": {}
|
|
},
|
|
"type": "m.room.message"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_ne!(
|
|
set.get_match(&message, context).unwrap().rule_id(),
|
|
PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
|
|
);
|
|
|
|
let message = serde_json::from_str::<Raw<JsonValue>>(
|
|
r#"{
|
|
"content": {
|
|
"body": "@room"
|
|
},
|
|
"sender": "@admin:server.name",
|
|
"type": "m.room.message"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
set.get_match(&message, context).unwrap().rule_id(),
|
|
PredefinedOverrideRuleId::RoomNotif.as_ref()
|
|
);
|
|
|
|
let message = serde_json::from_str::<Raw<JsonValue>>(
|
|
r#"{
|
|
"content": {
|
|
"body": "@room",
|
|
"m.mentions": {}
|
|
},
|
|
"sender": "@admin:server.name",
|
|
"type": "m.room.message"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_ne!(
|
|
set.get_match(&message, context).unwrap().rule_id(),
|
|
PredefinedOverrideRuleId::RoomNotif.as_ref()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn intentional_mentions_apply() {
|
|
let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
|
|
|
|
let context = &PushConditionRoomCtx {
|
|
room_id: owned_room_id!("!far_west:server.name"),
|
|
member_count: uint!(100),
|
|
user_id: owned_user_id!("@jj:server.name"),
|
|
user_display_name: "Jolly Jumper".into(),
|
|
power_levels: Some(power_levels()),
|
|
#[cfg(feature = "unstable-msc3931")]
|
|
supported_features: Default::default(),
|
|
};
|
|
|
|
let message = serde_json::from_str::<Raw<JsonValue>>(
|
|
r#"{
|
|
"content": {
|
|
"body": "Hey jolly_jumper!",
|
|
"m.mentions": {
|
|
"user_ids": ["@jolly_jumper:server.name"]
|
|
}
|
|
},
|
|
"sender": "@admin:server.name",
|
|
"type": "m.room.message"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
set.get_match(&message, context).unwrap().rule_id(),
|
|
PredefinedOverrideRuleId::IsUserMention.as_ref()
|
|
);
|
|
|
|
let message = serde_json::from_str::<Raw<JsonValue>>(
|
|
r#"{
|
|
"content": {
|
|
"body": "Listen room!",
|
|
"m.mentions": {
|
|
"room": true
|
|
}
|
|
},
|
|
"sender": "@admin:server.name",
|
|
"type": "m.room.message"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
set.get_match(&message, context).unwrap().rule_id(),
|
|
PredefinedOverrideRuleId::IsRoomMention.as_ref()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invite_for_me_applies() {
|
|
let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
|
|
|
|
let context = &PushConditionRoomCtx {
|
|
room_id: owned_room_id!("!far_west:server.name"),
|
|
member_count: uint!(100),
|
|
user_id: owned_user_id!("@jj:server.name"),
|
|
user_display_name: "Jolly Jumper".into(),
|
|
// `invite_state` usually doesn't include the power levels.
|
|
power_levels: None,
|
|
#[cfg(feature = "unstable-msc3931")]
|
|
supported_features: Default::default(),
|
|
};
|
|
|
|
let message = serde_json::from_str::<Raw<JsonValue>>(
|
|
r#"{
|
|
"content": {
|
|
"membership": "invite"
|
|
},
|
|
"state_key": "@jolly_jumper:server.name",
|
|
"sender": "@admin:server.name",
|
|
"type": "m.room.member"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
set.get_match(&message, context).unwrap().rule_id(),
|
|
PredefinedOverrideRuleId::InviteForMe.as_ref()
|
|
);
|
|
}
|
|
}
|