1extern 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
45static ENVVAR_TTRSS_URL: &str = "TTRSS_API_URL";
47static ENVVAR_TTRSS_USERID: &str = "TTRSS_USERID";
49static ENVVAR_TTRSS_PASSWORD: &str = "TTRSS_PASSWORD";
51
52static TIMEOUT: u64 = 60;
54
55static mut SESSION_ID: Option<String> = None;
57
58
59type ResponseResult = std::result::Result<Response, TTRSSAPIError>;
61
62
63pub 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
83pub 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
104pub 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
122pub 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#[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#[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#[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 #[serde(flatten)]
204 pub extra: HashMap<String, Value>,
205}
206
207
208#[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#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
222pub enum Guid {
223 GuidVerbose,
224 String,
225}
226
227#[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 #[serde(flatten)]
271 pub extra: HashMap<String, Value>,
272
273}
274
275#[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#[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#[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#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
331pub struct FeedTree {
332 pub categories: FeedTreeCategory,
333}
334
335#[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#[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 #[serde(rename = "type")]
374 pub itype: Option<String>,
375
376 #[serde(flatten)]
377 pub extra: HashMap<String, Value>,
378}
379
380#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
388pub struct Login {
389 pub session_id: String,
390 pub api_level: u8
391}
392
393
394#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
402pub struct ApiLevel {
403 pub level: u8,
404}
405
406
407#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
415pub struct Version {
416 pub version: String,
417}
418
419#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
427pub struct ApiError {
428 pub error: String,
429}
430
431
432
433
434#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
442pub struct LoggedIn {
443 pub status: bool,
444}
445
446#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
454pub struct Status {
455 pub status: String,
456 pub updated: Option<i64>,
457}
458
459
460#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
468pub struct Unread {
469 pub unread: u64,
470}
471
472#[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#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
496pub struct Preference {
497 pub value: Value,
498}
499
500
501#[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(Vec<Category>),
522 GetCounters(Vec<Counter>),
523}
524
525
526#[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
579pub 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
601pub 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
610pub 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
620pub 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
630pub 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
642pub 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
667pub 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
681pub 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
694pub 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
704pub 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
716pub 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
725pub 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
734pub 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
745pub 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
755pub 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
768pub 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
778pub 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
788pub 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
800pub 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
809pub 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
818pub 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
827fn 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
842pub 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
853pub 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 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 resp = resp.replacen("},[{", ",\"headlines\":[{", 1);
872 resp = resp.replacen("}]]}", "}]}]}", 1);
873 }
875
876 Ok(serde_json::from_str(&resp)?)
879 } else {
880 Err(TTRSSAPIError::InvalidRequest(format!("response status: {:?}", response.status())))
881 }
882}
883
884
885fn 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
893fn 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
905fn 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}