Skip to main content

ruma_common/push/
action.rs

1use as_variant::as_variant;
2use ruma_macros::StringEnum;
3use serde::{Deserialize, Serialize, de};
4use serde_json::value::RawValue as RawJsonValue;
5
6mod action_serde;
7
8use crate::{
9    PrivOwnedStr,
10    serde::{JsonObject, from_raw_json_value},
11};
12
13/// This represents the different actions that should be taken when a rule is matched, and
14/// controls how notifications are delivered to the client.
15///
16/// See [the spec](https://spec.matrix.org/v1.18/client-server-api/#actions) for details.
17#[derive(Clone, Debug)]
18#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
19pub enum Action {
20    /// Causes matching events to generate a notification (both in-app and remote / push).
21    Notify,
22
23    /// Causes matching events to generate an in-app notification but no remote / push
24    /// notification.
25    #[cfg(feature = "unstable-msc3768")]
26    NotifyInApp,
27
28    /// Sets an entry in the 'tweaks' dictionary sent to the push gateway.
29    SetTweak(Tweak),
30
31    /// An unknown action.
32    #[doc(hidden)]
33    _Custom(CustomAction),
34}
35
36impl Action {
37    /// Creates a new `Action`.
38    ///
39    /// Prefer to use the public variants of `Action` where possible; this constructor is meant
40    /// be used for unsupported actions only and does not allow setting arbitrary data for
41    /// supported ones.
42    ///
43    /// # Errors
44    ///
45    /// Returns an error if the action type is known and deserialization of `data` to the
46    /// corresponding variant fails.
47    pub fn new(data: CustomActionData) -> serde_json::Result<Self> {
48        Ok(match data {
49            CustomActionData::String(s) => match s.as_str() {
50                "notify" => Self::Notify,
51                #[cfg(feature = "unstable-msc3768")]
52                "org.matrix.msc3768.notify_in_app" => Self::NotifyInApp,
53                _ => Self::_Custom(CustomAction(CustomActionData::String(s))),
54            },
55            CustomActionData::Object(o) => {
56                if o.contains_key("set_tweak") {
57                    Self::SetTweak(serde_json::from_value(o.into())?)
58                } else {
59                    Self::_Custom(CustomAction(CustomActionData::Object(o)))
60                }
61            }
62        })
63    }
64
65    /// Whether this action is an `Action::SetTweak(Tweak::Highlight(true))`.
66    pub fn is_highlight(&self) -> bool {
67        matches!(self, Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)))
68    }
69
70    /// Whether this action should trigger a notification (either in-app or remote / push).
71    pub fn should_notify(&self) -> bool {
72        match self {
73            Action::Notify => true,
74            #[cfg(feature = "unstable-msc3768")]
75            Action::NotifyInApp => true,
76            _ => false,
77        }
78    }
79
80    /// Whether this action should trigger a remote / push notification.
81    #[cfg(feature = "unstable-msc3768")]
82    pub fn should_notify_remote(&self) -> bool {
83        matches!(self, Action::Notify)
84    }
85
86    /// The sound that should be played with this action, if any.
87    pub fn sound(&self) -> Option<&SoundTweakValue> {
88        as_variant!(self, Action::SetTweak(Tweak::Sound(sound)) => sound)
89    }
90
91    /// Access the data if this is a custom action.
92    pub fn custom_data(&self) -> Option<&CustomActionData> {
93        as_variant!(self, Self::_Custom).map(|action| &action.0)
94    }
95}
96
97/// A custom action.
98#[doc(hidden)]
99#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(transparent)]
101pub struct CustomAction(CustomActionData);
102
103/// The data of a custom action.
104#[allow(unknown_lints, unnameable_types)]
105#[derive(Debug, Clone, Serialize, Deserialize)]
106#[serde(untagged)]
107pub enum CustomActionData {
108    /// A string.
109    String(String),
110
111    /// An object.
112    Object(JsonObject),
113}
114
115/// The `set_tweak` action.
116#[derive(Clone, Debug)]
117#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
118pub enum Tweak {
119    /// The sound to be played when this notification arrives.
120    ///
121    /// A device may choose to alert the user by some other means if appropriate, eg. vibration.
122    Sound(SoundTweakValue),
123
124    /// A boolean representing whether or not this message should be highlighted in the UI.
125    Highlight(HighlightTweakValue),
126
127    #[doc(hidden)]
128    _Custom(CustomTweak),
129}
130
131impl Tweak {
132    /// Creates a new `Tweak`.
133    ///
134    /// Prefer to use the public variants of `Tweak` where possible; this constructor is meant
135    /// be used for unsupported tweaks only and does not allow setting arbitrary data for
136    /// supported ones.
137    ///
138    /// # Errors
139    ///
140    /// Returns an error if the `set_tweak` is known and deserialization of `value` to the
141    /// corresponding variant fails.
142    pub fn new(set_tweak: String, value: Option<Box<RawJsonValue>>) -> serde_json::Result<Self> {
143        Ok(match set_tweak.as_str() {
144            "sound" => Self::Sound(from_raw_json_value(
145                &value.ok_or_else(|| de::Error::missing_field("value"))?,
146            )?),
147            "highlight" => {
148                let value =
149                    value.map(|value| from_raw_json_value::<bool, _>(&value)).transpose()?;
150
151                let highlight = if value.is_none_or(|value| value) {
152                    HighlightTweakValue::Yes
153                } else {
154                    HighlightTweakValue::No
155                };
156
157                Self::Highlight(highlight)
158            }
159            _ => Self::_Custom(CustomTweak { set_tweak, value }),
160        })
161    }
162
163    /// Access the `set_tweak` value.
164    pub fn set_tweak(&self) -> &str {
165        match self {
166            Self::Sound(_) => "sound",
167            Self::Highlight(_) => "highlight",
168            Self::_Custom(CustomTweak { set_tweak, .. }) => set_tweak,
169        }
170    }
171
172    /// Access the value, if it is a custom tweak.
173    pub fn custom_value(&self) -> Option<&RawJsonValue> {
174        as_variant!(self, Self::_Custom).and_then(|tweak| tweak.value.as_deref())
175    }
176}
177
178impl From<SoundTweakValue> for Tweak {
179    fn from(value: SoundTweakValue) -> Self {
180        Self::Sound(value)
181    }
182}
183
184impl From<HighlightTweakValue> for Tweak {
185    fn from(value: HighlightTweakValue) -> Self {
186        Self::Highlight(value)
187    }
188}
189
190/// A sound to play when a notification arrives.
191#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
192#[derive(Clone, StringEnum)]
193#[ruma_enum(rename_all = "lowercase")]
194#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
195pub enum SoundTweakValue {
196    /// Play the default notification sound.
197    Default,
198
199    #[doc(hidden)]
200    _Custom(PrivOwnedStr),
201}
202
203/// Whether or not a message should be highlighted in the UI.
204///
205/// This will normally take the form of presenting the message in a different color and/or
206/// style. The UI might also be adjusted to draw particular attention to the room in which the
207/// event occurred.
208#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
209#[allow(clippy::exhaustive_enums)]
210pub enum HighlightTweakValue {
211    /// Highlight the message.
212    #[default]
213    Yes,
214
215    /// Don't highlight the message.
216    No,
217}
218
219impl From<bool> for HighlightTweakValue {
220    fn from(value: bool) -> Self {
221        if value { Self::Yes } else { Self::No }
222    }
223}
224
225/// A custom tweak.
226#[doc(hidden)]
227#[derive(Clone, Debug, Serialize, Deserialize)]
228pub struct CustomTweak {
229    /// The kind of the custom tweak.
230    set_tweak: String,
231
232    /// The value of the custom tweak.
233    #[serde(skip_serializing_if = "Option::is_none")]
234    value: Option<Box<RawJsonValue>>,
235}
236
237#[cfg(test)]
238mod tests {
239    use assert_matches2::assert_matches;
240    use serde_json::{from_value as from_json_value, json};
241
242    use super::{Action, HighlightTweakValue, SoundTweakValue, Tweak};
243    use crate::assert_to_canonical_json_eq;
244
245    #[test]
246    fn serialize_notify() {
247        assert_to_canonical_json_eq!(Action::Notify, json!("notify"));
248    }
249
250    #[cfg(feature = "unstable-msc3768")]
251    #[test]
252    fn serialize_notify_in_app() {
253        assert_to_canonical_json_eq!(
254            Action::NotifyInApp,
255            json!("org.matrix.msc3768.notify_in_app"),
256        );
257    }
258
259    #[test]
260    fn serialize_tweak_sound() {
261        assert_to_canonical_json_eq!(
262            Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)),
263            json!({ "set_tweak": "sound", "value": "default" })
264        );
265    }
266
267    #[test]
268    fn serialize_tweak_highlight() {
269        assert_to_canonical_json_eq!(
270            Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
271            json!({ "set_tweak": "highlight" })
272        );
273
274        assert_to_canonical_json_eq!(
275            Action::SetTweak(Tweak::Highlight(HighlightTweakValue::No)),
276            json!({ "set_tweak": "highlight", "value": false })
277        );
278    }
279
280    #[test]
281    fn deserialize_notify() {
282        assert_matches!(from_json_value::<Action>(json!("notify")), Ok(Action::Notify));
283    }
284
285    #[cfg(feature = "unstable-msc3768")]
286    #[test]
287    fn deserialize_notify_in_app() {
288        assert_matches!(
289            from_json_value::<Action>(json!("org.matrix.msc3768.notify_in_app")),
290            Ok(Action::NotifyInApp)
291        );
292    }
293
294    #[test]
295    fn deserialize_tweak_sound() {
296        let json_data = json!({
297            "set_tweak": "sound",
298            "value": "default"
299        });
300        assert_matches!(
301            from_json_value::<Action>(json_data),
302            Ok(Action::SetTweak(Tweak::Sound(value)))
303        );
304        assert_eq!(value, SoundTweakValue::Default);
305
306        let json_data = json!({
307            "set_tweak": "sound",
308            "value": "custom"
309        });
310        assert_matches!(
311            from_json_value::<Action>(json_data),
312            Ok(Action::SetTweak(Tweak::Sound(value)))
313        );
314        assert_eq!(value.as_str(), "custom");
315    }
316
317    #[test]
318    fn deserialize_tweak_highlight() {
319        let json_data = json!({
320            "set_tweak": "highlight",
321            "value": true
322        });
323        assert_matches!(
324            from_json_value::<Action>(json_data),
325            Ok(Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)))
326        );
327    }
328
329    #[test]
330    fn deserialize_tweak_highlight_with_default_value() {
331        assert_matches!(
332            from_json_value::<Action>(json!({ "set_tweak": "highlight" })),
333            Ok(Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)))
334        );
335    }
336}