Merge branch 'rewrite' into main
continuous-integration/drone/push Build is passing Details

This commit is contained in:
tastytea 2019-12-28 10:35:31 +01:00
commit bca496c87a
Signed by: tastytea
GPG Key ID: CFC39497F1B26E07
6 changed files with 129 additions and 67 deletions

View File

@ -2,7 +2,7 @@
:doctype: manpage :doctype: manpage
:Author: tastytea :Author: tastytea
:Email: tastytea@tastytea.de :Email: tastytea@tastytea.de
:Date: 2019-12-25 :Date: 2019-12-28
:Revision: 0.0.0 :Revision: 0.0.0
:man source: mastorss :man source: mastorss
:man manual: General Commands Manual :man manual: General Commands Manual
@ -157,15 +157,16 @@ proxy support yet, sorry.
== ERROR CODES == ERROR CODES
[cols=">,<"] [cols=">,<"]
|=========================================================== |===============================================================================
| Code | Explanation | Code | Explanation
| 1 | No profile specified. | 1 | No profile specified.
| 2 | Network error. | 2 | Network error.
| 3 | File error. | 3 | File error.
| 4 | Mastodon API error. | 4 | Mastodon API error.
| 5 | JSON error, most likely the file is wrongly formatted.
| 9 | Unknown error. | 9 | Unknown error.
|=========================================================== |===============================================================================
== DEBUGGING == DEBUGGING

View File

@ -74,8 +74,8 @@ std::ostream &operator <<(std::ostream &out, const ProfileData &data)
return out; return out;
} }
Config::Config(string profile) Config::Config(string profile_name)
:_profile{move(profile)} : profile{move(profile_name)}
{ {
const fs::path filename = get_filename(); const fs::path filename = get_filename();
BOOST_LOG_TRIVIAL(debug) << "Config filename is: " << 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"); char *envdir = getenv("XDG_CONFIG_HOME");
fs::path dir; fs::path dir;
@ -122,7 +122,12 @@ fs::path Config::get_filename() const
BOOST_LOG_TRIVIAL(debug) << "Created config dir: " << dir; 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() void Config::generate()
@ -131,39 +136,39 @@ void Config::generate()
cout << "Instance (domain): "; cout << "Instance (domain): ";
getline(cin, line); 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: "; cout << "URL of the feed: ";
std::getline(cin, line); std::getline(cin, line);
data.feedurl = line; profiledata.feedurl = line;
cout << "Post only titles? [y/n]: "; cout << "Post only titles? [y/n]: ";
std::getline(cin, line); std::getline(cin, line);
if (line[0] == 'y') if (line[0] == 'y')
{ {
data.titles_only = true; profiledata.titles_only = true;
} }
else else
{ {
data.titles_only = false; profiledata.titles_only = false;
} }
cout << "Post titles as cw? [y/n]: "; cout << "Post titles as cw? [y/n]: ";
std::getline(cin, line); std::getline(cin, line);
if (line[0] == 'y') if (line[0] == 'y')
{ {
data.titles_as_cw = true; profiledata.titles_as_cw = true;
} }
else else
{ {
data.titles_as_cw = false; profiledata.titles_as_cw = false;
} }
cout << "Append this string to each post: "; cout << "Append this string to each post: ";
std::getline(cin, line); std::getline(cin, line);
data.append = line; profiledata.append = line;
cout << "Interval between posts in seconds [30]: "; cout << "Interval between posts in seconds [30]: ";
std::getline(cin, line); std::getline(cin, line);
@ -171,7 +176,7 @@ void Config::generate()
{ {
line = "30"; line = "30";
} }
data.interval = static_cast<uint32_t>(stoul(line)); profiledata.interval = static_cast<uint32_t>(stoul(line));
cout << "Maximum size of posts [500]: "; cout << "Maximum size of posts [500]: ";
std::getline(cin, line); std::getline(cin, line);
@ -179,7 +184,7 @@ void Config::generate()
{ {
line = "500"; line = "500";
} }
data.max_size = stoul(line); profiledata.max_size = stoul(line);
BOOST_LOG_TRIVIAL(debug) << "Generated configuration."; BOOST_LOG_TRIVIAL(debug) << "Generated configuration.";
write(); write();
@ -220,48 +225,48 @@ string Config::get_access_token(const string &instance) const
void Config::parse() void Config::parse()
{ {
data.access_token = _json[_profile]["access_token"].asString(); profiledata.access_token = _json[profile]["access_token"].asString();
data.append = _json[_profile]["append"].asString(); profiledata.append = _json[profile]["append"].asString();
data.feedurl = _json[_profile]["feedurl"].asString(); profiledata.feedurl = _json[profile]["feedurl"].asString();
for (const auto &fix : _json[_profile]["fixes"]) 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(); profiledata.instance = _json[profile]["instance"].asString();
if (!_json[_profile]["interval"].isNull()) if (!_json[profile]["interval"].isNull())
{ {
data.interval = profiledata.interval =
static_cast<uint32_t>(_json[_profile]["interval"].asUInt64()); static_cast<uint32_t>(_json[profile]["interval"].asUInt64());
} }
data.last_guid = _json[_profile]["last_guid"].asString(); profiledata.last_guid = _json[profile]["last_guid"].asString();
if (!_json[_profile]["max_size"].isNull()) 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(); profiledata.titles_as_cw = _json[profile]["titles_as_cw"].asBool();
data.titles_only = _json[_profile]["titles_only"].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() void Config::write()
{ {
_json[_profile]["access_token"] = data.access_token; _json[profile]["access_token"] = profiledata.access_token;
_json[_profile]["append"] = data.append; _json[profile]["append"] = profiledata.append;
_json[_profile]["feedurl"] = data.feedurl; _json[profile]["feedurl"] = profiledata.feedurl;
// Leave fixes. // Leave fixes.
_json[_profile]["instance"] = data.instance; _json[profile]["instance"] = profiledata.instance;
_json[_profile]["interval"] = data.interval; _json[profile]["interval"] = profiledata.interval;
_json[_profile]["last_guid"] = data.last_guid; _json[profile]["last_guid"] = profiledata.last_guid;
_json[_profile]["max_size"] _json[profile]["max_size"]
= static_cast<Json::Value::UInt64>(data.max_size); = static_cast<Json::Value::UInt64>(profiledata.max_size);
// Leave skip. // Leave skip.
_json[_profile]["titles_as_cw"] = data.titles_as_cw; _json[profile]["titles_as_cw"] = profiledata.titles_as_cw;
_json[_profile]["titles_only"] = data.titles_only; _json[profile]["titles_only"] = profiledata.titles_only;
ofstream file{get_filename().c_str()}; ofstream file{get_filename().c_str()};
if (file.good()) if (file.good())

View File

@ -18,7 +18,7 @@
#define MASTORSS_CONFIG_HPP #define MASTORSS_CONFIG_HPP
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <jsoncpp/json/json.h> #include <json/json.h>
#include <cstdint> #include <cstdint>
#include <string> #include <string>
@ -64,14 +64,16 @@ struct ProfileData
class Config class Config
{ {
public: public:
explicit Config(string profile); explicit Config(string profile_name);
ProfileData data; const string profile;
ProfileData profiledata;
void write(); void write();
[[nodiscard]]
fs::path get_config_dir() const;
private: private:
const string _profile;
Json::Value _json; Json::Value _json;
[[nodiscard]] [[nodiscard]]

View File

@ -21,9 +21,12 @@
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/property_tree/xml_parser.hpp> #include <boost/property_tree/xml_parser.hpp>
#include <boost/regex.hpp> #include <boost/regex.hpp>
#include <json/json.h>
#include <mastodon-cpp/mastodon-cpp.hpp> #include <mastodon-cpp/mastodon-cpp.hpp>
#include <restclient-cpp/connection.h> #include <restclient-cpp/connection.h>
#include <algorithm>
#include <fstream>
#include <list> #include <list>
#include <sstream> #include <sstream>
#include <string> #include <string>
@ -33,8 +36,11 @@ namespace mastorss
{ {
using boost::regex; using boost::regex;
using boost::regex_replace; using boost::regex_replace;
using std::transform;
using std::ifstream;
using std::list; using std::list;
using std::istringstream; using std::istringstream;
using std::stringstream;
using std::string; using std::string;
using std::move; using std::move;
@ -45,7 +51,7 @@ bool operator !=(const Item &a, const Item &b)
Document::Document(Config &cfg) Document::Document(Config &cfg)
: _cfg{cfg} : _cfg{cfg}
, _profile{cfg.data} , _profiledata{_cfg.profiledata}
{ {
RestClient::init(); RestClient::init();
@ -70,20 +76,20 @@ void Document::download(const string &uri)
case 200: case 200:
{ {
_raw_doc = response.body; _raw_doc = response.body;
BOOST_LOG_TRIVIAL(debug) << "Downloaded feed: " << _profile.feedurl; BOOST_LOG_TRIVIAL(debug) << "Downloaded feed: " << _profiledata.feedurl;
break; break;
} }
case 301: case 301:
case 308: case 308:
{ {
_profile.feedurl = extract_location(response.headers); _profiledata.feedurl = extract_location(response.headers);
if (_profile.feedurl.empty()) if (_profiledata.feedurl.empty())
{ {
throw HTTPException{response.code}; throw HTTPException{response.code};
} }
BOOST_LOG_TRIVIAL(debug) << "Feed has new location (permanent): " BOOST_LOG_TRIVIAL(debug) << "Feed has new location (permanent): "
<< _profile.feedurl; << _profiledata.feedurl;
_cfg.write(); _cfg.write();
download(); download();
break; break;
@ -99,7 +105,7 @@ void Document::download(const string &uri)
} }
BOOST_LOG_TRIVIAL(debug) << "Feed has new location (temporary): " BOOST_LOG_TRIVIAL(debug) << "Feed has new location (temporary): "
<< _profile.feedurl; << _profiledata.feedurl;
download(newuri); download(newuri);
break; break;
} }
@ -116,7 +122,7 @@ void Document::download(const string &uri)
void Document::download() void Document::download()
{ {
download(_profile.feedurl); download(_profiledata.feedurl);
} }
void Document::parse() void Document::parse()
@ -145,14 +151,14 @@ void Document::parse_rss(const pt::ptree &tree)
{ {
guid = rssitem.get<string>("link"); guid = rssitem.get<string>("link");
} }
if (guid == _profile.last_guid) if (guid == _profiledata.last_guid)
{ {
break; break;
} }
bool skipthis{false}; bool skipthis{false};
string title{rssitem.get<string>("title")}; string title{rssitem.get<string>("title")};
for (const auto &skip : _profile.skip) for (const auto &skip : _profiledata.skip)
{ {
if (title.substr(0, skip.length()) == skip) if (title.substr(0, skip.length()) == skip)
{ {
@ -171,11 +177,11 @@ void Document::parse_rss(const pt::ptree &tree)
{ {
string desc string desc
{remove_html(rssitem.get<string>("description"))}; {remove_html(rssitem.get<string>("description"))};
for (const auto &fix : _profile.fixes) for (const auto &fix : _profiledata.fixes)
{ {
desc = regex_replace(desc, regex{fix}, ""); desc = regex_replace(desc, regex{fix}, "");
} }
return desc; return add_hashtags(desc);
}(); }();
item.guid = move(guid); item.guid = move(guid);
item.link = rssitem.get<string>("link"); item.link = rssitem.get<string>("link");
@ -184,7 +190,7 @@ void Document::parse_rss(const pt::ptree &tree)
BOOST_LOG_TRIVIAL(debug) << "Found GUID: " << item.guid; 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."; BOOST_LOG_TRIVIAL(debug) << "This is the first run.";
break; break;
@ -232,4 +238,45 @@ string Document::extract_location(const RestClient::HeaderFields &headers) const
} }
return location; 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<string> 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 } // namespace mastorss

View File

@ -69,7 +69,7 @@ public:
private: private:
Config &_cfg; Config &_cfg;
ProfileData &_profile; ProfileData &_profiledata;
string _raw_doc; string _raw_doc;
void parse_rss(const pt::ptree &tree); void parse_rss(const pt::ptree &tree);
@ -77,6 +77,7 @@ private:
string remove_html(string html) const; string remove_html(string html) const;
[[nodiscard]] [[nodiscard]]
string extract_location(const RestClient::HeaderFields &headers) const; string extract_location(const RestClient::HeaderFields &headers) const;
string add_hashtags(const string &text);
}; };
} // namespace mastorss } // namespace mastorss

View File

@ -34,6 +34,7 @@ constexpr int noprofile = 1;
constexpr int network = 2; constexpr int network = 2;
constexpr int file = 3; constexpr int file = 3;
constexpr int mastodon = 4; constexpr int mastodon = 4;
constexpr int json = 5;
constexpr int unknown = 9; constexpr int unknown = 9;
} // namespace error } // namespace error
@ -90,25 +91,25 @@ int main(int argc, char *argv[])
} }
else else
{ {
const string_view profile{args[1]}; const string_view profilename{args[1]};
BOOST_LOG_TRIVIAL(debug) << "Using profile: " << profile; BOOST_LOG_TRIVIAL(debug) << "Using profile: " << profilename;
try try
{ {
Config cfg{profile.data()}; Config cfg{profilename.data()};
Document doc{cfg}; Document doc{cfg};
doc.parse(); doc.parse();
MastoAPI masto{cfg.data}; MastoAPI masto{cfg.profiledata};
if (!doc.new_items.empty()) if (!doc.new_items.empty())
{ {
for (const auto &item : doc.new_items) for (const auto &item : doc.new_items)
{ {
masto.post_item(item); masto.post_item(item);
cfg.data.last_guid = item.guid; cfg.profiledata.last_guid = item.guid;
if (item != *doc.new_items.rbegin()) if (item != *doc.new_items.rbegin())
{ // Don't sleep if this is the last item. { // Don't sleep if this is the last item.
sleep_for(seconds(cfg.data.interval)); sleep_for(seconds(cfg.profiledata.interval));
} }
} }
cfg.write(); cfg.write();
@ -134,6 +135,11 @@ int main(int argc, char *argv[])
cerr << e.what() << '\n'; cerr << e.what() << '\n';
return error::network; return error::network;
} }
catch (const Json::RuntimeError &e)
{
cerr << "JSON error:\n" << e.what() << '\n';
return error::json;
}
catch (const runtime_error &e) catch (const runtime_error &e)
{ {
cerr << e.what() << '\n'; cerr << e.what() << '\n';