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
+
+ 1
+
+
+ LabelWithStatus
+ QWidget
+
+ 1
+
+
+ MessageCountSpinBox
+ QSpinBox
+
+
+
+ HelpSpoiler
+ QWidget
+
+ 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