1pub mod error;
2#[cfg(test)]
3mod tests;
4
5pub use crate::error::{ApiError, ApiErrorKind};
6use failure::ResultExt;
7use reqwest::{multipart, Client, StatusCode};
8use serde_json::{self, Value};
9use url::Url;
10
11pub struct NewsBlurApi {
12 base_uri: Url,
13 username: String,
14 password: String,
15 cookie: Option<String>,
16}
17
18impl NewsBlurApi {
19 pub fn new(url: &Url, username: &str, password: &str, cookie: Option<String>) -> Self {
21 NewsBlurApi {
22 base_uri: url.clone(),
23 username: username.to_string(),
24 password: password.to_string(),
25 cookie,
26 }
27 }
28
29 pub async fn login(&mut self, client: &Client) -> Result<String, ApiError> {
33 let form = multipart::Form::new()
34 .text("username", self.username.clone())
35 .text("password", self.password.clone());
36
37 let api_url: Url = self.base_uri.join("api/login").context(ApiErrorKind::Url)?;
38
39 let response = client
40 .post(api_url)
41 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
42 .multipart(form)
43 .send()
44 .await
45 .context(ApiErrorKind::Http)?;
46
47 let status = response.status();
48 if status != StatusCode::OK {
49 return Err(ApiErrorKind::AccessDenied.into());
50 }
51
52 let cookie = response
53 .cookies()
54 .next()
55 .ok_or(ApiErrorKind::AccessDenied)?;
56 let cookie_string = format!("{}={}", cookie.name(), cookie.value());
57 self.cookie = Some(cookie_string.clone());
58
59 Ok(cookie_string)
60 }
61
62 pub async fn logout(&self, client: &Client) -> Result<(), ApiError> {
64 let api_url: Url = self
65 .base_uri
66 .join("api/logout")
67 .context(ApiErrorKind::Url)?;
68
69 let response = client
70 .post(api_url)
71 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
72 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
73 .send()
74 .await
75 .context(ApiErrorKind::Http)?;
76
77 let status = response.status();
78 if status != StatusCode::OK {
79 return Err(ApiErrorKind::AccessDenied.into());
80 }
81
82 Ok(())
83 }
84
85 pub async fn signup(&self, _client: &Client) -> Result<(), ApiError> {
87 panic!("Unimplemented");
88 }
89
90 pub async fn search_feed(&self, client: &Client, address: &str) -> Result<(), ApiError> {
92 let form = multipart::Form::new().text("address", address.to_string());
93
94 let api_url: Url = self
95 .base_uri
96 .join("rss_feeds/search_feed")
97 .context(ApiErrorKind::Url)?;
98
99 let response = client
100 .get(api_url)
101 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
102 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
103 .multipart(form)
104 .send()
105 .await
106 .context(ApiErrorKind::Http)?;
107
108 let status = response.status();
109 if status != StatusCode::OK {
110 return Err(ApiErrorKind::AccessDenied.into());
111 }
112
113 Ok(())
114 }
115
116 pub async fn get_feeds(&self, client: &Client) -> Result<Value, ApiError> {
118 let form = multipart::Form::new().text("include_favicons", "false");
119
120 let api_url: Url = self
121 .base_uri
122 .join("reader/feeds")
123 .context(ApiErrorKind::Url)?;
124
125 let response = client
126 .get(api_url)
127 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
128 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
129 .multipart(form)
130 .send()
131 .await
132 .context(ApiErrorKind::Http)?;
133
134 let status = response.status();
135 if status != StatusCode::OK {
136 return Err(ApiErrorKind::AccessDenied.into());
137 }
138
139 let response_json = response.json().await.unwrap();
140
141 Ok(response_json)
142 }
143
144 pub async fn favicons(&self, client: &Client, feed_id: &str) -> Result<Value, ApiError> {
149 let form = multipart::Form::new().text("feed_ids", feed_id.to_string());
150
151 let api_url: Url = self
152 .base_uri
153 .join("reader/favicons")
154 .context(ApiErrorKind::Url)?;
155
156 let response = client
157 .get(api_url)
158 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
159 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
160 .multipart(form)
161 .send()
162 .await
163 .context(ApiErrorKind::Http)?;
164
165 let status = response.status();
166 if status != StatusCode::OK {
167 return Err(ApiErrorKind::AccessDenied.into());
168 }
169
170 let response_json = response.json().await.unwrap();
171
172 Ok(response_json)
173 }
174
175 pub async fn get_original_page(&self, client: &Client, id: &str) -> Result<String, ApiError> {
177 let request = format!("reader/page/{}", id);
178
179 let api_url: Url = self.base_uri.join(&request).context(ApiErrorKind::Url)?;
180
181 let response = client
182 .get(api_url)
183 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
184 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
185 .send()
186 .await
187 .context(ApiErrorKind::Http)?;
188
189 let status = response.status();
190 if status != StatusCode::OK {
191 return Err(ApiErrorKind::AccessDenied.into());
192 }
193
194 let response_text = response.text().await.unwrap();
195
196 Ok(response_text)
197 }
198
199 pub async fn get_original_text(&self, client: &Client, id: &str) -> Result<String, ApiError> {
201 let api_url: Url = self
202 .base_uri
203 .join("rss_feeds/original_text")
204 .context(ApiErrorKind::Url)?;
205
206 let query = vec![("story_hash", id.to_string())];
207
208 let response = client
209 .get(api_url)
210 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
211 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
212 .query(&query)
213 .send()
214 .await
215 .context(ApiErrorKind::Http)?;
216
217 let status = response.status();
218 if status != StatusCode::OK {
219 return Err(ApiErrorKind::AccessDenied.into());
220 }
221
222 let response_text = response.text().await.unwrap();
223
224 Ok(response_text)
225 }
226
227 pub async fn refresh_feeds(&self, client: &Client) -> Result<Value, ApiError> {
230 let api_url: Url = self
231 .base_uri
232 .join("reader/refresh_feeds")
233 .context(ApiErrorKind::Url)?;
234
235 let response = client
236 .get(api_url)
237 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
238 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
239 .send()
240 .await
241 .context(ApiErrorKind::Http)?;
242
243 let status = response.status();
244 if status != StatusCode::OK {
245 return Err(ApiErrorKind::AccessDenied.into());
246 }
247
248 let response_json = response.json().await.unwrap();
249
250 Ok(response_json)
251 }
252
253 pub async fn get_read_stories(&self, client: &Client, page: u32) -> Result<Value, ApiError> {
255 let mut query = Vec::new();
256 query.push(("page", format!("{}", page)));
257
258 let api_url: Url = self
259 .base_uri
260 .join("reader/read_stories")
261 .context(ApiErrorKind::Url)?;
262
263 let response = client
264 .get(api_url)
265 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
266 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
267 .query(&query)
268 .send()
269 .await
270 .context(ApiErrorKind::Http)?;
271
272 let status = response.status();
273 if status != StatusCode::OK {
274 return Err(ApiErrorKind::AccessDenied.into());
275 }
276
277 let response_json = response.json().await.unwrap();
278
279 Ok(response_json)
280 }
281
282 pub async fn get_stories(
284 &self,
285 client: &Client,
286 id: &str,
287 include_content: bool,
288 page: u32,
289 ) -> Result<Value, ApiError> {
290 let request = format!("reader/feed/{}", id);
291 let mut query = Vec::new();
292
293 if include_content {
294 query.push(("include_content", "true".to_string()));
295 } else {
296 query.push(("include_content", "false".to_string()));
297 }
298 query.push(("page", format!("{}", page)));
299
300 let api_url: Url = self.base_uri.join(&request).context(ApiErrorKind::Url)?;
301
302 let response = client
303 .get(api_url)
304 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
305 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
306 .query(&query)
307 .send()
308 .await
309 .context(ApiErrorKind::Http)?;
310
311 let status = response.status();
312 if status != StatusCode::OK {
313 return Err(ApiErrorKind::AccessDenied.into());
314 }
315
316 let response_json = response.json().await.unwrap();
317
318 Ok(response_json)
319 }
320
321 pub async fn mark_stories_read(
323 &self,
324 client: &Client,
325 story_hash: &str,
326 ) -> Result<(), ApiError> {
327 let form = multipart::Form::new().text("story_hash", story_hash.to_string());
328
329 let api_url: Url = self
330 .base_uri
331 .join("reader/mark_story_hashes_as_read")
332 .context(ApiErrorKind::Url)?;
333
334 let response = client
335 .post(api_url)
336 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
337 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
338 .multipart(form)
339 .send()
340 .await
341 .context(ApiErrorKind::Http)?;
342
343 let status = response.status();
344 if status != StatusCode::OK {
345 return Err(ApiErrorKind::AccessDenied.into());
346 }
347
348 Ok(())
349 }
350
351 pub async fn mark_story_unread(
353 &self,
354 client: &Client,
355 story_hash: &str,
356 ) -> Result<(), ApiError> {
357 let form = multipart::Form::new().text("story_hash", story_hash.to_string());
358
359 let api_url: Url = self
360 .base_uri
361 .join("reader/mark_story_hash_as_unread")
362 .context(ApiErrorKind::Url)?;
363
364 let response = client
365 .post(api_url)
366 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
367 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
368 .multipart(form)
369 .send()
370 .await
371 .context(ApiErrorKind::Http)?;
372
373 let status = response.status();
374 if status != StatusCode::OK {
375 return Err(ApiErrorKind::AccessDenied.into());
376 }
377
378 Ok(())
379 }
380
381 pub async fn mark_story_hash_as_starred(
383 &self,
384 client: &Client,
385 story_hash: &str,
386 ) -> Result<(), ApiError> {
387 let form = multipart::Form::new().text("story_hash", story_hash.to_string());
388
389 let api_url: Url = self
390 .base_uri
391 .join("reader/mark_story_hash_as_starred")
392 .context(ApiErrorKind::Url)?;
393
394 let response = client
395 .post(api_url)
396 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
397 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
398 .multipart(form)
399 .send()
400 .await
401 .context(ApiErrorKind::Http)?;
402
403 let status = response.status();
404 if status != StatusCode::OK {
405 return Err(ApiErrorKind::AccessDenied.into());
406 }
407
408 Ok(())
409 }
410
411 pub async fn mark_story_hash_as_unstarred(
413 &self,
414 client: &Client,
415 story_hash: &str,
416 ) -> Result<(), ApiError> {
417 let form = multipart::Form::new().text("story_hash", story_hash.to_string());
418
419 let api_url: Url = self
420 .base_uri
421 .join("reader/mark_story_hash_as_unstarred")
422 .context(ApiErrorKind::Url)?;
423
424 let response = client
425 .post(api_url)
426 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
427 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
428 .multipart(form)
429 .send()
430 .await
431 .context(ApiErrorKind::Http)?;
432
433 let status = response.status();
434 if status != StatusCode::OK {
435 return Err(ApiErrorKind::AccessDenied.into());
436 }
437
438 Ok(())
439 }
440
441 pub async fn get_unread_story_hashes(&self, client: &Client) -> Result<Value, ApiError> {
445 let api_url: Url = self
446 .base_uri
447 .join("reader/unread_story_hashes")
448 .context(ApiErrorKind::Url)?;
449
450 let response = client
451 .get(api_url)
452 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
453 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
454 .send()
455 .await
456 .context(ApiErrorKind::Http)?;
457
458 let status = response.status();
459 if status != StatusCode::OK {
460 return Err(ApiErrorKind::AccessDenied.into());
461 }
462
463 let response_json = response.json().await.unwrap();
464
465 Ok(response_json)
466 }
467
468 pub async fn get_stared_story_hashes(&self, client: &Client) -> Result<Value, ApiError> {
469 let api_url: Url = self
470 .base_uri
471 .join("reader/starred_story_hashes")
472 .context(ApiErrorKind::Url)?;
473
474 let response = client
475 .get(api_url)
476 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
477 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
478 .send()
479 .await
480 .context(ApiErrorKind::Http)?;
481
482 let status = response.status();
483 if status != StatusCode::OK {
484 return Err(ApiErrorKind::AccessDenied.into());
485 }
486
487 let response_json = response.json().await.unwrap();
488
489 Ok(response_json)
490 }
491
492 pub async fn get_river_stories(
494 &self,
495 client: &Client,
496 hashes: &[&str],
497 ) -> Result<Value, ApiError> {
498 let api_url: Url = self
499 .base_uri
500 .join("reader/river_stories")
501 .context(ApiErrorKind::Url)?;
502 let mut query = Vec::new();
503
504 for hash in hashes {
505 query.push(("h", hash));
506 }
507
508 let response = client
509 .get(api_url)
510 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
511 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
512 .query(&query)
513 .send()
514 .await
515 .context(ApiErrorKind::Http)?;
516
517 let status = response.status();
518 if status != StatusCode::OK {
519 return Err(ApiErrorKind::AccessDenied.into());
520 }
521
522 let response_json = response.json().await.unwrap();
523
524 Ok(response_json)
525 }
526
527 pub async fn mark_feed_read(&self, client: &Client, feed_id: &str) -> Result<(), ApiError> {
528 let form = multipart::Form::new().text("feed_id", feed_id.to_string());
529
530 let api_url: Url = self
531 .base_uri
532 .join("reader/mark_feed_as_read")
533 .context(ApiErrorKind::Url)?;
534
535 let response = client
536 .post(api_url)
537 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
538 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
539 .multipart(form)
540 .send()
541 .await
542 .context(ApiErrorKind::Http)?;
543
544 let status = response.status();
545 if status != StatusCode::OK {
546 return Err(ApiErrorKind::AccessDenied.into());
547 }
548
549 Ok(())
550 }
551
552 pub async fn mark_all_read(&self, client: &Client) -> Result<(), ApiError> {
553 let api_url: Url = self
554 .base_uri
555 .join("reader/mark_all_as_read")
556 .context(ApiErrorKind::Url)?;
557
558 let response = client
559 .post(api_url)
560 .header(reqwest::header::USER_AGENT, "curl/7.64.0")
561 .header(reqwest::header::COOKIE, self.cookie.as_ref().unwrap())
562 .send()
563 .await
564 .context(ApiErrorKind::Http)?;
565
566 let status = response.status();
567 if status != StatusCode::OK {
568 return Err(ApiErrorKind::AccessDenied.into());
569 }
570
571 Ok(())
572 }
573}