wallabag_api/types/
entry.rs

1// Copyright 2018 Samuel Walladge <samuel@swalladge.net>
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8use crate::utils::serde::parse_hashmap_with_null_values;
9use crate::utils::serde::parse_intbool;
10
11use super::annotations::Annotations;
12use super::common::ID;
13use super::tags::Tags;
14
15/// type alias: a list of entries as returned from some endpoints
16pub type Entries = Vec<Entry>;
17
18/// A struct representing an entry from wallabag (a full saved article including
19/// all annotations and tags; annotations and tags do not need to be requested
20/// separately).
21///
22/// Most fields are controlled by the server. When creating an entry, the server will send a
23/// request to the given url and use the response to populate many of the fields. This response is
24/// what `headers`, `http_status`, `mimetype`, etc. are referring to.
25#[derive(Deserialize, Serialize, Debug)]
26pub struct Entry {
27    /// Annotation objects for this entry.
28    pub annotations: Option<Annotations>,
29
30    /// Content. Should be HTML if present.
31    pub content: Option<String>,
32
33    /// The timestamp of when the entry was created on the server.
34    pub created_at: DateTime<Utc>,
35
36    /// The resolved domain name of the url. Could be None if the server couldn't resolve the url.
37    pub domain_name: Option<String>,
38
39    /// A map of header name -> header value. These appear to be headers from the original source
40    /// url.
41    #[serde(deserialize_with = "parse_hashmap_with_null_values")]
42    pub headers: Option<HashMap<String, String>>,
43
44    /// I'm guessing this is the status the server got when retrieving the content from the url.
45    pub http_status: Option<String>,
46
47    /// ID of the entry. Should be an integer. Should also be unique, so can use this directly as
48    /// the local id if storing the entry in a DB.
49    pub id: ID,
50
51    /// The archived (or read) status of the entry. These boolean options are sometimes represented
52    /// as 0 or 1 from the API, which makes parsing in a strongly typed language annoying.
53    #[serde(deserialize_with = "parse_intbool")]
54    pub is_archived: bool,
55
56    /// The public shared status of the entry. If this is true, then there should be a public link
57    /// to this exact entry on the server. The link will be based around the value of the `uid`
58    /// field and (TODO: confirm if this can be relied on) formatted as BASE_URL/share/UID.
59    #[serde(deserialize_with = "parse_intbool")]
60    pub is_public: bool,
61
62    /// The starred status of the entry.
63    #[serde(deserialize_with = "parse_intbool")]
64    pub is_starred: bool,
65
66    /// The language of the entry - probably generated by the server from inspecting the response.
67    pub language: Option<String>,
68
69    /// The mimetype of the entry - probably generated by the server from inspecting the response.
70    /// Not sure about the support status for other mimetypes. Observed behaviour suggests that the
71    /// server converts everything to HTML - eg. a text/plain mimetype content will be plain text
72    /// surrounded by `<pre>` tags.
73    pub mimetype: Option<String>,
74
75    /// Supposedly the original url given will be stored here. If a shortened link is submitted to
76    /// the server, the short link will be here, but the resolved link will be in URL. Observed
77    /// behaviour is that this field is never set.
78    pub origin_url: Option<String>,
79
80    /// Optional url for an image related to the entry. Eg. for displaying as a background image to
81    /// the entry tile.
82    pub preview_picture: Option<String>,
83
84    /// Data about when the entry was published (scraped from the original web page).
85    pub published_at: Option<DateTime<Utc>>,
86
87    /// Data about who published the entry (scraped from the original web page).
88    pub published_by: Option<Vec<Option<String>>>,
89
90    /// Estimated reading time in minutes. Generated by the server, probably based off your set
91    /// reading speed or a default.
92    pub reading_time: u32,
93
94    /// Timestamp of when the entry was starred, if it is starred. Unstarring an entry sets this to
95    /// None.
96    pub starred_at: Option<DateTime<Utc>>,
97
98    /// A list of tag objects associated with this entry.
99    pub tags: Tags,
100
101    /// An optional title for the entry.
102    pub title: Option<String>,
103
104    /// This will be only set by the server as a unique id to identify the entry if it has been
105    /// shared. For example if you share via public link on framabag and the uid is FOO, then the
106    /// public url will be framabag.org/share/FOO
107    pub uid: Option<String>,
108
109    /// Timestamp when the entry was last updated. This is bumped for any change to any field
110    /// attached to the entry except for annotations.
111    ///
112    /// TODO: check if entry updates if a tag is globally edited (eg. renamed)
113    pub updated_at: DateTime<Utc>,
114
115    /// Resolved url of the entry. If the origin_url redirected to a different url (eg. via a
116    /// shortened link), the final url will be stored here.
117    pub url: Option<String>,
118
119    /// Email of the user who owns this entry. Currently `user_*` fields are redundant since you
120    /// can only access entries that belong to you. Entry sharing between users is planned for the
121    /// future so this may become relevant soon.
122    pub user_email: String,
123
124    /// ID of the user who owns this entry.
125    pub user_id: ID,
126
127    /// username of the user who owns this entry.
128    pub user_name: String,
129}
130
131/// A struct representing a deleted entry from wallabag (a full saved article including
132/// annotations and tags). The only difference from the full entry is that this
133/// doesn't have an id. Only used internally because a full entry gets
134/// reconstituted before being returned to the client.
135#[derive(Deserialize, Debug)]
136pub(crate) struct DeletedEntry {
137    pub annotations: Option<Annotations>,
138    pub content: Option<String>,
139    pub created_at: DateTime<Utc>,
140    pub domain_name: Option<String>,
141    pub headers: Option<HashMap<String, String>>,
142    pub http_status: Option<String>,
143
144    #[serde(deserialize_with = "parse_intbool")]
145    pub is_archived: bool,
146
147    #[serde(deserialize_with = "parse_intbool")]
148    pub is_public: bool,
149
150    #[serde(deserialize_with = "parse_intbool")]
151    pub is_starred: bool,
152    pub language: Option<String>,
153    pub mimetype: Option<String>,
154    pub origin_url: Option<String>,
155    pub preview_picture: Option<String>,
156    pub published_at: Option<DateTime<Utc>>,
157    pub published_by: Option<Vec<Option<String>>>,
158    pub reading_time: u32,
159    pub starred_at: Option<DateTime<Utc>>,
160    pub tags: Tags,
161    pub title: Option<String>,
162    pub uid: Option<String>,
163    pub updated_at: DateTime<Utc>,
164    pub url: Option<String>,
165    pub user_email: String,
166    pub user_id: ID,
167    pub user_name: String,
168}
169
170/// This is implemented so that an Entry can be used interchangeably with an ID
171/// for some client methods. For convenience.
172impl From<Entry> for ID {
173    fn from(entry: Entry) -> Self {
174        entry.id
175    }
176}
177
178/// This is implemented so that an &Entry can be used interchangeably with an ID
179/// for some client methods. For convenience.
180impl From<&Entry> for ID {
181    fn from(entry: &Entry) -> Self {
182        entry.id
183    }
184}
185
186/// Internal struct for retrieving a list of entries from the api when
187/// paginated.
188#[derive(Deserialize, Debug)]
189pub(crate) struct PaginatedEntries {
190    pub limit: u32,
191    pub page: u32,
192    pub pages: u32,
193    pub total: u32,
194    #[serde(rename = "_embedded")]
195    pub embedded: EmbeddedEntries,
196}
197
198/// Entries as stored in `PaginatedEntries`.
199#[derive(Deserialize, Debug)]
200pub(crate) struct EmbeddedEntries {
201    pub items: Entries,
202}
203
204/// Represents a page of Entries returned. Includes both the payload and metadata about the page.
205#[derive(Debug)]
206pub struct EntriesPage {
207    /// Number of entries returned per page. This is set by the server; useful to know if you're
208    /// accepting the server default because this will inform what the server default is.
209    pub per_page: u32,
210
211    /// The current page number of results.
212    pub current_page: u32,
213
214    /// Total number of pages in the set.
215    pub total_pages: u32,
216
217    /// Total number of entries in the query set.
218    pub total_entries: u32,
219
220    /// The list of entries returned.
221    pub entries: Entries,
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_entry_header_with_string_value() {
230        let text = r###"{
231            "is_archived": 1,
232            "is_starred": 0,
233            "user_name": "sibben",
234            "user_email": "detlef@posteo.org",
235            "user_id": 15568,
236            "tags": [],
237            "is_public": false,
238            "id": 10849650,
239            "uid": null,
240            "title": "Klettenwurzel\u00f6l",
241            "url": "https:\/\/oelerini.com\/klettenwurzeloel",
242            "hashed_url": "baff1dd17cb2cc15578cb9b6955971dfb8ada45a",
243            "origin_url": null,
244            "given_url": "https:\/\/oelerini.com\/klettenwurzeloel",
245            "hashed_given_url": "baff1dd17cb2cc15578cb9b6955971dfb8ada45a",
246            "archived_at": "2020-02-12T10:20:58+0100",
247            "content": "Dummy content",
248            "created_at": "2019-01-14T18:16:36+0100",
249            "updated_at": "2020-02-12T10:20:58+0100",
250            "published_at": null,
251            "published_by": null,
252            "starred_at": null,
253            "annotations": [],
254            "mimetype": "text\/html",
255            "language": "de",
256            "reading_time": 12,
257            "domain_name": "oelerini.com",
258            "preview_picture": "https:\/\/oelerini.com\/img\/klettenwurzeloel.jpg",
259            "http_status": null,
260            "headers": {
261                "content-type": "text\/html"
262            },
263            "_links": {
264                "self": {
265                    "href": "\/api\/entries\/10849650"
266                }
267            }
268        }
269        "###;
270        let entry: Entry = serde_json::from_str(&text).unwrap();
271        assert_eq!(
272            entry.headers,
273            Some(HashMap::from([("content-type".into(), "text/html".into())]))
274        );
275    }
276
277    #[test]
278    fn test_entry_header_with_null_value() {
279        let text = r###"{
280            "is_archived": 1,
281            "is_starred": 0,
282            "user_name": "sibben",
283            "user_email": "detlef@posteo.org",
284            "user_id": 15568,
285            "tags": [],
286            "is_public": false,
287            "id": 10849669,
288            "uid": null,
289            "title": "Erfahrungen beim Jurtenaufbau Es geht auch zu zweit!",
290            "url": "https:\/\/www.jurte.com\/de\/berichte\/ammertal.html",
291            "hashed_url": "f3c65c41ecef84d95e7a7afc12dbebdd04a8a471",
292            "origin_url": null,
293            "given_url": "https:\/\/www.jurte.com\/de\/berichte\/ammertal.html",
294            "hashed_given_url": "f3c65c41ecef84d95e7a7afc12dbebdd04a8a471",
295            "archived_at": "2020-02-12T10:20:59+0100",
296            "content": "wallabag can't retrieve contents for this article. Please <a href=\"http:\/\/doc.wallabag.org\/en\/user\/errors_during_fetching.html#how-can-i-help-to-fix-that\">troubleshoot this issue<\/a>.",
297            "created_at": "2018-08-23T22:28:58+0200",
298            "updated_at": "2020-02-12T10:20:59+0100",
299            "published_at": null,
300            "published_by": null,
301            "starred_at": null,
302            "annotations": [],
303            "mimetype": null,
304            "language": null,
305            "reading_time": 0,
306            "domain_name": "www.jurte.com",
307            "preview_picture": "https:\/\/www.jurte.com\/images\/ammertal\/aushub_192.jpg",
308            "http_status": null,
309            "headers": {
310                "content-type": null
311            },
312            "_links": {
313                "self": {
314                    "href": "\/api\/entries\/10849669"
315                }
316            }
317        }
318        "###;
319        let entry: Entry = serde_json::from_str(&text).unwrap();
320        assert_eq!(entry.headers, Some(HashMap::from([])));
321    }
322}