diff --git a/resources/graphics/misc/reddit.png b/resources/graphics/misc/reddit.png new file mode 100755 index 000000000..aa75b0cd2 Binary files /dev/null and b/resources/graphics/misc/reddit.png differ diff --git a/resources/rssguard.qrc b/resources/rssguard.qrc index 29b6e4ff5..183960aa3 100644 --- a/resources/rssguard.qrc +++ b/resources/rssguard.qrc @@ -34,6 +34,7 @@ graphics/misc/image-placeholder.png graphics/misc/inoreader.png graphics/misc/nextcloud.png + graphics/misc/reddit.png graphics/misc/reedah.png graphics/misc/theoldreader.png graphics/misc/tt-rss.png diff --git a/src/librssguard/definitions/definitions.h b/src/librssguard/definitions/definitions.h index b11222b44..c50f2250b 100644 --- a/src/librssguard/definitions/definitions.h +++ b/src/librssguard/definitions/definitions.h @@ -13,6 +13,7 @@ #define SERVICE_CODE_FEEDLY "feedly" #define SERVICE_CODE_INOREADER "inoreader" #define SERVICE_CODE_GMAIL "gmail" +#define SERVICE_CODE_REDDIT "reddit" #define ADBLOCK_SERVER_PORT 48484 #define ADBLOCK_HOWTO "https://github.com/martinrotter/rssguard/blob/master/resources/docs/Documentation.md#adblock" @@ -132,6 +133,7 @@ #define LOGSEC_TTRSS "tt-rss: " #define LOGSEC_GMAIL "gmail: " #define LOGSEC_OAUTH "oauth: " +#define LOGSEC_REDDIT "reddit: " #define MAX_ZOOM_FACTOR 5.0f #define MIN_ZOOM_FACTOR 0.25f diff --git a/src/librssguard/librssguard.pro b/src/librssguard/librssguard.pro index c7be264fb..0048fd149 100644 --- a/src/librssguard/librssguard.pro +++ b/src/librssguard/librssguard.pro @@ -192,6 +192,12 @@ HEADERS += core/feeddownloader.h \ services/owncloud/owncloudnetworkfactory.h \ services/owncloud/owncloudserviceentrypoint.h \ services/owncloud/owncloudserviceroot.h \ + services/reddit/definitions.h \ + services/reddit/gui/formeditredditaccount.h \ + services/reddit/gui/redditaccountdetails.h \ + services/reddit/redditentrypoint.h \ + services/reddit/redditnetworkfactory.h \ + services/reddit/redditserviceroot.h \ services/standard/atomparser.h \ services/standard/definitions.h \ services/standard/feedparser.h \ @@ -368,6 +374,11 @@ SOURCES += core/feeddownloader.cpp \ services/owncloud/owncloudnetworkfactory.cpp \ services/owncloud/owncloudserviceentrypoint.cpp \ services/owncloud/owncloudserviceroot.cpp \ + services/reddit/gui/formeditredditaccount.cpp \ + services/reddit/gui/redditaccountdetails.cpp \ + services/reddit/redditentrypoint.cpp \ + services/reddit/redditnetworkfactory.cpp \ + services/reddit/redditserviceroot.cpp \ services/standard/atomparser.cpp \ services/standard/feedparser.cpp \ services/standard/gui/formeditstandardaccount.cpp \ @@ -432,6 +443,7 @@ FORMS += gui/dialogs/formabout.ui \ services/gmail/gui/gmailaccountdetails.ui \ services/greader/gui/greaderaccountdetails.ui \ services/owncloud/gui/owncloudaccountdetails.ui \ + services/reddit/gui/redditaccountdetails.ui \ services/standard/gui/formstandardimportexport.ui \ services/standard/gui/standardfeeddetails.ui \ services/tt-rss/gui/ttrssaccountdetails.ui \ diff --git a/src/librssguard/miscellaneous/feedreader.cpp b/src/librssguard/miscellaneous/feedreader.cpp index cad1a441a..728374251 100644 --- a/src/librssguard/miscellaneous/feedreader.cpp +++ b/src/librssguard/miscellaneous/feedreader.cpp @@ -18,6 +18,7 @@ #include "services/gmail/gmailentrypoint.h" #include "services/greader/greaderentrypoint.h" #include "services/owncloud/owncloudserviceentrypoint.h" +#include "services/reddit/redditentrypoint.h" #include "services/standard/standardserviceentrypoint.h" #include "services/tt-rss/ttrssserviceentrypoint.h" @@ -60,6 +61,11 @@ QList FeedReader::feedServices() { m_feedServices.append(new GmailEntryPoint()); m_feedServices.append(new GreaderEntryPoint()); m_feedServices.append(new OwnCloudServiceEntryPoint()); + +#if defined(DEBUG) + m_feedServices.append(new RedditEntryPoint()); +#endif + m_feedServices.append(new StandardServiceEntryPoint()); m_feedServices.append(new TtRssServiceEntryPoint()); } diff --git a/src/librssguard/services/reddit/definitions.h b/src/librssguard/services/reddit/definitions.h new file mode 100755 index 000000000..a8cc677b0 --- /dev/null +++ b/src/librssguard/services/reddit/definitions.h @@ -0,0 +1,21 @@ +// For license of this file, see /LICENSE.md. + +#ifndef REDDIT_DEFINITIONS_H +#define REDDIT_DEFINITIONS_H + +#define REDDIT_OAUTH_REDIRECT_URI_PORT 14499 +#define REDDIT_OAUTH_AUTH_URL "https://www.reddit.com/api/v1/authorize" +#define REDDIT_OAUTH_TOKEN_URL "https://www.reddit.com/api/v1/access_token" +#define REDDIT_OAUTH_SCOPE "identity" + +#define REDDIT_REG_API_URL "https://www.reddit.com/prefs/apps" + +#define REDDIT_API_GET_PROFILE "https://oauth.reddit.com/api/v1/me" + +#define REDDIT_DEFAULT_BATCH_SIZE 100 +#define REDDIT_MAX_BATCH_SIZE 999 + +#define REDDIT_CONTENT_TYPE_HTTP "application/http" +#define REDDIT_CONTENT_TYPE_JSON "application/json" + +#endif // REDDIT_DEFINITIONS_H diff --git a/src/librssguard/services/reddit/gui/formeditredditaccount.cpp b/src/librssguard/services/reddit/gui/formeditredditaccount.cpp new file mode 100755 index 000000000..f6930ba41 --- /dev/null +++ b/src/librssguard/services/reddit/gui/formeditredditaccount.cpp @@ -0,0 +1,68 @@ +// For license of this file, see /LICENSE.md. + +#include "services/reddit/gui/formeditredditaccount.h" + +#include "gui/guiutilities.h" +#include "miscellaneous/application.h" +#include "miscellaneous/iconfactory.h" +#include "network-web/oauth2service.h" +#include "network-web/webfactory.h" +#include "services/reddit/definitions.h" +#include "services/reddit/redditserviceroot.h" +#include "services/reddit/gui/redditaccountdetails.h" + +FormEditRedditAccount::FormEditRedditAccount(QWidget* parent) + : FormAccountDetails(qApp->icons()->miscIcon(QSL("reddit")), parent), m_details(new RedditAccountDetails(this)) { + insertCustomTab(m_details, tr("Server setup"), 0); + activateTab(0); + + m_details->m_ui.m_txtUsername->setFocus(); + connect(m_details->m_ui.m_btnTestSetup, &QPushButton::clicked, this, [this]() { + m_details->testSetup(m_proxyDetails->proxy()); + }); +} + +void FormEditRedditAccount::apply() { + FormAccountDetails::apply(); + + bool using_another_acc = + m_details->m_ui.m_txtUsername->lineEdit()->text() !=account()->network()->username(); + + // Make sure that the data copied from GUI are used for brand new login. + 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(), + true); + + account()->network()->setUsername(m_details->m_ui.m_txtUsername->lineEdit()->text()); + account()->network()->setBatchSize(m_details->m_ui.m_spinLimitMessages->value()); + account()->network()->setDownloadOnlyUnreadMessages(m_details->m_ui.m_cbDownloadOnlyUnreadMessages->isChecked()); + + account()->saveAccountDataToDatabase(); + accept(); + + if (!m_creatingNew) { + if (using_another_acc) { + account()->completelyRemoveAllData(); + } + + account()->start(true); + } +} + +void FormEditRedditAccount::loadAccountData() { + FormAccountDetails::loadAccountData(); + + m_details->m_oauth = account()->network()->oauth(); + m_details->hookNetwork(); + + // Setup the GUI. + 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(account()->network()->username()); + m_details->m_ui.m_spinLimitMessages->setValue(account()->network()->batchSize()); + m_details->m_ui.m_cbDownloadOnlyUnreadMessages->setChecked(account()->network()->downloadOnlyUnreadMessages()); +} diff --git a/src/librssguard/services/reddit/gui/formeditredditaccount.h b/src/librssguard/services/reddit/gui/formeditredditaccount.h new file mode 100755 index 000000000..51618a790 --- /dev/null +++ b/src/librssguard/services/reddit/gui/formeditredditaccount.h @@ -0,0 +1,29 @@ +// For license of this file, see /LICENSE.md. + +#ifndef FORMEDITINOREADERACCOUNT_H +#define FORMEDITINOREADERACCOUNT_H + +#include "services/abstract/gui/formaccountdetails.h" + +#include "services/reddit/redditnetworkfactory.h" + +class RedditServiceRoot; +class RedditAccountDetails; + +class FormEditRedditAccount : public FormAccountDetails { + Q_OBJECT + + public: + explicit FormEditRedditAccount(QWidget* parent = nullptr); + + protected slots: + virtual void apply(); + + protected: + virtual void loadAccountData(); + + private: + RedditAccountDetails* m_details; +}; + +#endif // FORMEDITINOREADERACCOUNT_H diff --git a/src/librssguard/services/reddit/gui/redditaccountdetails.cpp b/src/librssguard/services/reddit/gui/redditaccountdetails.cpp new file mode 100755 index 000000000..0458d3fea --- /dev/null +++ b/src/librssguard/services/reddit/gui/redditaccountdetails.cpp @@ -0,0 +1,124 @@ +// For license of this file, see /LICENSE.md. + +#include "services/reddit/gui/redditaccountdetails.h" + +#include "exceptions/applicationexception.h" +#include "gui/guiutilities.h" +#include "miscellaneous/application.h" +#include "network-web/oauth2service.h" +#include "network-web/webfactory.h" +#include "services/reddit/definitions.h" +#include "services/reddit/redditnetworkfactory.h" + +RedditAccountDetails::RedditAccountDetails(QWidget* parent) + : QWidget(parent), m_oauth(nullptr), m_lastProxy({}) { + m_ui.setupUi(this); + + m_ui.m_lblInfo->setHelpText(tr("You have to fill in your client ID/secret and also fill in correct redirect URL."), + true); + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Information, + tr("Not tested yet."), + tr("Not tested yet.")); + m_ui.m_lblTestResult->label()->setWordWrap(true); + m_ui.m_txtUsername->lineEdit()->setPlaceholderText(tr("User-visible username")); + + setTabOrder(m_ui.m_txtUsername->lineEdit(), 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_spinLimitMessages); + setTabOrder(m_ui.m_spinLimitMessages, m_ui.m_btnTestSetup); + + connect(m_ui.m_txtAppId->lineEdit(), &BaseLineEdit::textChanged, this, &RedditAccountDetails::checkOAuthValue); + connect(m_ui.m_txtAppKey->lineEdit(), &BaseLineEdit::textChanged, this, &RedditAccountDetails::checkOAuthValue); + connect(m_ui.m_txtRedirectUrl->lineEdit(), &BaseLineEdit::textChanged, this, &RedditAccountDetails::checkOAuthValue); + connect(m_ui.m_txtUsername->lineEdit(), &BaseLineEdit::textChanged, this, &RedditAccountDetails::checkUsername); + connect(m_ui.m_btnRegisterApi, &QPushButton::clicked, this, &RedditAccountDetails::registerApi); + + emit m_ui.m_txtUsername->lineEdit()->textChanged(m_ui.m_txtUsername->lineEdit()->text()); + 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()); + + hookNetwork(); +} + +void RedditAccountDetails::testSetup(const QNetworkProxy& custom_proxy) { + 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_lastProxy = custom_proxy; + m_oauth->login(); +} + +void RedditAccountDetails::checkUsername(const QString& username) { + if (username.isEmpty()) { + m_ui.m_txtUsername->setStatus(WidgetWithStatus::StatusType::Error, tr("No username entered.")); + } + else { + m_ui.m_txtUsername->setStatus(WidgetWithStatus::StatusType::Ok, tr("Some username entered.")); + } +} + +void RedditAccountDetails::onAuthFailed() { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error, + tr("You did not grant access."), + tr("There was error during testing.")); +} + +void RedditAccountDetails::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 RedditAccountDetails::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 { + RedditNetworkFactory fac; + + fac.setOauth(m_oauth); + + auto resp = fac.me(m_lastProxy); + + m_ui.m_txtUsername->lineEdit()->setText(resp[QSL("name")].toString()); + } + catch (const ApplicationException& ex) { + qCriticalNN << LOGSEC_REDDIT + << "Failed to obtain profile with error:" + << QUOTE_W_SPACE_DOT(ex.message()); + } +} + +void RedditAccountDetails::hookNetwork() { + connect(m_oauth, &OAuth2Service::tokensRetrieved, this, &RedditAccountDetails::onAuthGranted); + connect(m_oauth, &OAuth2Service::tokensRetrieveError, this, &RedditAccountDetails::onAuthError); + connect(m_oauth, &OAuth2Service::authFailed, this, &RedditAccountDetails::onAuthFailed); +} + +void RedditAccountDetails::registerApi() { + qApp->web()->openUrlInExternalBrowser(QSL(REDDIT_REG_API_URL)); +} + +void RedditAccountDetails::checkOAuthValue(const QString& value) { + auto* line_edit = qobject_cast(sender()->parent()); + + if (line_edit != nullptr) { + if (value.isEmpty()) { +#if defined(REDDIT_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.")); + } + } +} diff --git a/src/librssguard/services/reddit/gui/redditaccountdetails.h b/src/librssguard/services/reddit/gui/redditaccountdetails.h new file mode 100755 index 000000000..b383a7eb7 --- /dev/null +++ b/src/librssguard/services/reddit/gui/redditaccountdetails.h @@ -0,0 +1,44 @@ +// For license of this file, see /LICENSE.md. + +#ifndef REDDITACCOUNTDETAILS_H +#define REDDITACCOUNTDETAILS_H + +#include + +#include "ui_redditaccountdetails.h" + +#include + +class OAuth2Service; + +class RedditAccountDetails : public QWidget { + Q_OBJECT + + friend class FormEditRedditAccount; + + public: + explicit RedditAccountDetails(QWidget* parent = nullptr); + + public slots: + void testSetup(const QNetworkProxy& custom_proxy); + + private slots: + void registerApi(); + void checkOAuthValue(const QString& value); + void checkUsername(const QString& username); + void onAuthFailed(); + void onAuthError(const QString& error, const QString& detailed_description); + void onAuthGranted(); + + private: + void hookNetwork(); + + private: + Ui::RedditAccountDetails m_ui; + + // Pointer to live OAuth. + OAuth2Service* m_oauth; + QNetworkProxy m_lastProxy; +}; + +#endif // REDDITACCOUNTDETAILS_H diff --git a/src/librssguard/services/reddit/gui/redditaccountdetails.ui b/src/librssguard/services/reddit/gui/redditaccountdetails.ui new file mode 100755 index 000000000..da0a54a31 --- /dev/null +++ b/src/librssguard/services/reddit/gui/redditaccountdetails.ui @@ -0,0 +1,202 @@ + + + RedditAccountDetails + + + + 0 + 0 + 431 + 259 + + + + + + + Username + + + + + + + + + + + 0 + 1 + + + + OAuth 2.0 settings + + + + + + Client ID + + + m_txtAppId + + + + + + + + + + Client secret + + + m_txtAppKey + + + + + + + + + + Redirect URL + + + m_txtRedirectUrl + + + + + + + + + + + + Get my credentials + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + Only download newest X articles per feed + + + m_spinLimitMessages + + + + + + + + 140 + 16777215 + + + + + + + + + + + + &Login + + + + + + + Qt::RightToLeft + + + + + + + + + Qt::Vertical + + + + 410 + 0 + + + + + + + + Download unread articles only + + + + + + + + LineEditWithStatus + QWidget +
lineeditwithstatus.h
+ 1 +
+ + LabelWithStatus + QWidget +
labelwithstatus.h
+ 1 +
+ + MessageCountSpinBox + QSpinBox +
messagecountspinbox.h
+
+ + HelpSpoiler + QWidget +
helpspoiler.h
+ 1 +
+
+ + m_btnRegisterApi + m_cbDownloadOnlyUnreadMessages + m_spinLimitMessages + m_btnTestSetup + + + +
diff --git a/src/librssguard/services/reddit/redditentrypoint.cpp b/src/librssguard/services/reddit/redditentrypoint.cpp new file mode 100755 index 000000000..abff8f4e7 --- /dev/null +++ b/src/librssguard/services/reddit/redditentrypoint.cpp @@ -0,0 +1,45 @@ +// For license of this file, see /LICENSE.md. + +#include "services/reddit/redditentrypoint.h" + +#include "database/databasequeries.h" +#include "definitions/definitions.h" +#include "miscellaneous/application.h" +#include "miscellaneous/iconfactory.h" +#include "services/reddit/definitions.h" +#include "services/reddit/gui/formeditredditaccount.h" +#include "services/reddit/redditserviceroot.h" + +#include + +ServiceRoot* RedditEntryPoint::createNewRoot() const { + FormEditRedditAccount form_acc(qApp->mainFormWidget()); + + return form_acc.addEditAccount(); +} + +QList RedditEntryPoint::initializeSubtree() const { + QSqlDatabase database = qApp->database()->driver()->connection(QSL("RedditEntryPoint")); + + return DatabaseQueries::getAccounts(database, code()); +} + +QString RedditEntryPoint::name() const { + return QSL("Reddit"); +} + +QString RedditEntryPoint::code() const { + return QSL(SERVICE_CODE_REDDIT); +} + +QString RedditEntryPoint::description() const { + return QObject::tr("Simplistic Reddit client."); +} + +QString RedditEntryPoint::author() const { + return QSL(APP_AUTHOR); +} + +QIcon RedditEntryPoint::icon() const { + return qApp->icons()->miscIcon(QSL("reddit")); +} diff --git a/src/librssguard/services/reddit/redditentrypoint.h b/src/librssguard/services/reddit/redditentrypoint.h new file mode 100755 index 000000000..74a47caf1 --- /dev/null +++ b/src/librssguard/services/reddit/redditentrypoint.h @@ -0,0 +1,19 @@ +// For license of this file, see /LICENSE.md. + +#ifndef REDDITENTRYPOINT_H +#define REDDITENTRYPOINT_H + +#include "services/abstract/serviceentrypoint.h" + +class RedditEntryPoint : public ServiceEntryPoint { + public: + virtual ServiceRoot* createNewRoot() const; + virtual QList initializeSubtree() const; + virtual QString name() const; + virtual QString code() const; + virtual QString description() const; + virtual QString author() const; + virtual QIcon icon() const; +}; + +#endif // REDDITENTRYPOINT_H diff --git a/src/librssguard/services/reddit/redditnetworkfactory.cpp b/src/librssguard/services/reddit/redditnetworkfactory.cpp new file mode 100755 index 000000000..7dfffc256 --- /dev/null +++ b/src/librssguard/services/reddit/redditnetworkfactory.cpp @@ -0,0 +1,155 @@ +// For license of this file, see /LICENSE.md. + +#include "services/reddit/redditnetworkfactory.h" + +#include "database/databasequeries.h" +#include "definitions/definitions.h" +#include "exceptions/applicationexception.h" +#include "exceptions/networkexception.h" +#include "gui/dialogs/formmain.h" +#include "gui/tabwidget.h" +#include "miscellaneous/application.h" +#include "miscellaneous/textfactory.h" +#include "network-web/networkfactory.h" +#include "network-web/oauth2service.h" +#include "network-web/silentnetworkaccessmanager.h" +#include "network-web/webfactory.h" +#include "services/abstract/category.h" +#include "services/reddit/definitions.h" +#include "services/reddit/redditserviceroot.h" + +#include +#include +#include +#include +#include +#include +#include + +RedditNetworkFactory::RedditNetworkFactory(QObject* parent) : QObject(parent), + m_service(nullptr), m_username(QString()), m_batchSize(REDDIT_DEFAULT_BATCH_SIZE), + m_downloadOnlyUnreadMessages(false), + m_oauth2(new OAuth2Service(QSL(REDDIT_OAUTH_AUTH_URL), QSL(REDDIT_OAUTH_TOKEN_URL), + {}, {}, QSL(REDDIT_OAUTH_SCOPE), this)) { + initializeOauth(); +} + +void RedditNetworkFactory::setService(RedditServiceRoot* service) { + m_service = service; +} + +OAuth2Service* RedditNetworkFactory::oauth() const { + return m_oauth2; +} + +QString RedditNetworkFactory::username() const { + return m_username; +} + +int RedditNetworkFactory::batchSize() const { + return m_batchSize; +} + +void RedditNetworkFactory::setBatchSize(int batch_size) { + m_batchSize = batch_size; +} + +void RedditNetworkFactory::initializeOauth() { + m_oauth2->setUseHttpBasicAuthWithClientData(true); + m_oauth2->setRedirectUrl(QSL(OAUTH_REDIRECT_URI) + + QL1C(':') + + QString::number(REDDIT_OAUTH_REDIRECT_URI_PORT), + true); + + connect(m_oauth2, &OAuth2Service::tokensRetrieveError, this, &RedditNetworkFactory::onTokensError); + connect(m_oauth2, &OAuth2Service::authFailed, this, &RedditNetworkFactory::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_service != nullptr && !refresh_token.isEmpty()) { + QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className()); + + DatabaseQueries::storeNewOauthTokens(database, refresh_token, m_service->accountId()); + } + }); +} + +bool RedditNetworkFactory::downloadOnlyUnreadMessages() const { + return m_downloadOnlyUnreadMessages; +} + +void RedditNetworkFactory::setDownloadOnlyUnreadMessages(bool download_only_unread_messages) { + m_downloadOnlyUnreadMessages = download_only_unread_messages; +} + +void RedditNetworkFactory::setOauth(OAuth2Service* oauth) { + m_oauth2 = oauth; +} + +void RedditNetworkFactory::setUsername(const QString& username) { + m_username = username; +} + +QVariantHash RedditNetworkFactory::me(const QNetworkProxy& custom_proxy) { + QString bearer = m_oauth2->bearer().toLocal8Bit(); + + if (bearer.isEmpty()) { + throw ApplicationException(tr("you are not logged in")); + } + + QList> headers; + + headers.append(QPair(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), + m_oauth2->bearer().toLocal8Bit())); + + int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray output; + auto result = NetworkFactory::performNetworkOperation(QSL(REDDIT_API_GET_PROFILE), + timeout, + {}, + output, + QNetworkAccessManager::Operation::GetOperation, + headers, + false, + {}, + {}, + custom_proxy).first; + + if (result != QNetworkReply::NetworkError::NoError) { + throw NetworkException(result, output); + } + else { + QJsonDocument doc = QJsonDocument::fromJson(output); + + return doc.object().toVariantHash(); + } +} + +void RedditNetworkFactory::onTokensError(const QString& error, const QString& error_description) { + Q_UNUSED(error) + + qApp->showGuiMessage(Notification::Event::LoginFailure, { + tr("Reddit: authentication error"), + tr("Click this to login again. Error is: '%1'").arg(error_description), + QSystemTrayIcon::MessageIcon::Critical }, + {}, { + tr("Login"), + [this]() { + m_oauth2->setAccessToken(QString()); + m_oauth2->setRefreshToken(QString()); + m_oauth2->login(); + } }); +} + +void RedditNetworkFactory::onAuthFailed() { + qApp->showGuiMessage(Notification::Event::LoginFailure, { + tr("Reddit: authorization denied"), + tr("Click this to login again."), + QSystemTrayIcon::MessageIcon::Critical }, + {}, { + tr("Login"), + [this]() { + m_oauth2->login(); + } }); +} diff --git a/src/librssguard/services/reddit/redditnetworkfactory.h b/src/librssguard/services/reddit/redditnetworkfactory.h new file mode 100755 index 000000000..befef2cd6 --- /dev/null +++ b/src/librssguard/services/reddit/redditnetworkfactory.h @@ -0,0 +1,59 @@ +// For license of this file, see /LICENSE.md. + +#ifndef REDDITNETWORKFACTORY_H +#define REDDITNETWORKFACTORY_H + +#include + +#include "core/message.h" + +#include "3rd-party/mimesis/mimesis.hpp" +#include "services/abstract/feed.h" +#include "services/abstract/rootitem.h" + +#include + +class RootItem; +class RedditServiceRoot; +class OAuth2Service; +class Downloader; + +class RedditNetworkFactory : public QObject { + Q_OBJECT + + public: + explicit RedditNetworkFactory(QObject* parent = nullptr); + + void setService(RedditServiceRoot* service); + + OAuth2Service* oauth() const; + void setOauth(OAuth2Service* oauth); + + QString username() const; + void setUsername(const QString& username); + + int batchSize() const; + void setBatchSize(int batch_size); + + bool downloadOnlyUnreadMessages() const; + void setDownloadOnlyUnreadMessages(bool download_only_unread_messages); + + // API methods. + QVariantHash me(const QNetworkProxy& custom_proxy); + + private slots: + void onTokensError(const QString& error, const QString& error_description); + void onAuthFailed(); + + private: + void initializeOauth(); + + private: + RedditServiceRoot* m_service; + QString m_username; + int m_batchSize; + bool m_downloadOnlyUnreadMessages; + OAuth2Service* m_oauth2; +}; + +#endif // REDDITNETWORKFACTORY_H diff --git a/src/librssguard/services/reddit/redditserviceroot.cpp b/src/librssguard/services/reddit/redditserviceroot.cpp new file mode 100755 index 000000000..1953b8a63 --- /dev/null +++ b/src/librssguard/services/reddit/redditserviceroot.cpp @@ -0,0 +1,127 @@ +// For license of this file, see /LICENSE.md. + +#include "services/reddit/redditserviceroot.h" + +#include "database/databasequeries.h" +#include "exceptions/feedfetchexception.h" +#include "miscellaneous/application.h" +#include "miscellaneous/iconfactory.h" +#include "network-web/oauth2service.h" +#include "services/abstract/importantnode.h" +#include "services/abstract/recyclebin.h" +#include "services/reddit/definitions.h" +#include "services/reddit/gui/formeditredditaccount.h" +#include "services/reddit/redditentrypoint.h" +#include "services/reddit/redditnetworkfactory.h" + +#include + +RedditServiceRoot::RedditServiceRoot(RootItem* parent) + : ServiceRoot(parent), m_network(new RedditNetworkFactory(this)) { + m_network->setService(this); + setIcon(RedditEntryPoint().icon()); +} + +void RedditServiceRoot::updateTitle() { + setTitle(TextFactory::extractUsernameFromEmail(m_network->username()) + QSL(" (Reddit)")); +} + +RootItem* RedditServiceRoot::obtainNewTreeForSyncIn() const { + auto* root = new RootItem(); + + return root; +} + +QVariantHash RedditServiceRoot::customDatabaseData() const { + QVariantHash data; + + data[QSL("username")] = m_network->username(); + data[QSL("batch_size")] = m_network->batchSize(); + data[QSL("download_only_unread")] = m_network->downloadOnlyUnreadMessages(); + data[QSL("client_id")] = m_network->oauth()->clientId(); + data[QSL("client_secret")] = m_network->oauth()->clientSecret(); + data[QSL("refresh_token")] = m_network->oauth()->refreshToken(); + data[QSL("redirect_uri")] = m_network->oauth()->redirectUrl(); + + return data; +} + +void RedditServiceRoot::setCustomDatabaseData(const QVariantHash& data) { + m_network->setUsername(data[QSL("username")].toString()); + m_network->setBatchSize(data[QSL("batch_size")].toInt()); + m_network->setDownloadOnlyUnreadMessages(data[QSL("download_only_unread")].toBool()); + m_network->oauth()->setClientId(data[QSL("client_id")].toString()); + m_network->oauth()->setClientSecret(data[QSL("client_secret")].toString()); + m_network->oauth()->setRefreshToken(data[QSL("refresh_token")].toString()); + m_network->oauth()->setRedirectUrl(data[QSL("redirect_uri")].toString(), true); +} + +QList RedditServiceRoot::obtainNewMessages(Feed* feed, + const QHash& stated_messages, + const QHash& tagged_messages) { + Q_UNUSED(stated_messages) + Q_UNUSED(tagged_messages) + Q_UNUSED(feed) + + QList messages; + + return messages; +} + +bool RedditServiceRoot::isSyncable() const { + return true; +} + +bool RedditServiceRoot::canBeEdited() const { + return true; +} + +bool RedditServiceRoot::editViaGui() { + FormEditRedditAccount form_pointer(qApp->mainFormWidget()); + + form_pointer.addEditAccount(this); + return true; +} + +bool RedditServiceRoot::supportsFeedAdding() const { + return false; +} + +bool RedditServiceRoot::supportsCategoryAdding() const { + return false; +} + +void RedditServiceRoot::start(bool freshly_activated) { + if (!freshly_activated) { + DatabaseQueries::loadFromDatabase(this); + loadCacheFromFile(); + } + + updateTitle(); + + /* + if (getSubTreeFeeds().isEmpty()) { + syncIn(); + } + */ + + m_network->oauth()->login(); +} + +QString RedditServiceRoot::code() const { + return RedditEntryPoint().code(); +} + +QString RedditServiceRoot::additionalTooltip() const { + return tr("Authentication status: %1\n" + "Login tokens expiration: %2").arg(network()->oauth()->isFullyLoggedIn() + ? tr("logged-in") + : tr("NOT logged-in"), + network()->oauth()->tokensExpireIn().isValid() ? + network()->oauth()->tokensExpireIn().toString() : QSL("-")); +} + +void RedditServiceRoot::saveAllCachedData(bool ignore_errors) { + Q_UNUSED(ignore_errors) + auto msg_cache = takeMessageCache(); +} diff --git a/src/librssguard/services/reddit/redditserviceroot.h b/src/librssguard/services/reddit/redditserviceroot.h new file mode 100755 index 000000000..5600e0a95 --- /dev/null +++ b/src/librssguard/services/reddit/redditserviceroot.h @@ -0,0 +1,53 @@ +// For license of this file, see /LICENSE.md. + +#ifndef REDDITSERVICEROOT_H +#define REDDITSERVICEROOT_H + +#include "services/abstract/cacheforserviceroot.h" +#include "services/abstract/serviceroot.h" + +class RedditNetworkFactory; + +class RedditServiceRoot : public ServiceRoot, public CacheForServiceRoot { + Q_OBJECT + + public: + explicit RedditServiceRoot(RootItem* parent = nullptr); + + void setNetwork(RedditNetworkFactory* network); + RedditNetworkFactory* network() const; + + virtual bool isSyncable() const; + virtual bool canBeEdited() const; + virtual bool editViaGui(); + virtual bool supportsFeedAdding() const; + virtual bool supportsCategoryAdding() const; + virtual void start(bool freshly_activated); + virtual QString code() const; + virtual QString additionalTooltip() const; + virtual void saveAllCachedData(bool ignore_errors); + virtual QVariantHash customDatabaseData() const; + virtual void setCustomDatabaseData(const QVariantHash& data); + virtual QList obtainNewMessages(Feed* feed, + const QHash& stated_messages, + const QHash& tagged_messages); + + protected: + virtual RootItem* obtainNewTreeForSyncIn() const; + + private: + void updateTitle(); + + private: + RedditNetworkFactory* m_network; +}; + +inline void RedditServiceRoot::setNetwork(RedditNetworkFactory* network) { + m_network = network; +} + +inline RedditNetworkFactory* RedditServiceRoot::network() const { + return m_network; +} + +#endif // REDDITSERVICEROOT_H