Skip to main content

nextcloud_notifications/
notification.rs

1//! Notification model representing a Nextcloud notification item.
2
3use serde::{Deserialize, Serialize};
4
5use crate::action::NotificationAction;
6use crate::wire::{lossy_string, lossy_u64};
7
8/// A Nextcloud notification item.
9///
10/// Returned by the Nextcloud Notifications OCS API v2.
11///
12/// See the official [Nextcloud Notifications API v2 Notification Element documentation](https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md#notification-element).
13#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
14pub struct Notification {
15    /// Unique identifier of the notification, used to retrieve or dismiss it.
16    #[serde(deserialize_with = "lossy_u64")]
17    pub notification_id: u64,
18
19    /// Name of the Nextcloud app that triggered the notification (e.g. `"files_sharing"`).
20    pub app: String,
21
22    /// User ID of the user that receives the notification.
23    pub user: String,
24
25    /// ISO 8601 date and time when the notification was published.
26    pub datetime: String,
27
28    /// Type of the object the notification is about (e.g. `"remote_share"`).
29    pub object_type: String,
30
31    /// Identifier of the object within the originating app.
32    #[serde(deserialize_with = "lossy_string")]
33    pub object_id: String,
34
35    /// Translated short subject presented to the user.
36    pub subject: String,
37
38    /// Optional translated detailed message text.
39    #[serde(default)]
40    pub message: String,
41
42    /// Optional link to follow when the subject or message is clicked.
43    #[serde(default)]
44    pub link: String,
45
46    /// Optional list of interactive actions available for this notification.
47    #[serde(default)]
48    pub actions: Vec<NotificationAction>,
49}
50
51impl Notification {
52    /// Creates a new notification instance.
53    #[allow(clippy::too_many_arguments)]
54    pub fn new(
55        notification_id: u64,
56        app: String,
57        user: String,
58        datetime: String,
59        object_type: String,
60        object_id: String,
61        subject: String,
62        message: String,
63        link: String,
64        actions: Vec<NotificationAction>,
65    ) -> Self {
66        Self {
67            notification_id,
68            app,
69            user,
70            datetime,
71            object_type,
72            object_id,
73            subject,
74            message,
75            link,
76            actions,
77        }
78    }
79
80    /// Unique identifier of the notification.
81    pub const fn notification_id(&self) -> u64 {
82        self.notification_id
83    }
84
85    /// Name of the Nextcloud app that triggered the notification.
86    pub fn app(&self) -> &str {
87        &self.app
88    }
89
90    /// User ID of the recipient.
91    pub fn user(&self) -> &str {
92        &self.user
93    }
94
95    /// ISO 8601 publication timestamp.
96    pub fn datetime(&self) -> &str {
97        &self.datetime
98    }
99
100    /// Classification of the object this notification references.
101    pub fn object_type(&self) -> &str {
102        &self.object_type
103    }
104
105    /// Identifier of the object within the originating app.
106    pub fn object_id(&self) -> &str {
107        &self.object_id
108    }
109
110    /// Short human-readable subject text.
111    pub fn subject(&self) -> &str {
112        &self.subject
113    }
114
115    /// Detailed message text, or empty if none was provided.
116    pub fn message(&self) -> &str {
117        &self.message
118    }
119
120    /// URL link associated with the notification, or empty if none was provided.
121    pub fn link(&self) -> &str {
122        &self.link
123    }
124
125    /// Interactive actions available for this notification.
126    pub fn actions(&self) -> &[NotificationAction] {
127        &self.actions
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn parses_notification_from_json() {
137        let json = r#"{
138            "notification_id": 61,
139            "app": "files_sharing",
140            "user": "admin",
141            "datetime": "2026-10-24T12:00:00+00:00",
142            "object_type": "remote_share",
143            "object_id": "13",
144            "subject": "Share invitation received",
145            "message": "User shared a folder",
146            "link": "https://example.com/share/13",
147            "actions": [
148                {
149                    "label": "Accept",
150                    "link": "https://example.com/share/accept",
151                    "type": "POST",
152                    "primary": true
153                }
154            ]
155        }"#;
156
157        let n: Notification = serde_json::from_str(json).unwrap();
158        assert_eq!(n.notification_id(), 61);
159        assert_eq!(n.app(), "files_sharing");
160        assert_eq!(n.user(), "admin");
161        assert_eq!(n.datetime(), "2026-10-24T12:00:00+00:00");
162        assert_eq!(n.object_type(), "remote_share");
163        assert_eq!(n.object_id(), "13");
164        assert_eq!(n.subject(), "Share invitation received");
165        assert_eq!(n.message(), "User shared a folder");
166        assert_eq!(n.link(), "https://example.com/share/13");
167        assert_eq!(n.actions().len(), 1);
168        assert_eq!(n.actions()[0].label(), "Accept");
169    }
170
171    #[test]
172    fn parses_numeric_object_id_and_string_notification_id() {
173        let json = r#"{
174            "notification_id": "99",
175            "app": "core",
176            "user": "alice",
177            "datetime": "2026-10-24T12:00:00+00:00",
178            "object_type": "user",
179            "object_id": 1234,
180            "subject": "Password reset"
181        }"#;
182
183        let n: Notification = serde_json::from_str(json).unwrap();
184        assert_eq!(n.notification_id(), 99);
185        assert_eq!(n.object_id(), "1234");
186        assert_eq!(n.message(), "");
187        assert_eq!(n.link(), "");
188        assert!(n.actions().is_empty());
189    }
190}