1#[cfg(feature = "ffi")]
2uniffi::setup_scaffolding!();
3
4pub mod bookmark_assets;
5pub mod bookmarks;
6pub mod tags;
7pub mod users;
8
9use bookmark_assets::{BookmarkAsset, ListBookmarkAssetsResponse};
10pub use bookmarks::{
11 Bookmark, CheckUrlResponse, CreateBookmarkBody, ListBookmarksArgs, ListBookmarksResponse,
12 UpdateBookmarkBody,
13};
14use reqwest::{
15 blocking::multipart::Part,
16 header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
17 StatusCode,
18};
19pub use tags::{ListTagsArgs, ListTagsResponse, TagData};
20use thiserror::Error;
21pub use users::{DateDisplay, LinkTarget, SelectedTheme, SortBy, TagSearchMethod, UserProfile};
22
23#[derive(Error, Debug)]
24#[cfg_attr(feature = "ffi", derive(uniffi::Error))]
25#[cfg_attr(feature = "ffi", uniffi(flat_error))]
26pub enum LinkDingError {
27 #[error("Error building URL")]
28 ParseUrl(url::ParseError),
29 #[error("Error sending HTTP request")]
30 SendHttpError(#[from] reqwest::Error),
31 #[error("Could not parse response from API")]
32 ParseResponse(#[from] std::io::Error),
33 #[error("Could not serialize JSON body")]
34 JsonSerialize(#[from] serde_json::Error),
35}
36
37#[derive(Debug, Clone)]
38#[cfg_attr(feature = "ffi", derive(uniffi::Enum))]
39pub enum Endpoint {
40 ListBookmarks(ListBookmarksArgs),
41 ListArchivedBookmarks(ListBookmarksArgs),
42 GetBookmark(i32),
43 CheckUrl(String),
44 CreateBookmark,
45 UpdateBookmark(i32),
46 ArchiveBookmark(i32),
47 UnarchiveBookmark(i32),
48 DeleteBookmark(i32),
49 ListTags(ListTagsArgs),
50 GetTag(i32),
51 CreateTag,
52 GetUserProfile,
53 ListBookmarkAssets(i32),
54 RetrieveBookmarkAsset(i32, i32),
55 DownloadBookmarkAsset(i32, i32),
56 UploadBookmarkAsset(i32),
57 DeleteBookmarkAsset(i32, i32),
58}
59
60impl QueryString for Endpoint {
61 fn query_string(&self) -> String {
62 match self {
63 Self::ListBookmarks(args) | Self::ListArchivedBookmarks(args) => args.query_string(),
64 Self::ListTags(args) => args.query_string(),
65 Self::GetBookmark(_)
66 | Self::CheckUrl(_)
67 | Self::CreateBookmark
68 | Self::UpdateBookmark(_)
69 | Self::ArchiveBookmark(_)
70 | Self::UnarchiveBookmark(_)
71 | Self::DeleteBookmark(_)
72 | Self::GetTag(_)
73 | Self::CreateTag
74 | Self::GetUserProfile
75 | Self::ListBookmarkAssets(_)
76 | Self::RetrieveBookmarkAsset(_, _)
77 | Self::DownloadBookmarkAsset(_, _)
78 | Self::UploadBookmarkAsset(_)
79 | Self::DeleteBookmarkAsset(_, _) => "".to_string(),
80 }
81 }
82}
83
84impl From<Endpoint> for String {
85 fn from(val: Endpoint) -> Self {
86 let path = match &val {
87 Endpoint::ListBookmarks(_) => "/api/bookmarks/".to_string(),
88 Endpoint::ListArchivedBookmarks(_) => "/api/bookmarks/archived/".to_string(),
89 Endpoint::GetBookmark(id)
90 | Endpoint::UpdateBookmark(id)
91 | Endpoint::DeleteBookmark(id) => {
92 format!("/api/bookmarks/{}/", &id)
93 }
94 Endpoint::CheckUrl(_) => "/api/bookmarks/check/".to_string(),
95 Endpoint::CreateBookmark => "/api/bookmarks/".to_string(),
96 Endpoint::ArchiveBookmark(id) => format!("/api/bookmarks/{}/archive/", &id),
97 Endpoint::UnarchiveBookmark(id) => format!("/api/bookmarks/{}/unarchive/", &id),
98 Endpoint::ListTags(_) | Endpoint::CreateTag => "/api/tags/".to_string(),
99 Endpoint::GetTag(id) => format!("/api/tags/{}/", &id),
100 Endpoint::GetUserProfile => "/api/user/profile/".to_string(),
101 Endpoint::ListBookmarkAssets(id) => format!("/api/bookmarks/{}/assets/", id),
102 Endpoint::RetrieveBookmarkAsset(bookmark_id, asset_id)
103 | Endpoint::DeleteBookmarkAsset(bookmark_id, asset_id) => {
104 format!("/api/bookmarks/{}/assets/{}/", bookmark_id, asset_id)
105 }
106 Endpoint::DownloadBookmarkAsset(bookmark_id, asset_id) => format!(
107 "/api/bookmarks/{}/assets/{}/download/",
108 bookmark_id, asset_id
109 ),
110 Endpoint::UploadBookmarkAsset(id) => format!("/api/bookmarks/{}/assets/upload/", id),
111 };
112 match &val {
113 Endpoint::ListBookmarks(args) | Endpoint::ListArchivedBookmarks(args) => {
114 let query_string = args.query_string();
115 format!("{}?{}", path, query_string)
116 }
117 Endpoint::ListTags(args) => {
118 let query_string = args.query_string();
119 format!("{}?{}", path, query_string)
120 }
121 Endpoint::CheckUrl(url) => {
122 format!("{}?url={}", path, url)
123 }
124 Endpoint::GetBookmark(_)
125 | Endpoint::UpdateBookmark(_)
126 | Endpoint::ArchiveBookmark(_)
127 | Endpoint::UnarchiveBookmark(_)
128 | Endpoint::DeleteBookmark(_)
129 | Endpoint::CreateBookmark
130 | Endpoint::GetTag(_)
131 | Endpoint::CreateTag
132 | Endpoint::GetUserProfile
133 | Endpoint::ListBookmarkAssets(_)
134 | Endpoint::RetrieveBookmarkAsset(_, _)
135 | Endpoint::DownloadBookmarkAsset(_, _)
136 | Endpoint::UploadBookmarkAsset(_)
137 | Endpoint::DeleteBookmarkAsset(_, _) => path,
138 }
139 }
140}
141
142impl From<Endpoint> for reqwest::Method {
143 fn from(val: Endpoint) -> Self {
144 match val {
145 Endpoint::ListBookmarks(_) => reqwest::Method::GET,
146 Endpoint::ListArchivedBookmarks(_) => reqwest::Method::GET,
147 Endpoint::GetBookmark(_) => reqwest::Method::GET,
148 Endpoint::CheckUrl(_) => reqwest::Method::GET,
149 Endpoint::CreateBookmark => reqwest::Method::POST,
150 Endpoint::UpdateBookmark(_) => reqwest::Method::PATCH,
151 Endpoint::ArchiveBookmark(_) => reqwest::Method::POST,
152 Endpoint::UnarchiveBookmark(_) => reqwest::Method::POST,
153 Endpoint::DeleteBookmark(_) => reqwest::Method::DELETE,
154 Endpoint::ListTags(_) => reqwest::Method::GET,
155 Endpoint::GetTag(_) => reqwest::Method::GET,
156 Endpoint::CreateTag => reqwest::Method::POST,
157 Endpoint::GetUserProfile => reqwest::Method::GET,
158 Endpoint::ListBookmarkAssets(_) => reqwest::Method::GET,
159 Endpoint::RetrieveBookmarkAsset(_, _) => reqwest::Method::GET,
160 Endpoint::DownloadBookmarkAsset(_, _) => reqwest::Method::GET,
161 Endpoint::UploadBookmarkAsset(_) => reqwest::Method::POST,
162 Endpoint::DeleteBookmarkAsset(_, _) => reqwest::Method::DELETE,
163 }
164 }
165}
166
167impl From<Endpoint> for reqwest::header::HeaderMap {
168 fn from(val: Endpoint) -> Self {
169 let mut headers = reqwest::header::HeaderMap::new();
170 match val {
171 Endpoint::ListBookmarks(_)
172 | Endpoint::ListArchivedBookmarks(_)
173 | Endpoint::GetBookmark(_)
174 | Endpoint::CheckUrl(_)
175 | Endpoint::CreateBookmark
176 | Endpoint::UpdateBookmark(_)
177 | Endpoint::ArchiveBookmark(_)
178 | Endpoint::UnarchiveBookmark(_)
179 | Endpoint::DeleteBookmark(_)
180 | Endpoint::ListTags(_)
181 | Endpoint::GetTag(_)
182 | Endpoint::CreateTag
183 | Endpoint::GetUserProfile
184 | Endpoint::ListBookmarkAssets(_)
185 | Endpoint::RetrieveBookmarkAsset(_, _)
186 | Endpoint::UploadBookmarkAsset(_)
187 | Endpoint::DeleteBookmarkAsset(_, _) => {
188 headers.insert(
189 CONTENT_TYPE,
190 "application/json"
191 .parse()
192 .expect("Could not parse content type header value"),
193 );
194 headers.insert(
195 ACCEPT,
196 "application/json"
197 .parse()
198 .expect("Could not parse accept header value"),
199 );
200 }
201 Endpoint::DownloadBookmarkAsset(_, _) => {
202 headers.insert(ACCEPT, reqwest::header::HeaderValue::from_static("*/*"));
203 }
204 };
205 headers
206 }
207}
208
209trait QueryString {
210 fn query_string(&self) -> String;
211}
212
213#[derive(Debug, Clone)]
237#[cfg_attr(feature = "ffi", derive(uniffi::Object))]
238pub struct LinkDingClient {
239 token: String,
240 url: String,
241 client: reqwest::blocking::Client,
242}
243
244impl LinkDingClient {
245 fn prepare_request(
246 &self,
247 endpoint: Endpoint,
248 ) -> Result<reqwest::blocking::RequestBuilder, LinkDingError> {
249 let base_url: reqwest::Url = self.url.parse().map_err(LinkDingError::ParseUrl)?;
250 let path_and_query: String = endpoint.clone().into();
251 let url = base_url
252 .join(&path_and_query)
253 .map_err(LinkDingError::ParseUrl)?;
254 let method: reqwest::Method = endpoint.clone().into();
255 let mut endpoint_headers: reqwest::header::HeaderMap = endpoint.clone().into();
256 endpoint_headers.insert(
257 AUTHORIZATION,
258 format!("Token {}", &self.token)
259 .parse()
260 .expect("Could not parse authorization header value"),
261 );
262
263 let builder = self.client.request(method, url).headers(endpoint_headers);
264
265 Ok(builder)
266 }
267}
268
269#[cfg_attr(feature = "ffi", uniffi::export)]
270impl LinkDingClient {
271 #[cfg_attr(feature = "ffi", uniffi::constructor)]
272 pub fn new(url: &str, token: &str) -> Self {
273 LinkDingClient {
274 token: token.to_string(),
275 url: url.to_string(),
276 client: reqwest::blocking::Client::builder()
277 .build()
278 .expect("Could not create web client"),
279 }
280 }
281
282 pub fn list_bookmarks(
284 &self,
285 args: ListBookmarksArgs,
286 ) -> Result<ListBookmarksResponse, LinkDingError> {
287 let endpoint = Endpoint::ListBookmarks(args);
288 let request = self.prepare_request(endpoint)?.build()?;
289 let body: ListBookmarksResponse = self.client.execute(request)?.json()?;
290
291 Ok(body)
292 }
293
294 pub fn list_archived_bookmarks(
296 &self,
297 args: ListBookmarksArgs,
298 ) -> Result<ListBookmarksResponse, LinkDingError> {
299 let endpoint = Endpoint::ListArchivedBookmarks(args);
300 let request = self.prepare_request(endpoint)?.build()?;
301 let body: ListBookmarksResponse = self.client.execute(request)?.json()?;
302 Ok(body)
303 }
304
305 pub fn get_bookmark(&self, id: i32) -> Result<Bookmark, LinkDingError> {
307 let endpoint = Endpoint::GetBookmark(id);
308 let request = self.prepare_request(endpoint)?.build()?;
309 let body: Bookmark = self.client.execute(request)?.json()?;
310 Ok(body)
311 }
312
313 pub fn check_url(&self, url: &str) -> Result<CheckUrlResponse, LinkDingError> {
319 let endpoint = Endpoint::CheckUrl(url.to_string());
320 let request = self.prepare_request(endpoint)?.build()?;
321 let body: CheckUrlResponse = self.client.execute(request)?.json()?;
322 Ok(body)
323 }
324
325 pub fn create_bookmark(&self, body: CreateBookmarkBody) -> Result<Bookmark, LinkDingError> {
329 let endpoint = Endpoint::CreateBookmark;
330 let request = self
331 .prepare_request(endpoint)?
332 .body(serde_json::to_string(&body)?)
333 .build()?;
334 let body: Bookmark = self.client.execute(request)?.json()?;
335 Ok(body)
336 }
337
338 pub fn update_bookmark(
342 &self,
343 id: i32,
344 body: UpdateBookmarkBody,
345 ) -> Result<Bookmark, LinkDingError> {
346 let endpoint = Endpoint::UpdateBookmark(id);
347 let request = self
348 .prepare_request(endpoint)?
349 .body(serde_json::to_string(&body)?)
350 .build()?;
351 let body: Bookmark = self.client.execute(request)?.json()?;
352 Ok(body)
353 }
354
355 pub fn archive_bookmark(&self, id: i32) -> Result<bool, LinkDingError> {
357 let endpoint = Endpoint::ArchiveBookmark(id);
358 let request = self.prepare_request(endpoint)?.build()?;
359 let response = self.client.execute(request)?;
360
361 Ok(response.status() == StatusCode::NO_CONTENT)
362 }
363
364 pub fn unarchive_bookmark(&self, id: i32) -> Result<bool, LinkDingError> {
366 let endpoint = Endpoint::UnarchiveBookmark(id);
367 let request = self.prepare_request(endpoint)?.build()?;
368 let response = self.client.execute(request)?;
369 Ok(response.status() == StatusCode::NO_CONTENT)
370 }
371
372 pub fn delete_bookmark(&self, id: i32) -> Result<bool, LinkDingError> {
374 let endpoint = Endpoint::DeleteBookmark(id);
375 let request = self.prepare_request(endpoint)?.build()?;
376 let response = self.client.execute(request)?;
377 Ok(response.status() == StatusCode::NO_CONTENT)
378 }
379
380 pub fn list_tags(&self, args: ListTagsArgs) -> Result<ListTagsResponse, LinkDingError> {
382 let endpoint = Endpoint::ListTags(args);
383 let request = self.prepare_request(endpoint)?.build()?;
384 let body: ListTagsResponse = self.client.execute(request)?.json()?;
385 Ok(body)
386 }
387
388 pub fn get_tag(&self, id: i32) -> Result<TagData, LinkDingError> {
390 let endpoint = Endpoint::GetTag(id);
391 let request = self.prepare_request(endpoint)?.build()?;
392 let body: TagData = self.client.execute(request)?.json()?;
393 Ok(body)
394 }
395
396 pub fn create_tag(&self, name: &str) -> Result<TagData, LinkDingError> {
398 let endpoint = Endpoint::CreateTag;
399 let body = serde_json::json!({ "name": name });
400 let request = self
401 .prepare_request(endpoint)?
402 .body(serde_json::to_string(&body)?)
403 .build()?;
404 let body: TagData = self.client.execute(request)?.json()?;
405 Ok(body)
406 }
407
408 pub fn get_user_profile(&self) -> Result<UserProfile, LinkDingError> {
410 let endpoint = Endpoint::GetUserProfile;
411 let request = self.prepare_request(endpoint)?.build()?;
412 let body: UserProfile = self.client.execute(request)?.json()?;
413 Ok(body)
414 }
415
416 pub fn list_bookmark_assets(
418 &self,
419 id: i32,
420 ) -> Result<ListBookmarkAssetsResponse, LinkDingError> {
421 let endpoint = Endpoint::ListBookmarkAssets(id);
422 let request = self.prepare_request(endpoint)?.build()?;
423 let body: ListBookmarkAssetsResponse = self.client.execute(request)?.json()?;
424 Ok(body)
425 }
426
427 pub fn retrieve_bookmark_asset(
429 &self,
430 bookmark_id: i32,
431 asset_id: i32,
432 ) -> Result<BookmarkAsset, LinkDingError> {
433 let endpoint = Endpoint::RetrieveBookmarkAsset(bookmark_id, asset_id);
434 let request = self.prepare_request(endpoint)?.build()?;
435 let body: BookmarkAsset = self.client.execute(request)?.json()?;
436 Ok(body)
437 }
438
439 pub fn download_bookmark_asset(
441 &self,
442 bookmark_id: i32,
443 asset_id: i32,
444 ) -> Result<Vec<u8>, LinkDingError> {
445 let endpoint = Endpoint::DownloadBookmarkAsset(bookmark_id, asset_id);
446 let request = self.prepare_request(endpoint)?.build()?;
447 let response = self.client.execute(request)?;
448 Ok(response.bytes()?.into())
449 }
450
451 pub fn upload_bookmark_asset(
453 &self,
454 bookmark_id: i32,
455 bytes: &[u8],
456 ) -> Result<BookmarkAsset, LinkDingError> {
457 let endpoint = Endpoint::UploadBookmarkAsset(bookmark_id);
458 let bytes_part = Part::bytes(bytes.to_owned());
459 let form = reqwest::blocking::multipart::Form::new().part("file", bytes_part);
460 let request = self.prepare_request(endpoint)?.multipart(form).build()?;
461 let body: BookmarkAsset = self.client.execute(request)?.json()?;
462 Ok(body)
463 }
464
465 pub fn delete_bookmark_asset(
467 &self,
468 bookmark_id: i32,
469 asset_id: i32,
470 ) -> Result<bool, LinkDingError> {
471 let endpoint = Endpoint::DeleteBookmarkAsset(bookmark_id, asset_id);
472 let request = self.prepare_request(endpoint)?.build()?;
473 let response = self.client.execute(request)?;
474 Ok(response.status() == StatusCode::NO_CONTENT)
475 }
476}