1 #include "ttrssapi.h"
2
3 #include <algorithm>
4 #include <cinttypes>
5 #include <cstring>
6 #include <thread>
7 #include <time.h>
8
9 #include "3rd-party/json.hpp"
10 #include "logger.h"
11 #include "remoteapi.h"
12 #include "rss/feed.h"
13 #include "strprintf.h"
14 #include "utils.h"
15
16 using json = nlohmann::json;
17
18 namespace newsboat {
19
TtRssApi(ConfigContainer * c)20 TtRssApi::TtRssApi(ConfigContainer* c)
21 : RemoteApi(c)
22 {
23 single = (cfg->get_configvalue("ttrss-mode") == "single");
24 if (single) {
25 auth_info = strprintf::fmt("%s:%s",
26 cfg->get_configvalue("ttrss-login"),
27 cfg->get_configvalue("ttrss-password"));
28 } else {
29 auth_info = "";
30 }
31 sid = "";
32 }
33
~TtRssApi()34 TtRssApi::~TtRssApi() {}
35
authenticate()36 bool TtRssApi::authenticate()
37 {
38 if (auth_lock.try_lock()) {
39 sid = retrieve_sid();
40 auth_lock.unlock();
41 } else {
42 // wait for other thread to finish and return its result:
43 auth_lock.lock();
44 auth_lock.unlock();
45 }
46
47 return sid != "";
48 }
49
retrieve_sid()50 std::string TtRssApi::retrieve_sid()
51 {
52 std::map<std::string, std::string> args;
53
54 Credentials cred = get_credentials("ttrss", "Tiny Tiny RSS");
55 if (cred.user.empty() || cred.pass.empty()) {
56 return "";
57 }
58
59 args["user"] = single ? "admin" : cred.user.c_str();
60 args["password"] = cred.pass.c_str();
61 if (single) {
62 auth_info = strprintf::fmt("%s:%s", cred.user, cred.pass);
63 } else {
64 auth_info = "";
65 }
66 json content = run_op("login", args);
67
68 if (content.is_null()) {
69 return "";
70 }
71
72 std::string sid;
73 try {
74 sid = content["session_id"];
75 } catch (json::exception& e) {
76 LOG(Level::INFO,
77 "TtRssApi::retrieve_sid: couldn't extract session_id: "
78 "%s",
79 e.what());
80 }
81
82 try {
83 api_level = content["api_level"];
84 } catch (json::exception& e) {
85 LOG(Level::INFO,
86 "TtRssApi::retrieve_sid: couldn't determine api_level "
87 "from response: %s",
88 e.what());
89 }
90
91 LOG(Level::DEBUG, "TtRssApi::retrieve_sid: sid = '%s'", sid);
92
93 return sid;
94 }
95
query_api_level()96 unsigned int TtRssApi::query_api_level()
97 {
98 if (api_level == -1) {
99 // api level was never queried. Do it now.
100 std::map<std::string, std::string> args;
101 json content = run_op("getApiLevel", args);
102
103 try {
104 api_level = content["level"];
105 LOG(Level::DEBUG,
106 "TtRssApi::query_api_level: determined level: "
107 "%d",
108 api_level);
109 } catch (json::exception& e) {
110 // From
111 // https://git.tt-rss.org/git/tt-rss/wiki/ApiReference
112 // "Whether tt-rss returns error for this method (e.g.
113 // version:1.5.7 and below) client should assume API
114 // level 0."
115 LOG(Level::DEBUG,
116 "TtRssApi::query_api_level: failed to "
117 "determine "
118 "level, assuming 0");
119 api_level = 0;
120 }
121 }
122 return api_level;
123 }
124
run_op(const std::string & op,const std::map<std::string,std::string> & args,bool try_login,CURL * cached_handle)125 json TtRssApi::run_op(const std::string& op,
126 const std::map<std::string, std::string>& args,
127 bool try_login, /* = true */
128 CURL* cached_handle /* = nullptr */)
129 {
130 std::string url =
131 strprintf::fmt("%s/api/", cfg->get_configvalue("ttrss-url"));
132
133 // First build the request payload
134 std::string req_data;
135 {
136 json requestparam;
137
138 requestparam["op"] = op;
139 if (!sid.empty()) {
140 requestparam["sid"] = sid;
141 }
142
143 // Note: We are violating the upstream-api's types here by
144 // packing all information
145 // into strings. If things start to break, this would be a
146 // good place to start.
147 for (const auto& arg : args) {
148 requestparam[arg.first] = arg.second;
149 }
150
151 req_data = requestparam.dump();
152 }
153
154 std::string result = utils::retrieve_url(
155 url, cfg, auth_info, &req_data, utils::HTTPMethod::POST, cached_handle);
156
157 LOG(Level::DEBUG,
158 "TtRssApi::run_op(%s,...): post=%s reply = %s",
159 op,
160 req_data,
161 result);
162
163 json reply;
164 try {
165 reply = json::parse(result);
166 } catch (json::parse_error& e) {
167 LOG(Level::ERROR,
168 "TtRssApi::run_op: reply failed to parse: %s",
169 result);
170 return json(nullptr);
171 }
172
173 int status;
174 try {
175 status = reply.at("status");
176 } catch (json::exception& e) {
177 LOG(Level::ERROR,
178 "TtRssApi::run_op: no status code: %s",
179 e.what());
180 return json(nullptr);
181 }
182
183 json content;
184 try {
185 content = reply.at("content");
186 } catch (json::exception& e) {
187 LOG(Level::ERROR,
188 "TtRssApi::run_op: no content part in answer from "
189 "server");
190 return json(nullptr);
191 }
192
193 if (status != 0) {
194 if (content["error"] == "NOT_LOGGED_IN" && try_login) {
195 if (authenticate()) {
196 return run_op(op, args, false, cached_handle);
197 } else {
198 return json(nullptr);
199 }
200 } else {
201 LOG(Level::ERROR,
202 "TtRssApi::run_op: status: %d, error: '%s'",
203 status,
204 content["error"].dump());
205 return json(nullptr);
206 }
207 }
208
209 return content;
210 }
211
feed_from_json(const json & jfeed,const std::vector<std::string> & addtags)212 TaggedFeedUrl TtRssApi::feed_from_json(const json& jfeed,
213 const std::vector<std::string>& addtags)
214 {
215 const int feed_id = jfeed["id"];
216 const std::string feed_title = jfeed["title"];
217 const std::string feed_url = jfeed["feed_url"];
218
219 std::vector<std::string> tags;
220 // automatically tag by feedtitle
221 tags.push_back(std::string("~") + feed_title);
222
223 // add additional tags
224 tags.insert(tags.end(), addtags.cbegin(), addtags.cend());
225
226 auto url = strprintf::fmt("%s#%d", feed_url, feed_id);
227 return TaggedFeedUrl(url, tags);
228 // TODO: cache feed_id -> feed_url (or feed_url -> feed_id ?)
229 }
230
parse_category_id(const json & jcatid)231 int TtRssApi::parse_category_id(const json& jcatid)
232 {
233 int cat_id;
234 // TTRSS (commit "b0113adac42383b8039eb92ccf3ee2ec0ee70346") returns a
235 // string for regular items and an integer for predefined categories
236 // (like -1)
237 if (jcatid.is_string()) {
238 cat_id = std::stoi(jcatid.get<std::string>());
239 } else {
240 cat_id = jcatid;
241 }
242 return cat_id;
243 }
244
get_subscribed_urls()245 std::vector<TaggedFeedUrl> TtRssApi::get_subscribed_urls()
246 {
247 std::vector<TaggedFeedUrl> feeds;
248
249 json categories =
250 run_op("getCategories", std::map<std::string, std::string>());
251
252 if (query_api_level() >= 2) {
253 // getFeeds with cat_id -3 since 1.5.0, so at least since
254 // api-level 2
255 std::map<int, std::string> category_names;
256 for (const auto& cat : categories) {
257 std::string cat_name = cat["title"];
258 int cat_id = parse_category_id(cat["id"]);
259 category_names[cat_id] = cat_name;
260 }
261
262 std::map<std::string, std::string> args;
263 // All feeds, excluding virtual feeds (e.g. Labels and such)
264 args["cat_id"] = "-3";
265 json feedlist = run_op("getFeeds", args);
266
267 if (feedlist.is_null()) {
268 LOG(Level::ERROR,
269 "TtRssApi::get_subscribed_urls: Failed to "
270 "retrieve feedlist");
271 return feeds;
272 }
273
274 for (json& feed : feedlist) {
275 const int cat_id = feed["cat_id"];
276 std::vector<std::string> tags;
277 if (cat_id > 0) {
278 tags.push_back(category_names[cat_id]);
279 }
280 feeds.push_back(feed_from_json(feed, tags));
281 }
282
283 } else {
284 try {
285 // first fetch feeds within no category
286 fetch_feeds_per_category(json(nullptr), feeds);
287
288 // then fetch the feeds of all categories
289 for (const auto& i : categories) {
290 fetch_feeds_per_category(i, feeds);
291 }
292 } catch (json::exception& e) {
293 LOG(Level::ERROR,
294 "TtRssApi::get_subscribed_urls:"
295 " Failed to determine subscribed urls: %s",
296 e.what());
297 return std::vector<TaggedFeedUrl>();
298 }
299 }
300
301 return feeds;
302 }
303
add_custom_headers(curl_slist **)304 void TtRssApi::add_custom_headers(curl_slist** /* custom_headers */)
305 {
306 // nothing required
307 }
308
mark_all_read(const std::string & feed_url)309 bool TtRssApi::mark_all_read(const std::string& feed_url)
310 {
311 std::map<std::string, std::string> args;
312 args["feed_id"] = url_to_id(feed_url);
313 json content = run_op("catchupFeed", args);
314
315 if (content.is_null()) {
316 return false;
317 }
318
319 return true;
320 }
321
mark_article_read(const std::string & guid,bool read)322 bool TtRssApi::mark_article_read(const std::string& guid, bool read)
323 {
324 // Do this in a thread, as we don't care about the result enough to wait
325 // for it.
326 std::thread t{[=]()
327 {
328 LOG(Level::DEBUG,
329 "TtRssApi::mark_article_read: inside thread, marking "
330 "thread as read...");
331
332 // Call the TtRssApi's update_article function as a thread.
333 this->update_article(guid, 2, read ? 0 : 1);
334 }};
335 t.detach();
336 return true;
337 }
338
update_article_flags(const std::string & oldflags,const std::string & newflags,const std::string & guid)339 bool TtRssApi::update_article_flags(const std::string& oldflags,
340 const std::string& newflags,
341 const std::string& guid)
342 {
343 std::string star_flag = cfg->get_configvalue("ttrss-flag-star");
344 std::string publish_flag = cfg->get_configvalue("ttrss-flag-publish");
345 bool success = true;
346
347 if (star_flag.length() > 0) {
348 if (strchr(oldflags.c_str(), star_flag[0]) == nullptr &&
349 strchr(newflags.c_str(), star_flag[0]) != nullptr) {
350 success = star_article(guid, true);
351 } else if (strchr(oldflags.c_str(), star_flag[0]) != nullptr &&
352 strchr(newflags.c_str(), star_flag[0]) == nullptr) {
353 success = star_article(guid, false);
354 }
355 }
356
357 if (publish_flag.length() > 0) {
358 if (strchr(oldflags.c_str(), publish_flag[0]) == nullptr &&
359 strchr(newflags.c_str(), publish_flag[0]) != nullptr) {
360 success = publish_article(guid, true);
361 } else if (strchr(oldflags.c_str(), publish_flag[0]) !=
362 nullptr &&
363 strchr(newflags.c_str(), publish_flag[0]) == nullptr) {
364 success = publish_article(guid, false);
365 }
366 }
367
368 return success;
369 }
370
fetch_feed(const std::string & id,CURL * cached_handle)371 rsspp::Feed TtRssApi::fetch_feed(const std::string& id, CURL* cached_handle)
372 {
373 rsspp::Feed f;
374
375 f.rss_version = rsspp::Feed::TTRSS_JSON;
376
377 std::map<std::string, std::string> args;
378 args["feed_id"] = id;
379 args["show_content"] = "1";
380 args["include_attachments"] = "1";
381 json content = run_op("getHeadlines", args, true, cached_handle);
382
383 if (content.is_null()) {
384 return f;
385 }
386
387 if (!content.is_array()) {
388 LOG(Level::ERROR,
389 "TtRssApi::fetch_feed: content is not an array");
390 return f;
391 }
392
393 LOG(Level::DEBUG,
394 "TtRssApi::fetch_feed: %" PRIu64 " items",
395 static_cast<uint64_t>(content.size()));
396
397 try {
398 for (const auto& item_obj : content) {
399 rsspp::Item item;
400
401 if (!item_obj["title"].is_null()) {
402 item.title = item_obj["title"];
403 }
404
405 if (!item_obj["link"].is_null()) {
406 item.link = item_obj["link"];
407 }
408
409 if (!item_obj["author"].is_null()) {
410 item.author = item_obj["author"];
411 }
412
413 if (!item_obj["content"].is_null()) {
414 item.content_encoded = item_obj["content"];
415 }
416
417 if (!item_obj["attachments"].is_null()) {
418 for (const json& a : item_obj["attachments"]) {
419 if (!a["content_url"].is_null() && !a["content_type"].is_null()
420 && newsboat::utils::is_valid_podcast_type(a["content_type"])) {
421 item.enclosure_type =
422 a["content_type"];
423 item.enclosure_url = a["content_url"];
424 break;
425 }
426 }
427 }
428
429 int id = item_obj["id"];
430 item.guid = strprintf::fmt("%d", id);
431
432 bool unread = item_obj["unread"];
433 if (unread) {
434 item.labels.push_back("ttrss:unread");
435 } else {
436 item.labels.push_back("ttrss:read");
437 }
438
439 int updated_time = item_obj["updated"];
440 time_t updated = static_cast<time_t>(updated_time);
441
442 item.pubDate = utils::mt_strf_localtime(
443 "%a, %d %b %Y %H:%M:%S %z",
444 updated);
445 item.pubDate_ts = updated;
446
447 f.items.push_back(item);
448 }
449 } catch (json::exception& e) {
450 LOG(Level::ERROR,
451 "Exception occurred while parsing feeed: ",
452 e.what());
453 }
454
455 std::sort(f.items.begin(),
456 f.items.end(),
457 [](const rsspp::Item& a, const rsspp::Item& b) {
458 return a.pubDate_ts > b.pubDate_ts;
459 });
460
461 return f;
462 }
463
fetch_feeds_per_category(const json & cat,std::vector<TaggedFeedUrl> & feeds)464 void TtRssApi::fetch_feeds_per_category(const json& cat,
465 std::vector<TaggedFeedUrl>& feeds)
466 {
467 json cat_name;
468
469 if (cat.is_null()) {
470 // As uncategorized is a category itself (id = 0) and the
471 // default value for a getFeeds is id = 0, the feeds in
472 // uncategorized will appear twice
473 return;
474 }
475
476 std::string cat_id;
477 // TTRSS (commit "b0113adac42383b8039eb92ccf3ee2ec0ee70346") returns a
478 // string for regular items and an integer for predefined categories
479 // (like -1)
480 if (cat["id"].is_string()) {
481 cat_id = cat["id"];
482 } else {
483 int i = cat["id"];
484 cat_id = std::to_string(i);
485 }
486
487 // ignore special categories, for now
488 if (std::stoi(cat_id) < 0) {
489 return;
490 }
491
492 cat_name = cat["title"];
493 LOG(Level::DEBUG,
494 "TtRssApi::fetch_feeds_per_category: fetching id = %s title = "
495 "%s",
496 cat_id,
497 cat_name.is_null() ? "<null>" : cat_name.get<std::string>());
498
499 std::map<std::string, std::string> args;
500 args["cat_id"] = cat_id;
501
502 json feed_list_obj = run_op("getFeeds", args);
503
504 if (feed_list_obj.is_null()) {
505 return;
506 }
507
508 // Automatically provide the category as a tag
509 std::vector<std::string> tags;
510 if (!cat_name.is_null()) {
511 tags.push_back(cat_name.get<std::string>());
512 }
513
514 for (json& feed : feed_list_obj) {
515 feeds.push_back(feed_from_json(feed, tags));
516 }
517 }
518
star_article(const std::string & guid,bool star)519 bool TtRssApi::star_article(const std::string& guid, bool star)
520 {
521 return update_article(guid, 0, star ? 1 : 0);
522 }
523
publish_article(const std::string & guid,bool publish)524 bool TtRssApi::publish_article(const std::string& guid, bool publish)
525 {
526 return update_article(guid, 1, publish ? 1 : 0);
527 }
528
update_article(const std::string & guid,int field,int mode)529 bool TtRssApi::update_article(const std::string& guid, int field, int mode)
530 {
531 std::map<std::string, std::string> args;
532 args["article_ids"] = guid;
533 args["field"] = std::to_string(field);
534 args["mode"] = std::to_string(mode);
535 json content = run_op("updateArticle", args);
536
537 if (content.is_null()) {
538 return false;
539 }
540
541 return true;
542 }
543
url_to_id(const std::string & url)544 std::string TtRssApi::url_to_id(const std::string& url)
545 {
546 const std::string::size_type pound = url.find_first_of('#');
547 if (pound == std::string::npos) {
548 return "";
549 } else {
550 return url.substr(pound + 1);
551 }
552 }
553
554 } // namespace newsboat
555