ruma_push_gateway_api/
send_event_notification.rs

1//! `POST /_matrix/push/*/notify`
2//!
3//! Notify a push gateway about an event or update the number of unread notifications a user has.
4
5pub mod v1 {
6    //! `/v1/` ([spec])
7    //!
8    //! [spec]: https://spec.matrix.org/latest/push-gateway-api/#post_matrixpushv1notify
9
10    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 type for the `send_event_notification` endpoint.
32    #[request]
33    pub struct Request {
34        /// Information about the push notification
35        pub notification: Notification,
36    }
37
38    /// Response type for the `send_event_notification` endpoint.
39    #[response]
40    #[derive(Default)]
41    pub struct Response {
42        /// A list of all pushkeys given in the notification request that are not valid.
43        ///
44        /// These could have been rejected by an upstream gateway because they have expired or have
45        /// never been valid. Homeservers must cease sending notification requests for these
46        /// pushkeys and remove the associated pushers. It may not necessarily be the notification
47        /// in the request that failed: it could be that a previous notification to the same
48        /// pushkey failed. May be empty.
49        pub rejected: Vec<String>,
50    }
51
52    impl Request {
53        /// Creates a new `Request` with the given notification.
54        pub fn new(notification: Notification) -> Self {
55            Self { notification }
56        }
57    }
58
59    impl Response {
60        /// Creates a new `Response` with the given list of rejected pushkeys.
61        pub fn new(rejected: Vec<String>) -> Self {
62            Self { rejected }
63        }
64    }
65
66    /// Type for passing information about a push notification
67    #[derive(Clone, Debug, Default, Deserialize, Serialize)]
68    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
69    pub struct Notification {
70        /// The Matrix event ID of the event being notified about.
71        ///
72        /// Required if the notification is about a particular Matrix event. May be omitted for
73        /// notifications that only contain updated badge counts. This ID can and should be used to
74        /// detect duplicate notification requests.
75        #[serde(skip_serializing_if = "Option::is_none")]
76        pub event_id: Option<OwnedEventId>,
77
78        /// The ID of the room in which this event occurred.
79        ///
80        /// Required if the notification relates to a specific Matrix event.
81        #[serde(skip_serializing_if = "Option::is_none")]
82        pub room_id: Option<OwnedRoomId>,
83
84        /// The type of the event as in the event's `type` field.
85        #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
86        pub event_type: Option<TimelineEventType>,
87
88        /// The sender of the event as in the corresponding event field.
89        #[serde(skip_serializing_if = "Option::is_none")]
90        pub sender: Option<OwnedUserId>,
91
92        /// The current display name of the sender in the room in which the event occurred.
93        #[serde(skip_serializing_if = "Option::is_none")]
94        pub sender_display_name: Option<String>,
95
96        /// The name of the room in which the event occurred.
97        #[serde(skip_serializing_if = "Option::is_none")]
98        pub room_name: Option<String>,
99
100        /// An alias to display for the room in which the event occurred.
101        #[serde(skip_serializing_if = "Option::is_none")]
102        pub room_alias: Option<OwnedRoomAliasId>,
103
104        /// Whether the user receiving the notification is the subject of a member event (i.e. the
105        /// `state_key` of the member event is equal to the user's Matrix ID).
106        #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
107        pub user_is_target: bool,
108
109        /// The priority of the notification.
110        ///
111        /// If omitted, `high` is assumed. This may be used by push gateways to deliver less
112        /// time-sensitive notifications in a way that will preserve battery power on mobile
113        /// devices.
114        #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
115        pub prio: NotificationPriority,
116
117        /// The `content` field from the event, if present.
118        ///
119        /// The pusher may omit this if the event had no content or for any other reason.
120        #[serde(skip_serializing_if = "Option::is_none")]
121        pub content: Option<Box<RawJsonValue>>,
122
123        /// Current number of unacknowledged communications for the recipient user.
124        ///
125        /// Counts whose value is zero should be omitted.
126        #[serde(default, skip_serializing_if = "NotificationCounts::is_default")]
127        pub counts: NotificationCounts,
128
129        /// An array of devices that the notification should be sent to.
130        pub devices: Vec<Device>,
131    }
132
133    impl Notification {
134        /// Create a new notification for the given devices.
135        pub fn new(devices: Vec<Device>) -> Self {
136            Notification { devices, ..Default::default() }
137        }
138    }
139
140    /// Type for passing information about notification priority.
141    ///
142    /// This may be used by push gateways to deliver less time-sensitive
143    /// notifications in a way that will preserve battery power on mobile devices.
144    ///
145    /// This type can hold an arbitrary string. To build this with a custom value, convert it from a
146    /// string with `::from()` / `.into()`. To check for values that are not available as a
147    /// documented variant here, use its string representation, obtained through `.as_str()`.
148    #[derive(Clone, Default, StringEnum)]
149    #[ruma_enum(rename_all = "snake_case")]
150    #[non_exhaustive]
151    pub enum NotificationPriority {
152        /// A high priority notification
153        #[default]
154        High,
155
156        /// A low priority notification
157        Low,
158
159        #[doc(hidden)]
160        _Custom(PrivOwnedStr),
161    }
162
163    /// Type for passing information about notification counts.
164    #[derive(Clone, Debug, Default, Deserialize, Serialize)]
165    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
166    pub struct NotificationCounts {
167        /// The number of unread messages a user has across all of the rooms they
168        /// are a member of.
169        #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
170        pub unread: UInt,
171
172        /// The number of unacknowledged missed calls a user has across all rooms of
173        /// which they are a member.
174        #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
175        pub missed_calls: UInt,
176    }
177
178    impl NotificationCounts {
179        /// Create new notification counts from the given unread and missed call
180        /// counts.
181        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    /// Type for passing information about devices.
191    #[derive(Clone, Debug, Deserialize, Serialize)]
192    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
193    pub struct Device {
194        /// The `app_id` given when the pusher was created.
195        ///
196        /// Max length: 64 chars.
197        pub app_id: String,
198
199        /// The `pushkey` given when the pusher was created.
200        ///
201        /// Max length: 512 bytes.
202        pub pushkey: String,
203
204        /// The unix timestamp (in seconds) when the pushkey was last updated.
205        #[serde(skip_serializing_if = "Option::is_none")]
206        pub pushkey_ts: Option<SecondsSinceUnixEpoch>,
207
208        /// A dictionary of additional pusher-specific data.
209        #[serde(default, skip_serializing_if = "PusherData::is_empty")]
210        pub data: PusherData,
211
212        /// A dictionary of customisations made to the way this notification is to be presented.
213        ///
214        /// These are added by push rules.
215        #[serde(with = "tweak_serde", skip_serializing_if = "Vec::is_empty")]
216        pub tweaks: Vec<Tweak>,
217    }
218
219    impl Device {
220        /// Create a new device with the given app id and pushkey
221        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    /// Information for the pusher implementation itself.
233    ///
234    /// This is the data dictionary passed in at pusher creation minus the `url` key.
235    ///
236    /// It can be constructed from [`ruma_common::push::HttpPusherData`] with `::from()` /
237    /// `.into()`.
238    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
239    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
240    pub struct PusherData {
241        /// The format to use when sending notifications to the Push Gateway.
242        #[serde(skip_serializing_if = "Option::is_none")]
243        pub format: Option<PushFormat>,
244
245        /// Custom data for the pusher.
246        #[serde(flatten, default, skip_serializing_if = "JsonObject::is_empty")]
247        pub data: JsonObject,
248    }
249
250    impl PusherData {
251        /// Creates an empty `PusherData`.
252        pub fn new() -> Self {
253            Default::default()
254        }
255
256        /// Returns `true` if all fields are `None` or empty.
257        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                        // If a highlight tweak is given with no value, its value is defined to be
315                        // true.
316                        "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 no highlight tweak is given at all then the value of highlight is defined to
326                // be false.
327                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}