1mod error;
2pub mod models;
3#[cfg(test)]
4mod tests;
5
6pub use crate::error::ApiError;
7use crate::models::{
8 cache::Cache,
9 cache::CacheRequestResponse,
10 cache::CacheResult,
11 entry::Entry,
12 entry::UpdateEntryStarredInput,
13 entry::UpdateEntryUnreadInput,
14 icon::Icon,
15 subscription::Subscription,
16 subscription::SubscriptionMode,
17 subscription::UpdateSubscriptionInput,
18 subscription::{CreateSubscriptionInput, CreateSubscriptionResult},
19 tagging::CreateTaggingInput,
20 tagging::DeleteTagInput,
21 tagging::RenameTagInput,
22 tagging::Tagging,
23};
24use chrono::{DateTime, Utc};
25use core::marker::Sized;
26use models::SubscriptionOption;
27use reqwest::header::{CONTENT_TYPE, ETAG, IF_MODIFIED_SINCE, IF_NONE_MATCH, LAST_MODIFIED};
28use reqwest::{Client, Method, RequestBuilder, Response, StatusCode};
29use serde::{Deserialize, Serialize};
30use std::collections::HashMap;
31use url::Url;
32
33pub type FeedID = u64;
34pub type EntryID = u64;
35pub type SubscriptionID = u64;
36pub type TaggingID = u64;
37
38const EMPTY_PARAMS: Option<&HashMap<String, String>> = None;
39
40pub struct FeedbinApi {
41 base_url: Url,
42 username: String,
43 password: String,
44}
45
46impl FeedbinApi {
47 pub fn new<S: Into<String>>(base_url: &Url, username: S, password: S) -> Self {
48 FeedbinApi {
49 base_url: base_url.clone(),
50 username: username.into(),
51 password: password.into(),
52 }
53 }
54
55 pub fn with_base_url(&self, base_url: &Url) -> Self {
56 FeedbinApi {
57 base_url: base_url.clone(),
58 username: self.username.clone(),
59 password: self.password.clone(),
60 }
61 }
62
63 pub fn with_password<S: Into<String>>(&self, password: S) -> Self {
64 FeedbinApi {
65 base_url: self.base_url.clone(),
66 username: self.username.clone(),
67 password: password.into(),
68 }
69 }
70
71 fn build_url(&self, path: &str) -> Url {
72 self.base_url.clone().join(path).unwrap() }
74
75 async fn deserialize<T: for<'a> Deserialize<'a>>(response: Response) -> Result<T, ApiError> {
76 let json = response.text().await?;
77 let result: T =
78 serde_json::from_str(&json).map_err(|source| ApiError::Json { source, json })?;
79 Ok(result)
80 }
81
82 async fn request<F: FnOnce(RequestBuilder) -> RequestBuilder, T: Serialize + ?Sized>(
83 &self,
84 client: &Client,
85 method: Method,
86 path: &str,
87 params: Option<&T>,
88 f: F,
89 ) -> Result<Response, ApiError> {
90 let url = self.build_url(path);
91 let request = client
92 .request(method, url)
93 .basic_auth(&self.username, Some(&self.password));
94
95 let request = match params {
96 Some(params) => request.query(params),
97 None => request,
98 };
99
100 let request = f(request);
101 let response = request.send().await?;
102
103 match response.status().as_u16() {
104 401 => Err(ApiError::InvalidLogin),
105 403 => Err(ApiError::AccessDenied),
106 200 | 201 | 204 | 302 | 404 => Ok(response),
107 _ => Err(ApiError::ServerIsBroken),
108 }
109 }
110
111 async fn get<T: Serialize + ?Sized>(
112 &self,
113 client: &Client,
114 path: &str,
115 params: Option<&T>,
116 cache: Option<Cache>,
117 ) -> Result<CacheRequestResponse<Response>, ApiError> {
118 if let Some(cache) = cache {
120 let response = client
121 .request(Method::GET, self.build_url(path))
122 .basic_auth(&self.username, Some(&self.password))
123 .header(IF_MODIFIED_SINCE, cache.last_modified)
124 .header(IF_NONE_MATCH, cache.etag)
125 .send()
126 .await?;
127
128 if response.status() == StatusCode::NOT_MODIFIED {
129 return Ok(CacheRequestResponse::NotModified);
130 }
131 }
132
133 let response = self
134 .request(client, Method::GET, path, params, |req| req)
135 .await?;
136
137 if let Some(etag) = response.headers().get(ETAG) {
139 if let Some(last_modified) = response.headers().get(LAST_MODIFIED) {
140 if let Ok(etag) = etag.to_str() {
141 if let Ok(last_modified) = last_modified.to_str() {
142 let cache = Cache {
143 etag: etag.into(),
144 last_modified: last_modified.into(),
145 };
146 return Ok(CacheRequestResponse::Modified(CacheResult {
147 value: response,
148 cache: Some(cache),
149 }));
150 }
151 }
152 }
153 }
154
155 Ok(CacheRequestResponse::Modified(CacheResult {
156 value: response,
157 cache: None,
158 }))
159 }
160
161 async fn delete_with_body<F: FnOnce(RequestBuilder) -> RequestBuilder>(
162 &self,
163 client: &Client,
164 path: &str,
165 f: F,
166 ) -> Result<Response, ApiError> {
167 self.request(client, Method::DELETE, path, EMPTY_PARAMS, f)
168 .await
169 }
170
171 async fn delete(&self, client: &Client, path: &str) -> Result<Response, ApiError> {
172 self.request(client, Method::DELETE, path, EMPTY_PARAMS, |req| req)
173 .await
174 }
175
176 async fn post<F: FnOnce(RequestBuilder) -> RequestBuilder>(
177 &self,
178 client: &Client,
179 path: &str,
180 f: F,
181 ) -> Result<Response, ApiError> {
182 self.request(client, Method::POST, path, EMPTY_PARAMS, f)
183 .await
184 }
185
186 pub async fn is_authenticated(&self, client: &Client) -> Result<bool, ApiError> {
187 match self
188 .get(client, "/v2/authentication.json", EMPTY_PARAMS, None)
189 .await
190 {
191 Err(err) => match err {
192 ApiError::InvalidLogin => Ok(false),
193 _ => Err(err),
194 },
195 Ok(CacheRequestResponse::Modified(CacheResult {
196 value: response,
197 cache: _,
198 })) => match response.status().as_u16() {
199 200 => Ok(true),
200 _ => Err(ApiError::ServerIsBroken),
201 },
202 Ok(CacheRequestResponse::NotModified) => Err(ApiError::InvalidCaching),
203 }
204 }
205
206 pub async fn is_reachable(&self, client: &Client) -> Result<bool, ApiError> {
207 match self.is_authenticated(client).await {
208 Ok(_) => Ok(true),
209 Err(err) => match err {
210 ApiError::ServerIsBroken => Ok(true),
211 ApiError::Network(_) => Ok(false),
212 _ => Err(err),
213 },
214 }
215 }
216
217 #[allow(clippy::too_many_arguments)]
219 pub async fn get_entries(
220 &self,
221 client: &Client,
222 page: Option<u32>,
223 since: Option<DateTime<Utc>>,
224 ids: Option<&[EntryID]>,
225 starred: Option<bool>,
226 enclosure: Option<bool>,
227 extended: bool,
228 ) -> Result<Vec<Entry>, ApiError> {
229 let api_endpoint = "/v2/entries.json";
230 let mut params = HashMap::new();
231
232 if let Some(page) = page {
233 params.insert(String::from("page"), page.to_string());
234 }
235 if let Some(since) = since {
236 params.insert(
237 String::from("since"),
238 since.format("%Y-%m-%dT%H:%M:%S%.f").to_string(),
239 );
240 }
241 if let Some(ids) = ids {
242 let id_strings = ids.iter().map(|id| id.to_string()).collect::<Vec<String>>();
243 params.insert(String::from("ids"), id_strings.join(","));
244 }
245 if let Some(starred) = starred {
246 params.insert(String::from("starred"), starred.to_string());
247 }
248 if let Some(enclosure) = enclosure {
249 params.insert(String::from("include_enclosure"), enclosure.to_string());
250 }
251 if extended {
252 params.insert(String::from("mode"), String::from("extended"));
253 }
254
255 match self.get(client, api_endpoint, Some(¶ms), None).await? {
256 CacheRequestResponse::Modified(CacheResult {
257 value: response,
258 cache: _cache,
259 }) => Self::deserialize::<Vec<Entry>>(response).await,
260 CacheRequestResponse::NotModified => Err(ApiError::InvalidCaching),
261 }
262 }
263
264 pub async fn get_entries_for_feed(
266 &self,
267 client: &Client,
268 feed_id: FeedID,
269 cache: Option<Cache>,
270 ) -> Result<CacheRequestResponse<Vec<Entry>>, ApiError> {
271 let path = format!("/v2/feeds/{}/entries.json", feed_id);
272 match self.get(client, &path, EMPTY_PARAMS, cache).await? {
273 CacheRequestResponse::Modified(CacheResult {
274 value: response,
275 cache,
276 }) => {
277 let res = Self::deserialize::<Vec<Entry>>(response).await?;
278 Ok(CacheRequestResponse::Modified(CacheResult {
279 value: res,
280 cache,
281 }))
282 }
283 CacheRequestResponse::NotModified => Ok(CacheRequestResponse::NotModified),
284 }
285 }
286
287 pub async fn get_unread_entry_ids(&self, client: &Client) -> Result<Vec<EntryID>, ApiError> {
289 match self
290 .get(client, "/v2/unread_entries.json", EMPTY_PARAMS, None)
291 .await?
292 {
293 CacheRequestResponse::Modified(CacheResult {
294 value: response,
295 cache: _cache,
296 }) => Self::deserialize::<Vec<EntryID>>(response).await,
297 CacheRequestResponse::NotModified => Err(ApiError::InvalidCaching),
298 }
299 }
300
301 pub async fn set_entries_unread(
303 &self,
304 client: &Client,
305 entry_ids: &[EntryID],
306 ) -> Result<(), ApiError> {
307 if entry_ids.len() > 1000 {
308 return Err(ApiError::InputSize);
309 }
310 let input = UpdateEntryUnreadInput {
311 unread_entries: entry_ids.into(),
312 };
313 self.post(client, "/v2/unread_entries.json", |r| r.json(&input))
314 .await
315 .map(|_| ())
316 }
317
318 pub async fn set_entries_read(
320 &self,
321 client: &Client,
322 entry_ids: &[EntryID],
323 ) -> Result<(), ApiError> {
324 if entry_ids.len() > 1000 {
325 return Err(ApiError::InputSize);
326 }
327 let input = UpdateEntryUnreadInput {
328 unread_entries: entry_ids.into(),
329 };
330 self.delete_with_body(client, "/v2/unread_entries.json", |r| r.json(&input))
331 .await
332 .map(|_| ())
333 }
334
335 pub async fn get_starred_entry_ids(&self, client: &Client) -> Result<Vec<EntryID>, ApiError> {
337 match self
338 .get(client, "/v2/starred_entries.json", EMPTY_PARAMS, None)
339 .await?
340 {
341 CacheRequestResponse::Modified(CacheResult {
342 value: response,
343 cache: _cache,
344 }) => Self::deserialize::<Vec<EntryID>>(response).await,
345 CacheRequestResponse::NotModified => Err(ApiError::InvalidCaching),
346 }
347 }
348
349 pub async fn set_entries_starred(
351 &self,
352 client: &Client,
353 entry_ids: &[EntryID],
354 ) -> Result<(), ApiError> {
355 if entry_ids.len() > 1000 {
356 return Err(ApiError::InputSize);
357 }
358 let input = UpdateEntryStarredInput {
359 starred_entries: entry_ids.into(),
360 };
361 self.post(client, "/v2/starred_entries.json", |r| r.json(&input))
362 .await
363 .map(|_| ())
364 }
365
366 pub async fn set_entries_unstarred(
368 &self,
369 client: &Client,
370 entry_ids: &[EntryID],
371 ) -> Result<(), ApiError> {
372 if entry_ids.len() > 1000 {
373 return Err(ApiError::InputSize);
374 }
375 let input = UpdateEntryStarredInput {
376 starred_entries: entry_ids.into(),
377 };
378 self.delete_with_body(client, "/v2/starred_entries.json", |r| r.json(&input))
379 .await
380 .map(|_| ())
381 }
382
383 pub async fn get_entry(&self, client: &Client, entry_id: EntryID) -> Result<Entry, ApiError> {
385 let path = format!("/v2/entries/{}.json", entry_id);
386 match self.get(client, &path, EMPTY_PARAMS, None).await? {
387 CacheRequestResponse::Modified(CacheResult {
388 value: response,
389 cache: _cache,
390 }) => Self::deserialize::<Entry>(response).await,
391 CacheRequestResponse::NotModified => Err(ApiError::InvalidCaching),
392 }
393 }
394
395 pub async fn get_subscriptions(
397 &self,
398 client: &Client,
399 since: Option<DateTime<Utc>>,
400 mode: Option<SubscriptionMode>,
401 cache: Option<Cache>,
402 ) -> Result<CacheRequestResponse<Vec<Subscription>>, ApiError> {
403 let api_endpoint = "/v2/subscriptions.json";
404 let mut params: HashMap<String, String> = HashMap::new();
405
406 if let Some(since) = since {
407 params.insert(
408 String::from("since"),
409 since.format("%Y-%m-%dT%H:%M:%S%.f").to_string(),
410 );
411 }
412 if let Some(mode) = mode {
413 params.insert(String::from("mode"), mode.to_string());
414 }
415
416 match self.get(client, api_endpoint, Some(¶ms), cache).await? {
417 CacheRequestResponse::Modified(CacheResult {
418 value: response,
419 cache,
420 }) => {
421 let res = Self::deserialize::<Vec<Subscription>>(response).await?;
422 Ok(CacheRequestResponse::Modified(CacheResult {
423 value: res,
424 cache,
425 }))
426 }
427 CacheRequestResponse::NotModified => Ok(CacheRequestResponse::NotModified),
428 }
429 }
430
431 pub async fn get_subscription(
433 &self,
434 client: &Client,
435 subscription_id: SubscriptionID,
436 ) -> Result<Subscription, ApiError> {
437 let path = format!("/v2/subscriptions/{}.json", subscription_id);
438 match self.get(client, &path, EMPTY_PARAMS, None).await? {
439 CacheRequestResponse::Modified(CacheResult {
440 value: response,
441 cache: _cache,
442 }) => Self::deserialize::<Subscription>(response).await,
443 CacheRequestResponse::NotModified => Err(ApiError::InvalidCaching),
444 }
445 }
446
447 pub async fn create_subscription<S: Into<String>>(
449 &self,
450 client: &Client,
451 url: S,
452 ) -> Result<CreateSubscriptionResult, ApiError> {
453 let input = CreateSubscriptionInput {
454 feed_url: url.into(),
455 };
456 let res = self
457 .post(client, "/v2/subscriptions.json", |request| {
458 request.json(&input)
459 })
460 .await?;
461 match res.status().as_u16() {
462 201 => {
463 let subscription = Self::deserialize::<Subscription>(res).await?;
464 Ok(CreateSubscriptionResult::Created(subscription))
465 }
466 300 => {
467 let options = Self::deserialize::<Vec<SubscriptionOption>>(res).await?;
468 Ok(CreateSubscriptionResult::MultipleOptions(options))
469 }
470 303 => {
471 let location = res
472 .headers()
473 .get("Location")
474 .ok_or(ApiError::ServerIsBroken)?
475 .to_str()
476 .map_err(|_| ApiError::ServerIsBroken)?;
477 let location = Url::parse(location)?;
478 Ok(CreateSubscriptionResult::Found(location))
479 }
480 404 => Ok(CreateSubscriptionResult::NotFound),
481 _ => Err(ApiError::ServerIsBroken),
482 }
483 }
484
485 pub async fn delete_subscription(
486 &self,
487 client: &Client,
488 subscription_id: SubscriptionID,
489 ) -> Result<(), ApiError> {
490 let path = format!("/v2/subscriptions/{}.json", subscription_id);
491 self.delete(client, &path).await.map(|_| ())
492 }
493
494 pub async fn update_subscription<S: Into<String>>(
496 &self,
497 client: &Client,
498 subscription_id: SubscriptionID,
499 title: S,
500 ) -> Result<(), ApiError> {
501 let input = UpdateSubscriptionInput {
502 title: title.into(),
503 };
504 let path = format!("/v2/subscriptions/{}/update.json", subscription_id);
505 self.post(client, &path, |request| request.json(&input))
506 .await
507 .map(|_| ())
508 }
509
510 pub async fn get_taggings(
512 &self,
513 client: &Client,
514 cache: Option<Cache>,
515 ) -> Result<CacheRequestResponse<Vec<Tagging>>, ApiError> {
516 match self
517 .get(client, "/v2/taggings.json", EMPTY_PARAMS, cache)
518 .await?
519 {
520 CacheRequestResponse::Modified(CacheResult {
521 value: response,
522 cache,
523 }) => {
524 let res = Self::deserialize::<Vec<Tagging>>(response).await?;
525 Ok(CacheRequestResponse::Modified(CacheResult {
526 value: res,
527 cache,
528 }))
529 }
530 CacheRequestResponse::NotModified => Ok(CacheRequestResponse::NotModified),
531 }
532 }
533
534 pub async fn get_tagging(
536 &self,
537 client: &Client,
538 tagging_id: TaggingID,
539 ) -> Result<Tagging, ApiError> {
540 let path = format!("/v2/taggings/{}.json", tagging_id);
541 match self.get(client, &path, EMPTY_PARAMS, None).await? {
542 CacheRequestResponse::Modified(CacheResult {
543 value: response,
544 cache: _cache,
545 }) => Self::deserialize::<Tagging>(response).await,
546 CacheRequestResponse::NotModified => Err(ApiError::InvalidCaching),
547 }
548 }
549
550 pub async fn create_tagging(
552 &self,
553 client: &Client,
554 feed_id: FeedID,
555 name: &str,
556 ) -> Result<(), ApiError> {
557 let input = CreateTaggingInput {
558 feed_id,
559 name: name.into(),
560 };
561 self.post(client, "/v2/taggings.json", |r| r.json(&input))
562 .await
563 .map(|_| ())
564 }
565
566 pub async fn delete_tagging(
568 &self,
569 client: &Client,
570 tagging_id: TaggingID,
571 ) -> Result<(), ApiError> {
572 let path = format!("/v2/taggings/{}.json", tagging_id);
573 self.delete(client, &path).await.map(|_| ())
574 }
575
576 pub async fn rename_tag(
578 &self,
579 client: &Client,
580 old_name: &str,
581 new_name: &str,
582 ) -> Result<(), ApiError> {
583 let input = RenameTagInput {
584 old_name: old_name.into(),
585 new_name: new_name.into(),
586 };
587 self.post(client, "/v2/tags.json", |r| r.json(&input))
588 .await
589 .map(|_| ())
590 }
591
592 pub async fn delete_tag(&self, client: &Client, name: &str) -> Result<(), ApiError> {
594 let input = DeleteTagInput { name: name.into() };
595 self.delete_with_body(client, "/v2/tags.json", |r| r.json(&input))
596 .await
597 .map(|_| ())
598 }
599
600 pub async fn get_icons(&self, client: &Client) -> Result<Vec<Icon>, ApiError> {
602 match self
603 .get(client, "/v2/icons.json", EMPTY_PARAMS, None)
604 .await?
605 {
606 CacheRequestResponse::Modified(CacheResult {
607 value: response,
608 cache: _cache,
609 }) => Self::deserialize::<Vec<Icon>>(response).await,
610 CacheRequestResponse::NotModified => Err(ApiError::InvalidCaching),
611 }
612 }
613
614 pub async fn import_opml(&self, client: &Client, opml: &str) -> Result<(), ApiError> {
615 self.post(client, "/v2/imports.json", |req_builder| {
616 req_builder
617 .header(CONTENT_TYPE, "text/xml")
618 .body(opml.to_owned())
619 })
620 .await
621 .map(|_| ())
622 }
623}