1use nextcloud_ocs::{OcsEnvelope, OcsStatus};
4use nextcloud_sdk_core::{
5 ApiPath, AuthProvider, DecodeError, Error, Headers, HttpMethod, Request, Response, StatusCode,
6 StatusError, Transport,
7};
8use serde::Deserialize;
9
10use crate::notification::Notification;
11
12#[cfg(test)]
13mod tests;
14
15pub const NOTIFICATIONS_API_PATH: &str = "apps/notifications/api/v2/notifications";
19
20const OCS_REQUEST_HEADER: &str = "OCS-APIRequest";
22const ACCEPT_HEADER: &str = "Accept";
24const ACCEPT_JSON: &str = "application/json";
25
26#[derive(Clone, Copy, Debug)]
37pub struct NotificationClient<'a, T: Transport, A: AuthProvider> {
38 transport: &'a T,
39 auth: &'a A,
40}
41
42impl<'a, T: Transport, A: AuthProvider> NotificationClient<'a, T, A> {
43 pub const fn new(transport: &'a T, auth: &'a A) -> Self {
45 Self { transport, auth }
46 }
47
48 pub const fn transport(&self) -> &'a T {
50 self.transport
51 }
52
53 pub const fn auth(&self) -> &'a A {
55 self.auth
56 }
57
58 pub async fn list(&self) -> Result<Vec<Notification>, Error> {
67 tracing::debug!("Listing user notifications");
68 let api_path = ApiPath::ocs(NOTIFICATIONS_API_PATH)?;
69 let headers = self.authenticated_headers();
70 let request = Request::get(api_path.clone()).with_headers(headers);
71
72 let response = self.transport.execute(request).await?;
73 self.decode_list(api_path, response)
74 }
75
76 pub async fn get(&self, notification_id: u64) -> Result<Notification, Error> {
82 tracing::debug!(notification_id, "Fetching notification details");
83 let api_path = ApiPath::ocs(format!("{NOTIFICATIONS_API_PATH}/{notification_id}"))?;
84 let headers = self.authenticated_headers();
85 let request = Request::get(api_path.clone()).with_headers(headers);
86
87 let response = self.transport.execute(request).await?;
88 self.decode_single(api_path, response)
89 }
90
91 pub async fn delete(&self, notification_id: u64) -> Result<(), Error> {
97 tracing::info!(notification_id, "Deleting notification");
98 let api_path = ApiPath::ocs(format!("{NOTIFICATIONS_API_PATH}/{notification_id}"))?;
99 let headers = self.authenticated_headers();
100 let request = Request::new(HttpMethod::Delete, api_path.clone()).with_headers(headers);
101
102 let response = self.transport.execute(request).await?;
103 self.decode_empty(HttpMethod::Delete, api_path, response)
104 }
105
106 pub async fn delete_all(&self) -> Result<(), Error> {
112 tracing::info!("Deleting all user notifications");
113 let api_path = ApiPath::ocs(NOTIFICATIONS_API_PATH)?;
114 let headers = self.authenticated_headers();
115 let request = Request::new(HttpMethod::Delete, api_path.clone()).with_headers(headers);
116
117 let response = self.transport.execute(request).await?;
118 self.decode_empty(HttpMethod::Delete, api_path, response)
119 }
120
121 fn authenticated_headers(&self) -> Headers {
122 let mut headers = Headers::new();
123 headers.set(OCS_REQUEST_HEADER, "true");
124 headers.set(ACCEPT_HEADER, ACCEPT_JSON);
125 self.auth.authenticate(&mut headers);
126 headers
127 }
128
129 fn decode_list(&self, path: ApiPath, response: Response) -> Result<Vec<Notification>, Error> {
130 if response.status() == StatusCode::new(204) {
131 return Ok(Vec::new());
132 }
133
134 if !response.status().is_success() {
135 return Err(Error::Status(StatusError::new(
136 HttpMethod::Get.as_str(),
137 path,
138 response.status(),
139 response.body(),
140 )));
141 }
142
143 if response.body().is_empty() {
144 return Ok(Vec::new());
145 }
146
147 let envelope: OcsEnvelope<Option<Vec<Notification>>> =
148 serde_json::from_slice(response.body())
149 .map_err(|cause| {
150 DecodeError::new(
151 std::any::type_name::<OcsEnvelope<Vec<Notification>>>(),
152 cause,
153 )
154 })
155 .map_err(Error::Decode)?;
156
157 if envelope.meta().verdict() == Some(OcsStatus::Failure) {
158 return Err(Error::Status(StatusError::new(
159 HttpMethod::Get.as_str(),
160 path,
161 response.status(),
162 response.body(),
163 )));
164 }
165
166 Ok(envelope.into_data().unwrap_or_default())
167 }
168
169 fn decode_single(&self, path: ApiPath, response: Response) -> Result<Notification, Error> {
170 if !response.status().is_success() {
171 return Err(Error::Status(StatusError::new(
172 HttpMethod::Get.as_str(),
173 path,
174 response.status(),
175 response.body(),
176 )));
177 }
178
179 let envelope: OcsEnvelope<NotificationOrList> = serde_json::from_slice(response.body())
180 .map_err(|cause| {
181 DecodeError::new(std::any::type_name::<OcsEnvelope<Notification>>(), cause)
182 })
183 .map_err(Error::Decode)?;
184
185 if envelope.meta().verdict() == Some(OcsStatus::Failure) {
186 return Err(Error::Status(StatusError::new(
187 HttpMethod::Get.as_str(),
188 path,
189 response.status(),
190 response.body(),
191 )));
192 }
193
194 envelope.into_data().into_notification()
195 }
196
197 fn decode_empty(
198 &self,
199 method: HttpMethod,
200 path: ApiPath,
201 response: Response,
202 ) -> Result<(), Error> {
203 if !response.status().is_success() {
204 return Err(Error::Status(StatusError::new(
205 method.as_str(),
206 path,
207 response.status(),
208 response.body(),
209 )));
210 }
211
212 if response.body().is_empty() {
213 return Ok(());
214 }
215
216 let envelope: OcsEnvelope<serde_json::Value> = serde_json::from_slice(response.body())
217 .map_err(|cause| {
218 DecodeError::new(
219 std::any::type_name::<OcsEnvelope<serde_json::Value>>(),
220 cause,
221 )
222 })
223 .map_err(Error::Decode)?;
224
225 if envelope.meta().verdict() == Some(OcsStatus::Failure) {
226 return Err(Error::Status(StatusError::new(
227 method.as_str(),
228 path,
229 response.status(),
230 response.body(),
231 )));
232 }
233
234 Ok(())
235 }
236}
237
238#[derive(Deserialize)]
241#[serde(untagged)]
242enum NotificationOrList {
243 Single(Notification),
244 List(Vec<Notification>),
245}
246
247impl NotificationOrList {
248 fn into_notification(self) -> Result<Notification, Error> {
249 match self {
250 Self::Single(notification) => Ok(notification),
251 Self::List(mut list) => list.pop().ok_or_else(|| {
252 Error::Decode(DecodeError::new(
253 std::any::type_name::<Notification>(),
254 std::io::Error::new(
255 std::io::ErrorKind::NotFound,
256 "notification response list was empty",
257 ),
258 ))
259 }),
260 }
261 }
262}