sync-in works for freshrss

This commit is contained in:
Martin Rotter 2021-01-29 10:40:32 +01:00
parent 2a1289e70a
commit 51e0f1675d
13 changed files with 313 additions and 49 deletions

View File

@ -30,7 +30,7 @@
<url type="donation">https://martinrotter.github.io/donate/</url>
<content_rating type="oars-1.1" />
<releases>
<release version="3.8.4" date="2021-01-28"/>
<release version="3.8.4" date="2021-01-29"/>
</releases>
<content_rating type="oars-1.0">
<content_attribute id="violence-cartoon">none</content_attribute>

@ -1 +1 @@
Subproject commit 9c10723bfbaf6cb85107d6ee16e0324e9e487749
Subproject commit 47f4125753452eff8800dbd6600c5a05540b15d9

View File

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

View File

@ -255,8 +255,10 @@ Assignment DatabaseQueries::getCategories(const QSqlDatabase& db, int account_id
}
template<typename T>
Assignment DatabaseQueries::getFeeds(const QSqlDatabase& db, const QList<MessageFilter*>& global_filters,
int account_id, bool* ok) {
Assignment DatabaseQueries::getFeeds(const QSqlDatabase& db,
const QList<MessageFilter*>& global_filters,
int account_id,
bool* ok) {
Assignment feeds;
// All categories are now loaded.

View File

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

View File

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

View File

@ -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<QPair<int, RootItem*>> Assignment;
typedef QPair<int, RootItem*> AssignmentItem;
typedef QPair<Message, RootItem::Importance> ImportanceChange;

View File

@ -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

View File

@ -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 <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
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<Message> 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<Message> 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<QString, RootItem*> cats;
QList<RootItem*> 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<QByteArray, QByteArray> 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;
}
}

View File

@ -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<Message> messages(ServiceRoot* root, const QString& stream_id, Feed::Status& error);
// Stream contents for a feed/label/etc.
QList<Message> 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<QByteArray, QByteArray> 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

View File

@ -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() {

View File

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

View File

@ -145,18 +145,8 @@ QList<RootItem*> 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);