diff --git a/src/librssguard/gui/reusable/resizablestackedwidget.cpp b/src/librssguard/gui/reusable/resizablestackedwidget.cpp new file mode 100755 index 000000000..b018d4917 --- /dev/null +++ b/src/librssguard/gui/reusable/resizablestackedwidget.cpp @@ -0,0 +1,13 @@ +// For license of this file, see /LICENSE.md. + +#include "gui/reusable/resizablestackedwidget.h" + +ResizableStackedWidget::ResizableStackedWidget(QWidget* parent) : QStackedWidget(parent) {} + +QSize ResizableStackedWidget::sizeHint() const { + return currentWidget()->sizeHint(); +} + +QSize ResizableStackedWidget::minimumSizeHint() const { + return currentWidget()->minimumSizeHint(); +} diff --git a/src/librssguard/gui/reusable/resizablestackedwidget.h b/src/librssguard/gui/reusable/resizablestackedwidget.h new file mode 100755 index 000000000..f05860bda --- /dev/null +++ b/src/librssguard/gui/reusable/resizablestackedwidget.h @@ -0,0 +1,16 @@ +// For license of this file, see /LICENSE.md. + +#ifndef RESIZABLESTACKEDWIDGET_H +#define RESIZABLESTACKEDWIDGET_H + +#include + +class ResizableStackedWidget : public QStackedWidget { + public: + explicit ResizableStackedWidget(QWidget* parent = nullptr); + + virtual QSize sizeHint() const; + virtual QSize minimumSizeHint() const; +}; + +#endif // RESIZABLESTACKEDWIDGET_H diff --git a/src/librssguard/librssguard.pro b/src/librssguard/librssguard.pro index 7e0d57be4..89b3a7480 100644 --- a/src/librssguard/librssguard.pro +++ b/src/librssguard/librssguard.pro @@ -65,6 +65,7 @@ HEADERS += core/feeddownloader.h \ gui/notifications/singlenotificationeditor.h \ gui/reusable/baselineedit.h \ gui/reusable/progressbarwithtext.h \ + gui/reusable/resizablestackedwidget.h \ gui/settings/settingsnotifications.h \ gui/toolbars/basetoolbar.h \ gui/reusable/comboboxwithstatus.h \ @@ -251,6 +252,7 @@ SOURCES += core/feeddownloader.cpp \ gui/notifications/singlenotificationeditor.cpp \ gui/reusable/baselineedit.cpp \ gui/reusable/progressbarwithtext.cpp \ + gui/reusable/resizablestackedwidget.cpp \ gui/settings/settingsnotifications.cpp \ gui/toolbars/basetoolbar.cpp \ gui/reusable/comboboxwithstatus.cpp \ diff --git a/src/librssguard/network-web/oauth2service.cpp b/src/librssguard/network-web/oauth2service.cpp index 8f6ae9125..d37da3bbb 100644 --- a/src/librssguard/network-web/oauth2service.cpp +++ b/src/librssguard/network-web/oauth2service.cpp @@ -299,8 +299,8 @@ QString OAuth2Service::redirectUrl() const { return m_redirectionHandler->listenAddressPort(); } -void OAuth2Service::setRedirectUrl(const QString& redirect_url) { - m_redirectionHandler->setListenAddressPort(redirect_url); +void OAuth2Service::setRedirectUrl(const QString& redirect_url, bool start_handler) { + m_redirectionHandler->setListenAddressPort(redirect_url, start_handler); } QString OAuth2Service::refreshToken() const { diff --git a/src/librssguard/network-web/oauth2service.h b/src/librssguard/network-web/oauth2service.h index 4205aaf3c..06a891fa3 100644 --- a/src/librssguard/network-web/oauth2service.h +++ b/src/librssguard/network-web/oauth2service.h @@ -57,7 +57,7 @@ class OAuth2Service : public QObject { void setRefreshToken(const QString& refresh_token); QString redirectUrl() const; - void setRedirectUrl(const QString& redirect_url); + void setRedirectUrl(const QString& redirect_url, bool start_handler); QString clientId() const; void setClientId(const QString& client_id); diff --git a/src/librssguard/network-web/oauthhttphandler.cpp b/src/librssguard/network-web/oauthhttphandler.cpp index 33f9c3023..b36f8ea1e 100644 --- a/src/librssguard/network-web/oauthhttphandler.cpp +++ b/src/librssguard/network-web/oauthhttphandler.cpp @@ -29,7 +29,7 @@ bool OAuthHttpHandler::isListening() const { return m_httpServer.isListening(); } -void OAuthHttpHandler::setListenAddressPort(const QString& full_uri) { +void OAuthHttpHandler::setListenAddressPort(const QString& full_uri, bool start_handler) { QUrl url = QUrl::fromUserInput(full_uri); QHostAddress listen_address; quint16 listen_port = quint16(url.port(80)); @@ -41,7 +41,9 @@ void OAuthHttpHandler::setListenAddressPort(const QString& full_uri) { listen_address = QHostAddress(url.host()); } - if (listen_address == m_listenAddress && listen_port == m_listenPort && m_httpServer.isListening()) { + if (listen_address == m_listenAddress && + listen_port == m_listenPort && + start_handler == m_httpServer.isListening()) { // NOTE: We do not need to change listener's settings or re-start it. return; } @@ -51,6 +53,16 @@ void OAuthHttpHandler::setListenAddressPort(const QString& full_uri) { stop(); } + m_listenAddress = listen_address; + m_listenPort = listen_port; + m_listenAddressPort = full_uri; + + if (!start_handler) { + qDebugNN << LOGSEC_OAUTH + << "User does not want handler to be running."; + return; + } + if (!m_httpServer.listen(listen_address, listen_port)) { qCriticalNN << LOGSEC_OAUTH << "OAuth redirect handler FAILED TO START TO LISTEN on address" @@ -61,10 +73,6 @@ void OAuthHttpHandler::setListenAddressPort(const QString& full_uri) { << QUOTE_W_SPACE_DOT(m_httpServer.errorString()); } else { - m_listenAddress = listen_address; - m_listenPort = listen_port; - m_listenAddressPort = full_uri; - qDebugNN << LOGSEC_OAUTH << "OAuth redirect handler IS LISTENING on address" << QUOTE_W_SPACE(m_listenAddress.toString()) diff --git a/src/librssguard/network-web/oauthhttphandler.h b/src/librssguard/network-web/oauthhttphandler.h index 42a0d1cec..93cd2a417 100644 --- a/src/librssguard/network-web/oauthhttphandler.h +++ b/src/librssguard/network-web/oauthhttphandler.h @@ -30,7 +30,7 @@ class OAuthHttpHandler : public QObject { QString listenAddressPort() const; // Sets full URL string, for example "http://localhost:123456". - void setListenAddressPort(const QString& full_uri); + void setListenAddressPort(const QString& full_uri, bool start_handler); signals: void authRejected(const QString& error_description, const QString& state); diff --git a/src/librssguard/services/gmail/gmailnetworkfactory.cpp b/src/librssguard/services/gmail/gmailnetworkfactory.cpp index 04c8237ad..089b54775 100755 --- a/src/librssguard/services/gmail/gmailnetworkfactory.cpp +++ b/src/librssguard/services/gmail/gmailnetworkfactory.cpp @@ -127,7 +127,8 @@ void GmailNetworkFactory::initializeOauth() { m_oauth2->setRedirectUrl(QString(OAUTH_REDIRECT_URI) + QL1C(':') + - QString::number(GMAIL_OAUTH_REDIRECT_URI_PORT)); + QString::number(GMAIL_OAUTH_REDIRECT_URI_PORT), + true); connect(m_oauth2, &OAuth2Service::tokensRetrieveError, this, &GmailNetworkFactory::onTokensError); connect(m_oauth2, &OAuth2Service::authFailed, this, &GmailNetworkFactory::onAuthFailed); diff --git a/src/librssguard/services/gmail/gmailserviceroot.cpp b/src/librssguard/services/gmail/gmailserviceroot.cpp index aa9bdb222..32a43c48a 100644 --- a/src/librssguard/services/gmail/gmailserviceroot.cpp +++ b/src/librssguard/services/gmail/gmailserviceroot.cpp @@ -71,7 +71,7 @@ void GmailServiceRoot::setCustomDatabaseData(const QVariantHash& data) { m_network->oauth()->setClientId(data["client_id"].toString()); m_network->oauth()->setClientSecret(data["client_secret"].toString()); m_network->oauth()->setRefreshToken(data["refresh_token"].toString()); - m_network->oauth()->setRedirectUrl(data["redirect_uri"].toString()); + m_network->oauth()->setRedirectUrl(data["redirect_uri"].toString(), true); } QList GmailServiceRoot::obtainNewMessages(Feed* feed, diff --git a/src/librssguard/services/gmail/gui/formeditgmailaccount.cpp b/src/librssguard/services/gmail/gui/formeditgmailaccount.cpp index f1e80d8cb..c1a561c3f 100644 --- a/src/librssguard/services/gmail/gui/formeditgmailaccount.cpp +++ b/src/librssguard/services/gmail/gui/formeditgmailaccount.cpp @@ -29,7 +29,8 @@ void FormEditGmailAccount::apply() { account()->network()->oauth()->logout(false); account()->network()->oauth()->setClientId(m_details->m_ui.m_txtAppId->lineEdit()->text()); account()->network()->oauth()->setClientSecret(m_details->m_ui.m_txtAppKey->lineEdit()->text()); - account()->network()->oauth()->setRedirectUrl(m_details->m_ui.m_txtRedirectUrl->lineEdit()->text()); + account()->network()->oauth()->setRedirectUrl(m_details->m_ui.m_txtRedirectUrl->lineEdit()->text(), + true); account()->network()->setUsername(m_details->m_ui.m_txtUsername->lineEdit()->text()); account()->network()->setBatchSize(m_details->m_ui.m_spinLimitMessages->value()); diff --git a/src/librssguard/services/gmail/gui/gmailaccountdetails.cpp b/src/librssguard/services/gmail/gui/gmailaccountdetails.cpp index 024d3cecf..06da93b43 100644 --- a/src/librssguard/services/gmail/gui/gmailaccountdetails.cpp +++ b/src/librssguard/services/gmail/gui/gmailaccountdetails.cpp @@ -53,10 +53,10 @@ GmailAccountDetails::GmailAccountDetails(QWidget* parent) } void GmailAccountDetails::testSetup(const QNetworkProxy& custom_proxy) { - m_oauth->logout(); + m_oauth->logout(true); m_oauth->setClientId(m_ui.m_txtAppId->lineEdit()->text()); m_oauth->setClientSecret(m_ui.m_txtAppKey->lineEdit()->text()); - m_oauth->setRedirectUrl(m_ui.m_txtRedirectUrl->lineEdit()->text()); + m_oauth->setRedirectUrl(m_ui.m_txtRedirectUrl->lineEdit()->text(), true); m_lastProxy = custom_proxy; m_oauth->login(); diff --git a/src/librssguard/services/greader/definitions.h b/src/librssguard/services/greader/definitions.h index 80e69c2b4..e1fdfea28 100755 --- a/src/librssguard/services/greader/definitions.h +++ b/src/librssguard/services/greader/definitions.h @@ -4,9 +4,10 @@ #define GREADER_DEFAULT_BATCH_SIZE 100 // URLs. -#define GREADER_URL_REEDAH "https://www.reedah.com" -#define GREADER_URL_TOR "https://theoldreader.com" -#define GREADER_URL_BAZQUX "https://bazqux.com" +#define GREADER_URL_REEDAH "https://www.reedah.com" +#define GREADER_URL_TOR "https://theoldreader.com" +#define GREADER_URL_BAZQUX "https://bazqux.com" +#define GREADER_URL_INOREADER "https://www.inoreader.com" // States. #define GREADER_API_STATE_READING_LIST "state/com.google/reading-list" @@ -41,6 +42,15 @@ #define TOR_SPONSORED_STREAM_ID "tor/sponsored" #define TOR_ITEM_CONTENTS_BATCH 9999 +#define INO_HEADER_APPID "AppId" +#define INO_HEADER_APPKEY "AppKey" + +#define INO_OAUTH_REDIRECT_URI_PORT 14488 +#define INO_OAUTH_SCOPE "read write" +#define INO_OAUTH_TOKEN_URL "https://www.inoreader.com/oauth2/token" +#define INO_OAUTH_AUTH_URL "https://www.inoreader.com/oauth2/auth" +#define INO_REG_API_URL "https://www.inoreader.com/developers/register-app" + // FreshRSS. #define FRESHRSS_BASE_URL_PATH "api/greader.php/" diff --git a/src/librssguard/services/greader/greaderentrypoint.cpp b/src/librssguard/services/greader/greaderentrypoint.cpp index 498fa4c8e..8916c7d0a 100755 --- a/src/librssguard/services/greader/greaderentrypoint.cpp +++ b/src/librssguard/services/greader/greaderentrypoint.cpp @@ -2,9 +2,9 @@ #include "services/greader/greaderentrypoint.h" +#include "database/databasequeries.h" #include "definitions/definitions.h" #include "miscellaneous/application.h" -#include "database/databasequeries.h" #include "miscellaneous/iconfactory.h" #include "services/greader/definitions.h" #include "services/greader/greaderserviceroot.h" @@ -32,7 +32,7 @@ QString GreaderEntryPoint::code() const { QString GreaderEntryPoint::description() const { return QObject::tr("Google Reader API is used by many online RSS readers. This is here to support") + - QSL(" FreshRSS, Bazqux, TheOldReader, Reedah, ..."); + QSL(" Inoreader, FreshRSS, Bazqux, TheOldReader, Reedah, ..."); } QString GreaderEntryPoint::author() const { diff --git a/src/librssguard/services/greader/greadernetwork.cpp b/src/librssguard/services/greader/greadernetwork.cpp index 1ffc1892a..ef533804b 100755 --- a/src/librssguard/services/greader/greadernetwork.cpp +++ b/src/librssguard/services/greader/greadernetwork.cpp @@ -3,10 +3,12 @@ #include "services/greader/greadernetwork.h" #include "3rd-party/boolinq/boolinq.h" +#include "database/databasequeries.h" #include "exceptions/applicationexception.h" #include "exceptions/networkexception.h" #include "miscellaneous/application.h" #include "network-web/networkfactory.h" +#include "network-web/oauth2service.h" #include "network-web/webfactory.h" #include "services/abstract/category.h" #include "services/abstract/label.h" @@ -18,9 +20,12 @@ #include GreaderNetwork::GreaderNetwork(QObject* parent) - : QObject(parent), m_service(GreaderServiceRoot::Service::FreshRss), m_username(QString()), m_password(QString()), - m_baseUrl(QString()), m_batchSize(GREADER_DEFAULT_BATCH_SIZE), m_downloadOnlyUnreadMessages(false), - m_prefetchedMessages({}), m_performGlobalFetching(false), m_intelligentSynchronization(false) { + : QObject(parent), m_root(nullptr), m_service(GreaderServiceRoot::Service::FreshRss), m_username(QString()), + m_password(QString()), m_baseUrl(QString()), m_batchSize(GREADER_DEFAULT_BATCH_SIZE), m_downloadOnlyUnreadMessages(false), + m_prefetchedMessages({}), m_performGlobalFetching(false), m_intelligentSynchronization(false), + m_oauth2(new OAuth2Service(INO_OAUTH_AUTH_URL, INO_OAUTH_TOKEN_URL, + {}, {}, INO_OAUTH_SCOPE, this)) { + initializeOauth(); clearCredentials(); } @@ -819,27 +824,11 @@ void GreaderNetwork::setBaseUrl(const QString& base_url) { m_baseUrl = base_url; } -QString GreaderNetwork::serviceToString(GreaderServiceRoot::Service service) { - switch (service) { - case GreaderServiceRoot::Service::FreshRss: - return QSL("FreshRSS"); - - case GreaderServiceRoot::Service::Bazqux: - return QSL("Bazqux"); - - case GreaderServiceRoot::Service::Reedah: - return QSL("Reedah"); - - case GreaderServiceRoot::Service::TheOldReader: - return QSL("The Old Reader"); - - default: - return tr("Other services"); - } -} - QPair GreaderNetwork::authHeader() const { - return { QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), QSL("GoogleLogin auth=%1").arg(m_authAuth).toLocal8Bit() }; + QPair header = { QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), + QSL("GoogleLogin auth=%1").arg(m_authAuth).toLocal8Bit() }; + + return header; } bool GreaderNetwork::ensureLogin(const QNetworkProxy& proxy, QNetworkReply::NetworkError* output) { @@ -1053,6 +1042,69 @@ QString GreaderNetwork::generateFullUrl(GreaderNetwork::Operations operation) co } } +void GreaderNetwork::onTokensError(const QString& error, const QString& error_description) { + Q_UNUSED(error) + + qApp->showGuiMessage(Notification::Event::GeneralEvent, + tr("Inoreader: authentication error"), + tr("Click this to login again. Error is: '%1'").arg(error_description), + QSystemTrayIcon::MessageIcon::Critical, + {}, {}, + [this]() { + m_oauth2->setAccessToken(QString()); + m_oauth2->setRefreshToken(QString()); + m_oauth2->login(); + }); +} + +void GreaderNetwork::onAuthFailed() { + qApp->showGuiMessage(Notification::Event::GeneralEvent, + tr("Inoreader: authorization denied"), + tr("Click this to login again."), + QSystemTrayIcon::MessageIcon::Critical, + {}, {}, + [this]() { + m_oauth2->login(); + }); +} + +void GreaderNetwork::initializeOauth() { +#if defined(INOREADER_OFFICIAL_SUPPORT) + m_oauth2->setClientSecretId(TextFactory::decrypt(INOREADER_CLIENT_ID, OAUTH_DECRYPTION_KEY)); + m_oauth2->setClientSecretSecret(TextFactory::decrypt(INOREADER_CLIENT_SECRET, OAUTH_DECRYPTION_KEY)); +#endif + + m_oauth2->setRedirectUrl(QString(OAUTH_REDIRECT_URI) + + QL1C(':') + + QString::number(INO_OAUTH_REDIRECT_URI_PORT), + false); + + connect(m_oauth2, &OAuth2Service::tokensRetrieveError, this, &GreaderNetwork::onTokensError); + connect(m_oauth2, &OAuth2Service::authFailed, this, &GreaderNetwork::onAuthFailed); + connect(m_oauth2, &OAuth2Service::tokensRetrieved, this, [this](QString access_token, QString refresh_token, int expires_in) { + Q_UNUSED(expires_in) + Q_UNUSED(access_token) + + if (m_root != nullptr && m_root->accountId() > 0 && !refresh_token.isEmpty()) { + QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className()); + + DatabaseQueries::storeNewOauthTokens(database, refresh_token, m_root->accountId()); + } + }); +} + +OAuth2Service* GreaderNetwork::oauth() const { + return m_oauth2; +} + +void GreaderNetwork::setOauth(OAuth2Service* oauth) { + m_oauth2 = oauth; +} + +void GreaderNetwork::setRoot(GreaderServiceRoot* root) { + m_root = root; +} + bool GreaderNetwork::intelligentSynchronization() const { return m_intelligentSynchronization; } diff --git a/src/librssguard/services/greader/greadernetwork.h b/src/librssguard/services/greader/greadernetwork.h index b8125e870..af44ff797 100755 --- a/src/librssguard/services/greader/greadernetwork.h +++ b/src/librssguard/services/greader/greadernetwork.h @@ -9,6 +9,8 @@ #include "services/abstract/feed.h" #include "services/greader/greaderserviceroot.h" +class OAuth2Service; + class GreaderNetwork : public QObject { Q_OBJECT @@ -87,14 +89,21 @@ class GreaderNetwork : public QObject { void clearCredentials(); - static QString serviceToString(GreaderServiceRoot::Service service); - bool downloadOnlyUnreadMessages() const; void setDownloadOnlyUnreadMessages(bool download_only_unread); bool intelligentSynchronization() const; void setIntelligentSynchronization(bool intelligent_synchronization); + void setRoot(GreaderServiceRoot* root); + + OAuth2Service* oauth() const; + void setOauth(OAuth2Service* oauth); + + private slots: + void onTokensError(const QString& error, const QString& error_description); + void onAuthFailed(); + private: QPair authHeader() const; @@ -110,8 +119,10 @@ class GreaderNetwork : public QObject { RootItem* decodeTagsSubscriptions(const QString& categories, const QString& feeds, bool obtain_icons, const QNetworkProxy& proxy); QString sanitizedBaseUrl() const; QString generateFullUrl(Operations operation) const; + void initializeOauth(); private: + GreaderServiceRoot* m_root; GreaderServiceRoot::Service m_service; QString m_username; QString m_password; @@ -124,6 +135,7 @@ class GreaderNetwork : public QObject { QList m_prefetchedMessages; bool m_performGlobalFetching; bool m_intelligentSynchronization; + OAuth2Service* m_oauth2; }; #endif // GREADERNETWORK_H diff --git a/src/librssguard/services/greader/greaderserviceroot.cpp b/src/librssguard/services/greader/greaderserviceroot.cpp index b09490773..26ad8a2d1 100755 --- a/src/librssguard/services/greader/greaderserviceroot.cpp +++ b/src/librssguard/services/greader/greaderserviceroot.cpp @@ -9,6 +9,7 @@ #include "miscellaneous/iconfactory.h" #include "miscellaneous/mutex.h" #include "miscellaneous/textfactory.h" +#include "network-web/oauth2service.h" #include "services/abstract/importantnode.h" #include "services/abstract/recyclebin.h" #include "services/greader/greaderentrypoint.h" @@ -18,6 +19,7 @@ GreaderServiceRoot::GreaderServiceRoot(RootItem* parent) : ServiceRoot(parent), m_network(new GreaderNetwork(this)) { setIcon(GreaderEntryPoint().icon()); + m_network->setRoot(this); } bool GreaderServiceRoot::isSyncable() const { @@ -46,6 +48,13 @@ QVariantHash GreaderServiceRoot::customDatabaseData() const { data["download_only_unread"] = m_network->downloadOnlyUnreadMessages(); data["intelligent_synchronization"] = m_network->intelligentSynchronization(); + if (m_network->service() == Service::Inoreader) { + data["client_id"] = m_network->oauth()->clientId(); + data["client_secret"] = m_network->oauth()->clientSecret(); + data["refresh_token"] = m_network->oauth()->refreshToken(); + data["redirect_uri"] = m_network->oauth()->redirectUrl(); + } + return data; } @@ -57,6 +66,13 @@ void GreaderServiceRoot::setCustomDatabaseData(const QVariantHash& data) { m_network->setBatchSize(data["batch_size"].toInt()); m_network->setDownloadOnlyUnreadMessages(data["download_only_unread"].toBool()); m_network->setIntelligentSynchronization(data["intelligent_synchronization"].toBool()); + + if (m_network->service() == Service::Inoreader) { + m_network->oauth()->setClientId(data["client_id"].toString()); + m_network->oauth()->setClientSecret(data["client_secret"].toString()); + m_network->oauth()->setRefreshToken(data["refresh_token"].toString()); + m_network->oauth()->setRedirectUrl(data["redirect_uri"].toString(), true); + } } void GreaderServiceRoot::aboutToBeginFeedFetching(const QList& feeds, @@ -70,6 +86,28 @@ void GreaderServiceRoot::aboutToBeginFeedFetching(const QList& feeds, } } +QString GreaderServiceRoot::serviceToString(Service service) { + switch (service) { + case Service::FreshRss: + return QSL("FreshRSS"); + + case Service::Bazqux: + return QSL("Bazqux"); + + case Service::Reedah: + return QSL("Reedah"); + + case Service::TheOldReader: + return QSL("The Old Reader"); + + case Service::Inoreader: + return QSL("Inoreader"); + + default: + return tr("Other services"); + } +} + QList GreaderServiceRoot::obtainNewMessages(Feed* feed, const QHash& stated_messages, const QHash& tagged_messages) { @@ -109,7 +147,17 @@ void GreaderServiceRoot::start(bool freshly_activated) { updateTitleIcon(); if (getSubTreeFeeds().isEmpty()) { - syncIn(); + if (m_network->service() == Service::Inoreader) { + m_network->oauth()->login([this]() { + syncIn(); + }); + } + else { + syncIn(); + } + } + else if (m_network->service() == Service::Inoreader) { + m_network->oauth()->login(); } } @@ -199,7 +247,7 @@ ServiceRoot::LabelOperation GreaderServiceRoot::supportedLabelOperations() const void GreaderServiceRoot::updateTitleIcon() { setTitle(QString("%1 (%2)").arg(TextFactory::extractUsernameFromEmail(m_network->username()), - m_network->serviceToString(m_network->service()))); + GreaderServiceRoot::serviceToString(m_network->service()))); switch (m_network->service()) { case Service::TheOldReader: @@ -218,6 +266,10 @@ void GreaderServiceRoot::updateTitleIcon() { setIcon(qApp->icons()->miscIcon(QSL("reedah"))); break; + case Service::Inoreader: + setIcon(qApp->icons()->miscIcon(QSL("inoreader"))); + break; + default: setIcon(GreaderEntryPoint().icon()); break; diff --git a/src/librssguard/services/greader/greaderserviceroot.h b/src/librssguard/services/greader/greaderserviceroot.h index f5609f6ab..8dad76f66 100755 --- a/src/librssguard/services/greader/greaderserviceroot.h +++ b/src/librssguard/services/greader/greaderserviceroot.h @@ -17,6 +17,7 @@ class GreaderServiceRoot : public ServiceRoot, public CacheForServiceRoot { TheOldReader = 2, Bazqux = 4, Reedah = 8, + Inoreader = 16, Other = 1024 }; @@ -41,6 +42,8 @@ class GreaderServiceRoot : public ServiceRoot, public CacheForServiceRoot { GreaderNetwork* network() const; + static QString serviceToString(Service service); + protected: virtual RootItem* obtainNewTreeForSyncIn() const; diff --git a/src/librssguard/services/greader/gui/formeditgreaderaccount.cpp b/src/librssguard/services/greader/gui/formeditgreaderaccount.cpp index ba7ec1a8e..8443f8251 100755 --- a/src/librssguard/services/greader/gui/formeditgreaderaccount.cpp +++ b/src/librssguard/services/greader/gui/formeditgreaderaccount.cpp @@ -5,6 +5,7 @@ #include "gui/guiutilities.h" #include "miscellaneous/iconfactory.h" #include "network-web/networkfactory.h" +#include "network-web/oauth2service.h" #include "services/greader/definitions.h" #include "services/greader/greadernetwork.h" #include "services/greader/greaderserviceroot.h" @@ -23,20 +24,31 @@ FormEditGreaderAccount::FormEditGreaderAccount(QWidget* parent) void FormEditGreaderAccount::apply() { FormAccountDetails::apply(); - account()->network()->setBaseUrl(m_details->m_ui.m_txtUrl->lineEdit()->text()); - account()->network()->setUsername(m_details->m_ui.m_txtUsername->lineEdit()->text()); - account()->network()->setPassword(m_details->m_ui.m_txtPassword->lineEdit()->text()); - account()->network()->setBatchSize(m_details->m_ui.m_spinLimitMessages->value()); - account()->network()->setDownloadOnlyUnreadMessages(m_details->m_ui.m_cbDownloadOnlyUnreadMessages->isChecked()); - account()->network()->setService(m_details->service()); - account()->network()->setIntelligentSynchronization(m_details->m_ui.m_cbNewAlgorithm->isChecked()); + GreaderServiceRoot* existing_root = account(); - account()->saveAccountDataToDatabase(); + existing_root->network()->setBaseUrl(m_details->m_ui.m_txtUrl->lineEdit()->text()); + existing_root->network()->setUsername(m_details->m_ui.m_txtUsername->lineEdit()->text()); + existing_root->network()->setPassword(m_details->m_ui.m_txtPassword->lineEdit()->text()); + existing_root->network()->setBatchSize(m_details->m_ui.m_spinLimitMessages->value()); + existing_root->network()->setDownloadOnlyUnreadMessages(m_details->m_ui.m_cbDownloadOnlyUnreadMessages->isChecked()); + existing_root->network()->setService(m_details->service()); + existing_root->network()->setIntelligentSynchronization(m_details->m_ui.m_cbNewAlgorithm->isChecked()); + + existing_root->network()->oauth()->logout(true); + + if (existing_root->network()->service() == GreaderServiceRoot::Service::Inoreader) { + existing_root->network()->oauth()->setClientId(m_details->m_ui.m_txtAppId->lineEdit()->text()); + existing_root->network()->oauth()->setClientSecret(m_details->m_ui.m_txtAppKey->lineEdit()->text()); + existing_root->network()->oauth()->setRedirectUrl(m_details->m_ui.m_txtRedirectUrl->lineEdit()->text(), + true); + } + + existing_root->saveAccountDataToDatabase(); accept(); if (!m_creatingNew) { - account()->completelyRemoveAllData(); - account()->start(true); + existing_root->completelyRemoveAllData(); + existing_root->start(true); } } @@ -46,7 +58,15 @@ void FormEditGreaderAccount::loadAccountData() { GreaderServiceRoot* existing_root = account(); setWindowIcon(existing_root->icon()); + m_details->setService(existing_root->network()->service()); + m_details->m_oauth = existing_root->network()->oauth(); + m_details->hookNetwork(); + + m_details->m_ui.m_txtAppId->lineEdit()->setText(m_details->m_oauth->clientId()); + m_details->m_ui.m_txtAppKey->lineEdit()->setText(m_details->m_oauth->clientSecret()); + m_details->m_ui.m_txtRedirectUrl->lineEdit()->setText(m_details->m_oauth->redirectUrl()); + m_details->m_ui.m_txtUsername->lineEdit()->setText(existing_root->network()->username()); m_details->m_ui.m_txtPassword->lineEdit()->setText(existing_root->network()->password()); m_details->m_ui.m_txtUrl->lineEdit()->setText(existing_root->network()->baseUrl()); diff --git a/src/librssguard/services/greader/gui/greaderaccountdetails.cpp b/src/librssguard/services/greader/gui/greaderaccountdetails.cpp index c72893460..a5775cbaa 100755 --- a/src/librssguard/services/greader/gui/greaderaccountdetails.cpp +++ b/src/librssguard/services/greader/gui/greaderaccountdetails.cpp @@ -5,21 +5,26 @@ #include "definitions/definitions.h" #include "exceptions/applicationexception.h" #include "gui/guiutilities.h" +#include "miscellaneous/application.h" #include "miscellaneous/systemfactory.h" +#include "network-web/oauth2service.h" +#include "network-web/webfactory.h" #include "services/greader/definitions.h" #include "services/greader/greadernetwork.h" #include -GreaderAccountDetails::GreaderAccountDetails(QWidget* parent) : QWidget(parent) { +GreaderAccountDetails::GreaderAccountDetails(QWidget* parent) : QWidget(parent), + m_oauth(nullptr), m_lastProxy({}) { m_ui.setupUi(this); for (auto serv : { GreaderServiceRoot::Service::Bazqux, GreaderServiceRoot::Service::FreshRss, + GreaderServiceRoot::Service::Inoreader, GreaderServiceRoot::Service::Reedah, GreaderServiceRoot::Service::TheOldReader, GreaderServiceRoot::Service::Other }) { - m_ui.m_cmbService->addItem(GreaderNetwork::serviceToString(serv), QVariant::fromValue(serv)); + m_ui.m_cmbService->addItem(GreaderServiceRoot::serviceToString(serv), QVariant::fromValue(serv)); } m_ui.m_lblTestResult->label()->setWordWrap(true); @@ -34,19 +39,37 @@ GreaderAccountDetails::GreaderAccountDetails(QWidget* parent) : QWidget(parent) "articles faster, but if your feed contains more articles " "than specified limit, then some older articles might not be " "fetched at all.")); + GuiUtilities::setLabelAsNotice(*m_ui.m_lblLimitMessages, true); m_ui.m_lblNewAlgorithm->setText(tr("If you select intelligent synchronization, then only not-yet-fetched " "or updated articles are downloaded. Network usage is greatly reduced and " "overall synchronization speed is greatly improved.")); + GuiUtilities::setLabelAsNotice(*m_ui.m_lblNewAlgorithm, false); +#if defined(INOREADER_OFFICIAL_SUPPORT) + m_ui.m_lblInfo->setText(tr("There are some preconfigured OAuth tokens so you do not have to fill in your " + "client ID/secret, but it is strongly recommended to obtain your " + "own as preconfigured tokens have limited global usage quota. If you wish " + "to use preconfigured tokens, simply leave all above fields to their default values even " + "if they are empty.")); +#else + m_ui.m_lblInfo->setText(tr("You have to fill in your client ID/secret and also fill in correct redirect URL.")); +#endif + + GuiUtilities::setLabelAsNotice(*m_ui.m_lblInfo, true); + connect(m_ui.m_checkShowPassword, &QCheckBox::toggled, this, &GreaderAccountDetails::displayPassword); connect(m_ui.m_txtPassword->lineEdit(), &BaseLineEdit::textChanged, this, &GreaderAccountDetails::onPasswordChanged); connect(m_ui.m_txtUsername->lineEdit(), &BaseLineEdit::textChanged, this, &GreaderAccountDetails::onUsernameChanged); connect(m_ui.m_txtUrl->lineEdit(), &BaseLineEdit::textChanged, this, &GreaderAccountDetails::onUrlChanged); connect(m_ui.m_cmbService, QOverload::of(&QComboBox::currentIndexChanged), this, &GreaderAccountDetails::fillPredefinedUrl); connect(m_ui.m_cbNewAlgorithm, &QCheckBox::toggled, m_ui.m_spinLimitMessages, &MessageCountSpinBox::setDisabled); + connect(m_ui.m_txtAppId->lineEdit(), &BaseLineEdit::textChanged, this, &GreaderAccountDetails::checkOAuthValue); + connect(m_ui.m_txtAppKey->lineEdit(), &BaseLineEdit::textChanged, this, &GreaderAccountDetails::checkOAuthValue); + connect(m_ui.m_txtRedirectUrl->lineEdit(), &BaseLineEdit::textChanged, this, &GreaderAccountDetails::checkOAuthValue); + connect(m_ui.m_btnRegisterApi, &QPushButton::clicked, this, &GreaderAccountDetails::registerApi); setTabOrder(m_ui.m_cmbService, m_ui.m_txtUrl->lineEdit()); setTabOrder(m_ui.m_txtUrl->lineEdit(), m_ui.m_cbDownloadOnlyUnreadMessages); @@ -55,12 +78,83 @@ GreaderAccountDetails::GreaderAccountDetails(QWidget* parent) : QWidget(parent) setTabOrder(m_ui.m_spinLimitMessages, m_ui.m_txtUsername->lineEdit()); setTabOrder(m_ui.m_txtUsername->lineEdit(), m_ui.m_txtPassword->lineEdit()); setTabOrder(m_ui.m_txtPassword->lineEdit(), m_ui.m_checkShowPassword); - setTabOrder(m_ui.m_checkShowPassword, m_ui.m_btnTestSetup); + setTabOrder(m_ui.m_checkShowPassword, m_ui.m_txtAppId); + setTabOrder(m_ui.m_txtAppId, m_ui.m_txtAppKey); + setTabOrder(m_ui.m_txtAppKey, m_ui.m_txtRedirectUrl); + setTabOrder(m_ui.m_txtRedirectUrl, m_ui.m_btnRegisterApi); + setTabOrder(m_ui.m_btnRegisterApi, m_ui.m_btnTestSetup); onPasswordChanged(); onUsernameChanged(); onUrlChanged(); displayPassword(false); + + emit m_ui.m_txtAppId->lineEdit()->textChanged(m_ui.m_txtAppId->lineEdit()->text()); + emit m_ui.m_txtAppKey->lineEdit()->textChanged(m_ui.m_txtAppKey->lineEdit()->text()); + emit m_ui.m_txtRedirectUrl->lineEdit()->textChanged(m_ui.m_txtAppKey->lineEdit()->text()); +} + +void GreaderAccountDetails::onAuthFailed() { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error, + tr("You did not grant access."), + tr("There was error during testing.")); +} + +void GreaderAccountDetails::onAuthError(const QString& error, const QString& detailed_description) { + Q_UNUSED(error) + + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error, + tr("There is error. %1").arg(detailed_description), + tr("There was error during testing.")); +} + +void GreaderAccountDetails::onAuthGranted() { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Ok, + tr("Tested successfully. You may be prompted to login once more."), + tr("Your access was approved.")); + + try { + GreaderNetwork fac; + + fac.setOauth(m_oauth); + auto resp = fac.userInfo(m_lastProxy); + + m_ui.m_txtUsername->lineEdit()->setText(resp["userEmail"].toString()); + } + catch (const ApplicationException& ex) { + qCriticalNN << LOGSEC_GREADER + << "Failed to obtain profile with error:" + << QUOTE_W_SPACE_DOT(ex.message()); + } +} + +void GreaderAccountDetails::hookNetwork() { + if (m_oauth != nullptr) { + connect(m_oauth, &OAuth2Service::tokensRetrieved, this, &GreaderAccountDetails::onAuthGranted); + connect(m_oauth, &OAuth2Service::tokensRetrieveError, this, &GreaderAccountDetails::onAuthError); + connect(m_oauth, &OAuth2Service::authFailed, this, &GreaderAccountDetails::onAuthFailed); + } +} + +void GreaderAccountDetails::registerApi() { + qApp->web()->openUrlInExternalBrowser(INO_REG_API_URL); +} + +void GreaderAccountDetails::checkOAuthValue(const QString& value) { + auto* line_edit = qobject_cast(sender()->parent()); + + if (line_edit != nullptr) { + if (value.isEmpty()) { +#if defined(INOREADER_OFFICIAL_SUPPORT) + line_edit->setStatus(WidgetWithStatus::StatusType::Ok, tr("Preconfigured client ID/secret will be used.")); +#else + line_edit->setStatus(WidgetWithStatus::StatusType::Error, tr("Empty value is entered.")); +#endif + } + else { + line_edit->setStatus(WidgetWithStatus::StatusType::Ok, tr("Some value is entered.")); + } + } } GreaderServiceRoot::Service GreaderAccountDetails::service() const { @@ -76,25 +170,38 @@ void GreaderAccountDetails::displayPassword(bool display) { } void GreaderAccountDetails::performTest(const QNetworkProxy& custom_proxy) { - GreaderNetwork factory; + m_lastProxy = custom_proxy; - factory.setUsername(m_ui.m_txtUsername->lineEdit()->text()); - 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); - - if (result != QNetworkReply::NetworkError::NoError) { - m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error, - tr("Network error: '%1'.").arg(NetworkFactory::networkErrorText(result)), - tr("Network error, have you entered correct Nextcloud endpoint and password?")); + if (service() == GreaderServiceRoot::Service::Inoreader) { + if (m_oauth != nullptr) { + m_oauth->logout(true); + m_oauth->setClientId(m_ui.m_txtAppId->lineEdit()->text()); + m_oauth->setClientSecret(m_ui.m_txtAppKey->lineEdit()->text()); + m_oauth->setRedirectUrl(m_ui.m_txtRedirectUrl->lineEdit()->text(), true); + m_oauth->login(); + } } else { - m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Ok, - tr("You are good to go!"), - tr("Yeah.")); + GreaderNetwork factory; + + factory.setUsername(m_ui.m_txtUsername->lineEdit()->text()); + 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); + + if (result != QNetworkReply::NetworkError::NoError) { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error, + tr("Network error: '%1'.").arg(NetworkFactory::networkErrorText(result)), + tr("Network error, have you entered correct Nextcloud endpoint and password?")); + } + else { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Ok, + tr("You are good to go!"), + tr("Yeah.")); + } } } @@ -145,9 +252,17 @@ void GreaderAccountDetails::fillPredefinedUrl() { m_ui.m_txtUrl->lineEdit()->setText(QSL(GREADER_URL_TOR)); break; + case GreaderServiceRoot::Service::Inoreader: + m_ui.m_txtUrl->lineEdit()->setText(QSL(GREADER_URL_INOREADER)); + break; + default: m_ui.m_txtUrl->lineEdit()->clear(); m_ui.m_txtUrl->setFocus(); break; } + + // Show OAuth settings for Inoreader and classic for other services. + m_ui.m_stackedAuth->setCurrentIndex(service() == GreaderServiceRoot::Service::Inoreader ? 1 : 0); + m_ui.m_txtUrl->setDisabled(service() == GreaderServiceRoot::Service::Inoreader); } diff --git a/src/librssguard/services/greader/gui/greaderaccountdetails.h b/src/librssguard/services/greader/gui/greaderaccountdetails.h index 669e15d83..c0e27ad19 100755 --- a/src/librssguard/services/greader/gui/greaderaccountdetails.h +++ b/src/librssguard/services/greader/gui/greaderaccountdetails.h @@ -11,6 +11,8 @@ #include +class OAuth2Service; + class GreaderAccountDetails : public QWidget { Q_OBJECT @@ -29,9 +31,25 @@ class GreaderAccountDetails : public QWidget { void onPasswordChanged(); void onUrlChanged(); void fillPredefinedUrl(); + void checkOAuthValue(const QString& value); + void registerApi(); + void onAuthFailed(); + void onAuthError(const QString& error, const QString& detailed_description); + void onAuthGranted(); + + private: + void hookNetwork(); private: Ui::GreaderAccountDetails m_ui; + + // Testing OAuth service. This object is not ever copied + // to new living account instance, instead only its properties + // like tokens are copied. + // If editing existing account, then the pointer points + // directly to existing OAuth from the account. + OAuth2Service* m_oauth; + QNetworkProxy m_lastProxy; }; #endif // GREADERACCOUNTDETAILS_H diff --git a/src/librssguard/services/greader/gui/greaderaccountdetails.ui b/src/librssguard/services/greader/gui/greaderaccountdetails.ui index d77677c3e..137a4dc3b 100755 --- a/src/librssguard/services/greader/gui/greaderaccountdetails.ui +++ b/src/librssguard/services/greader/gui/greaderaccountdetails.ui @@ -7,7 +7,7 @@ 0 0 430 - 390 + 437 @@ -34,6 +34,27 @@ + + + + Download unread articles only + + + + + + + Intelligent synchronization algorithm + + + + + + + true + + + @@ -59,54 +80,211 @@ - - - Some feeds require authentication, including GMail feeds. BASIC, NTLM-2 and DIGEST-MD5 authentication schemes are supported. + + + + 0 + 0 + - - Authentication + + 0 - - false - - - false - - - - - - Username - - - m_txtUsername - - - - - - - Password - - - m_txtPassword - - - - - - - - - - - - - Show password - - - - + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Some feeds require authentication, including GMail feeds. BASIC, NTLM-2 and DIGEST-MD5 authentication schemes are supported. + + + Authentication + + + false + + + false + + + + + + Username + + + m_txtUsername + + + + + + + + + + Password + + + m_txtPassword + + + + + + + + + + Show password + + + + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + OAuth 2.0 settings + + + + + + App ID + + + m_txtAppId + + + + + + + + + + App key + + + m_txtAppKey + + + + + + + + + + Redirect URL + + + m_txtRedirectUrl + + + + + + + + + + + + Get my own App ID + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 0 + 1 + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + @@ -146,27 +324,6 @@ - - - - Download unread articles only - - - - - - - Intelligent synchronization algorithm - - - - - - - true - - - @@ -187,6 +344,12 @@ QSpinBox
messagecountspinbox.h
+ + ResizableStackedWidget + QStackedWidget +
resizablestackedwidget.h
+ 1 +
m_cmbService diff --git a/src/librssguard/services/inoreader/gui/formeditinoreaderaccount.cpp b/src/librssguard/services/inoreader/gui/formeditinoreaderaccount.cpp index c8f1bb10a..652a81701 100644 --- a/src/librssguard/services/inoreader/gui/formeditinoreaderaccount.cpp +++ b/src/librssguard/services/inoreader/gui/formeditinoreaderaccount.cpp @@ -30,7 +30,8 @@ void FormEditInoreaderAccount::apply() { account()->network()->oauth()->logout(false); account()->network()->oauth()->setClientId(m_details->m_ui.m_txtAppId->lineEdit()->text()); account()->network()->oauth()->setClientSecret(m_details->m_ui.m_txtAppKey->lineEdit()->text()); - account()->network()->oauth()->setRedirectUrl(m_details->m_ui.m_txtRedirectUrl->lineEdit()->text()); + account()->network()->oauth()->setRedirectUrl(m_details->m_ui.m_txtRedirectUrl->lineEdit()->text(), + true); account()->network()->setUsername(m_details->m_ui.m_txtUsername->lineEdit()->text()); account()->network()->setBatchSize(m_details->m_ui.m_spinLimitMessages->value()); diff --git a/src/librssguard/services/inoreader/gui/inoreaderaccountdetails.cpp b/src/librssguard/services/inoreader/gui/inoreaderaccountdetails.cpp index dd2aa2386..173716e5b 100755 --- a/src/librssguard/services/inoreader/gui/inoreaderaccountdetails.cpp +++ b/src/librssguard/services/inoreader/gui/inoreaderaccountdetails.cpp @@ -58,10 +58,10 @@ void InoreaderAccountDetails::testSetup(const QNetworkProxy& custom_proxy) { m_lastProxy = custom_proxy; if (m_oauth != nullptr) { - m_oauth->logout(); + m_oauth->logout(true); m_oauth->setClientId(m_ui.m_txtAppId->lineEdit()->text()); m_oauth->setClientSecret(m_ui.m_txtAppKey->lineEdit()->text()); - m_oauth->setRedirectUrl(m_ui.m_txtRedirectUrl->lineEdit()->text()); + m_oauth->setRedirectUrl(m_ui.m_txtRedirectUrl->lineEdit()->text(), true); m_oauth->login(); } } diff --git a/src/librssguard/services/inoreader/inoreadernetworkfactory.cpp b/src/librssguard/services/inoreader/inoreadernetworkfactory.cpp index 94d942b23..e06ade20a 100755 --- a/src/librssguard/services/inoreader/inoreadernetworkfactory.cpp +++ b/src/librssguard/services/inoreader/inoreadernetworkfactory.cpp @@ -61,7 +61,8 @@ void InoreaderNetworkFactory::initializeOauth() { m_oauth2->setRedirectUrl(QString(OAUTH_REDIRECT_URI) + QL1C(':') + - QString::number(INOREADER_OAUTH_REDIRECT_URI_PORT)); + QString::number(INOREADER_OAUTH_REDIRECT_URI_PORT), + true); connect(m_oauth2, &OAuth2Service::tokensRetrieveError, this, &InoreaderNetworkFactory::onTokensError); connect(m_oauth2, &OAuth2Service::authFailed, this, &InoreaderNetworkFactory::onAuthFailed); diff --git a/src/librssguard/services/inoreader/inoreaderserviceroot.cpp b/src/librssguard/services/inoreader/inoreaderserviceroot.cpp index c878c432c..0aeb1ccd8 100644 --- a/src/librssguard/services/inoreader/inoreaderserviceroot.cpp +++ b/src/librssguard/services/inoreader/inoreaderserviceroot.cpp @@ -53,7 +53,7 @@ void InoreaderServiceRoot::setCustomDatabaseData(const QVariantHash& data) { m_network->oauth()->setClientId(data["client_id"].toString()); m_network->oauth()->setClientSecret(data["client_secret"].toString()); m_network->oauth()->setRefreshToken(data["refresh_token"].toString()); - m_network->oauth()->setRedirectUrl(data["redirect_uri"].toString()); + m_network->oauth()->setRedirectUrl(data["redirect_uri"].toString(), true); } QList InoreaderServiceRoot::obtainNewMessages(Feed* feed,