From 596fc3c3dff3b7c7175e071baac466355eacf678 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 8 Jul 2019 17:48:23 -0700 Subject: [PATCH] Convert m.push_rules to the new API. --- src/lib.rs | 2 +- src/push_rules.rs | 564 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 509 insertions(+), 57 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e614c7f3..e380879f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,7 +133,7 @@ pub mod fully_read; pub mod ignored_user_list; pub mod key; pub mod presence; -// pub mod push_rules; +pub mod push_rules; pub mod receipt; pub mod room; pub mod room_key; diff --git a/src/push_rules.rs b/src/push_rules.rs index b44b3a5d..5bd29292 100644 --- a/src/push_rules.rs +++ b/src/push_rules.rs @@ -5,20 +5,25 @@ use std::{ str::FromStr, }; -use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; +use ruma_events_macros::ruma_event; +use serde::{ + de::{Error, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_json::{from_value, Value}; use super::{default_true, FromStrError}; -event! { +ruma_event! { /// Describes all push rules for a user. - pub struct PushRulesEvent(PushRulesEventContent) {} -} - -/// The payload of an *m.push_rules* event. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct PushRulesEventContent { - /// The global ruleset. - pub global: Ruleset, + PushRulesEvent { + kind: Event, + event_type: PushRules, + content: { + /// The global ruleset. + pub global: Ruleset, + }, + } } /// A push ruleset scopes a set of rules according to some criteria. @@ -28,11 +33,11 @@ pub struct PushRulesEventContent { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct Ruleset { /// These rules configure behaviour for (unencrypted) messages that match certain patterns. - pub content: Vec, + pub content: Vec, /// These user-configured rules are given the highest priority. #[serde(rename = "override")] - pub override_rules: Vec, + pub override_rules: Vec, /// These rules change the behaviour of all messages for a given room. pub room: Vec, @@ -42,7 +47,7 @@ pub struct Ruleset { /// These rules are identical to override rules, but have a lower priority than `content`, /// `room` and `sender` rules. - pub underride: Vec, + pub underride: Vec, } /// A push rule is a single rule that states under what conditions an event should be passed onto a @@ -63,18 +68,50 @@ pub struct PushRule { /// The ID of this rule. pub rule_id: String, +} + +/// Like `PushRule`, but with an additional `conditions` field. +/// +/// Only applicable to underride and override rules. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct ConditionalPushRule { + /// Actions to determine if and how a notification is delivered for events matching this rule. + pub actions: Vec, + + /// 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. - /// - /// Only applicable to underride and override rules. - pub conditions: Option>, + pub conditions: Vec, +} + +/// Like `PushRule`, but with an additional `pattern` field. +/// +/// Only applicable to content rules. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct PatternedPushRule { + /// Actions to determine if and how a notification is delivered for events matching this rule. + pub actions: Vec, + + /// 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. - /// - /// Only applicable to content rules. - pub pattern: Option, + pub pattern: String, } /// An action affects if and how a notification is delivered for a matching event. @@ -204,59 +241,190 @@ pub enum Tweak { } /// A condition that must apply for an associated push rule's action to be taken. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct PushCondition { - /// The kind of condition to apply. - pub kind: PushConditionKind, - - /// Required for `event_match` conditions. The dot-separated field of the event to match. - /// - /// Required for `sender_notification_permission` conditions. The field in the power level event - /// the user needs a minimum power level for. Fields must be specified under the `notifications` - /// property in the power level event's `content`. - pub key: Option, - - /// Required for `event_match` conditions. The glob-style pattern to match against. - /// - /// Patterns with no special glob characters should be treated as having asterisks prepended and - /// appended when testing the condition. - pub pattern: Option, - - /// Required for `room_member_count` conditions. A decimal integer optionally prefixed by one of - /// `==`, `<`, `>`, `>=` or `<=`. - /// - /// A prefix of `<` matches rooms where the member count is strictly less than the given number - /// and so forth. If no prefix is present, this parameter defaults to `==`. - pub is: Option, -} - -/// A kind of push rule condition. -#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] -pub enum PushConditionKind { +#[derive(Clone, Debug, PartialEq)] +pub enum PushCondition { /// This is a glob pattern match on a field of the event. - #[serde(rename = "event_match")] - EventMatch, + EventMatch(EventMatchCondition), /// This matches unencrypted messages where `content.body` contains the owner's display name in /// that room. - #[serde(rename = "contains_display_name")] ContainsDisplayName, /// This matches the current number of members in the room. - #[serde(rename = "room_member_count")] - RoomMemberCount, + RoomMemberCount(RoomMemberCountCondition), /// This takes into account the current power levels in the room, ensuring the sender of the /// event has high enough power to trigger the notification. - #[serde(rename = "sender_notification_permission")] - SenderNotificationPermission, + SenderNotificationPermission(SenderNotificationPermissionCondition), + + /// Additional variants may be added in the future and will not be considered breaking changes + /// to ruma-events. + #[doc(hidden)] + __Nonexhaustive, +} + +impl Serialize for PushCondition { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + PushCondition::EventMatch(ref condition) => condition.serialize(serializer), + PushCondition::ContainsDisplayName => { + let mut state = serializer.serialize_struct("ContainsDisplayNameCondition", 1)?; + + state.serialize_field("kind", "contains_display_name")?; + + state.end() + } + PushCondition::RoomMemberCount(ref condition) => condition.serialize(serializer), + PushCondition::SenderNotificationPermission(ref condition) => { + condition.serialize(serializer) + } + PushCondition::__Nonexhaustive => { + panic!("__Nonexhaustive enum variant is not intended for use."); + } + } + } +} + +impl<'de> Deserialize<'de> for PushCondition { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value: Value = Deserialize::deserialize(deserializer)?; + + let kind_value = match value.get("kind") { + Some(value) => value.clone(), + None => return Err(D::Error::missing_field("kind")), + }; + + let kind = match kind_value.as_str() { + Some(kind) => kind, + None => return Err(D::Error::custom("field `kind` must be a string")), + }; + + match kind { + "event_match" => { + let condition = match from_value::(value) { + Ok(condition) => condition, + Err(error) => return Err(D::Error::custom(error.to_string())), + }; + + Ok(PushCondition::EventMatch(condition)) + } + "contains_display_name" => Ok(PushCondition::ContainsDisplayName), + "room_member_count" => { + let condition = match from_value::(value) { + Ok(condition) => condition, + Err(error) => return Err(D::Error::custom(error.to_string())), + }; + + Ok(PushCondition::RoomMemberCount(condition)) + } + "sender_notification_permission" => { + let condition = match from_value::(value) { + Ok(condition) => condition, + Err(error) => return Err(D::Error::custom(error.to_string())), + }; + + Ok(PushCondition::SenderNotificationPermission(condition)) + } + unknown_kind => { + return Err(D::Error::custom(&format!( + "unknown condition kind `{}`", + unknown_kind + ))) + } + } + } +} +/// A push condition that matches a glob pattern match on a field of the event. +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct EventMatchCondition { + /// The dot-separated field of the event to match. + pub key: String, + + /// The glob-style pattern to match against. + /// + /// Patterns with no special glob characters should be treated as having asterisks prepended and + /// appended when testing the condition. + pub pattern: String, +} + +impl Serialize for EventMatchCondition { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("EventMatchCondition", 3)?; + + state.serialize_field("key", &self.key)?; + state.serialize_field("kind", "event_match")?; + state.serialize_field("pattern", &self.pattern)?; + + state.end() + } +} + +/// A push condition that matches the current number of members in the room. +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct RoomMemberCountCondition { + /// A decimal integer optionally prefixed by one of `==`, `<`, `>`, `>=` or `<=`. + /// + /// A prefix of `<` matches rooms where the member count is strictly less than the given number + /// and so forth. If no prefix is present, this parameter defaults to `==`. + pub is: String, +} + +impl Serialize for RoomMemberCountCondition { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("RoomMemberCountCondition", 2)?; + + state.serialize_field("is", &self.is)?; + state.serialize_field("kind", "room_member_count")?; + + state.end() + } +} + +/// A push condition that takes into account the current power levels in the room, ensuring the +/// sender of the event has high enough power to trigger the notification. +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct SenderNotificationPermissionCondition { + /// The field in the power level event the user needs a minimum power level for. + /// + /// Fields must be specified under the `notifications` property in the power level event's + /// `content`. + pub key: String, +} + +impl Serialize for SenderNotificationPermissionCondition { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("SenderNotificationPermissionCondition", 2)?; + + state.serialize_field("key", &self.key)?; + state.serialize_field("kind", "sender_notification_permission")?; + + state.end() + } } #[cfg(test)] mod tests { use serde_json::{from_str, to_string}; - use super::{Action, Tweak}; + use super::{ + Action, EventMatchCondition, PushCondition, PushRulesEvent, RoomMemberCountCondition, + SenderNotificationPermissionCondition, Tweak, + }; #[test] fn serialize_string_action() { @@ -312,4 +480,288 @@ mod tests { Action::SetTweak(Tweak::Highlight { value: true }) ); } + + #[test] + fn serialize_event_match_condition() { + assert_eq!( + to_string(&PushCondition::EventMatch(EventMatchCondition { + key: "content.msgtype".to_string(), + pattern: "m.notice".to_string(), + })) + .unwrap(), + r#"{"key":"content.msgtype","kind":"event_match","pattern":"m.notice"}"# + ); + } + + #[test] + fn serialize_contains_display_name_condition() { + assert_eq!( + to_string(&PushCondition::ContainsDisplayName).unwrap(), + r#"{"kind":"contains_display_name"}"# + ); + } + + #[test] + fn serialize_room_member_count_condition() { + assert_eq!( + to_string(&PushCondition::RoomMemberCount(RoomMemberCountCondition { + is: "2".to_string(), + })) + .unwrap(), + r#"{"is":"2","kind":"room_member_count"}"# + ); + } + + #[test] + fn serialize_sender_notification_permission_condition() { + assert_eq!( + r#"{"key":"room","kind":"sender_notification_permission"}"#, + to_string(&PushCondition::SenderNotificationPermission( + SenderNotificationPermissionCondition { + key: "room".to_string(), + } + )) + .unwrap(), + ); + } + + #[test] + fn deserialize_event_match_condition() { + assert_eq!( + from_str::( + r#"{"key":"content.msgtype","kind":"event_match","pattern":"m.notice"}"# + ) + .unwrap(), + PushCondition::EventMatch(EventMatchCondition { + key: "content.msgtype".to_string(), + pattern: "m.notice".to_string(), + }) + ); + } + + #[test] + fn deserialize_contains_display_name_condition() { + assert_eq!( + from_str::(r#"{"kind":"contains_display_name"}"#).unwrap(), + PushCondition::ContainsDisplayName, + ); + } + + #[test] + fn deserialize_room_member_count_condition() { + assert_eq!( + from_str::(r#"{"is":"2","kind":"room_member_count"}"#).unwrap(), + PushCondition::RoomMemberCount(RoomMemberCountCondition { + is: "2".to_string(), + }) + ); + } + + #[test] + fn deserialize_sender_notification_permission_condition() { + assert_eq!( + from_str::(r#"{"key":"room","kind":"sender_notification_permission"}"#) + .unwrap(), + PushCondition::SenderNotificationPermission(SenderNotificationPermissionCondition { + key: "room".to_string(), + }) + ); + } + + #[test] + fn sanity_check() { + // This is a full example of a push rules event from the specification. + let json = r#"{ + "content": { + "global": { + "content": [ + { + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight" + } + ], + "default": true, + "enabled": true, + "pattern": "alice", + "rule_id": ".m.rule.contains_user_name" + } + ], + "override": [ + { + "actions": [ + "dont_notify" + ], + "conditions": [], + "default": true, + "enabled": false, + "rule_id": ".m.rule.master" + }, + { + "actions": [ + "dont_notify" + ], + "conditions": [ + { + "key": "content.msgtype", + "kind": "event_match", + "pattern": "m.notice" + } + ], + "default": true, + "enabled": true, + "rule_id": ".m.rule.suppress_notices" + } + ], + "room": [], + "sender": [], + "underride": [ + { + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "ring" + }, + { + "set_tweak": "highlight", + "value": false + } + ], + "conditions": [ + { + "key": "type", + "kind": "event_match", + "pattern": "m.call.invite" + } + ], + "default": true, + "enabled": true, + "rule_id": ".m.rule.call" + }, + { + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight" + } + ], + "conditions": [ + { + "kind": "contains_display_name" + } + ], + "default": true, + "enabled": true, + "rule_id": ".m.rule.contains_display_name" + }, + { + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight", + "value": false + } + ], + "conditions": [ + { + "is": "2", + "kind": "room_member_count" + } + ], + "default": true, + "enabled": true, + "rule_id": ".m.rule.room_one_to_one" + }, + { + "actions": [ + "notify", + { + "set_tweak": "sound", + "value": "default" + }, + { + "set_tweak": "highlight", + "value": false + } + ], + "conditions": [ + { + "key": "type", + "kind": "event_match", + "pattern": "m.room.member" + }, + { + "key": "content.membership", + "kind": "event_match", + "pattern": "invite" + }, + { + "key": "state_key", + "kind": "event_match", + "pattern": "@alice:example.com" + } + ], + "default": true, + "enabled": true, + "rule_id": ".m.rule.invite_for_me" + }, + { + "actions": [ + "notify", + { + "set_tweak": "highlight", + "value": false + } + ], + "conditions": [ + { + "key": "type", + "kind": "event_match", + "pattern": "m.room.member" + } + ], + "default": true, + "enabled": true, + "rule_id": ".m.rule.member_event" + }, + { + "actions": [ + "notify", + { + "set_tweak": "highlight", + "value": false + } + ], + "conditions": [ + { + "key": "type", + "kind": "event_match", + "pattern": "m.room.message" + } + ], + "default": true, + "enabled": true, + "rule_id": ".m.rule.message" + } + ] + } + }, + "type": "m.push_rules" +}"#; + assert!(json.parse::().is_ok()); + } }