ttrss_api/
lib.rs

1//! This crate provides an API on top of [TinyTinyRSS](https://tt-rss.org/)
2//! ## Usage
3//!
4//! Add this to your `Cargo.toml`:
5//!
6//! ```toml
7//! [dependencies]
8//! ttrss_api = "0.0.1"
9//! ```
10//!
11//! Then add this to your crate:
12//!
13//! ```rust
14//! extern crate ttrss_api;
15//! ```
16//!
17//! To use:
18//!
19//! ```ignore
20//! fn main() {
21//!     let apilevel: Option<ApiLevel> = match get_api_level().expect("Failed to get response").content {
22//!         Content::GetApiLevel(x) => { Some(x) },
23//!         _ => None,
24//!     };
25//!     println!("api level {:?}", apilevel.unwrap());
26//! ```
27//!
28extern crate chrono;
29extern crate strum;
30extern crate serde;
31extern crate serde_json;
32extern crate strum_macros;
33
34use std::env;
35use std::collections::HashMap;
36use std::fmt;
37
38use chrono::serde::ts_seconds;
39use chrono::prelude::*;
40use serde::{Serialize, Deserialize};
41use serde_json::Value;
42use strum_macros::Display;
43
44
45/// Environment variable name for the API to TinyTinyRSS (TTRSS).
46static ENVVAR_TTRSS_URL: &str = "TTRSS_API_URL";
47/// Environment variable name for the user id to login to the TTRSS instance
48static ENVVAR_TTRSS_USERID: &str = "TTRSS_USERID";
49/// Environment variable name for the user id's password to login to the TTRSS instance
50static ENVVAR_TTRSS_PASSWORD: &str = "TTRSS_PASSWORD";
51
52/// Requests to the API will timeout after this many seconds
53static TIMEOUT: u64 = 60;
54
55/// placeholder for the session_id for TTRSS API queries. This package will automatically login and populate the session ID per whether each API call requires a session ID or not
56static mut SESSION_ID: Option<String> = None;
57
58
59/// Result object returned from all API calls.
60type ResponseResult = std::result::Result<Response, TTRSSAPIError>;
61
62
63/// Used in getCounters
64pub enum CounterType {
65    Feeds,
66    Labels,
67    Categories,
68    Tags,
69}
70
71impl std::fmt::Display for CounterType {
72    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
73        write!(f, "{:?}", match self {
74            CounterType::Feeds => "f",
75            CounterType::Labels => "l",
76            CounterType::Categories => "c",
77            CounterType::Tags => "t",
78        })
79    }
80}
81
82
83/// used in get_headlines to filter describe how to filter headlines
84pub enum ViewMode {
85    Adaptive,
86    AllArticles,
87    Marked,
88    Unread,
89    Updated,
90}
91
92impl std::fmt::Display for ViewMode {
93    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
94        write!(f, "{:?}", match self {
95            ViewMode::Adaptive => "adaptive",
96            ViewMode::AllArticles => "all_articles",
97            ViewMode::Marked => "marked",
98            ViewMode::Unread => "unread",
99            ViewMode::Updated => "updated",
100        })
101    }
102}
103
104/// Used in update_article to describe the mode
105pub enum UpdateMode {
106    False,
107    True,
108    Toggle,
109}
110
111impl std::fmt::Display for UpdateMode {
112    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
113        write!(f, "{:?}", match self {
114            UpdateMode::False => 0,
115            UpdateMode::True => 1,
116            UpdateMode::Toggle => 2,
117        })
118    }
119}
120
121
122/// Used in update_article to describe which field to update
123pub enum UpdateArticleField {
124    Starred,
125    Published,
126    Unread,
127    Note,
128}
129
130impl std::fmt::Display for UpdateArticleField {
131    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
132        write!(f, "{:?}", match self {
133            UpdateArticleField::Starred => 0,
134            UpdateArticleField::Published => 1,
135            UpdateArticleField::Unread => 2,
136            UpdateArticleField::Note => 3,
137        })
138    }
139}
140
141/// Counter
142///
143/// # Rust
144/// * get_counters
145///
146/// # TTRSS
147/// * getCounters
148#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
149pub struct Counter {
150    pub auxcounter: Option<i64>,
151    pub counter: Option<i64>,
152    pub has_img: Option<i64>,
153    pub updated: Option<String>,
154    pub markedcounter: Option<i64>,
155    pub kind: Option<String>,
156    pub error: Option<String>,
157    pub id: Value,
158}
159
160
161/// Feed
162///
163/// # Rust
164/// * get_feeds
165///
166/// # TTRSS
167/// * getFeeds
168#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
169pub struct Feed {
170    pub feed_url: String,
171    pub title: String,
172    pub id: u32,
173    pub unread: u32,
174    pub has_icon: bool,
175    pub cat_id: i32,
176    pub order_id: u32,
177
178    #[serde(with = "ts_seconds")]
179    pub last_updated: DateTime<Utc>,
180}
181
182/// Attachment
183///
184/// # Rust
185/// * get_headlines (indirectly)
186/// * get_article (indirectly)
187///
188/// # TTRSS
189/// * getHeadlines (indirectly)
190/// * getArticle (indirectly)
191#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
192pub struct Attachment {
193    pub id: Value,
194    pub content_url: String,
195    pub content_type: String,
196    pub title: String,
197    pub duration: String,
198    pub width: i64,
199    pub height: i64,
200    pub post_id: i64,
201
202    /// Additional fields not specifically deefined in the struct
203    #[serde(flatten)]
204    pub extra: HashMap<String, Value>,
205}
206
207
208/// Guid structure with version and uid details, along with hash
209///
210/// The hash is in SHA:* format, although this parser doesn't validate
211#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
212pub struct GuidVerbose {
213    pub ver: Value,
214    pub uid: Value,
215    pub hash: String,
216}
217
218/// Guid
219///
220/// Unique udentifier. The String variant is observed to be a hash in SHA:* format, although this parser doesn't validate
221#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
222pub enum Guid {
223    GuidVerbose,
224    String,
225}
226
227/// Single Headline
228///
229/// # Rust
230/// * get_headlines (indirectly)
231/// * get_article (indirectly)
232///
233/// # TTRSS
234/// * getHeadlines (indirectly)
235/// * get_article (indirectly)
236#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
237pub struct Headline {
238    pub id: Value,
239
240    pub guid: Value,
241    pub unread: bool,
242    pub marked: bool,
243    pub published: bool,
244
245    #[serde(with = "ts_seconds")]
246    pub updated: DateTime<Utc>,
247    pub is_updated: Option<bool>,
248    pub comments: Option<String>,
249    pub title: String,
250    pub link: String,
251    pub feed_id: Option<i64>,
252    pub tags: Option<Vec<String>>,
253    pub attachments: Option<Vec<Attachment>>,
254    pub excerpt: Option<String>,
255    pub content: Option<String>,
256    pub labels: Option<Vec<String>>,
257    pub feed_title: String,
258    pub comments_count: Option<i64>,
259    pub comments_link: Option<String>,
260    pub always_display_attachments: Option<bool>,
261    pub author: Option<String>,
262    pub score: Option<i64>,
263    pub note: Option<String>,
264    pub lang: Option<String>,
265    pub flavor_image: Option<String>,
266    pub flavor_stream: Option<String>,
267    pub comments_kind: Option<i8>,
268
269    /// Additional fields not specifically deefined in the struct
270    #[serde(flatten)]
271    pub extra: HashMap<String, Value>,
272
273}
274
275/// Wraps some metadata around the headlines from get_headlines
276///
277/// # Rust
278/// * get_headlines (indirectly)
279///
280/// # TTRSS
281/// * getHeadlines (indirectly)
282#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
283pub struct HeadlineWrapper {
284    pub id: Value,
285    pub first_id: Value,
286    pub is_cat: bool,
287    pub headlines: Vec<Headline>,
288
289}
290
291/// Categories
292///
293/// # Rust
294/// * get_categories
295///
296/// # TTRSS
297/// * getCategories
298#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
299pub struct Category {
300    pub id: i64,
301    pub title: String,
302    pub unread: i64,
303    pub order_id: Option<i64>,
304}
305
306
307/// Labels
308///
309/// # Rust
310/// * get_labels
311///
312/// # TTRSS
313/// * getLabels
314#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
315pub struct Label {
316    pub id: i64,
317    pub caption: String,
318    pub fg_color: String,
319    pub bg_color: String,
320    pub checked: bool,
321}
322
323/// Feed Tree
324///
325/// # Rust
326/// * get_feed_tree
327///
328/// # TTRSS
329/// * getFeedTree
330#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
331pub struct FeedTree {
332    pub categories: FeedTreeCategory,
333}
334
335/// Feed Tree Category wraps metadata around lower level feed tree items
336///
337/// # Rust
338/// * get_feed_tree (indirectly)
339///
340/// # TTRSS
341/// * getFeedTre (indirectly)
342#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
343pub struct FeedTreeCategory {
344    pub identifier: String,
345    pub label: String,
346    pub items: Option<Vec<FeedTreeItem>>,
347}
348
349/// Feed Tree Item wraps represents the lowest level of item detail, although may contain child items
350///
351/// # Rust
352/// * get_feed_tree (indirectly)
353///
354/// # TTRSS
355/// * getFeedTree (indirectly)
356#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
357pub struct FeedTreeItem {
358    pub items: Option<Vec<FeedTreeItem>>,
359    pub id: String,
360    pub name: String,
361    pub unread: i64,
362    pub error: Option<String>,
363    pub updated: Option<String>,
364    pub bare_id: i64,
365    pub auxcounter: Option<i64>,
366    pub checkbox: Option<bool>,
367    pub child_unread: Option<i64>,
368    pub param: Option<String>,
369    pub updates_disabled: Option<i64>,
370    pub icon: Option<Value>,
371
372    /// type is a reserved keyword in rust, so aliasing type to itype
373    #[serde(rename = "type")]
374    pub itype: Option<String>,
375
376    #[serde(flatten)]
377    pub extra: HashMap<String, Value>,
378}
379
380/// Login
381///
382/// # Rust
383/// * login
384///
385/// # TTRSS
386/// * login
387#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
388pub struct Login {
389    pub session_id: String,
390    pub api_level: u8
391}
392
393
394/// API Level
395///
396/// # Rust
397/// * get_api_level
398///
399/// # TTRSS
400/// * getApiLevel
401#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
402pub struct ApiLevel {
403    pub level: u8,
404}
405
406
407/// Version
408///
409/// # Rust
410/// * get_version
411///
412/// # TTRSS
413/// * getVersion
414#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
415pub struct Version {
416    pub version: String,
417}
418
419/// Error response returned from API
420///
421/// # Rust
422/// * any that return an error from the API call
423///
424/// # TTRSS
425/// * any that return an error from the API call
426#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
427pub struct ApiError {
428    pub error: String,
429}
430
431
432
433
434/// Logged In
435///
436/// # Rust
437/// * is_logged_in
438///
439/// # TTRSS
440/// * isLoggedIn
441#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
442pub struct LoggedIn {
443    pub status: bool,
444}
445
446/// Status
447///
448/// # Rust
449/// * Used in many return types that update data
450///
451/// # TTRSS
452/// * Used in many return types that update data
453#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
454pub struct Status {
455    pub status: String,
456    pub updated: Option<i64>,
457}
458
459
460/// Unread
461///
462/// # Rust
463/// * get_unread
464///
465/// # TTRSS
466/// * getUnread
467#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
468pub struct Unread {
469    pub unread: u64,
470}
471
472/// Config
473///
474/// # Rust
475/// * get_config
476///
477/// # TTRSS
478/// * getConfig
479#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
480pub struct Config {
481    pub icons_dir: String,
482    pub icons_url: String,
483    pub daemon_is_running: bool,
484    pub num_feeds: i64,
485}
486
487
488/// Preference
489///
490/// # Rust
491/// * get_pref
492///
493/// # TTRSS
494/// * getPref
495#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
496pub struct Preference {
497    pub value: Value,
498}
499
500
501/// Represents the various types of responese from the TTRSS API. The user is expected to inspect the enum for the appropriate type.
502#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Display)]
503#[serde(untagged)]
504pub enum Content {
505    Login(Login),
506    GetApiLevel(ApiLevel),
507    GetVersion(Version),
508    Error(ApiError),
509    IsLoggedIn(LoggedIn),
510    Status(Status),
511    GetUnread(Unread),
512    GetFeeds(Vec<Feed>),
513    GetHeadlines(Vec<HeadlineWrapper>),
514    Labels(Vec<Label>),
515    GetArticle(Vec<Headline>),
516    GetConfig(Config),
517    GetPref(Preference),
518    FeedTree(FeedTree),
519
520    // getCategories and GetCounters should be at the end, in this order. As they're more generic, serde tends to use them more often
521    GetCategories(Vec<Category>),
522    GetCounters(Vec<Counter>),
523}
524
525
526/// TTRSS responses are wrapped around a seq and status, with the details per the API call within the `content` field. This object represents that wrapper.
527#[derive(Serialize, Deserialize, Debug, Clone)]
528pub struct Response {
529    pub seq: Option<u8>,
530    pub status: u8,
531    pub content: Content,
532}
533
534impl fmt::Display for Response {
535    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
536        write!(f, "sequence: {}, ttrss status: {}, content: {}", self.seq.unwrap(), self.status, self.content)
537    }
538}
539
540
541#[derive(Debug)]
542pub enum TTRSSAPIError {
543    SerdeError(serde_json::error::Error),
544    ReqwestError(reqwest::Error),
545    EnvVarError(std::env::VarError),
546    InvalidRequest(String),
547}
548
549impl fmt::Display for TTRSSAPIError {
550    fn fmt(&self, formatter: &mut fmt::Formatter) -> Result<(), fmt::Error> {
551        match self {
552            &TTRSSAPIError::SerdeError(ref e) => e.fmt(formatter),
553            &TTRSSAPIError::ReqwestError(ref e) => e.fmt(formatter),
554            &TTRSSAPIError::EnvVarError(ref e) => e.fmt(formatter),
555            &TTRSSAPIError::InvalidRequest(ref e) => formatter.write_str(&format!("Invalid request with message: {}", e))
556        }
557    }
558}
559
560impl From<serde_json::error::Error> for TTRSSAPIError {
561    fn from(err: serde_json::error::Error) -> TTRSSAPIError {
562        TTRSSAPIError::SerdeError(err)
563    }
564}
565
566impl From<std::env::VarError> for TTRSSAPIError {
567    fn from(err: std::env::VarError) -> TTRSSAPIError {
568        TTRSSAPIError::EnvVarError(err)
569    }
570}
571
572impl From<reqwest::Error> for TTRSSAPIError {
573    fn from(err: reqwest::Error) -> TTRSSAPIError {
574        TTRSSAPIError::ReqwestError(err)
575    }
576}
577
578
579/// Logout of the session.
580pub fn logout() -> ResponseResult {
581    let mut postdata: HashMap<&str, String> = HashMap::new();
582    postdata.insert("op", "logout".to_string());
583
584    unsafe {
585        if SESSION_ID.is_some() {
586            postdata.insert("sid", populate_session_id());
587            match request_from_api(postdata) {
588                Ok(x) => {
589                    SESSION_ID = None;
590                    Ok(x)
591                },
592                Err(x) => Err(x),
593            }
594        } else {
595            postdata.insert("sid", "".to_string());
596            request_from_api(postdata)
597        }
598    }
599}
600
601/// Is the session ID logged in?
602pub fn is_logged_in(session_id: String) -> ResponseResult {
603    let mut postdata: HashMap<&str, String> = HashMap::new();
604    postdata.insert("op", "isLoggedIn".to_string());
605    postdata.insert("sid", session_id);
606    request_from_api(postdata)
607}
608
609
610/// Return one preference value.
611pub fn get_pref(pref_name: String) -> ResponseResult {
612    let mut postdata: HashMap<&str, String> = HashMap::new();
613    postdata.insert("op", "getPref".to_string());
614    postdata.insert("sid", populate_session_id());
615    postdata.insert("pref_name", pref_name);
616    request_from_api(postdata)
617}
618
619
620/// Get article ID's contents.
621pub fn get_article(article_id: i64) -> ResponseResult {
622    let mut postdata: HashMap<&str, String> = HashMap::new();
623    postdata.insert("op", "getArticle".to_string());
624    postdata.insert("sid", populate_session_id());
625    postdata.insert("article_id", article_id.to_string());
626    request_from_api(postdata)
627}
628
629
630/// Get list of categories.
631pub fn get_categories(unread_only: bool, enable_nested: bool, include_empty: bool) -> ResponseResult {
632    let mut postdata: HashMap<&str, String> = HashMap::new();
633    postdata.insert("op", "getCategories".to_string());
634    postdata.insert("sid", populate_session_id());
635    postdata.insert("unread_only", unread_only.to_string());
636    postdata.insert("enable_nested", enable_nested.to_string());
637    postdata.insert("include_empty", include_empty.to_string());
638    request_from_api(postdata)
639}
640
641
642/// Get list of headlines.
643pub fn get_headlines(feed_id: i64, limit: i64, skip: i64, filter: String, is_cat: bool, show_excerpt: bool, show_content: bool, view_mode: ViewMode, include_attachments: bool, since_id: i64, include_nested: bool, order_by: String, sanitize: bool, force_update: bool, has_sandbox: bool, include_header: bool) -> ResponseResult {
644    let mut postdata: HashMap<&str, String> = HashMap::new();
645    postdata.insert("op", "getHeadlines".to_string());
646    postdata.insert("sid", populate_session_id());
647    postdata.insert("feed_id", feed_id.to_string());
648    postdata.insert("limit", limit.to_string());
649    postdata.insert("skip", skip.to_string());
650    postdata.insert("filter", filter);
651    postdata.insert("is_cat", is_cat.to_string());
652    postdata.insert("show_excerpt", show_excerpt.to_string());
653    postdata.insert("show_content", show_content.to_string());
654    postdata.insert("view_mode", view_mode.to_string());
655    postdata.insert("include_attachments", include_attachments.to_string());
656    postdata.insert("since_id", since_id.to_string());
657    postdata.insert("include_nested", include_nested.to_string());
658    postdata.insert("order_by", order_by.to_string());
659    postdata.insert("sanitize", sanitize.to_string());
660    postdata.insert("force_update", force_update.to_string());
661    postdata.insert("has_sandbox", has_sandbox.to_string());
662    postdata.insert("include_header", include_header.to_string());
663    request_from_api(postdata)
664}
665
666
667/// Get list of feeds.
668pub fn get_feeds(cat_id: i32, unread_only: bool, limit: u8, offset: u32, include_nested: bool) -> ResponseResult {
669    let mut postdata: HashMap<&str, String> = HashMap::new();
670    postdata.insert("op", "getFeeds".to_string());
671    postdata.insert("sid", populate_session_id());
672    postdata.insert("cat_id", cat_id.to_string());
673    postdata.insert("unread_only", unread_only.to_string());
674    postdata.insert("limit", limit.to_string());
675    postdata.insert("offset", offset.to_string());
676    postdata.insert("include_nested", include_nested.to_string());
677    request_from_api(postdata)
678}
679
680
681/// Update an article.
682pub fn update_article(article_ids: Vec<i64>, mode: UpdateMode, field: UpdateArticleField, data: String) -> ResponseResult {
683    let mut postdata: HashMap<&str, String> = HashMap::new();
684    postdata.insert("op", "updateArticle".to_string());
685    postdata.insert("sid", populate_session_id());
686    postdata.insert("data", data);
687    postdata.insert("article_ids", article_ids.iter().map(|s| s.to_string()).collect::<Vec<String>>().join(","));
688    postdata.insert("field", field.to_string());
689    postdata.insert("mode", mode.to_string());
690    request_from_api(postdata)
691}
692
693
694/// Get counters.
695pub fn get_counters(counter_type: Vec<CounterType>) -> ResponseResult {
696    let mut postdata: HashMap<&str, String> = HashMap::new();
697    postdata.insert("op", "getCounters".to_string());
698    postdata.insert("sid", populate_session_id());
699    postdata.insert("output_mode", counter_type.iter().map(|c| c.to_string()).collect::<Vec<String>>().join(""));
700    request_from_api(postdata)
701}
702
703
704/// Set an article's label.
705pub fn set_article_label(article_ids: Vec<i64>, label_id: i64, assign: bool) -> ResponseResult {
706    let mut postdata: HashMap<&str, String> = HashMap::new();
707    postdata.insert("op", "setArticleLabel".to_string());
708    postdata.insert("sid", populate_session_id());
709    postdata.insert("article_ids", article_ids.iter().map(|s| s.to_string()).collect::<Vec<String>>().join(","));
710    postdata.insert("label_id", label_id.to_string());
711    postdata.insert("assign", assign.to_string());
712    request_from_api(postdata)
713}
714
715
716/// Get all labels.
717pub fn get_labels() -> ResponseResult {
718    let mut postdata: HashMap<&str, String> = HashMap::new();
719    postdata.insert("op", "getLabels".to_string());
720    postdata.insert("sid", populate_session_id());
721    request_from_api(postdata)
722}
723
724
725/// Get API version.
726pub fn get_version() -> ResponseResult {
727    let mut postdata: HashMap<&str, String> = HashMap::new();
728    postdata.insert("op", "getVersion".to_string());
729    postdata.insert("sid", populate_session_id());
730    request_from_api(postdata)
731}
732
733
734/// Catchup feed.
735pub fn catchup_feed(feed_id: i64, is_cat: bool) -> ResponseResult {
736    let mut postdata: HashMap<&str, String> = HashMap::new();
737    postdata.insert("op", "updateFeed".to_string());
738    postdata.insert("feed_id", feed_id.to_string());
739    postdata.insert("is_cat", is_cat.to_string());
740    postdata.insert("sid", populate_session_id());
741    request_from_api(postdata)
742}
743
744
745/// Update the feed.
746pub fn update_feed(feed_id: i64) -> ResponseResult {
747    let mut postdata: HashMap<&str, String> = HashMap::new();
748    postdata.insert("op", "updateFeed".to_string());
749    postdata.insert("feed_id", feed_id.to_string());
750    postdata.insert("sid", populate_session_id());
751    request_from_api(postdata)
752}
753
754
755/// Subscribe to a feed.
756pub fn subscribe_to_feed(feed_url: String, category_id: i64, login: String, password: String) -> ResponseResult {
757    let mut postdata: HashMap<&str, String> = HashMap::new();
758    postdata.insert("op", "subscribeToFeed".to_string());
759    postdata.insert("sid", populate_session_id());
760    postdata.insert("feed_url", feed_url);
761    postdata.insert("category_id", category_id.to_string());
762    postdata.insert("login", login);
763    postdata.insert("password", password);
764    request_from_api(postdata)
765}
766
767
768/// Unsubscribe to a feed.
769pub fn unsubscribe_feed(feed_id: i64) -> ResponseResult {
770    let mut postdata: HashMap<&str, String> = HashMap::new();
771    postdata.insert("op", "unsubscribeFeed".to_string());
772    postdata.insert("sid", populate_session_id());
773    postdata.insert("feed_id", feed_id.to_string());
774    request_from_api(postdata)
775}
776
777
778/// Create tree of all feeds' and related info.
779pub fn get_feed_tree(include_empty: bool) -> ResponseResult {
780    let mut postdata: HashMap<&str, String> = HashMap::new();
781    postdata.insert("op", "getFeedTree".to_string());
782    postdata.insert("sid", populate_session_id());
783    postdata.insert("include_empty", include_empty.to_string());
784    request_from_api(postdata)
785}
786
787
788/// Share information to published feed.
789pub fn share_to_published(title: String, url: String, content: String) -> ResponseResult {
790    let mut postdata: HashMap<&str, String> = HashMap::new();
791    postdata.insert("op", "shareToPublished".to_string());
792    postdata.insert("sid", populate_session_id());
793    postdata.insert("title", title);
794    postdata.insert("url", url);
795    postdata.insert("content", content);
796    request_from_api(postdata)
797}
798
799
800/// Get configruration.
801pub fn get_config() -> ResponseResult {
802    let mut postdata: HashMap<&str, String> = HashMap::new();
803    postdata.insert("op", "getConfig".to_string());
804    postdata.insert("sid", populate_session_id());
805    request_from_api(postdata)
806}
807
808
809/// Get unread feeds.
810pub fn get_unread() -> ResponseResult {
811    let mut postdata: HashMap<&str, String> = HashMap::new();
812    postdata.insert("op", "getUnread".to_string());
813    postdata.insert("sid", populate_session_id());
814    request_from_api(postdata)
815}
816
817
818/// Get API level.
819pub fn get_api_level() -> ResponseResult {
820    let mut postdata: HashMap<&str, String> = HashMap::new();
821    postdata.insert("op", "getApiLevel".to_string());
822    postdata.insert("sid", populate_session_id());
823    request_from_api(postdata)
824}
825
826
827/// Helper method to get the session ID from a supposedly login response
828fn get_session_id_from_login(login: ResponseResult) -> Option<String> {
829    match login {
830        Ok(response) => {
831            match response.content {
832                Content::Login(x) => { Some(x.session_id) },
833                _ => None,
834            }
835        },
836        Err(_) => { None },
837    }
838
839}
840
841
842/// Login to TTRSS. This will automatically happen if the API call requires a session ID
843pub fn login() -> ResponseResult {
844    let mut postdata: HashMap<&str, String> = HashMap::new();
845    let (user, password): (&str, &str) = (&env::var(ENVVAR_TTRSS_USERID)?, &env::var(ENVVAR_TTRSS_PASSWORD)?);
846    postdata.insert("op", "login".to_string());
847    postdata.insert("user", user.to_string());
848    postdata.insert("password", password.to_string());
849    request_from_api(postdata)
850}
851
852
853/// Internal call to generalize communication to TTRSS' API among multiple API calls
854pub fn request_from_api(postdata: HashMap<&str, String>) -> ResponseResult {
855    validate_or_panic();
856    let client = reqwest::blocking::Client::builder()
857        .timeout(std::time::Duration::from_secs(TIMEOUT))
858        .build()?;
859    //let clients = reqwest::blocking::ClientB
860//    .timeout(Duration::from_secs(10))
861    let response = client.post(&env::var(ENVVAR_TTRSS_URL)?)
862        .json(&postdata)
863        .send()?;
864
865    if response.status().is_success() {
866        let mut resp = response.text()?;
867        if postdata.contains_key("op") && postdata.get("op").unwrap() == "getHeadlines" {
868            // reasoning for this: getHeadlines sends this:
869            // "{"seq":0,"status":0,"content":[{"id":100,"first_id":406482,"is_cat":false},[{"id":406482,"guid":"SHA1:bcf72e224fecc236174c682ff865fbaf1bd3d2d0","unread":true,"marked":false,"published":false,"updated":1589028361,"is_updated":false,"title":"..." ....
870            // Note that the [] which contains headline data doesn't have a key. serde_json doesn't flatten sequences (Vec), just maps and objects that map to their Value struct (vec isn't one of them), so I need to put a key in there
871            resp = resp.replacen("},[{", ",\"headlines\":[{", 1);
872            resp = resp.replacen("}]]}", "}]}]}", 1);
873            //                              ^ this 2nd { is added to close out the } removed from the first `replacen` which adds 'headlines'
874        }
875
876        //std::fs::write("/tmp/oo.txt", &resp).expect("bad");
877        //println!("{}", serde_json::to_string_pretty(&resp.replace("\\", "")).unwrap_or_default());
878        Ok(serde_json::from_str(&resp)?)
879    } else {
880        Err(TTRSSAPIError::InvalidRequest(format!("response status: {:?}", response.status())))
881    }
882}
883
884
885/// Internal method to validate environment variables are set
886fn validate_environment_variables() -> bool {
887    env::var(ENVVAR_TTRSS_URL).is_ok() &&
888    env::var(ENVVAR_TTRSS_USERID).is_ok() &&
889    env::var(ENVVAR_TTRSS_PASSWORD).is_ok()
890}
891
892
893/// Internal method to validate the environment or force exit
894fn validate_or_panic() {
895    if ! validate_environment_variables() {
896        panic!(r"Validate the environment variables are set. Values retrieved are:
897    - {:?}: {:?}
898    - {:?}: {:?}
899    - {:?}: {:?}
900        ", ENVVAR_TTRSS_URL, env::var(ENVVAR_TTRSS_URL), ENVVAR_TTRSS_USERID, env::var(ENVVAR_TTRSS_USERID), ENVVAR_TTRSS_PASSWORD, env::var(ENVVAR_TTRSS_PASSWORD));
901    }
902}
903
904
905/// Internal call to populate the session ID
906fn populate_session_id() -> String {
907    unsafe {
908        if SESSION_ID.is_none() {
909            let login: ResponseResult = login();
910            SESSION_ID = get_session_id_from_login(login);
911        }
912        match &SESSION_ID {
913            Some(x) => {x.to_string()},
914            None => "".to_string(),
915        }
916    }
917}