#include "ttrssapi.h" #include #include #include #include #include "3rd-party/json.hpp" #include "curlhandle.h" #include "logger.h" #include "remoteapi.h" #include "rss/feed.h" #include "strprintf.h" #include "utils.h" using json = nlohmann::json; namespace newsboat { TtRssApi::TtRssApi(ConfigContainer& c) : RemoteApi(c) { single = (cfg.get_configvalue("ttrss-mode") == "single"); if (single) { auth_info = strprintf::fmt("%s:%s", cfg.get_configvalue("ttrss-login"), cfg.get_configvalue("ttrss-password")); } else { auth_info = ""; } sid = ""; } bool TtRssApi::authenticate() { if (auth_lock.try_lock()) { sid = retrieve_sid(); auth_lock.unlock(); } else { // wait for other thread to finish and return its result: auth_lock.lock(); auth_lock.unlock(); } return sid != ""; } std::string TtRssApi::retrieve_sid() { std::map args; Credentials cred = get_credentials("ttrss", "Tiny Tiny RSS"); if (cred.user.empty() || cred.pass.empty()) { return ""; } args["user"] = single ? "admin" : cred.user; args["password"] = cred.pass; if (single) { auth_info = strprintf::fmt("%s:%s", cred.user, cred.pass); } else { auth_info = ""; } json content = run_op("login", args); if (content.is_null()) { return ""; } std::string sid; try { sid = content["session_id"]; } catch (json::exception& e) { LOG(Level::INFO, "TtRssApi::retrieve_sid: couldn't extract session_id: " "%s", e.what()); } try { api_level = content["api_level"]; } catch (json::exception& e) { LOG(Level::INFO, "TtRssApi::retrieve_sid: couldn't determine api_level " "from response: %s", e.what()); } LOG(Level::DEBUG, "TtRssApi::retrieve_sid: sid = '%s'", sid); return sid; } unsigned int TtRssApi::query_api_level() { if (api_level == -1) { // api level was never queried. Do it now. std::map args; json content = run_op("getApiLevel", args); try { api_level = content["level"]; LOG(Level::DEBUG, "TtRssApi::query_api_level: determined level: " "%d", api_level); } catch (json::exception& e) { // From // https://git.tt-rss.org/git/tt-rss/wiki/ApiReference // "Whether tt-rss returns error for this method (e.g. // version:1.5.7 and below) client should assume API // level 0." LOG(Level::DEBUG, "TtRssApi::query_api_level: failed to " "determine " "level, assuming 0"); api_level = 0; } } return api_level; } json TtRssApi::run_op(const std::string& op, const std::map& args, bool try_login /* = true */) { CurlHandle handle; return TtRssApi::run_op(op, args, handle, try_login); } json TtRssApi::run_op(const std::string& op, const std::map& args, CurlHandle& cached_handle, bool try_login /* = true */) { std::string url = strprintf::fmt("%s/api/", cfg.get_configvalue("ttrss-url")); // First build the request payload std::string req_data; { json requestparam; requestparam["op"] = op; if (!sid.empty()) { requestparam["sid"] = sid; } // Note: We are violating the upstream-api's types here by // packing all information // into strings. If things start to break, this would be a // good place to start. for (const auto& arg : args) { requestparam[arg.first] = arg.second; } req_data = requestparam.dump(); } std::string result = utils::retrieve_url( url, cached_handle, cfg, auth_info, &req_data, utils::HTTPMethod::POST); LOG(Level::DEBUG, "TtRssApi::run_op(%s,...): post=%s reply = %s", op, req_data, result); json reply; try { reply = json::parse(result); } catch (json::parse_error& e) { LOG(Level::ERROR, "TtRssApi::run_op: reply failed to parse: %s", result); return json(nullptr); } int status; try { status = reply.at("status"); } catch (json::exception& e) { LOG(Level::ERROR, "TtRssApi::run_op: no status code: %s", e.what()); return json(nullptr); } json content; try { content = reply.at("content"); } catch (json::exception& e) { LOG(Level::ERROR, "TtRssApi::run_op: no content part in answer from " "server"); return json(nullptr); } if (status != 0) { if (content["error"] == "NOT_LOGGED_IN" && try_login) { if (authenticate()) { return run_op(op, args, cached_handle, false); } else { return json(nullptr); } } else { LOG(Level::ERROR, "TtRssApi::run_op: status: %d, error: '%s'", status, content["error"].dump()); return json(nullptr); } } return content; } TaggedFeedUrl TtRssApi::feed_from_json(const json& jfeed, const std::vector& addtags) { const int feed_id = jfeed["id"]; const std::string feed_title = jfeed["title"]; const std::string feed_url = jfeed["feed_url"]; std::vector tags; // automatically tag by feedtitle tags.push_back(std::string("~") + feed_title); // add additional tags tags.insert(tags.end(), addtags.cbegin(), addtags.cend()); auto url = strprintf::fmt("%s#%d", feed_url, feed_id); return TaggedFeedUrl(url, tags); // TODO: cache feed_id -> feed_url (or feed_url -> feed_id ?) } int TtRssApi::parse_category_id(const json& jcatid) { int cat_id; // TTRSS (commit "b0113adac42383b8039eb92ccf3ee2ec0ee70346") returns a // string for regular items and an integer for predefined categories // (like -1) if (jcatid.is_string()) { cat_id = std::stoi(jcatid.get()); } else { cat_id = jcatid; } return cat_id; } std::vector TtRssApi::get_subscribed_urls() { std::vector feeds; json categories = run_op("getCategories", std::map()); if (query_api_level() >= 2) { // getFeeds with cat_id -3 since 1.5.0, so at least since // api-level 2 std::map category_names; for (const auto& cat : categories) { std::string cat_name = cat["title"]; int cat_id = parse_category_id(cat["id"]); category_names[cat_id] = cat_name; } std::map args; // All feeds, excluding virtual feeds (e.g. Labels and such) args["cat_id"] = "-3"; json feedlist = run_op("getFeeds", args); if (feedlist.is_null()) { LOG(Level::ERROR, "TtRssApi::get_subscribed_urls: Failed to " "retrieve feedlist"); return feeds; } for (json& feed : feedlist) { const int cat_id = feed["cat_id"]; std::vector tags; if (cat_id > 0) { tags.push_back(category_names[cat_id]); } feeds.push_back(feed_from_json(feed, tags)); } } else { try { // first fetch feeds within no category fetch_feeds_per_category(json(nullptr), feeds); // then fetch the feeds of all categories for (const auto& i : categories) { fetch_feeds_per_category(i, feeds); } } catch (json::exception& e) { LOG(Level::ERROR, "TtRssApi::get_subscribed_urls:" " Failed to determine subscribed urls: %s", e.what()); return std::vector(); } } return feeds; } void TtRssApi::add_custom_headers(curl_slist** /* custom_headers */) { // nothing required } bool TtRssApi::mark_all_read(const std::string& feed_url) { std::map args; args["feed_id"] = url_to_id(feed_url); json content = run_op("catchupFeed", args); return !content.is_null(); } bool TtRssApi::mark_article_read(const std::string& guid, bool read) { // Do this in a thread, as we don't care about the result enough to wait // for it. std::thread t{[=]() { LOG(Level::DEBUG, "TtRssApi::mark_article_read: inside thread, marking " "thread as read..."); // Call the TtRssApi's update_article function as a thread. this->update_article(guid, 2, read ? 0 : 1); }}; t.detach(); return true; } bool TtRssApi::update_article_flags(const std::string& oldflags, const std::string& newflags, const std::string& guid) { std::string star_flag = cfg.get_configvalue("ttrss-flag-star"); std::string publish_flag = cfg.get_configvalue("ttrss-flag-publish"); bool success = true; if (star_flag.length() > 0) { update_flag(oldflags, newflags, star_flag[0], [&](bool added) { success = star_article(guid, added); }); } if (publish_flag.length() > 0) { update_flag(oldflags, newflags, publish_flag[0], [&](bool added) { success = publish_article(guid, added); }); } return success; } rsspp::Feed TtRssApi::fetch_feed(const std::string& id, CurlHandle& cached_handle) { rsspp::Feed f; f.rss_version = rsspp::Feed::TTRSS_JSON; std::map args; args["feed_id"] = id; args["show_content"] = "1"; args["include_attachments"] = "1"; json content = run_op("getHeadlines", args, cached_handle, true); if (content.is_null()) { return f; } if (!content.is_array()) { LOG(Level::ERROR, "TtRssApi::fetch_feed: content is not an array"); return f; } LOG(Level::DEBUG, "TtRssApi::fetch_feed: %" PRIu64 " items", static_cast(content.size())); try { for (const auto& item_obj : content) { rsspp::Item item; if (!item_obj["title"].is_null()) { item.title = item_obj["title"]; } if (!item_obj["link"].is_null()) { item.link = item_obj["link"]; } if (!item_obj["author"].is_null()) { item.author = item_obj["author"]; } if (!item_obj["content"].is_null()) { item.content_encoded = item_obj["content"]; } if (!item_obj["attachments"].is_null()) { for (const json& a : item_obj["attachments"]) { if (!a["content_url"].is_null() && !a["content_type"].is_null()) { item.enclosures.push_back( rsspp::Enclosure { a["content_url"], a["content_type"], "", "", } ); break; } } } int id = item_obj["id"]; item.guid = strprintf::fmt("%d", id); bool unread = item_obj["unread"]; if (unread) { item.labels.push_back("ttrss:unread"); } else { item.labels.push_back("ttrss:read"); } int updated_time = item_obj["updated"]; time_t updated = static_cast(updated_time); item.pubDate = utils::mt_strf_localtime( "%a, %d %b %Y %H:%M:%S %z", updated); item.pubDate_ts = updated; f.items.push_back(item); } } catch (json::exception& e) { LOG(Level::ERROR, "Exception occurred while parsing feeed: ", e.what()); } std::sort(f.items.begin(), f.items.end(), [](const rsspp::Item& a, const rsspp::Item& b) { return a.pubDate_ts > b.pubDate_ts; }); return f; } void TtRssApi::fetch_feeds_per_category(const json& cat, std::vector& feeds) { json cat_name; if (cat.is_null()) { // As uncategorized is a category itself (id = 0) and the // default value for a getFeeds is id = 0, the feeds in // uncategorized will appear twice return; } std::string cat_id; // TTRSS (commit "b0113adac42383b8039eb92ccf3ee2ec0ee70346") returns a // string for regular items and an integer for predefined categories // (like -1) if (cat["id"].is_string()) { cat_id = cat["id"]; } else { int i = cat["id"]; cat_id = std::to_string(i); } // ignore special categories, for now if (std::stoi(cat_id) < 0) { return; } cat_name = cat["title"]; LOG(Level::DEBUG, "TtRssApi::fetch_feeds_per_category: fetching id = %s title = " "%s", cat_id, cat_name.is_null() ? "" : cat_name.get()); std::map args; args["cat_id"] = cat_id; json feed_list_obj = run_op("getFeeds", args); if (feed_list_obj.is_null()) { return; } // Automatically provide the category as a tag std::vector tags; if (!cat_name.is_null()) { tags.push_back(cat_name.get()); } for (json& feed : feed_list_obj) { feeds.push_back(feed_from_json(feed, tags)); } } bool TtRssApi::star_article(const std::string& guid, bool star) { return update_article(guid, 0, star ? 1 : 0); } bool TtRssApi::publish_article(const std::string& guid, bool publish) { return update_article(guid, 1, publish ? 1 : 0); } bool TtRssApi::update_article(const std::string& guid, int field, int mode) { std::map args; args["article_ids"] = guid; args["field"] = std::to_string(field); args["mode"] = std::to_string(mode); json content = run_op("updateArticle", args); return !content.is_null(); } std::string TtRssApi::url_to_id(const std::string& url) { const std::string::size_type pound = url.find_first_of('#'); if (pound == std::string::npos) { return ""; } else { return url.substr(pound + 1); } } } // namespace newsboat