ruma_common/push/
action.rs

1use std::collections::BTreeMap;
2
3use as_variant::as_variant;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue};
6
7use crate::serde::from_raw_json_value;
8
9/// This represents the different actions that should be taken when a rule is matched, and
10/// controls how notifications are delivered to the client.
11///
12/// See [the spec](https://spec.matrix.org/latest/client-server-api/#actions) for details.
13#[derive(Clone, Debug)]
14#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
15pub enum Action {
16    /// Causes matching events to generate a notification (both in-app and remote / push).
17    Notify,
18
19    /// Causes matching events to generate an in-app notification but no remote / push
20    /// notification.
21    #[cfg(feature = "unstable-msc3768")]
22    NotifyInApp,
23
24    /// Sets an entry in the 'tweaks' dictionary sent to the push gateway.
25    SetTweak(Tweak),
26
27    /// An unknown action.
28    #[doc(hidden)]
29    _Custom(CustomAction),
30}
31
32impl Action {
33    /// Whether this action is an `Action::SetTweak(Tweak::Highlight(true))`.
34    pub fn is_highlight(&self) -> bool {
35        matches!(self, Action::SetTweak(Tweak::Highlight(true)))
36    }
37
38    /// Whether this action should trigger a notification (either in-app or remote / push).
39    pub fn should_notify(&self) -> bool {
40        match self {
41            Action::Notify => true,
42            #[cfg(feature = "unstable-msc3768")]
43            Action::NotifyInApp => true,
44            _ => false,
45        }
46    }
47
48    /// Whether this action should trigger a remote / push notification.
49    #[cfg(feature = "unstable-msc3768")]
50    pub fn should_notify_remote(&self) -> bool {
51        matches!(self, Action::Notify)
52    }
53
54    /// The sound that should be played with this action, if any.
55    pub fn sound(&self) -> Option<&str> {
56        as_variant!(self, Action::SetTweak(Tweak::Sound(sound)) => sound)
57    }
58}
59
60/// The `set_tweak` action.
61#[derive(Clone, Debug, Deserialize, Serialize)]
62#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
63#[serde(from = "tweak_serde::Tweak", into = "tweak_serde::Tweak")]
64pub enum Tweak {
65    /// A string representing the sound to be played when this notification arrives.
66    ///
67    /// A value of "default" means to play a default sound. A device may choose to alert the user
68    /// by some other means if appropriate, eg. vibration.
69    Sound(String),
70
71    /// A boolean representing whether or not this message should be highlighted in the UI.
72    ///
73    /// This will normally take the form of presenting the message in a different color and/or
74    /// style. The UI might also be adjusted to draw particular attention to the room in which the
75    /// event occurred. If a `highlight` tweak is given with no value, its value is defined to be
76    /// `true`. If no highlight tweak is given at all then the value of `highlight` is defined to
77    /// be `false`.
78    Highlight(#[serde(default = "crate::serde::default_true")] bool),
79
80    /// A custom tweak
81    Custom {
82        /// The name of the custom tweak (`set_tweak` field)
83        name: String,
84
85        /// The value of the custom tweak
86        value: Box<RawJsonValue>,
87    },
88}
89
90impl<'de> Deserialize<'de> for Action {
91    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
92    where
93        D: Deserializer<'de>,
94    {
95        let json = Box::<RawJsonValue>::deserialize(deserializer)?;
96        let custom: CustomAction = from_raw_json_value(&json)?;
97
98        match &custom {
99            CustomAction::String(s) => match s.as_str() {
100                "notify" => Ok(Action::Notify),
101                #[cfg(feature = "unstable-msc3768")]
102                "org.matrix.msc3768.notify_in_app" => Ok(Action::NotifyInApp),
103                _ => Ok(Action::_Custom(custom)),
104            },
105            CustomAction::Object(o) => {
106                if o.get("set_tweak").is_some() {
107                    Ok(Action::SetTweak(from_raw_json_value(&json)?))
108                } else {
109                    Ok(Action::_Custom(custom))
110                }
111            }
112        }
113    }
114}
115
116impl Serialize for Action {
117    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
118    where
119        S: Serializer,
120    {
121        match self {
122            Action::Notify => serializer.serialize_unit_variant("Action", 0, "notify"),
123            #[cfg(feature = "unstable-msc3768")]
124            Action::NotifyInApp => {
125                serializer.serialize_unit_variant("Action", 0, "org.matrix.msc3768.notify_in_app")
126            }
127            Action::SetTweak(kind) => kind.serialize(serializer),
128            Action::_Custom(custom) => custom.serialize(serializer),
129        }
130    }
131}
132
133/// An unknown action.
134#[doc(hidden)]
135#[allow(unknown_lints, unnameable_types)]
136#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(untagged)]
138pub enum CustomAction {
139    /// A string.
140    String(String),
141
142    /// An object.
143    Object(BTreeMap<String, JsonValue>),
144}
145
146mod tweak_serde {
147    use serde::{Deserialize, Serialize};
148    use serde_json::value::RawValue as RawJsonValue;
149
150    /// Values for the `set_tweak` action.
151    #[derive(Clone, Deserialize, Serialize)]
152    #[serde(untagged)]
153    pub(crate) enum Tweak {
154        Sound(SoundTweak),
155        Highlight(HighlightTweak),
156        Custom {
157            #[serde(rename = "set_tweak")]
158            name: String,
159            value: Box<RawJsonValue>,
160        },
161    }
162
163    #[derive(Clone, PartialEq, Deserialize, Serialize)]
164    #[serde(tag = "set_tweak", rename = "sound")]
165    pub(crate) struct SoundTweak {
166        value: String,
167    }
168
169    #[derive(Clone, PartialEq, Deserialize, Serialize)]
170    #[serde(tag = "set_tweak", rename = "highlight")]
171    pub(crate) struct HighlightTweak {
172        #[serde(
173            default = "crate::serde::default_true",
174            skip_serializing_if = "crate::serde::is_true"
175        )]
176        value: bool,
177    }
178
179    impl From<super::Tweak> for Tweak {
180        fn from(tweak: super::Tweak) -> Self {
181            use super::Tweak::*;
182
183            match tweak {
184                Sound(value) => Self::Sound(SoundTweak { value }),
185                Highlight(value) => Self::Highlight(HighlightTweak { value }),
186                Custom { name, value } => Self::Custom { name, value },
187            }
188        }
189    }
190
191    impl From<Tweak> for super::Tweak {
192        fn from(tweak: Tweak) -> Self {
193            use Tweak::*;
194
195            match tweak {
196                Sound(SoundTweak { value }) => Self::Sound(value),
197                Highlight(HighlightTweak { value }) => Self::Highlight(value),
198                Custom { name, value } => Self::Custom { name, value },
199            }
200        }
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use assert_matches2::assert_matches;
207    use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
208
209    use super::{Action, Tweak};
210
211    #[test]
212    fn serialize_notify() {
213        assert_eq!(to_json_value(Action::Notify).unwrap(), json!("notify"));
214    }
215
216    #[cfg(feature = "unstable-msc3768")]
217    #[test]
218    fn serialize_notify_in_app() {
219        assert_eq!(
220            to_json_value(Action::NotifyInApp).unwrap(),
221            json!("org.matrix.msc3768.notify_in_app")
222        );
223    }
224
225    #[test]
226    fn serialize_tweak_sound() {
227        assert_eq!(
228            to_json_value(Action::SetTweak(Tweak::Sound("default".into()))).unwrap(),
229            json!({ "set_tweak": "sound", "value": "default" })
230        );
231    }
232
233    #[test]
234    fn serialize_tweak_highlight() {
235        assert_eq!(
236            to_json_value(Action::SetTweak(Tweak::Highlight(true))).unwrap(),
237            json!({ "set_tweak": "highlight" })
238        );
239
240        assert_eq!(
241            to_json_value(Action::SetTweak(Tweak::Highlight(false))).unwrap(),
242            json!({ "set_tweak": "highlight", "value": false })
243        );
244    }
245
246    #[test]
247    fn deserialize_notify() {
248        assert_matches!(from_json_value::<Action>(json!("notify")), Ok(Action::Notify));
249    }
250
251    #[cfg(feature = "unstable-msc3768")]
252    #[test]
253    fn deserialize_notify_in_app() {
254        assert_matches!(
255            from_json_value::<Action>(json!("org.matrix.msc3768.notify_in_app")),
256            Ok(Action::NotifyInApp)
257        );
258    }
259
260    #[test]
261    fn deserialize_tweak_sound() {
262        let json_data = json!({
263            "set_tweak": "sound",
264            "value": "default"
265        });
266        assert_matches!(
267            from_json_value::<Action>(json_data),
268            Ok(Action::SetTweak(Tweak::Sound(value)))
269        );
270        assert_eq!(value, "default");
271    }
272
273    #[test]
274    fn deserialize_tweak_highlight() {
275        let json_data = json!({
276            "set_tweak": "highlight",
277            "value": true
278        });
279        assert_matches!(
280            from_json_value::<Action>(json_data),
281            Ok(Action::SetTweak(Tweak::Highlight(true)))
282        );
283    }
284
285    #[test]
286    fn deserialize_tweak_highlight_with_default_value() {
287        assert_matches!(
288            from_json_value::<Action>(json!({ "set_tweak": "highlight" })),
289            Ok(Action::SetTweak(Tweak::Highlight(true)))
290        );
291    }
292}