linkding/
lib.rs

1#[cfg(feature = "ffi")]
2uniffi::setup_scaffolding!();
3
4pub mod bookmarks;
5pub mod tags;
6pub mod users;
7
8pub use bookmarks::{
9    Bookmark, CheckUrlResponse, CreateBookmarkBody, ListBookmarksArgs, ListBookmarksResponse,
10    UpdateBookmarkBody,
11};
12pub use tags::{ListTagsArgs, ListTagsResponse, TagData};
13use thiserror::Error;
14pub use users::{DateDisplay, LinkTarget, SelectedTheme, SortBy, TagSearchMethod, UserProfile};
15
16#[derive(Error, Debug)]
17#[cfg_attr(feature = "ffi", derive(uniffi::Error))]
18#[cfg_attr(feature = "ffi", uniffi(flat_error))]
19pub enum LinkDingError {
20    #[error("Invalid URL")]
21    InvalidUrl(#[from] http::uri::InvalidUri),
22    #[error("Error building URL")]
23    ParseUrlError(#[from] http::Error),
24    #[error("Error sending HTTP request")]
25    SendHttpError(#[from] ureq::Error),
26    #[error("Could not parse JSON response from API")]
27    JsonParse(#[from] std::io::Error),
28    #[error("Could not serialize JSON body")]
29    JsonSerialize(#[from] serde_json::Error),
30}
31
32#[derive(Debug, Clone)]
33#[cfg_attr(feature = "ffi", derive(uniffi::Enum))]
34pub enum Endpoint {
35    ListBookmarks(ListBookmarksArgs),
36    ListArchivedBookmarks(ListBookmarksArgs),
37    GetBookmark(i32),
38    CheckUrl(String),
39    CreateBookmark,
40    UpdateBookmark(i32),
41    ArchiveBookmark(i32),
42    UnarchiveBookmark(i32),
43    DeleteBookmark(i32),
44    ListTags(ListTagsArgs),
45    GetTag(i32),
46    CreateTag,
47    GetUserProfile,
48}
49
50impl TryInto<http::Uri> for Endpoint {
51    type Error = LinkDingError;
52
53    fn try_into(self) -> Result<http::Uri, Self::Error> {
54        match self.clone() {
55            Self::ListBookmarks(args) | Self::ListArchivedBookmarks(args) => {
56                let query_string = args.query_string();
57                let path: String = self.into();
58                http::Uri::try_from(format!("{}{}", path, query_string))
59                    .map_err(LinkDingError::InvalidUrl)
60            }
61            Self::ListTags(args) => {
62                let query_string = args.query_string();
63                let path: String = self.into();
64                http::Uri::try_from(format!("{}{}", path, query_string))
65                    .map_err(LinkDingError::InvalidUrl)
66            }
67            Self::CheckUrl(url) => {
68                let path: String = self.into();
69                http::Uri::try_from(format!("{}?url={}", path, url))
70                    .map_err(LinkDingError::InvalidUrl)
71            }
72            Self::GetBookmark(_)
73            | Self::UpdateBookmark(_)
74            | Self::ArchiveBookmark(_)
75            | Self::UnarchiveBookmark(_)
76            | Self::DeleteBookmark(_)
77            | Self::CreateBookmark
78            | Self::GetTag(_)
79            | Self::CreateTag
80            | Self::GetUserProfile => {
81                let path: String = self.into();
82                http::Uri::try_from(path).map_err(LinkDingError::InvalidUrl)
83            }
84        }
85    }
86}
87
88impl QueryString for Endpoint {
89    fn query_string(&self) -> String {
90        match self {
91            Self::ListBookmarks(args) | Self::ListArchivedBookmarks(args) => args.query_string(),
92            Self::ListTags(args) => args.query_string(),
93            Self::GetBookmark(_)
94            | Self::CheckUrl(_)
95            | Self::CreateBookmark
96            | Self::UpdateBookmark(_)
97            | Self::ArchiveBookmark(_)
98            | Self::UnarchiveBookmark(_)
99            | Self::DeleteBookmark(_)
100            | Self::GetTag(_)
101            | Self::CreateTag
102            | Self::GetUserProfile => "".to_string(),
103        }
104    }
105}
106
107impl Into<String> for Endpoint {
108    fn into(self) -> String {
109        match &self {
110            Self::ListBookmarks(_) => "/api/bookmarks/".to_string(),
111            Self::ListArchivedBookmarks(_) => "/api/bookmarks/archived/".to_string(),
112            Self::GetBookmark(id) | Self::UpdateBookmark(id) | Self::DeleteBookmark(id) => {
113                format!("/api/bookmarks/{}/", &id)
114            }
115            Self::CheckUrl(_) => "/api/bookmarks/check/".to_string(),
116            Self::CreateBookmark => "/api/bookmarks/".to_string(),
117            Self::ArchiveBookmark(id) => format!("/api/bookmarks/{}/archive/", &id),
118            Self::UnarchiveBookmark(id) => format!("/api/bookmarks/{}/unarchive/", &id),
119            Self::ListTags(_) | Self::CreateTag => "/api/tags/".to_string(),
120            Self::GetTag(id) => format!("/api/tags/{}/", &id),
121            Self::GetUserProfile => "/api/user/profile/".to_string(),
122        }
123    }
124}
125
126impl Into<http::Method> for Endpoint {
127    fn into(self) -> http::Method {
128        match self {
129            Self::ListBookmarks(_) => http::Method::GET,
130            Self::ListArchivedBookmarks(_) => http::Method::GET,
131            Self::GetBookmark(_) => http::Method::GET,
132            Self::CheckUrl(_) => http::Method::GET,
133            Self::CreateBookmark => http::Method::POST,
134            Self::UpdateBookmark(_) => http::Method::PATCH,
135            Self::ArchiveBookmark(_) => http::Method::POST,
136            Self::UnarchiveBookmark(_) => http::Method::POST,
137            Self::DeleteBookmark(_) => http::Method::DELETE,
138            Self::ListTags(_) => http::Method::GET,
139            Self::GetTag(_) => http::Method::GET,
140            Self::CreateTag => http::Method::POST,
141            Self::GetUserProfile => http::Method::GET,
142        }
143    }
144}
145
146trait QueryString {
147    fn query_string(&self) -> String;
148}
149
150/// A sync client for the LinkDing API.
151///
152/// This client is used to interact with the LinkDing API. It provides methods for
153/// managing bookmarks and tags, the full capability of the LinkDing API.
154///
155/// # Example
156///
157/// ```
158/// use linkding::{LinkDingClient, LinkDingError, CreateBookmarkBody};
159///
160/// fn main() -> Result<(), LinkDingError> {
161///     let client = LinkDingClient::new("https://linkding.local:9090", "YOUR_API_TOKEN");
162///     let new_bookmark = CreateBookmarkBody {
163///         url: "https://example.com".to_string(),
164///         ..Default::default()
165///     };
166///     let bookmark = client.create_bookmark(new_bookmark)?;
167///     println!("Bookmark created: {:?}", bookmark);
168///     client.delete_bookmark(bookmark.id)?;
169///     println!("Bookmark deleted");
170///     Ok(())
171/// }
172/// ```
173#[derive(Debug, Clone)]
174#[cfg_attr(feature = "ffi", derive(uniffi::Object))]
175pub struct LinkDingClient {
176    token: String,
177    url: String,
178}
179
180impl LinkDingClient {
181    fn prepare_request(&self, endpoint: Endpoint) -> Result<http::request::Builder, LinkDingError> {
182        let uri: http::Uri = self.url.parse()?;
183        let path_and_query_uri: http::Uri = endpoint.clone().try_into()?;
184        let uri = http::uri::Builder::from(uri)
185            .path_and_query(path_and_query_uri.to_string())
186            .build()
187            .map_err(LinkDingError::ParseUrlError)?;
188
189        Ok(http::request::Builder::new()
190            .method(endpoint)
191            .uri(uri)
192            .header("Content-Type", "application/json")
193            .header("Accept", "application/json")
194            .header("Authorization", &format!("Token {}", &self.token)))
195    }
196}
197
198#[cfg_attr(feature = "ffi", uniffi::export)]
199impl LinkDingClient {
200    #[cfg_attr(feature = "ffi", uniffi::constructor)]
201    pub fn new(url: &str, token: &str) -> Self {
202        LinkDingClient {
203            token: token.to_string(),
204            url: url.to_string(),
205        }
206    }
207
208    /// List unarchived bookmarks
209    pub fn list_bookmarks(
210        &self,
211        args: ListBookmarksArgs,
212    ) -> Result<ListBookmarksResponse, LinkDingError> {
213        let endpoint = Endpoint::ListBookmarks(args);
214        let request = self.prepare_request(endpoint)?.body(())?;
215        let body: ListBookmarksResponse = ureq::run(request)?.body_mut().read_json()?;
216        Ok(body)
217    }
218
219    /// List archived bookmarks
220    pub fn list_archived_bookmarks(
221        &self,
222        args: ListBookmarksArgs,
223    ) -> Result<ListBookmarksResponse, LinkDingError> {
224        let endpoint = Endpoint::ListArchivedBookmarks(args);
225        let request = self.prepare_request(endpoint)?.body(())?;
226        let body: ListBookmarksResponse = ureq::run(request)?.body_mut().read_json()?;
227        Ok(body)
228    }
229
230    /// Get a bookmark by ID
231    pub fn get_bookmark(&self, id: i32) -> Result<Bookmark, LinkDingError> {
232        let endpoint = Endpoint::GetBookmark(id);
233        let request = self.prepare_request(endpoint)?.body(())?;
234        let body: Bookmark = ureq::run(request)?.body_mut().read_json()?;
235        Ok(body)
236    }
237
238    /// Check if a URL has been bookmarked
239    ///
240    /// If the URL has already been bookmarked this will return the bookmark
241    /// data, otherwise the bookmark data will be `None`. The metadata of the
242    /// webpage will always be returned.
243    pub fn check_url(&self, url: &str) -> Result<CheckUrlResponse, LinkDingError> {
244        let endpoint = Endpoint::CheckUrl(url.to_string());
245        let request = self.prepare_request(endpoint)?.body(())?;
246        let body: CheckUrlResponse = ureq::run(request)?.body_mut().read_json()?;
247        Ok(body)
248    }
249
250    /// Create a bookmark
251    ///
252    /// If the bookmark already exists, it will be updated with the new data passed in the `body` parameter.
253    pub fn create_bookmark(&self, body: CreateBookmarkBody) -> Result<Bookmark, LinkDingError> {
254        let endpoint = Endpoint::CreateBookmark;
255        let request = self
256            .prepare_request(endpoint)?
257            .body(serde_json::to_string(&body)?)?;
258        let body: Bookmark = ureq::run(request)?.body_mut().read_json()?;
259        Ok(body)
260    }
261
262    /// Update a bookmark
263    ///
264    /// Pass only the fields you want to update in the `body` parameter.
265    pub fn update_bookmark(
266        &self,
267        id: i32,
268        body: UpdateBookmarkBody,
269    ) -> Result<Bookmark, LinkDingError> {
270        let endpoint = Endpoint::UpdateBookmark(id);
271        let request = self
272            .prepare_request(endpoint)?
273            .body(serde_json::to_string(&body)?)?;
274        let body: Bookmark = ureq::run(request)?.body_mut().read_json()?;
275        Ok(body)
276    }
277
278    /// Archive a bookmark
279    pub fn archive_bookmark(&self, id: i32) -> Result<bool, LinkDingError> {
280        let endpoint = Endpoint::ArchiveBookmark(id);
281        let request = self.prepare_request(endpoint)?.body(())?;
282        Ok(ureq::run(request)?.status() == http::StatusCode::NO_CONTENT)
283    }
284
285    /// Take a bookmark out of the archive
286    pub fn unarchive_bookmark(&self, id: i32) -> Result<bool, LinkDingError> {
287        let endpoint = Endpoint::UnarchiveBookmark(id);
288        let request = self.prepare_request(endpoint)?.body(())?;
289        Ok(ureq::run(request)?.status() == http::StatusCode::NO_CONTENT)
290    }
291
292    /// Delete a bookmark
293    pub fn delete_bookmark(&self, id: i32) -> Result<bool, LinkDingError> {
294        let endpoint = Endpoint::DeleteBookmark(id);
295        let request = self.prepare_request(endpoint)?.body(())?;
296        Ok(ureq::run(request)?.status() == http::StatusCode::NO_CONTENT)
297    }
298
299    /// List tags
300    pub fn list_tags(&self, args: ListTagsArgs) -> Result<ListTagsResponse, LinkDingError> {
301        let endpoint = Endpoint::ListTags(args);
302        let request = self.prepare_request(endpoint)?.body(())?;
303        let body: ListTagsResponse = ureq::run(request)?.body_mut().read_json()?;
304        Ok(body)
305    }
306
307    /// Get a tag by ID
308    pub fn get_tag(&self, id: i32) -> Result<TagData, LinkDingError> {
309        let endpoint = Endpoint::GetTag(id);
310        let request = self.prepare_request(endpoint)?.body(())?;
311        let body: TagData = ureq::run(request)?.body_mut().read_json()?;
312        Ok(body)
313    }
314
315    /// Create a tag
316    pub fn create_tag(&self, name: &str) -> Result<TagData, LinkDingError> {
317        let endpoint = Endpoint::CreateTag;
318        let body = serde_json::json!({ "name": name });
319        let request = self
320            .prepare_request(endpoint)?
321            .body(serde_json::to_string(&body)?)?;
322        let body: TagData = ureq::run(request)?.body_mut().read_json()?;
323        Ok(body)
324    }
325
326    /// Get the user's profile
327    pub fn get_user_profile(&self) -> Result<UserProfile, LinkDingError> {
328        let endpoint = Endpoint::GetUserProfile;
329        let request = self.prepare_request(endpoint)?.body(())?;
330        let body: UserProfile = ureq::run(request)?.body_mut().read_json()?;
331        Ok(body)
332    }
333}