1pub mod v1 {
6 use js_int::{uint, UInt};
11 use ruma_common::{
12 api::{request, response, Metadata},
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 const METADATA: Metadata = metadata! {
25 method: POST,
26 rate_limited: false,
27 authentication: None,
28 history: {
29 1.0 => "/_matrix/push/v1/notify",
30 }
31 };
32
33 #[request]
35 pub struct Request {
36 pub notification: Notification,
38 }
39
40 #[response]
42 #[derive(Default)]
43 pub struct Response {
44 pub rejected: Vec<String>,
52 }
53
54 impl Request {
55 pub fn new(notification: Notification) -> Self {
57 Self { notification }
58 }
59 }
60
61 impl Response {
62 pub fn new(rejected: Vec<String>) -> Self {
64 Self { rejected }
65 }
66 }
67
68 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
70 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
71 pub struct Notification {
72 #[serde(skip_serializing_if = "Option::is_none")]
78 pub event_id: Option<OwnedEventId>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
84 pub room_id: Option<OwnedRoomId>,
85
86 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
88 pub event_type: Option<TimelineEventType>,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub sender: Option<OwnedUserId>,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub sender_display_name: Option<String>,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub room_name: Option<String>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub room_alias: Option<OwnedRoomAliasId>,
105
106 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
109 pub user_is_target: bool,
110
111 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
117 pub prio: NotificationPriority,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
123 pub content: Option<Box<RawJsonValue>>,
124
125 #[serde(default, skip_serializing_if = "NotificationCounts::is_default")]
129 pub counts: NotificationCounts,
130
131 pub devices: Vec<Device>,
133 }
134
135 impl Notification {
136 pub fn new(devices: Vec<Device>) -> Self {
138 Notification { devices, ..Default::default() }
139 }
140 }
141
142 #[derive(Clone, Default, PartialEq, Eq, StringEnum)]
151 #[ruma_enum(rename_all = "snake_case")]
152 #[non_exhaustive]
153 pub enum NotificationPriority {
154 #[default]
156 High,
157
158 Low,
160
161 #[doc(hidden)]
162 _Custom(PrivOwnedStr),
163 }
164
165 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
167 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
168 pub struct NotificationCounts {
169 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
172 pub unread: UInt,
173
174 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
177 pub missed_calls: UInt,
178 }
179
180 impl NotificationCounts {
181 pub fn new(unread: UInt, missed_calls: UInt) -> Self {
184 NotificationCounts { unread, missed_calls }
185 }
186
187 fn is_default(&self) -> bool {
188 self.unread == uint!(0) && self.missed_calls == uint!(0)
189 }
190 }
191
192 #[derive(Clone, Debug, Deserialize, Serialize)]
194 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
195 pub struct Device {
196 pub app_id: String,
200
201 pub pushkey: String,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub pushkey_ts: Option<SecondsSinceUnixEpoch>,
209
210 #[serde(default, skip_serializing_if = "PusherData::is_empty")]
212 pub data: PusherData,
213
214 #[serde(with = "tweak_serde", skip_serializing_if = "Vec::is_empty")]
218 pub tweaks: Vec<Tweak>,
219 }
220
221 impl Device {
222 pub fn new(app_id: String, pushkey: String) -> Self {
224 Device {
225 app_id,
226 pushkey,
227 pushkey_ts: None,
228 data: PusherData::new(),
229 tweaks: Vec::new(),
230 }
231 }
232 }
233
234 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
241 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
242 pub struct PusherData {
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub format: Option<PushFormat>,
246
247 #[serde(flatten, default, skip_serializing_if = "JsonObject::is_empty")]
249 pub data: JsonObject,
250 }
251
252 impl PusherData {
253 pub fn new() -> Self {
255 Default::default()
256 }
257
258 pub fn is_empty(&self) -> bool {
260 self.format.is_none() && self.data.is_empty()
261 }
262 }
263
264 impl From<ruma_common::push::HttpPusherData> for PusherData {
265 fn from(data: ruma_common::push::HttpPusherData) -> Self {
266 let ruma_common::push::HttpPusherData { format, data, .. } = data;
267
268 Self { format, data }
269 }
270 }
271
272 mod tweak_serde {
273 use std::fmt;
274
275 use ruma_common::push::Tweak;
276 use serde::{
277 de::{MapAccess, Visitor},
278 ser::SerializeMap,
279 Deserializer, Serializer,
280 };
281
282 pub(super) fn serialize<S>(tweak: &[Tweak], serializer: S) -> Result<S::Ok, S::Error>
283 where
284 S: Serializer,
285 {
286 let mut map = serializer.serialize_map(Some(tweak.len()))?;
287 for item in tweak {
288 #[allow(unreachable_patterns)]
289 match item {
290 Tweak::Highlight(b) => map.serialize_entry("highlight", b)?,
291 Tweak::Sound(value) => map.serialize_entry("sound", value)?,
292 Tweak::Custom { value, name } => map.serialize_entry(name, value)?,
293 _ => unreachable!("variant added to Tweak not covered by Custom"),
294 }
295 }
296 map.end()
297 }
298
299 struct TweaksVisitor;
300
301 impl<'de> Visitor<'de> for TweaksVisitor {
302 type Value = Vec<Tweak>;
303
304 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
305 formatter.write_str("List of tweaks")
306 }
307
308 fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
309 where
310 M: MapAccess<'de>,
311 {
312 let mut tweaks = vec![];
313 while let Some(key) = access.next_key::<String>()? {
314 match &*key {
315 "sound" => tweaks.push(Tweak::Sound(access.next_value()?)),
316 "highlight" => {
319 let highlight = access.next_value().unwrap_or(true);
320
321 tweaks.push(Tweak::Highlight(highlight));
322 }
323 _ => tweaks.push(Tweak::Custom { name: key, value: access.next_value()? }),
324 };
325 }
326
327 if !tweaks.iter().any(|tw| matches!(tw, Tweak::Highlight(_))) {
330 tweaks.push(Tweak::Highlight(false));
331 }
332
333 Ok(tweaks)
334 }
335 }
336
337 pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Tweak>, D::Error>
338 where
339 D: Deserializer<'de>,
340 {
341 deserializer.deserialize_map(TweaksVisitor)
342 }
343 }
344
345 #[cfg(test)]
346 mod tests {
347 use js_int::uint;
348 use ruma_common::{
349 owned_event_id, owned_room_alias_id, owned_room_id, owned_user_id,
350 SecondsSinceUnixEpoch,
351 };
352 use ruma_events::TimelineEventType;
353 use serde_json::{
354 from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
355 };
356
357 use super::{Device, Notification, NotificationCounts, NotificationPriority, Tweak};
358
359 #[test]
360 fn serialize_request() {
361 let expected = json!({
362 "event_id": "$3957tyerfgewrf384",
363 "room_id": "!slw48wfj34rtnrf:example.com",
364 "type": "m.room.message",
365 "sender": "@exampleuser:matrix.org",
366 "sender_display_name": "Major Tom",
367 "room_alias": "#exampleroom:matrix.org",
368 "prio": "low",
369 "content": {},
370 "counts": {
371 "unread": 2,
372 },
373 "devices": [
374 {
375 "app_id": "org.matrix.matrixConsole.ios",
376 "pushkey": "V2h5IG9uIGVhcnRoIGRpZCB5b3UgZGVjb2RlIHRoaXM/",
377 "pushkey_ts": 123,
378 "tweaks": {
379 "sound": "silence",
380 "highlight": true,
381 "custom": "go wild"
382 }
383 }
384 ]
385 });
386
387 let eid = owned_event_id!("$3957tyerfgewrf384");
388 let rid = owned_room_id!("!slw48wfj34rtnrf:example.com");
389 let uid = owned_user_id!("@exampleuser:matrix.org");
390 let alias = owned_room_alias_id!("#exampleroom:matrix.org");
391
392 let count = NotificationCounts { unread: uint!(2), ..NotificationCounts::default() };
393
394 let device = Device {
395 pushkey_ts: Some(SecondsSinceUnixEpoch(uint!(123))),
396 tweaks: vec![
397 Tweak::Highlight(true),
398 Tweak::Sound("silence".into()),
399 Tweak::Custom {
400 name: "custom".into(),
401 value: from_json_value(JsonValue::String("go wild".into())).unwrap(),
402 },
403 ],
404 ..Device::new(
405 "org.matrix.matrixConsole.ios".into(),
406 "V2h5IG9uIGVhcnRoIGRpZCB5b3UgZGVjb2RlIHRoaXM/".into(),
407 )
408 };
409 let devices = vec![device];
410
411 let notice = Notification {
412 event_id: Some(eid),
413 room_id: Some(rid),
414 event_type: Some(TimelineEventType::RoomMessage),
415 sender: Some(uid),
416 sender_display_name: Some("Major Tom".to_owned()),
417 room_alias: Some(alias),
418 content: Some(serde_json::from_str("{}").unwrap()),
419 counts: count,
420 prio: NotificationPriority::Low,
421 devices,
422 ..Notification::default()
423 };
424
425 assert_eq!(expected, to_json_value(notice).unwrap());
426 }
427 }
428}