diff --git a/ruma-common/src/push.rs b/ruma-common/src/push.rs index c43b6643..8bc54f3f 100644 --- a/ruma-common/src/push.rs +++ b/ruma-common/src/push.rs @@ -8,13 +8,15 @@ use std::{ fmt::{self, Display, Formatter}, }; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use serde_json::value::RawValue as RawJsonValue; +use serde::{Deserialize, Serialize}; -pub use room_member_count_is::{ComparisonOperator, RoomMemberCountIs}; +pub use self::{ + action::{Action, Tweak}, + condition::{ComparisonOperator, PushCondition, RoomMemberCountIs}, +}; -mod room_member_count_is; -mod tweak_serde; +mod action; +mod condition; /// A push ruleset scopes a set of rules according to some criteria. /// @@ -227,321 +229,3 @@ impl TryFrom for ConditionalPushRule { } } } - -/// This represents the different actions that should be taken when a rule is matched, and -/// controls how notifications are delivered to the client. -/// -/// See https://matrix.org/docs/spec/client_server/r0.6.0#actions for details. -#[derive(Clone, Debug)] -#[non_exhaustive] -pub enum Action { - /// Causes matching events to generate a notification. - Notify, - - /// Prevents matching events from generating a notification. - DontNotify, - - /// Behaves like notify but homeservers may choose to coalesce multiple events - /// into a single notification. - Coalesce, - - /// Sets an entry in the 'tweaks' dictionary sent to the push gateway. - SetTweak(Tweak), -} - -/// The `set_tweak` action. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(from = "tweak_serde::Tweak", into = "tweak_serde::Tweak")] -pub enum Tweak { - /// A string representing the sound to be played when this notification arrives. - /// - /// A value of "default" means to play a default sound. A device may choose to alert the user by - /// some other means if appropriate, eg. vibration. - Sound(String), - - /// A boolean representing whether or not this message should be highlighted in the UI. - /// - /// This will normally take the form of presenting the message in a different color and/or - /// style. The UI might also be adjusted to draw particular attention to the room in which the - /// event occurred. If a `highlight` tweak is given with no value, its value is defined to be - /// `true`. If no highlight tweak is given at all then the value of `highlight` is defined to be - /// `false`. - Highlight(#[serde(default = "ruma_serde::default_true")] bool), - - /// A custom tweak - Custom { - /// The name of the custom tweak (`set_tweak` field) - name: String, - - /// The value of the custom tweak - value: Box, - }, -} - -impl<'de> Deserialize<'de> for Action { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - use serde::de::{MapAccess, Visitor}; - - struct ActionVisitor; - impl<'de> Visitor<'de> for ActionVisitor { - type Value = Action; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { - write!(formatter, "a valid action object") - } - - /// Match a simple action type - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - match v { - "notify" => Ok(Action::Notify), - "dont_notify" => Ok(Action::DontNotify), - "coalesce" => Ok(Action::Coalesce), - s => Err(E::unknown_variant(&s, &["notify", "dont_notify", "coalesce"])), - } - } - - /// Match the more complex set_tweaks action object as a key-value map - fn visit_map(self, map: A) -> Result - where - A: MapAccess<'de>, - { - Tweak::deserialize(serde::de::value::MapAccessDeserializer::new(map)) - .map(Action::SetTweak) - } - } - - deserializer.deserialize_any(ActionVisitor) - } -} - -impl Serialize for Action { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match self { - Action::Notify => serializer.serialize_unit_variant("Action", 0, "notify"), - Action::DontNotify => serializer.serialize_unit_variant("Action", 1, "dont_notify"), - Action::Coalesce => serializer.serialize_unit_variant("Action", 2, "coalesce"), - Action::SetTweak(kind) => kind.serialize(serializer), - } - } -} - -/// A condition that must apply for an associated push rule's action to be taken. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[non_exhaustive] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum PushCondition { - /// This is a glob pattern match on a field of the event. - EventMatch { - /// The dot-separated field of the event to match. - 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. - pattern: String, - }, - - /// This matches unencrypted messages where `content.body` contains the owner's display name in - /// that room. - ContainsDisplayName, - - /// This matches the current number of members in the room. - RoomMemberCount { - /// The condition on the current number of members in the room. - is: RoomMemberCountIs, - }, - - /// 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. - SenderNotificationPermission { - /// 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`. - key: String, - }, -} - -#[cfg(test)] -mod tests { - use js_int::uint; - use matches::assert_matches; - use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; - - use super::{Action, PushCondition, RoomMemberCountIs, Tweak}; - - #[test] - fn serialize_string_action() { - assert_eq!(to_json_value(&Action::Notify).unwrap(), json!("notify")); - } - - #[test] - fn serialize_tweak_sound_action() { - assert_eq!( - to_json_value(&Action::SetTweak(Tweak::Sound("default".into()))).unwrap(), - json!({ "set_tweak": "sound", "value": "default" }) - ); - } - - #[test] - fn serialize_tweak_highlight_action() { - assert_eq!( - to_json_value(&Action::SetTweak(Tweak::Highlight(true))).unwrap(), - json!({ "set_tweak": "highlight" }) - ); - - assert_eq!( - to_json_value(&Action::SetTweak(Tweak::Highlight(false))).unwrap(), - json!({ "set_tweak": "highlight", "value": false }) - ); - } - - #[test] - fn deserialize_string_action() { - assert_matches!(from_json_value::(json!("notify")).unwrap(), Action::Notify); - } - - #[test] - fn deserialize_tweak_sound_action() { - let json_data = json!({ - "set_tweak": "sound", - "value": "default" - }); - assert_matches!( - &from_json_value::(json_data).unwrap(), - Action::SetTweak(Tweak::Sound(value)) if value == "default" - ); - } - - #[test] - fn deserialize_tweak_highlight_action() { - let json_data = json!({ - "set_tweak": "highlight", - "value": true - }); - assert_matches!( - from_json_value::(json_data).unwrap(), - Action::SetTweak(Tweak::Highlight(true)) - ); - } - - #[test] - fn deserialize_tweak_highlight_action_with_default_value() { - assert_matches!( - from_json_value::(json!({ "set_tweak": "highlight" })).unwrap(), - Action::SetTweak(Tweak::Highlight(true)) - ); - } - - #[test] - fn serialize_event_match_condition() { - let json_data = json!({ - "key": "content.msgtype", - "kind": "event_match", - "pattern": "m.notice" - }); - assert_eq!( - to_json_value(&PushCondition::EventMatch { - key: "content.msgtype".into(), - pattern: "m.notice".into(), - }) - .unwrap(), - json_data - ); - } - - #[test] - fn serialize_contains_display_name_condition() { - assert_eq!( - to_json_value(&PushCondition::ContainsDisplayName).unwrap(), - json!({ "kind": "contains_display_name" }) - ); - } - - #[test] - fn serialize_room_member_count_condition() { - let json_data = json!({ - "is": "2", - "kind": "room_member_count" - }); - assert_eq!( - to_json_value(&PushCondition::RoomMemberCount { - is: RoomMemberCountIs::from(uint!(2)) - }) - .unwrap(), - json_data - ); - } - - #[test] - fn serialize_sender_notification_permission_condition() { - let json_data = json!({ - "key": "room", - "kind": "sender_notification_permission" - }); - assert_eq!( - json_data, - to_json_value(&PushCondition::SenderNotificationPermission { key: "room".into() }) - .unwrap() - ); - } - - #[test] - fn deserialize_event_match_condition() { - let json_data = json!({ - "key": "content.msgtype", - "kind": "event_match", - "pattern": "m.notice" - }); - assert_matches!( - from_json_value::(json_data).unwrap(), - PushCondition::EventMatch { key, pattern } - if key == "content.msgtype" && pattern == "m.notice" - ); - } - - #[test] - fn deserialize_contains_display_name_condition() { - assert_matches!( - from_json_value::(json!({ "kind": "contains_display_name" })).unwrap(), - PushCondition::ContainsDisplayName - ); - } - - #[test] - fn deserialize_room_member_count_condition() { - let json_data = json!({ - "is": "2", - "kind": "room_member_count" - }); - assert_matches!( - from_json_value::(json_data).unwrap(), - PushCondition::RoomMemberCount { is } - if is == RoomMemberCountIs::from(uint!(2)) - ); - } - - #[test] - fn deserialize_sender_notification_permission_condition() { - let json_data = json!({ - "key": "room", - "kind": "sender_notification_permission" - }); - assert_matches!( - from_json_value::(json_data).unwrap(), - PushCondition::SenderNotificationPermission { - key - } if key == "room" - ); - } -} diff --git a/ruma-common/src/push/action.rs b/ruma-common/src/push/action.rs new file mode 100644 index 00000000..acb52e44 --- /dev/null +++ b/ruma-common/src/push/action.rs @@ -0,0 +1,236 @@ +use std::fmt::{self, Formatter}; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::value::RawValue as RawJsonValue; + +/// This represents the different actions that should be taken when a rule is matched, and +/// controls how notifications are delivered to the client. +/// +/// See https://matrix.org/docs/spec/client_server/r0.6.0#actions for details. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum Action { + /// Causes matching events to generate a notification. + Notify, + + /// Prevents matching events from generating a notification. + DontNotify, + + /// Behaves like notify but homeservers may choose to coalesce multiple events + /// into a single notification. + Coalesce, + + /// Sets an entry in the 'tweaks' dictionary sent to the push gateway. + SetTweak(Tweak), +} + +/// The `set_tweak` action. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(from = "tweak_serde::Tweak", into = "tweak_serde::Tweak")] +pub enum Tweak { + /// A string representing the sound to be played when this notification arrives. + /// + /// A value of "default" means to play a default sound. A device may choose to alert the user by + /// some other means if appropriate, eg. vibration. + Sound(String), + + /// A boolean representing whether or not this message should be highlighted in the UI. + /// + /// This will normally take the form of presenting the message in a different color and/or + /// style. The UI might also be adjusted to draw particular attention to the room in which the + /// event occurred. If a `highlight` tweak is given with no value, its value is defined to be + /// `true`. If no highlight tweak is given at all then the value of `highlight` is defined to be + /// `false`. + Highlight(#[serde(default = "ruma_serde::default_true")] bool), + + /// A custom tweak + Custom { + /// The name of the custom tweak (`set_tweak` field) + name: String, + + /// The value of the custom tweak + value: Box, + }, +} + +impl<'de> Deserialize<'de> for Action { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::{MapAccess, Visitor}; + + struct ActionVisitor; + impl<'de> Visitor<'de> for ActionVisitor { + type Value = Action; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + write!(formatter, "a valid action object") + } + + /// Match a simple action type + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + match v { + "notify" => Ok(Action::Notify), + "dont_notify" => Ok(Action::DontNotify), + "coalesce" => Ok(Action::Coalesce), + s => Err(E::unknown_variant(&s, &["notify", "dont_notify", "coalesce"])), + } + } + + /// Match the more complex set_tweaks action object as a key-value map + fn visit_map(self, map: A) -> Result + where + A: MapAccess<'de>, + { + Tweak::deserialize(serde::de::value::MapAccessDeserializer::new(map)) + .map(Action::SetTweak) + } + } + + deserializer.deserialize_any(ActionVisitor) + } +} + +impl Serialize for Action { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Action::Notify => serializer.serialize_unit_variant("Action", 0, "notify"), + Action::DontNotify => serializer.serialize_unit_variant("Action", 1, "dont_notify"), + Action::Coalesce => serializer.serialize_unit_variant("Action", 2, "coalesce"), + Action::SetTweak(kind) => kind.serialize(serializer), + } + } +} + +mod tweak_serde { + use serde::{Deserialize, Serialize}; + use serde_json::value::RawValue as RawJsonValue; + + /// Values for the `set_tweak` action. + #[derive(Clone, Deserialize, Serialize)] + #[serde(untagged)] + pub enum Tweak { + Sound(SoundTweak), + Highlight(HighlightTweak), + Custom { + #[serde(rename = "set_tweak")] + name: String, + value: Box, + }, + } + + #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] + #[serde(tag = "set_tweak", rename = "sound")] + pub struct SoundTweak { + value: String, + } + + #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] + #[serde(tag = "set_tweak", rename = "highlight")] + pub struct HighlightTweak { + #[serde(default = "ruma_serde::default_true", skip_serializing_if = "ruma_serde::is_true")] + value: bool, + } + + impl From for Tweak { + fn from(tweak: super::Tweak) -> Self { + use super::Tweak::*; + + match tweak { + Sound(value) => Self::Sound(SoundTweak { value }), + Highlight(value) => Self::Highlight(HighlightTweak { value }), + Custom { name, value } => Self::Custom { name, value }, + } + } + } + + impl From for super::Tweak { + fn from(tweak: Tweak) -> Self { + use Tweak::*; + + match tweak { + Sound(SoundTweak { value }) => Self::Sound(value), + Highlight(HighlightTweak { value }) => Self::Highlight(value), + Custom { name, value } => Self::Custom { name, value }, + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{Action, Tweak}; + + use matches::assert_matches; + use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; + + #[test] + fn serialize_string() { + assert_eq!(to_json_value(&Action::Notify).unwrap(), json!("notify")); + } + + #[test] + fn serialize_tweak_sound() { + assert_eq!( + to_json_value(&Action::SetTweak(Tweak::Sound("default".into()))).unwrap(), + json!({ "set_tweak": "sound", "value": "default" }) + ); + } + + #[test] + fn serialize_tweak_highlight() { + assert_eq!( + to_json_value(&Action::SetTweak(Tweak::Highlight(true))).unwrap(), + json!({ "set_tweak": "highlight" }) + ); + + assert_eq!( + to_json_value(&Action::SetTweak(Tweak::Highlight(false))).unwrap(), + json!({ "set_tweak": "highlight", "value": false }) + ); + } + + #[test] + fn deserialize_string() { + assert_matches!(from_json_value::(json!("notify")).unwrap(), Action::Notify); + } + + #[test] + fn deserialize_tweak_sound() { + let json_data = json!({ + "set_tweak": "sound", + "value": "default" + }); + assert_matches!( + &from_json_value::(json_data).unwrap(), + Action::SetTweak(Tweak::Sound(value)) if value == "default" + ); + } + + #[test] + fn deserialize_tweak_highlight() { + let json_data = json!({ + "set_tweak": "highlight", + "value": true + }); + assert_matches!( + from_json_value::(json_data).unwrap(), + Action::SetTweak(Tweak::Highlight(true)) + ); + } + + #[test] + fn deserialize_tweak_highlight_with_default_value() { + assert_matches!( + from_json_value::(json!({ "set_tweak": "highlight" })).unwrap(), + Action::SetTweak(Tweak::Highlight(true)) + ); + } +} diff --git a/ruma-common/src/push/condition.rs b/ruma-common/src/push/condition.rs new file mode 100644 index 00000000..e971b6de --- /dev/null +++ b/ruma-common/src/push/condition.rs @@ -0,0 +1,154 @@ +use serde::{Deserialize, Serialize}; + +mod room_member_count_is; + +pub use room_member_count_is::{ComparisonOperator, RoomMemberCountIs}; + +/// A condition that must apply for an associated push rule's action to be taken. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum PushCondition { + /// This is a glob pattern match on a field of the event. + EventMatch { + /// The dot-separated field of the event to match. + 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. + pattern: String, + }, + + /// This matches unencrypted messages where `content.body` contains the owner's display name in + /// that room. + ContainsDisplayName, + + /// This matches the current number of members in the room. + RoomMemberCount { + /// The condition on the current number of members in the room. + is: RoomMemberCountIs, + }, + + /// 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. + SenderNotificationPermission { + /// 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`. + key: String, + }, +} + +#[cfg(test)] +mod tests { + use js_int::uint; + use matches::assert_matches; + use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; + + use super::{PushCondition, RoomMemberCountIs}; + + #[test] + fn serialize_event_match_condition() { + let json_data = json!({ + "key": "content.msgtype", + "kind": "event_match", + "pattern": "m.notice" + }); + assert_eq!( + to_json_value(&PushCondition::EventMatch { + key: "content.msgtype".into(), + pattern: "m.notice".into(), + }) + .unwrap(), + json_data + ); + } + + #[test] + fn serialize_contains_display_name_condition() { + assert_eq!( + to_json_value(&PushCondition::ContainsDisplayName).unwrap(), + json!({ "kind": "contains_display_name" }) + ); + } + + #[test] + fn serialize_room_member_count_condition() { + let json_data = json!({ + "is": "2", + "kind": "room_member_count" + }); + assert_eq!( + to_json_value(&PushCondition::RoomMemberCount { + is: RoomMemberCountIs::from(uint!(2)) + }) + .unwrap(), + json_data + ); + } + + #[test] + fn serialize_sender_notification_permission_condition() { + let json_data = json!({ + "key": "room", + "kind": "sender_notification_permission" + }); + assert_eq!( + json_data, + to_json_value(&PushCondition::SenderNotificationPermission { key: "room".into() }) + .unwrap() + ); + } + + #[test] + fn deserialize_event_match_condition() { + let json_data = json!({ + "key": "content.msgtype", + "kind": "event_match", + "pattern": "m.notice" + }); + assert_matches!( + from_json_value::(json_data).unwrap(), + PushCondition::EventMatch { key, pattern } + if key == "content.msgtype" && pattern == "m.notice" + ); + } + + #[test] + fn deserialize_contains_display_name_condition() { + assert_matches!( + from_json_value::(json!({ "kind": "contains_display_name" })).unwrap(), + PushCondition::ContainsDisplayName + ); + } + + #[test] + fn deserialize_room_member_count_condition() { + let json_data = json!({ + "is": "2", + "kind": "room_member_count" + }); + assert_matches!( + from_json_value::(json_data).unwrap(), + PushCondition::RoomMemberCount { is } + if is == RoomMemberCountIs::from(uint!(2)) + ); + } + + #[test] + fn deserialize_sender_notification_permission_condition() { + let json_data = json!({ + "key": "room", + "kind": "sender_notification_permission" + }); + assert_matches!( + from_json_value::(json_data).unwrap(), + PushCondition::SenderNotificationPermission { + key + } if key == "room" + ); + } +} diff --git a/ruma-common/src/push/room_member_count_is.rs b/ruma-common/src/push/condition/room_member_count_is.rs similarity index 100% rename from ruma-common/src/push/room_member_count_is.rs rename to ruma-common/src/push/condition/room_member_count_is.rs diff --git a/ruma-common/src/push/tweak_serde.rs b/ruma-common/src/push/tweak_serde.rs deleted file mode 100644 index 55169527..00000000 --- a/ruma-common/src/push/tweak_serde.rs +++ /dev/null @@ -1,52 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::value::RawValue as RawJsonValue; - -/// Values for the `set_tweak` action. -#[derive(Clone, Deserialize, Serialize)] -#[serde(untagged)] -pub enum Tweak { - Sound(SoundTweak), - Highlight(HighlightTweak), - Custom { - #[serde(rename = "set_tweak")] - name: String, - value: Box, - }, -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[serde(tag = "set_tweak", rename = "sound")] -pub struct SoundTweak { - value: String, -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[serde(tag = "set_tweak", rename = "highlight")] -pub struct HighlightTweak { - #[serde(default = "ruma_serde::default_true", skip_serializing_if = "ruma_serde::is_true")] - value: bool, -} - -impl From for Tweak { - fn from(tweak: super::Tweak) -> Self { - use super::Tweak::*; - - match tweak { - Sound(value) => Self::Sound(SoundTweak { value }), - Highlight(value) => Self::Highlight(HighlightTweak { value }), - Custom { name, value } => Self::Custom { name, value }, - } - } -} - -impl From for super::Tweak { - fn from(tweak: Tweak) -> Self { - use Tweak::*; - - match tweak { - Sound(SoundTweak { value }) => Self::Sound(value), - Highlight(HighlightTweak { value }) => Self::Highlight(value), - Custom { name, value } => Self::Custom { name, value }, - } - } -}