wallabag_api/
client.rs

1// Copyright 2018 Samuel Walladge <samuel@swalladge.net>
2// Copyright 2021 Pablo Baeyens <pbaeyens31+github@gmail.com>
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Client.
6
7// std libs
8use std::collections::HashMap;
9
10// extern crates
11use log::{debug, max_level, trace, LevelFilter};
12use serde::de::DeserializeOwned;
13use serde::ser::Serialize;
14use surf::http::{self, Method};
15use surf::Url;
16use surf::{http::StatusCode, Body};
17use surf::{Request, Response};
18
19// local imports
20use crate::errors::{
21    ClientError, ClientResult, CodeMessage, ResponseCodeMessageError, ResponseError,
22};
23use crate::types::{
24    Annotation, AnnotationRows, Annotations, Config, DeletedEntry, DeletedTag, Entries,
25    EntriesExistParams, EntriesFilter, EntriesPage, Entry, ExistsInfo, ExistsResponse, Format,
26    NewAnnotation, NewEntry, NewlyRegisteredInfo, PaginatedEntries, PatchEntry, RegisterInfo,
27    RequestEntriesFilter, Tag, TagString, Tags, TokenInfo, User, ID, UNIT,
28};
29use crate::utils::{EndPoint, UrlBuilder};
30
31/// The main thing that provides all the methods for interacting with the
32/// Wallabag API.
33#[derive(Debug)]
34pub struct Client {
35    client_id: String,
36    client_secret: String,
37    username: String,
38    password: String,
39    token_info: Option<TokenInfo>,
40    url_base: UrlBuilder,
41}
42
43impl Client {
44    /// Build a new client given the configuration.
45    pub fn new(config: Config) -> Self {
46        Self {
47            client_id: config.client_id,
48            client_secret: config.client_secret,
49            username: config.username,
50            password: config.password,
51            token_info: None,
52            url_base: UrlBuilder::new(config.base_url),
53        }
54    }
55
56    /// Internal method to get a valid access token. If no access token loaded
57    /// yet, then get a new one.
58    async fn get_token(&mut self) -> ClientResult<String> {
59        if let Some(ref t) = self.token_info {
60            Ok(t.access_token.clone())
61        } else {
62            debug!("No api token loaded yet");
63            self.load_token().await
64        }
65    }
66
67    /// Use credentials in the config to obtain an access token.
68    async fn load_token(&mut self) -> ClientResult<String> {
69        debug!("Requesting auth token");
70        let mut fields = HashMap::new();
71        fields.insert("grant_type".to_owned(), "password".to_owned());
72        fields.insert("client_id".to_owned(), self.client_id.clone());
73        fields.insert("client_secret".to_owned(), self.client_secret.clone());
74        fields.insert("username".to_owned(), self.username.clone());
75        fields.insert("password".to_owned(), self.password.clone());
76
77        let token_info: TokenInfo = self
78            .json_q(Method::Post, EndPoint::Token, UNIT, &fields, false)
79            .await?;
80        self.token_info = Some(token_info);
81
82        Ok(self.token_info.as_ref().unwrap().access_token.clone())
83    }
84
85    /// Use saved token if present to get a fresh access token.
86    async fn refresh_token(&mut self) -> ClientResult<String> {
87        if self.token_info.is_none() {
88            return self.load_token().await;
89        }
90
91        let mut fields = HashMap::new();
92        fields.insert("grant_type".to_owned(), "refresh_token".to_owned());
93        fields.insert("client_id".to_owned(), self.client_id.clone());
94        fields.insert("client_secret".to_owned(), self.client_secret.clone());
95        fields.insert(
96            "refresh_token".to_owned(),
97            self.token_info.as_ref().unwrap().refresh_token.clone(),
98        );
99
100        let token_info: TokenInfo = self
101            .json_q(Method::Post, EndPoint::Token, UNIT, &fields, false)
102            .await?;
103        self.token_info = Some(token_info);
104
105        Ok(self.token_info.as_ref().unwrap().access_token.clone())
106    }
107
108    /// Smartly run a request that expects to receive json back. Handles adding
109    /// authorization headers, and retry on expired token.
110    async fn smart_text_q<J, Q>(
111        &mut self,
112        method: Method,
113        end_point: EndPoint,
114        query: &Q,
115        json: &J,
116    ) -> ClientResult<String>
117    where
118        J: Serialize,
119        Q: Serialize,
120    {
121        Ok(self
122            .smart_q(method, end_point, query, json)
123            .await?
124            .body_string()
125            .await?)
126    }
127
128    /// Smartly run a request that expects to receive json back. Handles adding
129    /// authorization headers, and retry on expired token.
130    async fn smart_json_q<T, J, Q>(
131        &mut self,
132        method: Method,
133        end_point: EndPoint,
134        query: &Q,
135        json: &J,
136    ) -> ClientResult<T>
137    where
138        T: DeserializeOwned,
139        J: Serialize,
140        Q: Serialize,
141    {
142        if max_level() >= LevelFilter::Debug {
143            let text = self
144                .smart_q(method, end_point, query, json)
145                .await?
146                .body_string()
147                .await?;
148            match serde_json::from_str(&text) {
149                Ok(j) => {
150                    debug!("Deserialized json response body: {}", &text);
151                    Ok(j)
152                }
153                Err(e) => {
154                    debug!("Deserialize json failed for: {}", &text);
155                    Err(ClientError::SerdeJsonError(e))
156                }
157            }
158        } else {
159            Ok(self
160                .smart_q(method, end_point, query, json)
161                .await?
162                .body_json()
163                .await?)
164        }
165    }
166
167    /// Smartly run a request that expects to receive json back. Handles adding
168    /// authorization headers, and retry on expired token.
169    async fn smart_q<J, Q>(
170        &mut self,
171        method: Method,
172        end_point: EndPoint,
173        query: &Q,
174        json: &J,
175    ) -> ClientResult<Response>
176    where
177        J: Serialize,
178        Q: Serialize,
179    {
180        // ensure the token is populated. bit of a hack to avoid calling get_token from inside
181        // self.q (causes async recursion otherwise). Will fix sometime.
182        let _ = self.get_token().await?;
183        let response_result = self.q(method.clone(), end_point, query, json, true).await;
184
185        if let Err(ClientError::ExpiredToken) = response_result {
186            debug!("Token expired; refreshing");
187            self.refresh_token().await?;
188
189            // try the request again now
190            Ok(self.q(method, end_point, query, json, true).await?)
191        } else {
192            Ok(response_result?)
193        }
194    }
195
196    /// Just build and send a single request. Returns a json deserializable
197    /// response.
198    async fn json_q<T, J, Q>(
199        &mut self,
200        method: Method,
201        end_point: EndPoint,
202        query: &Q,
203        json: &J,
204        use_token: bool,
205    ) -> ClientResult<T>
206    where
207        T: DeserializeOwned,
208        J: Serialize,
209        Q: Serialize,
210    {
211        if max_level() >= LevelFilter::Debug {
212            let text = self
213                .q(method, end_point, query, json, use_token)
214                .await?
215                .body_string()
216                .await?;
217            match serde_json::from_str(&text) {
218                Ok(j) => {
219                    debug!("Deserialized json response body: {}", &text);
220                    Ok(j)
221                }
222                Err(e) => {
223                    debug!("Deserialize json failed for: {}", &text);
224                    Err(ClientError::SerdeJsonError(e))
225                }
226            }
227        } else {
228            Ok(self
229                .q(method, end_point, query, json, use_token)
230                .await?
231                .body_json()
232                .await?)
233        }
234    }
235
236    /// Build and send a single request. Does most of the heavy lifting.
237    async fn q<J, Q>(
238        &mut self,
239        method: Method,
240        end_point: EndPoint,
241        query: &Q,
242        json: &J,
243        use_token: bool,
244    ) -> ClientResult<Response>
245    where
246        J: Serialize,
247        Q: Serialize,
248    {
249        let url = self.url_base.build(end_point);
250        trace!("Sending request to {}", url);
251
252        let mut request = Request::builder(method, Url::parse(&url)?)
253            .body(Body::from_json(json)?)
254            .query(query)?;
255
256        if use_token {
257            if let Some(ref t) = self.token_info {
258                request = request.header(
259                    http::headers::AUTHORIZATION,
260                    format!("Bearer {}", t.access_token.clone()),
261                );
262            }
263        }
264
265        let mut response = request.await?;
266
267        trace!("response status: {:?}", response.status());
268        match response.status() {
269            StatusCode::Unauthorized => {
270                let info: ResponseError = response.body_json().await?;
271                if info.error_description.as_str().contains("expired") {
272                    Err(ClientError::ExpiredToken)
273                } else {
274                    Err(ClientError::Unauthorized(info))
275                }
276            }
277            StatusCode::Forbidden => {
278                let info: ResponseCodeMessageError = response.body_json().await?;
279                Err(ClientError::Forbidden(info))
280            }
281            StatusCode::NotFound => {
282                let info: ResponseCodeMessageError = match response.body_json().await {
283                    Ok(info) => info,
284                    Err(_) => ResponseCodeMessageError {
285                        error: CodeMessage {
286                            code: 404,
287                            message: "Not supplied".to_owned(),
288                        },
289                    },
290                };
291                Err(ClientError::NotFound(info))
292            }
293            StatusCode::NotModified => {
294                // reload entry returns this if no changes on re-crawl url or if failed to reload
295                Err(ClientError::NotModified)
296            }
297            status if status.is_success() => Ok(response),
298            status => Err(ClientError::Other(status, response.body_string().await?)),
299        }
300    }
301
302    /// Check if a list of urls already have entries. This is more efficient if
303    /// you want to batch check urls since only a single request is required.
304    /// Returns a hashmap where the urls given are the keys and the values are either:
305    ///
306    /// - `None`: no existing entry corresponding to the url
307    /// - `Some(ID)`: an entry exists and here's the ID
308    pub async fn check_urls_exist<T: Into<String>>(
309        &mut self,
310        urls: Vec<T>,
311    ) -> ClientResult<ExistsInfo> {
312        let params = EntriesExistParams {
313            return_id: 1,
314            urls: urls
315                .into_iter()
316                .map(|url| url.into())
317                .collect::<Vec<String>>(),
318        };
319
320        self.smart_json_q(Method::Get, EndPoint::Exists, &params, UNIT)
321            .await
322    }
323
324    /// Check if a url already has a corresponding entry. Returns `None` if not existing or the ID
325    /// of the entry if it does exist.
326    pub async fn check_url_exists<T: Into<String>>(&mut self, url: T) -> ClientResult<Option<ID>> {
327        let mut params = HashMap::new();
328        params.insert("url".to_owned(), url.into());
329        params.insert("return_id".to_owned(), "1".to_owned());
330
331        let exists_info: ExistsResponse = self
332            .smart_json_q(Method::Get, EndPoint::Exists, &params, UNIT)
333            .await?;
334
335        // extract and return the entry id
336        Ok(exists_info.exists)
337    }
338
339    /// Create a new entry. See docs for `NewEntry` for more information.
340    pub async fn create_entry(&mut self, new_entry: &NewEntry) -> ClientResult<Entry> {
341        self.smart_json_q(Method::Post, EndPoint::Entries, UNIT, new_entry)
342            .await
343    }
344
345    /// Update entry. To leave an editable field unchanged, set to `None`.
346    pub async fn update_entry<T: Into<ID>>(
347        &mut self,
348        id: T,
349        entry: &PatchEntry,
350    ) -> ClientResult<Entry> {
351        self.smart_json_q(Method::Patch, EndPoint::Entry(id.into()), UNIT, entry)
352            .await
353    }
354
355    /// Reload entry. This tells the server to re-fetch content from the url (or
356    /// origin url?) and use the result to refresh the entry contents.
357    ///
358    /// This returns `Err(ClientError::NotModified)` if the server either could
359    /// not refresh the contents, or the content does not get modified.
360    pub async fn reload_entry<T: Into<ID>>(&mut self, id: T) -> ClientResult<Entry> {
361        self.smart_json_q(Method::Patch, EndPoint::EntryReload(id.into()), UNIT, UNIT)
362            .await
363    }
364
365    /// Get an entry by id.
366    pub async fn get_entry<T: Into<ID>>(&mut self, id: T) -> ClientResult<Entry> {
367        self.smart_json_q(Method::Get, EndPoint::Entry(id.into()), UNIT, UNIT)
368            .await
369    }
370
371    /// Delete an entry by id.
372    pub async fn delete_entry<T: Into<ID>>(&mut self, id: T) -> ClientResult<Entry> {
373        let id = id.into();
374        let json: DeletedEntry = self
375            .smart_json_q(Method::Delete, EndPoint::Entry(id), UNIT, UNIT)
376            .await?;
377
378        // build an entry composed of the deleted entry returned and the id,
379        // because the entry returned does not include the id.
380        let entry = Entry {
381            id,
382            annotations: json.annotations,
383            content: json.content,
384            created_at: json.created_at,
385            domain_name: json.domain_name,
386            headers: json.headers,
387            http_status: json.http_status,
388            is_archived: json.is_archived,
389            is_public: json.is_public,
390            is_starred: json.is_starred,
391            language: json.language,
392            mimetype: json.mimetype,
393            origin_url: json.origin_url,
394            preview_picture: json.preview_picture,
395            published_at: json.published_at,
396            published_by: json.published_by,
397            reading_time: json.reading_time,
398            starred_at: json.starred_at,
399            tags: json.tags,
400            title: json.title,
401            uid: json.uid,
402            updated_at: json.updated_at,
403            url: json.url,
404            user_email: json.user_email,
405            user_id: json.user_id,
406            user_name: json.user_name,
407        };
408
409        Ok(entry)
410    }
411
412    /// Update an annotation.
413    pub async fn update_annotation(&mut self, annotation: &Annotation) -> ClientResult<Annotation> {
414        self.smart_json_q(
415            Method::Put,
416            EndPoint::Annotation(annotation.id),
417            UNIT,
418            annotation,
419        )
420        .await
421    }
422
423    /// Create a new annotation on an entry.
424    pub async fn create_annotation<T: Into<ID>>(
425        &mut self,
426        entry_id: T,
427        annotation: &NewAnnotation,
428    ) -> ClientResult<Annotation> {
429        self.smart_json_q(
430            Method::Post,
431            EndPoint::Annotation(entry_id.into()),
432            UNIT,
433            annotation,
434        )
435        .await
436    }
437
438    /// Delete an annotation by id
439    pub async fn delete_annotation<T: Into<ID>>(&mut self, id: T) -> ClientResult<Annotation> {
440        self.smart_json_q(Method::Delete, EndPoint::Annotation(id.into()), UNIT, UNIT)
441            .await
442    }
443
444    /// Get all annotations for an entry (by id).
445    pub async fn get_annotations<T: Into<ID>>(&mut self, id: T) -> ClientResult<Annotations> {
446        let json: AnnotationRows = self
447            .smart_json_q(Method::Get, EndPoint::Annotation(id.into()), UNIT, UNIT)
448            .await?;
449        Ok(json.rows)
450    }
451
452    /// Get all entries.
453    pub async fn get_entries(&mut self) -> ClientResult<Entries> {
454        self._get_entries(&EntriesFilter::default()).await
455    }
456
457    /// Get all entries, filtered by filter parameters.
458    pub async fn get_entries_with_filter(
459        &mut self,
460        filter: &EntriesFilter,
461    ) -> ClientResult<Entries> {
462        self._get_entries(filter).await
463    }
464
465    /// Get a page of entries, specified by page number. Useful when the expected list of results
466    /// is very large and you don't want to wait too long before getting a subset of the entries.
467    /// Will return a not found error if `page_number` is out of bounds.
468    pub async fn get_entries_page(
469        &mut self,
470        filter: &EntriesFilter,
471        page_number: u32,
472    ) -> ClientResult<EntriesPage> {
473        let params = RequestEntriesFilter {
474            page: page_number,
475            filter,
476        };
477        let json: PaginatedEntries = self
478            .smart_json_q(Method::Get, EndPoint::Entries, &params, UNIT)
479            .await?;
480
481        Ok(EntriesPage {
482            per_page: json.limit,
483            current_page: json.page,
484            total_pages: json.pages,
485            total_entries: json.total,
486            entries: json.embedded.items,
487        })
488    }
489
490    /// Does the actual work of retrieving the entries. Handles pagination.
491    async fn _get_entries(&mut self, filter: &EntriesFilter) -> ClientResult<Entries> {
492        let mut entries = Entries::new();
493
494        let mut params = RequestEntriesFilter { page: 1, filter };
495
496        // loop to handle pagination. No other api endpoints paginate so it's
497        // fine here.
498        loop {
499            debug!("retrieving PaginatedEntries page {}", params.page);
500            let json: PaginatedEntries = self
501                .smart_json_q(Method::Get, EndPoint::Entries, &params, UNIT)
502                .await?;
503
504            entries.extend(json.embedded.items.into_iter());
505
506            if json.page < json.pages {
507                params.page = json.page + 1;
508            } else {
509                break;
510            }
511        }
512
513        Ok(entries)
514    }
515
516    /// Get an export of an entry in a particular format.
517    pub async fn export_entry<T: Into<ID>>(
518        &mut self,
519        entry_id: T,
520        fmt: Format,
521    ) -> ClientResult<String> {
522        self.smart_text_q(
523            Method::Get,
524            EndPoint::Export(entry_id.into(), fmt),
525            UNIT,
526            UNIT,
527        )
528        .await
529    }
530
531    /// Get a list of all tags for an entry by entry id.
532    pub async fn get_tags_for_entry<T: Into<ID>>(&mut self, entry_id: T) -> ClientResult<Tags> {
533        self.smart_json_q(
534            Method::Get,
535            EndPoint::EntryTags(entry_id.into()),
536            UNIT,
537            UNIT,
538        )
539        .await
540    }
541
542    /// Add tags to an entry by entry id. Idempotent operation. No problems if
543    /// tags list is empty.
544    pub async fn add_tags_to_entry<T: Into<ID>, U: Into<String>>(
545        &mut self,
546        entry_id: T,
547        tags: Vec<U>,
548    ) -> ClientResult<Entry> {
549        let mut data = HashMap::new();
550        data.insert(
551            "tags",
552            tags.into_iter().map(|x| x.into()).collect::<Vec<String>>(),
553        );
554
555        self.smart_json_q(
556            Method::Post,
557            EndPoint::EntryTags(entry_id.into()),
558            UNIT,
559            &data,
560        )
561        .await
562    }
563
564    /// Delete a tag (by id) from an entry (by id). Returns err 404 if entry or
565    /// tag not found. Idempotent. Removing a tag that exists but doesn't exist
566    /// on the entry completes without error.
567    pub async fn delete_tag_from_entry<T: Into<ID>, U: Into<ID>>(
568        &mut self,
569        entry_id: T,
570        tag_id: U,
571    ) -> ClientResult<Entry> {
572        self.smart_json_q(
573            Method::Delete,
574            EndPoint::DeleteEntryTag(entry_id.into(), tag_id.into()),
575            UNIT,
576            UNIT,
577        )
578        .await
579    }
580
581    /// Get a list of all tags.
582    pub async fn get_tags(&mut self) -> ClientResult<Tags> {
583        self.smart_json_q(Method::Get, EndPoint::Tags, UNIT, UNIT)
584            .await
585    }
586
587    /// Permanently delete a tag by id. This removes the tag from all entries.
588    /// Appears to return success if attempting to delete a tag by id that
589    /// exists on the server but isn't accessible to the user.
590    pub async fn delete_tag<T: Into<ID>>(&mut self, id: T) -> ClientResult<Tag> {
591        let id = id.into();
592
593        // api does not return id of deleted tag, hence the temporary struct
594        let dt: DeletedTag = self
595            .smart_json_q(Method::Delete, EndPoint::Tag(id), UNIT, UNIT)
596            .await?;
597
598        Ok(Tag {
599            id,
600            label: dt.label,
601            slug: dt.slug,
602        })
603    }
604
605    /// Permanently delete a tag by label (tag names). This also exhibits the
606    /// privacy breaching behaviour of returning tag info of other users' tags.
607    /// Also, labels aren't necessarily unique across a wallabag installation.
608    /// The server should filter by tags belonging to a user in the same db
609    /// query.
610    ///
611    /// Note: this allows deleting a tag with a comma by label.
612    pub async fn delete_tag_by_label<T: Into<String>>(
613        &mut self,
614        label: T,
615    ) -> ClientResult<DeletedTag> {
616        let mut params = HashMap::new();
617        params.insert("tag".to_owned(), label.into());
618
619        let deleted_tag: DeletedTag = self
620            .smart_json_q(Method::Delete, EndPoint::TagLabel, &params, UNIT)
621            .await?;
622        Ok(deleted_tag)
623    }
624
625    /// Permanently batch delete tags by labels (tag names). Returns not found
626    /// if _all_ labels not found. If at least one found, then returns ok. For
627    /// some reason, (at least the framabag instance) the server returns success
628    /// and the tag data on attempting to delete for innaccessible tags (tags by
629    /// other users?).
630    ///
631    /// This method requires that tag names not contain commas. If you need to
632    /// delete a tag containing a comma, use `delete_tag_by_label` instead.
633    ///
634    /// Returns a list of tags that were deleted (sans IDs). Returns 404 not
635    /// found _only_ if _all_ tags were not found.
636    pub async fn delete_tags_by_label(
637        &mut self,
638        tags: Vec<TagString>,
639    ) -> ClientResult<Vec<DeletedTag>> {
640        let mut params = HashMap::new();
641        params.insert(
642            "tags",
643            tags.into_iter()
644                .map(|x| x.into_string())
645                .collect::<Vec<String>>()
646                .join(","),
647        );
648
649        // note: api doesn't return tag ids and no way to obtain since deleted
650        // by label
651        self.smart_json_q(Method::Delete, EndPoint::TagsLabel, &params, UNIT)
652            .await
653    }
654
655    /// Get the API version. Probably not useful because if the version isn't v2
656    /// then this library won't work anyway.
657    pub async fn get_api_version(&mut self) -> ClientResult<String> {
658        self.smart_json_q(Method::Get, EndPoint::Version, UNIT, UNIT)
659            .await
660    }
661
662    /// Get the currently logged in user information.
663    pub async fn get_user(&mut self) -> ClientResult<User> {
664        self.smart_json_q(Method::Get, EndPoint::User, UNIT, UNIT)
665            .await
666    }
667
668    /// Register a user and create a client.
669    pub async fn register_user(
670        &mut self,
671        info: &RegisterInfo,
672    ) -> ClientResult<NewlyRegisteredInfo> {
673        self.json_q(Method::Put, EndPoint::User, UNIT, info, false)
674            .await
675    }
676}