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);