1pub mod v1 {
6 use js_int::{uint, UInt};
11 use ruma_common::{
12 api::{auth_scheme::NoAuthentication, request, response},
13 metadata,
14 push::{PushFormat, Tweak},
15 serde::{JsonObject, StringEnum},
16 OwnedEventId, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, SecondsSinceUnixEpoch,
17 };
18 use ruma_events::TimelineEventType;
19 use serde::{Deserialize, Serialize};
20 use serde_json::value::RawValue as RawJsonValue;
21
22 use crate::PrivOwnedStr;
23
24 metadata! {
25 method: POST,
26 rate_limited: false,
27 authentication: NoAuthentication,
28 path: "/_matrix/push/v1/notify",
29 }
30
31 #[request]
33 pub struct Request {
34 pub notification: Notification,
36 }
37
38 #[response]
40 #[derive(Default)]
41 pub struct Response {
42 pub rejected: Vec<String>,
50 }
51
52 impl Request {
53 pub fn new(notification: Notification) -> Self {
55 Self { notification }
56 }
57 }
58
59 impl Response {
60 pub fn new(rejected: Vec<String>) -> Self {
62 Self { rejected }
63 }
64 }
65
66 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
68 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
69 pub struct Notification {
70 #[serde(skip_serializing_if = "Option::is_none")]
76 pub event_id: Option<OwnedEventId>,
77
78 #[serde(skip_serializing_if = "Option::is_none")]
82 pub room_id: Option<OwnedRoomId>,
83
84 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
86 pub event_type: Option<TimelineEventType>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub sender: Option<OwnedUserId>,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub sender_display_name: Option<String>,
95
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub room_name: Option<String>,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub room_alias: Option<OwnedRoomAliasId>,
103
104 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
107 pub user_is_target: bool,
108
109 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
115 pub prio: NotificationPriority,
116
117 #[serde(skip_serializing_if = "Option::is_none")]
121 pub content: Option<Box<RawJsonValue>>,
122
123 #[serde(default, skip_serializing_if = "NotificationCounts::is_default")]
127 pub counts: NotificationCounts,
128
129 pub devices: Vec<Device>,
131 }
132
133 impl Notification {
134 pub fn new(devices: Vec<Device>) -> Self {
136 Notification { devices, ..Default::default() }
137 }
138 }
139
140 #[derive(Clone, Default, StringEnum)]
149 #[ruma_enum(rename_all = "snake_case")]
150 #[non_exhaustive]
151 pub enum NotificationPriority {
152 #[default]
154 High,
155
156 Low,
158
159 #[doc(hidden)]
160 _Custom(PrivOwnedStr),
161 }
162
163 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
165 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
166 pub struct NotificationCounts {
167 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
170 pub unread: UInt,
171
172 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
175 pub missed_calls: UInt,
176 }
177
178 impl NotificationCounts {
179 pub fn new(unread: UInt, missed_calls: UInt) -> Self {
182 NotificationCounts { unread, missed_calls }
183 }
184
185 fn is_default(&self) -> bool {
186 self.unread == uint!(0) && self.missed_calls == uint!(0)
187 }
188 }
189
190 #[derive(Clone, Debug, Deserialize, Serialize)]
192 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
193 pub struct Device {
194 pub app_id: String,
198
199 pub pushkey: String,
203
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub pushkey_ts: Option<SecondsSinceUnixEpoch>,
207
208 #[serde(default, skip_serializing_if = "PusherData::is_empty")]
210 pub data: PusherData,
211
212 #[serde(with = "tweak_serde", skip_serializing_if = "Vec::is_empty")]
216 pub tweaks: Vec<Tweak>,
217 }
218
219 impl Device {
220 pub fn new(app_id: String, pushkey: String) -> Self {
222 Device {
223 app_id,
224 pushkey,
225 pushkey_ts: None,
226 data: PusherData::new(),
227 tweaks: Vec::new(),
228 }
229 }
230 }
231
232 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
239 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
240 pub struct PusherData {
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub format: Option<PushFormat>,
244
245 #[serde(flatten, default, skip_serializing_if = "JsonObject::is_empty")]
247 pub data: JsonObject,
248 }
249
250 impl PusherData {
251 pub fn new() -> Self {
253 Default::default()
254 }
255
256 pub fn is_empty(&self) -> bool {
258 self.format.is_none() && self.data.is_empty()
259 }
260 }
261
262 impl From<ruma_common::push::HttpPusherData> for PusherData {
263 fn from(data: ruma_common::push::HttpPusherData) -> Self {
264 let ruma_common::push::HttpPusherData { format, data, .. } = data;
265
266 Self { format, data }
267 }
268 }
269
270 mod tweak_serde {
271 use std::fmt;
272
273 use ruma_common::push::Tweak;
274 use serde::{
275 de::{MapAccess, Visitor},
276 ser::SerializeMap,
277 Deserializer, Serializer,
278 };
279
280 pub(super) fn serialize<S>(tweak: &[Tweak], serializer: S) -> Result<S::Ok, S::Error>
281 where
282 S: Serializer,
283 {
284 let mut map = serializer.serialize_map(Some(tweak.len()))?;
285 for item in tweak {
286 #[allow(unreachable_patterns)]
287 match item {
288 Tweak::Highlight(b) => map.serialize_entry("highlight", b)?,
289 Tweak::Sound(value) => map.serialize_entry("sound", value)?,
290 Tweak::Custom { value, name } => map.serialize_entry(name, value)?,
291 _ => unreachable!("variant added to Tweak not covered by Custom"),
292 }
293 }
294 map.end()
295 }
296
297 struct TweaksVisitor;
298
299 impl<'de> Visitor<'de> for TweaksVisitor {
300 type Value = Vec<Tweak>;
301
302 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
303 formatter.write_str("List of tweaks")
304 }
305
306 fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
307 where
308 M: MapAccess<'de>,
309 {
310 let mut tweaks = vec![];
311 while let Some(key) = access.next_key::<String>()? {
312 match &*key {
313 "sound" => tweaks.push(Tweak::Sound(access.next_value()?)),
314 "highlight" => {
317 let highlight = access.next_value().unwrap_or(true);
318
319 tweaks.push(Tweak::Highlight(highlight));
320 }
321 _ => tweaks.push(Tweak::Custom { name: key, value: access.next_value()? }),
322 }
323 }
324
325 if !tweaks.iter().any(|tw| matches!(tw, Tweak::Highlight(_))) {
328 tweaks.push(Tweak::Highlight(false));
329 }
330
331 Ok(tweaks)
332 }
333 }
334
335 pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Tweak>, D::Error>
336 where
337 D: Deserializer<'de>,
338 {
339 deserializer.deserialize_map(TweaksVisitor)
340 }
341 }
342
343 #[cfg(test)]
344 mod tests {
345 use js_int::uint;
346 use ruma_common::{
347 owned_event_id, owned_room_alias_id, owned_room_id, owned_user_id,
348 SecondsSinceUnixEpoch,
349 };
350 use ruma_events::TimelineEventType;
351 use serde_json::{
352 from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
353 };
354
355 use super::{Device, Notification, NotificationCounts, NotificationPriority, Tweak};
356
357 #[test]
358 fn serialize_request() {
359 let expected = json!({
360 "event_id": "$3957tyerfgewrf384",
361 "room_id": "!slw48wfj34rtnrf:example.com",
362 "type": "m.room.message",
363 "sender": "@exampleuser:matrix.org",
364 "sender_display_name": "Major Tom",
365 "room_alias": "#exampleroom:matrix.org",
366 "prio": "low",
367 "content": {},
368 "counts": {
369 "unread": 2,
370 },
371 "devices": [
372 {
373 "app_id": "org.matrix.matrixConsole.ios",
374 "pushkey": "V2h5IG9uIGVhcnRoIGRpZCB5b3UgZGVjb2RlIHRoaXM/",
375 "pushkey_ts": 123,
376 "tweaks": {
377 "sound": "silence",
378 "highlight": true,
379 "custom": "go wild"
380 }
381 }
382 ]
383 });
384
385 let eid = owned_event_id!("$3957tyerfgewrf384");
386 let rid = owned_room_id!("!slw48wfj34rtnrf:example.com");
387 let uid = owned_user_id!("@exampleuser:matrix.org");
388 let alias = owned_room_alias_id!("#exampleroom:matrix.org");
389
390 let count = NotificationCounts { unread: uint!(2), ..NotificationCounts::default() };
391
392 let device = Device {
393 pushkey_ts: Some(SecondsSinceUnixEpoch(uint!(123))),
394 tweaks: vec![
395 Tweak::Highlight(true),
396 Tweak::Sound("silence".into()),
397 Tweak::Custom {
398 name: "custom".into(),
399 value: from_json_value(JsonValue::String("go wild".into())).unwrap(),
400 },
401 ],
402 ..Device::new(
403 "org.matrix.matrixConsole.ios".into(),
404 "V2h5IG9uIGVhcnRoIGRpZCB5b3UgZGVjb2RlIHRoaXM/".into(),
405 )
406 };
407 let devices = vec![device];
408
409 let notice = Notification {
410 event_id: Some(eid),
411 room_id: Some(rid),
412 event_type: Some(TimelineEventType::RoomMessage),
413 sender: Some(uid),
414 sender_display_name: Some("Major Tom".to_owned()),
415 room_alias: Some(alias),
416 content: Some(serde_json::from_str("{}").unwrap()),
417 counts: count,
418 prio: NotificationPriority::Low,
419 devices,
420 ..Notification::default()
421 };
422
423 assert_eq!(expected, to_json_value(notice).unwrap());
424 }
425 }
426}