use std::collections::BTreeMap; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue}; use crate::serde::from_raw_json_value; /// This represents the different actions that should be taken when a rule is matched, and /// controls how notifications are delivered to the client. /// /// See [the spec](https://spec.matrix.org/latest/client-server-api/#actions) for details. #[derive(Clone, Debug)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), 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), /// An unknown action. #[doc(hidden)] _Custom(CustomAction), } impl Action { /// Whether this action is an `Action::SetTweak(Tweak::Highlight(true))`. pub fn is_highlight(&self) -> bool { matches!(self, Action::SetTweak(Tweak::Highlight(true))) } } /// The `set_tweak` action. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[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 = "crate::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>, { let json = Box::::deserialize(deserializer)?; let custom: CustomAction = from_raw_json_value(&json)?; match &custom { CustomAction::String(s) => match s.as_str() { "notify" => Ok(Action::Notify), "dont_notify" => Ok(Action::DontNotify), "coalesce" => Ok(Action::Coalesce), _ => Ok(Action::_Custom(custom)), }, CustomAction::Object(o) => { if o.get("set_tweak").is_some() { Ok(Action::SetTweak(from_raw_json_value(&json)?)) } else { Ok(Action::_Custom(custom)) } } } } } 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), Action::_Custom(custom) => custom.serialize(serializer), } } } /// An unknown action. #[doc(hidden)] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum CustomAction { /// A string. String(String), /// An object. Object(BTreeMap), } 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(crate) enum Tweak { Sound(SoundTweak), Highlight(HighlightTweak), Custom { #[serde(rename = "set_tweak")] name: String, value: Box, }, } #[derive(Clone, PartialEq, Deserialize, Serialize)] #[serde(tag = "set_tweak", rename = "sound")] pub(crate) struct SoundTweak { value: String, } #[derive(Clone, PartialEq, Deserialize, Serialize)] #[serde(tag = "set_tweak", rename = "highlight")] pub(crate) struct HighlightTweak { #[serde( default = "crate::serde::default_true", skip_serializing_if = "crate::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 assert_matches::assert_matches; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::{Action, Tweak}; #[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")), Ok(Action::Notify)); } #[test] fn deserialize_tweak_sound() { let json_data = json!({ "set_tweak": "sound", "value": "default" }); let value = assert_matches!( from_json_value::(json_data), Ok(Action::SetTweak(Tweak::Sound(value))) => value ); assert_eq!(value, "default"); } #[test] fn deserialize_tweak_highlight() { let json_data = json!({ "set_tweak": "highlight", "value": true }); assert_matches!( from_json_value::(json_data), Ok(Action::SetTweak(Tweak::Highlight(true))) ); } #[test] fn deserialize_tweak_highlight_with_default_value() { assert_matches!( from_json_value::(json!({ "set_tweak": "highlight" })), Ok(Action::SetTweak(Tweak::Highlight(true))) ); } }