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#[derive(Clone, Debug)]
18#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
19pub enum Action {
20 Notify,
22
23 #[cfg(feature = "unstable-msc3768")]
26 NotifyInApp,
27
28 SetTweak(Tweak),
30
31 #[doc(hidden)]
33 _Custom(CustomAction),
34}
35
36impl Action {
37 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 pub fn is_highlight(&self) -> bool {
67 matches!(self, Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)))
68 }
69
70 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 #[cfg(feature = "unstable-msc3768")]
82 pub fn should_notify_remote(&self) -> bool {
83 matches!(self, Action::Notify)
84 }
85
86 pub fn sound(&self) -> Option<&SoundTweakValue> {
88 as_variant!(self, Action::SetTweak(Tweak::Sound(sound)) => sound)
89 }
90
91 pub fn custom_data(&self) -> Option<&CustomActionData> {
93 as_variant!(self, Self::_Custom).map(|action| &action.0)
94 }
95}
96
97#[doc(hidden)]
99#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(transparent)]
101pub struct CustomAction(CustomActionData);
102
103#[allow(unknown_lints, unnameable_types)]
105#[derive(Debug, Clone, Serialize, Deserialize)]
106#[serde(untagged)]
107pub enum CustomActionData {
108 String(String),
110
111 Object(JsonObject),
113}
114
115#[derive(Clone, Debug)]
117#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
118pub enum Tweak {
119 Sound(SoundTweakValue),
123
124 Highlight(HighlightTweakValue),
126
127 #[doc(hidden)]
128 _Custom(CustomTweak),
129}
130
131impl Tweak {
132 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 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 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#[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 Default,
198
199 #[doc(hidden)]
200 _Custom(PrivOwnedStr),
201}
202
203#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
209#[allow(clippy::exhaustive_enums)]
210pub enum HighlightTweakValue {
211 #[default]
213 Yes,
214
215 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#[doc(hidden)]
227#[derive(Clone, Debug, Serialize, Deserialize)]
228pub struct CustomTweak {
229 set_tweak: String,
231
232 #[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}