diff --git a/src/librssguard/services/reddit/definitions.h b/src/librssguard/services/reddit/definitions.h index a8cc677b0..6f9d21e3e 100644 --- a/src/librssguard/services/reddit/definitions.h +++ b/src/librssguard/services/reddit/definitions.h @@ -3,19 +3,21 @@ #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_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 mysubreddits read" -#define REDDIT_REG_API_URL "https://www.reddit.com/prefs/apps" +#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_API_GET_PROFILE "https://oauth.reddit.com/api/v1/me" +#define REDDIT_API_SUBREDDITS "https://oauth.reddit.com/subreddits/mine/subscriber?limit=%1" +#define REDDIT_API_HOT "https://oauth.reddit.com/r/%2/hot?limit=%1&%3" -#define REDDIT_DEFAULT_BATCH_SIZE 100 -#define REDDIT_MAX_BATCH_SIZE 999 +#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" +#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/redditnetworkfactory.cpp b/src/librssguard/services/reddit/redditnetworkfactory.cpp index d70e038d3..612bf9415 100644 --- a/src/librssguard/services/reddit/redditnetworkfactory.cpp +++ b/src/librssguard/services/reddit/redditnetworkfactory.cpp @@ -26,11 +26,14 @@ #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)) { +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(); } @@ -56,23 +59,23 @@ void RedditNetworkFactory::setBatchSize(int 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); + 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) + 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()); + if (m_service != nullptr && !refresh_token.isEmpty()) { + QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className()); - DatabaseQueries::storeNewOauthTokens(database, refresh_token, m_service->accountId()); - } - }); + DatabaseQueries::storeNewOauthTokens(database, refresh_token, m_service->accountId()); + } + }); } bool RedditNetworkFactory::downloadOnlyUnreadMessages() const { @@ -114,7 +117,8 @@ QVariantHash RedditNetworkFactory::me(const QNetworkProxy& custom_proxy) { false, {}, {}, - custom_proxy).m_networkError; + custom_proxy) + .m_networkError; if (result != QNetworkReply::NetworkError::NoError) { throw NetworkException(result, output); @@ -126,30 +130,191 @@ QVariantHash RedditNetworkFactory::me(const QNetworkProxy& custom_proxy) { } } +QList RedditNetworkFactory::subreddits(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(); + QString after; + QList subs; + + do { + QByteArray output; + QString final_url = QSL(REDDIT_API_SUBREDDITS).arg(QString::number(100)); + + if (!after.isEmpty()) { + final_url += QSL("&after=%1").arg(after); + } + + auto result = NetworkFactory::performNetworkOperation(final_url, + timeout, + {}, + output, + QNetworkAccessManager::Operation::GetOperation, + headers, + false, + {}, + {}, + custom_proxy) + .m_networkError; + + if (result != QNetworkReply::NetworkError::NoError) { + throw NetworkException(result, output); + } + else { + QJsonDocument doc = QJsonDocument::fromJson(output); + QJsonObject root_doc = doc.object(); + + after = root_doc["data"].toObject()["after"].toString(); + + for (const QJsonValue& sub_val : root_doc["data"].toObject()["children"].toArray()) { + const auto sub_obj = sub_val.toObject()["data"].toObject(); + + Feed* new_sub = new Feed(); + + new_sub->setCustomId(sub_obj["id"].toString()); + new_sub->setTitle(sub_obj["title"].toString()); + new_sub->setDescription(sub_obj["public_description"].toString()); + + QIcon icon; + QString icon_url = sub_obj["community_icon"].toString(); + + if (icon_url.isEmpty()) { + icon_url = sub_obj["icon_img"].toString(); + } + + if (icon_url.contains(QL1S("?"))) { + icon_url = icon_url.mid(0, icon_url.indexOf(QL1S("?"))); + } + + if (!icon_url.isEmpty() && + NetworkFactory::downloadIcon({{icon_url, true}}, timeout, icon, headers, custom_proxy) == + QNetworkReply::NetworkError::NoError) { + new_sub->setIcon(icon); + } + + subs.append(new_sub); + } + } + } + while (!after.isEmpty()); + + // posty dle jmena redditu + // https://oauth.reddit.com//new + // + // komenty pro post dle id postu + // https://oauth.reddit.com//comments/ + + return subs; +} + +QList RedditNetworkFactory::hot(const QString& sub_name, 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(); + QString after; + QList msgs; + + do { + QByteArray output; + QString final_url = QSL(REDDIT_API_HOT).arg(QString::number(100), sub_name, QSL("GLOBAL")); + + if (!after.isEmpty()) { + final_url += QSL("&after=%1").arg(after); + } + + auto result = NetworkFactory::performNetworkOperation(final_url, + timeout, + {}, + output, + QNetworkAccessManager::Operation::GetOperation, + headers, + false, + {}, + {}, + custom_proxy) + .m_networkError; + + if (result != QNetworkReply::NetworkError::NoError) { + throw NetworkException(result, output); + } + else { + QJsonDocument doc = QJsonDocument::fromJson(output); + QJsonObject root_doc = doc.object(); + + after = root_doc["data"].toObject()["after"].toString(); + + for (const QJsonValue& sub_val : root_doc["data"].toObject()["children"].toArray()) { + const auto msg_obj = sub_val.toObject()["data"].toObject(); + + Message new_msg; + + new_msg.m_customId = msg_obj["id"].toString(); + new_msg.m_title = msg_obj["title"].toString(); + new_msg.m_author = msg_obj["author"].toString(); + new_msg.m_createdFromFeed = true; + new_msg.m_created = + QDateTime::fromSecsSinceEpoch(msg_obj["created_utc"].toVariant().toLongLong(), Qt::TimeSpec::UTC); + new_msg.m_url = QSL("https://reddit.com") + msg_obj["permalink"].toString(); + new_msg.m_contents = + msg_obj["description_html"] + .toString(); // když prazdny, je poustnutej třeba obrazek či odkaz?, viz property "post_hint"? + new_msg.m_rawContents = QJsonDocument(msg_obj).toJson(QJsonDocument::JsonFormat::Compact); + + msgs.append(new_msg); + } + } + } + while (!after.isEmpty()); + + // posty dle jmena redditu + // https://oauth.reddit.com//new + // + // komenty pro post dle id postu + // https://oauth.reddit.com//comments/ + + return msgs; +} + 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(); - } }); + 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(); - } }); + 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 index befef2cd6..b4105fc3a 100644 --- a/src/librssguard/services/reddit/redditnetworkfactory.h +++ b/src/librssguard/services/reddit/redditnetworkfactory.h @@ -18,8 +18,10 @@ class RedditServiceRoot; class OAuth2Service; class Downloader; +struct Subreddit {}; + class RedditNetworkFactory : public QObject { - Q_OBJECT + Q_OBJECT public: explicit RedditNetworkFactory(QObject* parent = nullptr); @@ -40,6 +42,8 @@ class RedditNetworkFactory : public QObject { // API methods. QVariantHash me(const QNetworkProxy& custom_proxy); + QList subreddits(const QNetworkProxy& custom_proxy); + QList hot(const QString& sub_name, const QNetworkProxy& custom_proxy); private slots: void onTokensError(const QString& error, const QString& error_description); diff --git a/src/librssguard/services/reddit/redditserviceroot.cpp b/src/librssguard/services/reddit/redditserviceroot.cpp index fbf8d232f..60ce620a6 100644 --- a/src/librssguard/services/reddit/redditserviceroot.cpp +++ b/src/librssguard/services/reddit/redditserviceroot.cpp @@ -30,6 +30,12 @@ void RedditServiceRoot::updateTitle() { RootItem* RedditServiceRoot::obtainNewTreeForSyncIn() const { auto* root = new RootItem(); + auto feeds = m_network->subreddits(networkProxy()); + + for (auto* feed : feeds) { + root->appendChild(feed); + } + return root; } @@ -58,13 +64,14 @@ void RedditServiceRoot::setCustomDatabaseData(const QVariantHash& data) { } QList RedditServiceRoot::obtainNewMessages(Feed* feed, - const QHash& stated_messages, + const QHash& + stated_messages, const QHash& tagged_messages) { Q_UNUSED(stated_messages) Q_UNUSED(tagged_messages) Q_UNUSED(feed) - QList messages; + QList messages = m_network->hot(feed->title(), networkProxy()); return messages; } @@ -100,13 +107,14 @@ void RedditServiceRoot::start(bool freshly_activated) { updateTitle(); - /* - if (getSubTreeFeeds().isEmpty()) { - syncIn(); - } - */ - - m_network->oauth()->login(); + if (getSubTreeFeeds().isEmpty()) { + m_network->oauth()->login([this]() { + syncIn(); + }); + } + else { + m_network->oauth()->login(); + } } QString RedditServiceRoot::code() const { @@ -115,11 +123,9 @@ QString RedditServiceRoot::code() const { 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("-")); + "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) {