diff --git a/resources/desktop/com.github.rssguard.appdata.xml b/resources/desktop/com.github.rssguard.appdata.xml index 3f524a6db..e8eeeb302 100644 --- a/resources/desktop/com.github.rssguard.appdata.xml +++ b/resources/desktop/com.github.rssguard.appdata.xml @@ -30,7 +30,7 @@ https://martinrotter.github.io/donate/ - + none diff --git a/resources/scripts/7za b/resources/scripts/7za index 9c10723bf..47f412575 160000 --- a/resources/scripts/7za +++ b/resources/scripts/7za @@ -1 +1 @@ -Subproject commit 9c10723bfbaf6cb85107d6ee16e0324e9e487749 +Subproject commit 47f4125753452eff8800dbd6600c5a05540b15d9 diff --git a/src/librssguard/miscellaneous/databasequeries.cpp b/src/librssguard/miscellaneous/databasequeries.cpp index 43c4cfd05..12481cadb 100755 --- a/src/librssguard/miscellaneous/databasequeries.cpp +++ b/src/librssguard/miscellaneous/databasequeries.cpp @@ -1485,8 +1485,8 @@ bool DatabaseQueries::storeAccountTree(const QSqlDatabase& db, RootItem* tree_ro query_feed.setForwardOnly(true); query_category.prepare("INSERT INTO Categories (parent_id, title, account_id, custom_id) " "VALUES (:parent_id, :title, :account_id, :custom_id);"); - query_feed.prepare("INSERT INTO Feeds (title, icon, category, protected, update_type, update_interval, account_id, custom_id) " - "VALUES (:title, :icon, :category, :protected, :update_type, :update_interval, :account_id, :custom_id);"); + query_feed.prepare("INSERT INTO Feeds (title, icon, url, category, protected, update_type, update_interval, account_id, custom_id) " + "VALUES (:title, :icon, :url, :category, :protected, :update_type, :update_interval, :account_id, :custom_id);"); // Iterate all children. for (RootItem* child : tree_root->getSubTree()) { @@ -1508,9 +1508,10 @@ bool DatabaseQueries::storeAccountTree(const QSqlDatabase& db, RootItem* tree_ro query_feed.bindValue(QSL(":title"), feed->title()); query_feed.bindValue(QSL(":icon"), qApp->icons()->toByteArray(feed->icon())); + query_feed.bindValue(QSL(":url"), feed->url()); query_feed.bindValue(QSL(":category"), feed->parent()->id()); query_feed.bindValue(QSL(":protected"), 0); - query_feed.bindValue(QSL(":update_type"), (int) feed->autoUpdateType()); + query_feed.bindValue(QSL(":update_type"), int(feed->autoUpdateType())); query_feed.bindValue(QSL(":update_interval"), feed->autoUpdateInitialInterval()); query_feed.bindValue(QSL(":account_id"), account_id); query_feed.bindValue(QSL(":custom_id"), feed->customId()); diff --git a/src/librssguard/miscellaneous/databasequeries.h b/src/librssguard/miscellaneous/databasequeries.h index 9241c86a4..5e5274d6c 100644 --- a/src/librssguard/miscellaneous/databasequeries.h +++ b/src/librssguard/miscellaneous/databasequeries.h @@ -255,8 +255,10 @@ Assignment DatabaseQueries::getCategories(const QSqlDatabase& db, int account_id } template -Assignment DatabaseQueries::getFeeds(const QSqlDatabase& db, const QList& global_filters, - int account_id, bool* ok) { +Assignment DatabaseQueries::getFeeds(const QSqlDatabase& db, + const QList& global_filters, + int account_id, + bool* ok) { Assignment feeds; // All categories are now loaded. diff --git a/src/librssguard/miscellaneous/textfactory.cpp b/src/librssguard/miscellaneous/textfactory.cpp index 7f7638f90..ddea95671 100755 --- a/src/librssguard/miscellaneous/textfactory.cpp +++ b/src/librssguard/miscellaneous/textfactory.cpp @@ -18,6 +18,19 @@ quint64 TextFactory::s_encryptionKey = 0x0; TextFactory::TextFactory() = default; +QColor TextFactory::generateColorFromText(const QString& text) { + quint32 color = 0; + + for (const QChar chr : text) { + color += chr.unicode(); + } + + color = QRandomGenerator(color).bounded(double(0xFFFFFF)) - 1; + auto color_name = QSL("#%1").arg(color, 6, 16); + + return QColor(color_name); +} + int TextFactory::stringHeight(const QString& string, const QFontMetrics& metrics) { const int count_lines = string.split(QL1C('\n')).size(); diff --git a/src/librssguard/miscellaneous/textfactory.h b/src/librssguard/miscellaneous/textfactory.h index 71771e9d1..35f32d211 100755 --- a/src/librssguard/miscellaneous/textfactory.h +++ b/src/librssguard/miscellaneous/textfactory.h @@ -15,6 +15,7 @@ class TextFactory { TextFactory(); public: + static QColor generateColorFromText(const QString& text); static int stringHeight(const QString& string, const QFontMetrics& metrics); static int stringWidth(const QString& string, const QFontMetrics& metrics); diff --git a/src/librssguard/services/abstract/serviceroot.h b/src/librssguard/services/abstract/serviceroot.h index 706f9300c..b7ee97768 100644 --- a/src/librssguard/services/abstract/serviceroot.h +++ b/src/librssguard/services/abstract/serviceroot.h @@ -19,7 +19,7 @@ class QAction; class MessagesModel; class CacheForServiceRoot; -// Car here represents ID (int, primary key) of the item. +// First item here represents ID (int, primary key) of the item. typedef QList> Assignment; typedef QPair AssignmentItem; typedef QPair ImportanceChange; diff --git a/src/librssguard/services/greader/definitions.h b/src/librssguard/services/greader/definitions.h index f47edd8f3..4bef79418 100755 --- a/src/librssguard/services/greader/definitions.h +++ b/src/librssguard/services/greader/definitions.h @@ -1,6 +1,15 @@ #ifndef GREADER_DEFINITIONS_H #define GREADER_DEFINITIONS_H -#define GREADER_UNLIMITED_BATCH_SIZE -1 +#define GREADER_UNLIMITED_BATCH_SIZE -1 + +// FreshRSS. +#define FRESHRSS_BASE_URL_PATH "api/greader.php/" + +// API. +#define GREADER_API_CLIENT_LOGIN "accounts/ClientLogin?Email=%1&Passwd=%2" +#define GREADER_API_TAG_LIST "reader/api/0/tag/list?output=json" +#define GREADER_API_SUBSCRIPTION_LIST "reader/api/0/subscription/list?output=json" +#define GREADER_API_STREAM_CONTENTS "reader/api/0/stream/contents/%1?output=json&n=%2" #endif // GREADER_DEFINITIONS_H diff --git a/src/librssguard/services/greader/greadernetwork.cpp b/src/librssguard/services/greader/greadernetwork.cpp index fc4c73cbf..d297313ff 100755 --- a/src/librssguard/services/greader/greadernetwork.cpp +++ b/src/librssguard/services/greader/greadernetwork.cpp @@ -4,14 +4,185 @@ #include "miscellaneous/application.h" #include "network-web/networkfactory.h" +#include "services/abstract/category.h" +#include "services/abstract/label.h" +#include "services/abstract/labelsnode.h" +#include "services/greader/definitions.h" +#include "services/greader/greaderfeed.h" + +#include +#include +#include GreaderNetwork::GreaderNetwork(QObject* parent) - : QObject(parent), m_service(GreaderServiceRoot::Service::FreshRss) {} + : QObject(parent), m_service(GreaderServiceRoot::Service::FreshRss), m_username(QString()), m_password(QString()), + m_baseUrl(QString()), m_batchSize(GREADER_UNLIMITED_BATCH_SIZE) { + clearCredentials(); +} + +QList GreaderNetwork::streamContents(ServiceRoot* root, const QString& stream_id, Feed::Status& error) { + QString full_url = generateFullUrl(Operations::StreamContents).arg(stream_id, batchSize()); + auto timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + + if (!ensureLogin(proxy)) { + return nullptr; + } + + QByteArray output_labels; + auto result_labels = NetworkFactory::performNetworkOperation(full_url, + timeout, + {}, + output_labels, + QNetworkAccessManager::Operation::GetOperation, + { authHeader() }, + false, + {}, + {}, + proxy); -QList GreaderNetwork::messages(ServiceRoot* root, const QString& stream_id, Feed::Status& error) { return {}; } +RootItem* GreaderNetwork::categoriesFeedsLabelsTree(bool obtain_icons, const QNetworkProxy& proxy) { + QString full_url = generateFullUrl(Operations::TagList); + auto timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + + if (!ensureLogin(proxy)) { + return nullptr; + } + + QByteArray output_labels; + auto result_labels = NetworkFactory::performNetworkOperation(full_url, + timeout, + {}, + output_labels, + QNetworkAccessManager::Operation::GetOperation, + { authHeader() }, + false, + {}, + {}, + proxy); + + if (result_labels.first != QNetworkReply::NetworkError::NoError) { + return nullptr; + } + + full_url = generateFullUrl(Operations::SubscriptionList); + QByteArray output_feeds; + auto result_feeds = NetworkFactory::performNetworkOperation(full_url, + timeout, + {}, + output_feeds, + QNetworkAccessManager::Operation::GetOperation, + { authHeader() }, + false, + {}, + {}, + proxy); + + if (result_feeds.first != QNetworkReply::NetworkError::NoError) { + return nullptr; + } + + auto root = decodeFeedCategoriesData(output_labels, output_feeds, obtain_icons); + + return root; +} + +RootItem* GreaderNetwork::decodeFeedCategoriesData(const QString& categories, const QString& feeds, bool obtain_icons) { + auto* parent = new RootItem(); + auto timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QJsonArray json = QJsonDocument::fromJson(categories.toUtf8()).object()["tags"].toArray(); + QMap cats; + QList lbls; + + cats.insert(QString(), parent); + + for (const QJsonValue& obj : json) { + auto label = obj.toObject(); + + if (label["type"].toString() == QL1S("folder")) { + QString label_id = label["id"].toString(); + + // We have label (not "state"). + auto* category = new Category(); + + category->setDescription(label["htmlUrl"].toString()); + category->setTitle(label_id.mid(label_id.lastIndexOf(QL1C('/')) + 1)); + category->setCustomId(label_id); + + cats.insert(category->customId(), category); + parent->appendChild(category); + } + else if (label["type"] == QL1S("tag")) { + QString name_id = label["id"].toString(); + QString plain_name = QRegularExpression(".+\\/([^\\/]+)").match(name_id).captured(1); + auto* new_lbl = new Label(plain_name, TextFactory::generateColorFromText(name_id)); + + new_lbl->setCustomId(name_id); + lbls.append(new_lbl); + } + } + + json = QJsonDocument::fromJson(feeds.toUtf8()).object()["subscriptions"].toArray(); + + for (const QJsonValue& obj : json) { + auto subscription = obj.toObject(); + QString id = subscription["id"].toString(); + QString title = subscription["title"].toString(); + QString url = subscription["htmlUrl"].toString(); + QString parent_label; + QJsonArray assigned_categories = subscription["categories"].toArray(); + + for (const QJsonValue& cat : assigned_categories) { + QString potential_id = cat.toObject()["id"].toString(); + + if (potential_id.contains(QSL("/label/"))) { + parent_label = potential_id; + break; + } + } + + // We have label (not "state"). + auto* feed = new GreaderFeed(); + + feed->setDescription(url); + feed->setUrl(url); + feed->setTitle(title); + feed->setCustomId(id); + + if (obtain_icons) { + QString icon_url = subscription["iconUrl"].toString(); + + if (!icon_url.isEmpty()) { + QByteArray icon_data; + + if (NetworkFactory::performNetworkOperation(icon_url, timeout, + {}, icon_data, + QNetworkAccessManager::Operation::GetOperation).first == + QNetworkReply::NetworkError::NoError) { + // Icon downloaded, set it up. + QPixmap icon_pixmap; + + icon_pixmap.loadFromData(icon_data); + feed->setIcon(QIcon(icon_pixmap)); + } + } + } + + if (cats.contains(parent_label)) { + cats[parent_label]->appendChild(feed); + } + } + + auto* lblroot = new LabelsNode(parent); + + lblroot->setChildItems(lbls); + parent->appendChild(lblroot); + + return parent; +} + QNetworkReply::NetworkError GreaderNetwork::clientLogin(const QNetworkProxy& proxy) { QString full_url = generateFullUrl(Operations::ClientLogin); auto timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); @@ -30,7 +201,38 @@ QNetworkReply::NetworkError GreaderNetwork::clientLogin(const QNetworkProxy& pro if (network_result.first == QNetworkReply::NetworkError::NoError) { // Save credentials. auto lines = QString::fromUtf8(output).replace(QSL("\r"), QString()).split('\n'); - int a = 5; + + for (const QString& line : lines) { + int eq = line.indexOf('='); + + if (eq > 0) { + QString id = line.mid(0, eq); + + if (id == QSL("SID")) { + m_authSid = line.mid(eq + 1); + } + else if (id == QSL("Auth")) { + m_authAuth = line.mid(eq + 1); + } + } + } + + QRegularExpression exp("^(unused|none|null)$"); + + if (exp.match(m_authSid).hasMatch()) { + m_authSid = QString(); + } + + if (exp.match(m_authAuth).hasMatch()) { + m_authAuth = QString(); + } + + if (m_authAuth.isEmpty() || + (service() == GreaderServiceRoot::Service::FreshRss && m_authSid.isEmpty())) { + clearCredentials(); + + return QNetworkReply::NetworkError::InternalServerError; + } } return network_result.first; @@ -84,6 +286,25 @@ QString GreaderNetwork::serviceToString(GreaderServiceRoot::Service service) { } } +QPair GreaderNetwork::authHeader() const { + return { QSL("Authorization").toLocal8Bit(), QSL("GoogleLogin auth=%1").arg(m_authAuth).toLocal8Bit() }; +} + +bool GreaderNetwork::ensureLogin(const QNetworkProxy& proxy) { + if (m_authSid.isEmpty()) { + auto login = clientLogin(proxy); + + if (login != QNetworkReply::NetworkError::NoError) { + qCriticalNN << LOGSEC_GREADER + << "Login failed with error:" + << QUOTE_W_SPACE_DOT(NetworkFactory::networkErrorText(login)); + return false; + } + } + + return true; +} + int GreaderNetwork::batchSize() const { return m_batchSize; } @@ -92,18 +313,38 @@ void GreaderNetwork::setBatchSize(int batch_size) { m_batchSize = batch_size; } +void GreaderNetwork::clearCredentials() { + m_authAuth = m_authSid = QString(); +} + QString GreaderNetwork::sanitizedBaseUrl() const { - if (m_baseUrl.endsWith('/')) { - return m_baseUrl; + auto base_url = m_baseUrl; + + if (!base_url.endsWith('/')) { + base_url = base_url + QL1C('/'); } - else { - return m_baseUrl + QL1C('/'); + + switch (m_service) { + case GreaderServiceRoot::Service::FreshRss: + base_url += FRESHRSS_BASE_URL_PATH; + break; + + default: + break; } + + return base_url; } QString GreaderNetwork::generateFullUrl(GreaderNetwork::Operations operation) const { switch (operation) { case Operations::ClientLogin: - return sanitizedBaseUrl() + QSL("accounts/ClientLogin?Email=%1&Passwd=%2").arg(username(), password()); + return sanitizedBaseUrl() + QSL(GREADER_API_CLIENT_LOGIN).arg(username(), password()); + + case Operations::TagList: + return sanitizedBaseUrl() + GREADER_API_TAG_LIST; + + case Operations::SubscriptionList: + return sanitizedBaseUrl() + GREADER_API_SUBSCRIPTION_LIST; } } diff --git a/src/librssguard/services/greader/greadernetwork.h b/src/librssguard/services/greader/greadernetwork.h index 07f2cdc01..dbbce27cc 100755 --- a/src/librssguard/services/greader/greadernetwork.h +++ b/src/librssguard/services/greader/greadernetwork.h @@ -14,18 +14,24 @@ class GreaderNetwork : public QObject { public: enum class Operations { - ClientLogin + ClientLogin, + TagList, + SubscriptionList, + StreamContents }; explicit GreaderNetwork(QObject* parent = nullptr); - // Network operations. - QList messages(ServiceRoot* root, const QString& stream_id, Feed::Status& error); + // Stream contents for a feed/label/etc. + QList streamContents(ServiceRoot* root, const QString& stream_id, Feed::Status& error); + + // Downloads and structures full tree for sync-in. + RootItem* categoriesFeedsLabelsTree(bool obtain_icons, const QNetworkProxy& proxy); // Performs client login, if successful, then saves SID, LSID and Auth. QNetworkReply::NetworkError clientLogin(const QNetworkProxy& proxy); - // Metadata. + // Getters/setters. GreaderServiceRoot::Service service() const; void setService(const GreaderServiceRoot::Service& service); @@ -38,12 +44,20 @@ class GreaderNetwork : public QObject { QString baseUrl() const; void setBaseUrl(const QString& base_url); - static QString serviceToString(GreaderServiceRoot::Service service); - int batchSize() const; void setBatchSize(int batch_size); + void clearCredentials(); + + static QString serviceToString(GreaderServiceRoot::Service service); + private: + QPair authHeader() const; + + // Make sure we are logged in and if we are not, return error. + bool ensureLogin(const QNetworkProxy& proxy); + + RootItem* decodeFeedCategoriesData(const QString& categories, const QString& feeds, bool obtain_icons); QString sanitizedBaseUrl() const; QString generateFullUrl(Operations operation) const; @@ -53,6 +67,8 @@ class GreaderNetwork : public QObject { QString m_password; QString m_baseUrl; int m_batchSize; + QString m_authSid; + QString m_authAuth; }; #endif // GREADERNETWORK_H diff --git a/src/librssguard/services/greader/greaderserviceroot.cpp b/src/librssguard/services/greader/greaderserviceroot.cpp index 825a9ac75..67a932cd3 100755 --- a/src/librssguard/services/greader/greaderserviceroot.cpp +++ b/src/librssguard/services/greader/greaderserviceroot.cpp @@ -131,16 +131,7 @@ void GreaderServiceRoot::saveAccountDataToDatabase(bool creating_new) { } RootItem* GreaderServiceRoot::obtainNewTreeForSyncIn() const { - return nullptr; - - /*OwnCloudGetFeedsCategoriesResponse feed_cats_response = m_network->feedsCategories(networkProxy()); - - if (feed_cats_response.networkError() == QNetworkReply::NetworkError::NoError) { - return feed_cats_response.feedsCategories(true); - } - else { - return nullptr; - }*/ + return m_network->categoriesFeedsLabelsTree(true, networkProxy()); } void GreaderServiceRoot::loadFromDatabase() { diff --git a/src/librssguard/services/greader/gui/greaderaccountdetails.cpp b/src/librssguard/services/greader/gui/greaderaccountdetails.cpp index 3bb3db733..f606c827a 100755 --- a/src/librssguard/services/greader/gui/greaderaccountdetails.cpp +++ b/src/librssguard/services/greader/gui/greaderaccountdetails.cpp @@ -14,14 +14,13 @@ GreaderAccountDetails::GreaderAccountDetails(QWidget* parent) : QWidget(parent) for (auto serv : { GreaderServiceRoot::Service::FreshRss, GreaderServiceRoot::Service::Bazqux, GreaderServiceRoot::Service::TheOldReader }) { - m_ui.m_cmbService->addItem(GreaderNetwork::serviceToString(serv), - QVariant::fromValue(serv)); + m_ui.m_cmbService->addItem(GreaderNetwork::serviceToString(serv), QVariant::fromValue(serv)); } m_ui.m_lblTestResult->label()->setWordWrap(true); - m_ui.m_txtPassword->lineEdit()->setPlaceholderText(tr("Password for your Nextcloud account")); - m_ui.m_txtUsername->lineEdit()->setPlaceholderText(tr("Username for your Nextcloud account")); - m_ui.m_txtUrl->lineEdit()->setPlaceholderText(tr("URL of your Nextcloud server, without any API path")); + m_ui.m_txtPassword->lineEdit()->setPlaceholderText(tr("Password for your account")); + m_ui.m_txtUsername->lineEdit()->setPlaceholderText(tr("Username for your account")); + m_ui.m_txtUrl->lineEdit()->setPlaceholderText(tr("URL of your server, without any service-specific path")); m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Information, tr("No test done yet."), tr("Here, results of connection test are shown.")); @@ -77,6 +76,7 @@ void GreaderAccountDetails::performTest(const QNetworkProxy& custom_proxy) { factory.setPassword(m_ui.m_txtPassword->lineEdit()->text()); factory.setBaseUrl(m_ui.m_txtUrl->lineEdit()->text()); factory.setService(service()); + factory.clearCredentials(); auto result = factory.clientLogin(custom_proxy); diff --git a/src/librssguard/services/inoreader/network/inoreadernetworkfactory.cpp b/src/librssguard/services/inoreader/network/inoreadernetworkfactory.cpp index 597fc3da9..fa27e4e60 100644 --- a/src/librssguard/services/inoreader/network/inoreadernetworkfactory.cpp +++ b/src/librssguard/services/inoreader/network/inoreadernetworkfactory.cpp @@ -145,18 +145,8 @@ QList InoreaderNetworkFactory::getLabels() { if (lbl_obj["type"] == QL1S("tag")) { QString name_id = lbl_obj["id"].toString(); - QString id = QRegularExpression("user\\/(\\d+)\\/").match(name_id).captured(1); QString plain_name = QRegularExpression(".+\\/([^\\/]+)").match(name_id).captured(1); - quint32 color = 0; - - for (const QChar chr : name_id) { - color += chr.unicode(); - } - - color = QRandomGenerator(color).bounded(double(0xFFFFFF)) - 1; - - auto color_name = QSL("#%1").arg(color, 6, 16); - auto* new_lbl = new Label(plain_name, QColor(color_name)); + auto* new_lbl = new Label(plain_name, TextFactory::generateColorFromText(name_id)); new_lbl->setCustomId(name_id); lbls.append(new_lbl);