diff --git a/man/mastorss.1.adoc b/man/mastorss.1.adoc index db32774..40158b3 100644 --- a/man/mastorss.1.adoc +++ b/man/mastorss.1.adoc @@ -2,7 +2,7 @@ :doctype: manpage :Author: tastytea :Email: tastytea@tastytea.de -:Date: 2019-12-25 +:Date: 2019-12-28 :Revision: 0.0.0 :man source: mastorss :man manual: General Commands Manual @@ -157,15 +157,16 @@ proxy support yet, sorry. == ERROR CODES [cols=">,<"] -|=========================================================== +|=============================================================================== | Code | Explanation | 1 | No profile specified. | 2 | Network error. | 3 | File error. | 4 | Mastodon API error. +| 5 | JSON error, most likely the file is wrongly formatted. | 9 | Unknown error. -|=========================================================== +|=============================================================================== == DEBUGGING diff --git a/src/config.cpp b/src/config.cpp index a2d9e35..54bea9d 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -74,8 +74,8 @@ std::ostream &operator <<(std::ostream &out, const ProfileData &data) return out; } -Config::Config(string profile) - :_profile{move(profile)} +Config::Config(string profile_name) + : profile{move(profile_name)} { const fs::path filename = get_filename(); BOOST_LOG_TRIVIAL(debug) << "Config filename is: " << filename; @@ -94,7 +94,7 @@ Config::Config(string profile) } } -fs::path Config::get_filename() const +fs::path Config::get_config_dir() const { char *envdir = getenv("XDG_CONFIG_HOME"); fs::path dir; @@ -122,7 +122,12 @@ fs::path Config::get_filename() const BOOST_LOG_TRIVIAL(debug) << "Created config dir: " << dir; } - return dir /= "config-" + _profile + ".json"; + return dir; +} + +fs::path Config::get_filename() const +{ + return get_config_dir() /= "config-" + profile + ".json"; } void Config::generate() @@ -131,39 +136,39 @@ void Config::generate() cout << "Instance (domain): "; getline(cin, line); - data.instance = line; + profiledata.instance = line; - data.access_token = get_access_token(line); + profiledata.access_token = get_access_token(line); cout << "URL of the feed: "; std::getline(cin, line); - data.feedurl = line; + profiledata.feedurl = line; cout << "Post only titles? [y/n]: "; std::getline(cin, line); if (line[0] == 'y') { - data.titles_only = true; + profiledata.titles_only = true; } else { - data.titles_only = false; + profiledata.titles_only = false; } cout << "Post titles as cw? [y/n]: "; std::getline(cin, line); if (line[0] == 'y') { - data.titles_as_cw = true; + profiledata.titles_as_cw = true; } else { - data.titles_as_cw = false; + profiledata.titles_as_cw = false; } cout << "Append this string to each post: "; std::getline(cin, line); - data.append = line; + profiledata.append = line; cout << "Interval between posts in seconds [30]: "; std::getline(cin, line); @@ -171,7 +176,7 @@ void Config::generate() { line = "30"; } - data.interval = static_cast(stoul(line)); + profiledata.interval = static_cast(stoul(line)); cout << "Maximum size of posts [500]: "; std::getline(cin, line); @@ -179,7 +184,7 @@ void Config::generate() { line = "500"; } - data.max_size = stoul(line); + profiledata.max_size = stoul(line); BOOST_LOG_TRIVIAL(debug) << "Generated configuration."; write(); @@ -220,48 +225,48 @@ string Config::get_access_token(const string &instance) const void Config::parse() { - data.access_token = _json[_profile]["access_token"].asString(); - data.append = _json[_profile]["append"].asString(); - data.feedurl = _json[_profile]["feedurl"].asString(); - for (const auto &fix : _json[_profile]["fixes"]) + profiledata.access_token = _json[profile]["access_token"].asString(); + profiledata.append = _json[profile]["append"].asString(); + profiledata.feedurl = _json[profile]["feedurl"].asString(); + for (const auto &fix : _json[profile]["fixes"]) { - data.fixes.push_back(fix.asString()); + profiledata.fixes.push_back(fix.asString()); } - data.instance = _json[_profile]["instance"].asString(); - if (!_json[_profile]["interval"].isNull()) + profiledata.instance = _json[profile]["instance"].asString(); + if (!_json[profile]["interval"].isNull()) { - data.interval = - static_cast(_json[_profile]["interval"].asUInt64()); + profiledata.interval = + static_cast(_json[profile]["interval"].asUInt64()); } - data.last_guid = _json[_profile]["last_guid"].asString(); - if (!_json[_profile]["max_size"].isNull()) + profiledata.last_guid = _json[profile]["last_guid"].asString(); + if (!_json[profile]["max_size"].isNull()) { - data.max_size = _json[_profile]["max_size"].asUInt64(); + profiledata.max_size = _json[profile]["max_size"].asUInt64(); } - for (const auto &skip : _json[_profile]["skip"]) + for (const auto &skip : _json[profile]["skip"]) { - data.skip.push_back(skip.asString()); + profiledata.skip.push_back(skip.asString()); } - data.titles_as_cw = _json[_profile]["titles_as_cw"].asBool(); - data.titles_only = _json[_profile]["titles_only"].asBool(); + profiledata.titles_as_cw = _json[profile]["titles_as_cw"].asBool(); + profiledata.titles_only = _json[profile]["titles_only"].asBool(); - BOOST_LOG_TRIVIAL(debug) << "Read config: " << data; + BOOST_LOG_TRIVIAL(debug) << "Read config: " << profiledata; } void Config::write() { - _json[_profile]["access_token"] = data.access_token; - _json[_profile]["append"] = data.append; - _json[_profile]["feedurl"] = data.feedurl; + _json[profile]["access_token"] = profiledata.access_token; + _json[profile]["append"] = profiledata.append; + _json[profile]["feedurl"] = profiledata.feedurl; // Leave fixes. - _json[_profile]["instance"] = data.instance; - _json[_profile]["interval"] = data.interval; - _json[_profile]["last_guid"] = data.last_guid; - _json[_profile]["max_size"] - = static_cast(data.max_size); + _json[profile]["instance"] = profiledata.instance; + _json[profile]["interval"] = profiledata.interval; + _json[profile]["last_guid"] = profiledata.last_guid; + _json[profile]["max_size"] + = static_cast(profiledata.max_size); // Leave skip. - _json[_profile]["titles_as_cw"] = data.titles_as_cw; - _json[_profile]["titles_only"] = data.titles_only; + _json[profile]["titles_as_cw"] = profiledata.titles_as_cw; + _json[profile]["titles_only"] = profiledata.titles_only; ofstream file{get_filename().c_str()}; if (file.good()) diff --git a/src/config.hpp b/src/config.hpp index 58fd99d..91d10f9 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -18,7 +18,7 @@ #define MASTORSS_CONFIG_HPP #include -#include +#include #include #include @@ -64,14 +64,16 @@ struct ProfileData class Config { public: - explicit Config(string profile); + explicit Config(string profile_name); - ProfileData data; + const string profile; + ProfileData profiledata; void write(); + [[nodiscard]] + fs::path get_config_dir() const; private: - const string _profile; Json::Value _json; [[nodiscard]] diff --git a/src/document.cpp b/src/document.cpp index e91c908..22fb81a 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -21,9 +21,12 @@ #include #include #include +#include #include #include +#include +#include #include #include #include @@ -33,8 +36,11 @@ namespace mastorss { using boost::regex; using boost::regex_replace; +using std::transform; +using std::ifstream; using std::list; using std::istringstream; +using std::stringstream; using std::string; using std::move; @@ -45,7 +51,7 @@ bool operator !=(const Item &a, const Item &b) Document::Document(Config &cfg) : _cfg{cfg} - , _profile{cfg.data} + , _profiledata{_cfg.profiledata} { RestClient::init(); @@ -70,20 +76,20 @@ void Document::download(const string &uri) case 200: { _raw_doc = response.body; - BOOST_LOG_TRIVIAL(debug) << "Downloaded feed: " << _profile.feedurl; + BOOST_LOG_TRIVIAL(debug) << "Downloaded feed: " << _profiledata.feedurl; break; } case 301: case 308: { - _profile.feedurl = extract_location(response.headers); - if (_profile.feedurl.empty()) + _profiledata.feedurl = extract_location(response.headers); + if (_profiledata.feedurl.empty()) { throw HTTPException{response.code}; } BOOST_LOG_TRIVIAL(debug) << "Feed has new location (permanent): " - << _profile.feedurl; + << _profiledata.feedurl; _cfg.write(); download(); break; @@ -99,7 +105,7 @@ void Document::download(const string &uri) } BOOST_LOG_TRIVIAL(debug) << "Feed has new location (temporary): " - << _profile.feedurl; + << _profiledata.feedurl; download(newuri); break; } @@ -116,7 +122,7 @@ void Document::download(const string &uri) void Document::download() { - download(_profile.feedurl); + download(_profiledata.feedurl); } void Document::parse() @@ -145,14 +151,14 @@ void Document::parse_rss(const pt::ptree &tree) { guid = rssitem.get("link"); } - if (guid == _profile.last_guid) + if (guid == _profiledata.last_guid) { break; } bool skipthis{false}; string title{rssitem.get("title")}; - for (const auto &skip : _profile.skip) + for (const auto &skip : _profiledata.skip) { if (title.substr(0, skip.length()) == skip) { @@ -171,11 +177,11 @@ void Document::parse_rss(const pt::ptree &tree) { string desc {remove_html(rssitem.get("description"))}; - for (const auto &fix : _profile.fixes) + for (const auto &fix : _profiledata.fixes) { desc = regex_replace(desc, regex{fix}, ""); } - return desc; + return add_hashtags(desc); }(); item.guid = move(guid); item.link = rssitem.get("link"); @@ -184,7 +190,7 @@ void Document::parse_rss(const pt::ptree &tree) BOOST_LOG_TRIVIAL(debug) << "Found GUID: " << item.guid; - if (_profile.last_guid.empty()) + if (_profiledata.last_guid.empty()) { BOOST_LOG_TRIVIAL(debug) << "This is the first run."; break; @@ -232,4 +238,45 @@ string Document::extract_location(const RestClient::HeaderFields &headers) const } return location; } + +string Document::add_hashtags(const string &text) +{ + Json::Value json; + const auto filepath = _cfg.get_config_dir() /= "watchwords.json"; + ifstream file{filepath}; + if (file.good()) + { + stringstream rawjson; + rawjson << file.rdbuf(); + rawjson >> json; + BOOST_LOG_TRIVIAL(debug) << "Read " << filepath; + } + else + { + throw FileException{"File Not found:" + + (_cfg.get_config_dir() /= "watchwords.json").string()}; + } + + list watchwords; + const auto &tags_profile = json[_cfg.profile]["tags"]; + const auto &tags_global = json["global"]["tags"]; + transform(tags_profile.begin(), tags_profile.end(), + std::back_inserter(watchwords), + [](const Json::Value &value) { return value.asString(); }); + transform(tags_global.begin(), tags_global.end(), + std::back_inserter(watchwords), + [](const Json::Value &value) { return value.asString(); }); + + string out{text}; + for (const auto &tag : watchwords) + { + regex re_tag("([[:space:][:punct:]]|^)(" + + tag + ")([[:space:][:punct:]]|$)", regex::icase); + out = regex_replace(out, re_tag, "$1#$2$3", boost::format_first_only); + } + + return out; +} + + } // namespace mastorss diff --git a/src/document.hpp b/src/document.hpp index 6dd842f..3682e2c 100644 --- a/src/document.hpp +++ b/src/document.hpp @@ -69,7 +69,7 @@ public: private: Config &_cfg; - ProfileData &_profile; + ProfileData &_profiledata; string _raw_doc; void parse_rss(const pt::ptree &tree); @@ -77,6 +77,7 @@ private: string remove_html(string html) const; [[nodiscard]] string extract_location(const RestClient::HeaderFields &headers) const; + string add_hashtags(const string &text); }; } // namespace mastorss diff --git a/src/main.cpp b/src/main.cpp index f6c4d60..fc9d433 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -34,6 +34,7 @@ constexpr int noprofile = 1; constexpr int network = 2; constexpr int file = 3; constexpr int mastodon = 4; +constexpr int json = 5; constexpr int unknown = 9; } // namespace error @@ -90,25 +91,25 @@ int main(int argc, char *argv[]) } else { - const string_view profile{args[1]}; - BOOST_LOG_TRIVIAL(debug) << "Using profile: " << profile; + const string_view profilename{args[1]}; + BOOST_LOG_TRIVIAL(debug) << "Using profile: " << profilename; try { - Config cfg{profile.data()}; + Config cfg{profilename.data()}; Document doc{cfg}; doc.parse(); - MastoAPI masto{cfg.data}; + MastoAPI masto{cfg.profiledata}; if (!doc.new_items.empty()) { for (const auto &item : doc.new_items) { masto.post_item(item); - cfg.data.last_guid = item.guid; + cfg.profiledata.last_guid = item.guid; if (item != *doc.new_items.rbegin()) { // Don't sleep if this is the last item. - sleep_for(seconds(cfg.data.interval)); + sleep_for(seconds(cfg.profiledata.interval)); } } cfg.write(); @@ -134,6 +135,11 @@ int main(int argc, char *argv[]) cerr << e.what() << '\n'; return error::network; } + catch (const Json::RuntimeError &e) + { + cerr << "JSON error:\n" << e.what() << '\n'; + return error::json; + } catch (const runtime_error &e) { cerr << e.what() << '\n';