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