// For license of this file, see /LICENSE.md. #include "services/inoreader/inoreadernetworkfactory.h" #include "3rd-party/boolinq/boolinq.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 "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/abstract/labelsnode.h" #include "services/inoreader/definitions.h" #include "services/inoreader/inoreaderserviceroot.h" #include #include #include #include #include #include InoreaderNetworkFactory::InoreaderNetworkFactory(QObject* parent) : QObject(parent), m_service(nullptr), m_username(QString()), m_downloadOnlyUnreadMessages(false), m_batchSize(INOREADER_DEFAULT_BATCH_SIZE), m_oauth2(new OAuth2Service(INOREADER_OAUTH_AUTH_URL, INOREADER_OAUTH_TOKEN_URL, {}, {}, INOREADER_OAUTH_SCOPE, this)) { initializeOauth(); } void InoreaderNetworkFactory::setService(InoreaderServiceRoot* service) { m_service = service; } OAuth2Service* InoreaderNetworkFactory::oauth() const { return m_oauth2; } QString InoreaderNetworkFactory::username() const { return m_username; } int InoreaderNetworkFactory::batchSize() const { return m_batchSize; } void InoreaderNetworkFactory::setBatchSize(int batch_size) { m_batchSize = batch_size; } void InoreaderNetworkFactory::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(INOREADER_OAUTH_REDIRECT_URI_PORT)); connect(m_oauth2, &OAuth2Service::tokensRetrieveError, this, &InoreaderNetworkFactory::onTokensError); connect(m_oauth2, &OAuth2Service::authFailed, this, &InoreaderNetworkFactory::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 && m_service->accountId() > 0 && !refresh_token.isEmpty()) { QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className()); DatabaseQueries::storeNewOauthTokens(database, refresh_token, m_service->accountId()); } }); } bool InoreaderNetworkFactory::downloadOnlyUnreadMessages() const { return m_downloadOnlyUnreadMessages; } void InoreaderNetworkFactory::setDownloadOnlyUnreadMessages(bool download_only_unread) { m_downloadOnlyUnreadMessages = download_only_unread; } void InoreaderNetworkFactory::setUsername(const QString& username) { m_username = username; } RootItem* InoreaderNetworkFactory::feedsCategories(bool obtain_icons) { QString bearer = m_oauth2->bearer().toLocal8Bit(); if (bearer.isEmpty()) { return nullptr; } QByteArray output_labels; auto result_labels = NetworkFactory::performNetworkOperation(INOREADER_API_LIST_LABELS, qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(), {}, output_labels, QNetworkAccessManager::Operation::GetOperation, { { QString(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), bearer.toLocal8Bit() } }, false, {}, {}, m_service->networkProxy()); if (result_labels.first != QNetworkReply::NetworkError::NoError) { return nullptr; } QByteArray output_feeds; auto result_feeds = NetworkFactory::performNetworkOperation(INOREADER_API_LIST_FEEDS, qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(), {}, output_feeds, QNetworkAccessManager::Operation::GetOperation, { { QString(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), bearer.toLocal8Bit() } }, false, {}, {}, m_service->networkProxy()); if (result_feeds.first != QNetworkReply::NetworkError::NoError) { return nullptr; } return decodeFeedCategoriesData(output_labels, output_feeds, obtain_icons); } QVariantHash InoreaderNetworkFactory::userInfo(const QNetworkProxy& custom_proxy) { QString bearer = m_oauth2->bearer().toLocal8Bit(); if (bearer.isEmpty()) { throw ApplicationException(tr("not logged in")); } int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); QByteArray output; auto res = NetworkFactory::performNetworkOperation(INOREADER_API_USER_INFO, timeout, {}, output, QNetworkAccessManager::Operation::GetOperation, { { QString(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), bearer.toLocal8Bit() } }, false, {}, {}, custom_proxy); if (res.first != QNetworkReply::NetworkError::NoError) { throw NetworkException(res.first); } return QJsonDocument::fromJson(output).object().toVariantHash(); } QList InoreaderNetworkFactory::getLabels() { QList lbls; QString bearer = m_oauth2->bearer().toLocal8Bit(); if (bearer.isEmpty()) { return lbls; } QByteArray output; auto result = NetworkFactory::performNetworkOperation(INOREADER_API_LIST_LABELS, qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(), {}, output, QNetworkAccessManager::Operation::GetOperation, { { QString(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), bearer.toLocal8Bit() } }, false, {}, {}, m_service->networkProxy()); QJsonDocument json_lbls = QJsonDocument::fromJson(output); auto json_tags = json_lbls.object()["tags"].toArray(); for (const QJsonValue& lbl_val : qAsConst(json_tags)) { QJsonObject lbl_obj = lbl_val.toObject(); if (lbl_obj["type"] == QL1S("tag")) { QString name_id = lbl_obj["id"].toString(); QString plain_name = QRegularExpression(".+\\/([^\\/]+)").match(name_id).captured(1); auto* new_lbl = new Label(plain_name, TextFactory::generateColorFromText(name_id)); new_lbl->setCustomId(name_id); lbls.append(new_lbl); } } return lbls; } QList InoreaderNetworkFactory::messages(ServiceRoot* root, const QString& stream_id, Feed::Status& error) { QString target_url = INOREADER_API_FEED_CONTENTS; QString bearer = m_oauth2->bearer().toLocal8Bit(); if (bearer.isEmpty()) { qCriticalNN << LOGSEC_INOREADER << "Cannot download messages for" << QUOTE_NO_SPACE(stream_id) << ", bearer is empty."; error = Feed::Status::AuthError; return QList(); } target_url += QSL("/") + QUrl::toPercentEncoding(stream_id) + QString("?n=%1").arg(batchSize() <= 0 ? INOREADER_MAX_BATCH_SIZE : batchSize()); if (downloadOnlyUnreadMessages()) { target_url += QSL("&xt=%1").arg(INOREADER_FULL_STATE_READ); } QByteArray output_msgs; auto result = NetworkFactory::performNetworkOperation(target_url, qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(), {}, output_msgs, QNetworkAccessManager::Operation::GetOperation, { { QString(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), bearer.toLocal8Bit() } }, false, {}, {}, m_service->networkProxy()); if (result.first != QNetworkReply::NetworkError::NoError) { qCriticalNN << LOGSEC_INOREADER << "Cannot download messages for " << QUOTE_NO_SPACE(stream_id) << ", network error:" << QUOTE_W_SPACE_DOT(result.first); error = Feed::Status::NetworkError; return QList(); } else { error = Feed::Status::Normal; return decodeMessages(root, output_msgs, stream_id); } } QNetworkReply::NetworkError InoreaderNetworkFactory::editLabels(const QString& state, bool assign, const QStringList& msg_custom_ids) { QString target_url = INOREADER_API_EDIT_TAG; if (assign) { target_url += QString("?a=") + state + "&"; } else { target_url += QString("?r=") + state + "&"; } QString bearer = m_oauth2->bearer().toLocal8Bit(); if (bearer.isEmpty()) { return QNetworkReply::NetworkError::AuthenticationRequiredError; } QList> headers; headers.append(QPair(QString(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), m_oauth2->bearer().toLocal8Bit())); QStringList trimmed_ids; for (const QString& id : msg_custom_ids) { trimmed_ids.append(QString("i=") + id); } QStringList working_subset; working_subset.reserve(std::min(INOREADER_API_EDIT_TAG_BATCH, trimmed_ids.size())); int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); // Now, we perform messages update in batches (max XX messages per batch). while (!trimmed_ids.isEmpty()) { // We take XX IDs. for (int i = 0; i < INOREADER_API_EDIT_TAG_BATCH && !trimmed_ids.isEmpty(); i++) { working_subset.append(trimmed_ids.takeFirst()); } QString batch_final_url = target_url + working_subset.join(QL1C('&')); // We send this batch. QByteArray output; auto result = NetworkFactory::performNetworkOperation(batch_final_url, timeout, {}, output, QNetworkAccessManager::Operation::GetOperation, headers, false, {}, {}, m_service->networkProxy()); if (result.first != QNetworkReply::NetworkError::NoError) { return result.first; } // Cleanup for next batch. working_subset.clear(); } return QNetworkReply::NetworkError::NoError; } QNetworkReply::NetworkError InoreaderNetworkFactory::markMessagesRead(RootItem::ReadStatus status, const QStringList& msg_custom_ids) { return editLabels(INOREADER_FULL_STATE_READ, status == RootItem::ReadStatus::Read, msg_custom_ids); } QNetworkReply::NetworkError InoreaderNetworkFactory::markMessagesStarred(RootItem::Importance importance, const QStringList& msg_custom_ids) { return editLabels(INOREADER_FULL_STATE_IMPORTANT, importance == RootItem::Importance::Important, msg_custom_ids); } void InoreaderNetworkFactory::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 InoreaderNetworkFactory::onAuthFailed() { qApp->showGuiMessage(Notification::Event::GeneralEvent, tr("Inoreader: authorization denied"), tr("Click this to login again."), QSystemTrayIcon::MessageIcon::Critical, {}, {}, [this]() { m_oauth2->login(); }); } QList InoreaderNetworkFactory::decodeMessages(ServiceRoot* root, const QString& messages_json_data, const QString& stream_id) { QList messages; QJsonArray json = QJsonDocument::fromJson(messages_json_data.toUtf8()).object()["items"].toArray(); auto active_labels = root->labelsNode() != nullptr ? root->labelsNode()->labels() : QList(); messages.reserve(json.count()); for (const QJsonValue& obj : json) { auto message_obj = obj.toObject(); Message message; message.m_title = qApp->web()->unescapeHtml(message_obj["title"].toString()); message.m_author = qApp->web()->unescapeHtml(message_obj["author"].toString()); message.m_created = QDateTime::fromSecsSinceEpoch(message_obj["published"].toInt(), Qt::UTC); message.m_createdFromFeed = true; message.m_customId = message_obj["id"].toString(); auto alternates = message_obj["alternate"].toArray(); auto enclosures = message_obj["enclosure"].toArray(); auto categories = message_obj["categories"].toArray(); for (const QJsonValue& alt : alternates) { auto alt_obj = alt.toObject(); QString mime = alt_obj["type"].toString(); QString href = alt_obj["href"].toString(); if (mime == QL1S("text/html")) { message.m_url = href; } else { message.m_enclosures.append(Enclosure(href, mime)); } } for (const QJsonValue& enc : enclosures) { auto enc_obj = enc.toObject(); QString mime = enc_obj["type"].toString(); QString href = enc_obj["href"].toString(); message.m_enclosures.append(Enclosure(href, mime)); } for (const QJsonValue& cat : categories) { QString category = cat.toString(); if (category.contains(INOREADER_STATE_READ)) { message.m_isRead = !category.contains(INOREADER_STATE_READING_LIST); } else if (category.contains(INOREADER_STATE_IMPORTANT)) { message.m_isImportant = category.contains(INOREADER_STATE_IMPORTANT); } else if (category.contains(QSL("label"))) { Label* label = boolinq::from(active_labels.begin(), active_labels.end()).firstOrDefault([category](Label* lbl) { return lbl->customId() == category; }); if (label != nullptr) { // We found live Label object for our assigned label. message.m_assignedLabels.append(label); } } } message.m_contents = message_obj["summary"].toObject()["content"].toString(); message.m_rawContents = QJsonDocument(message_obj).toJson(QJsonDocument::JsonFormat::Compact); message.m_feedId = stream_id; messages.append(message); } return messages; } RootItem* InoreaderNetworkFactory::decodeFeedCategoriesData(const QString& categories, const QString& feeds, bool obtain_icons) { auto* parent = new RootItem(); QJsonArray json = QJsonDocument::fromJson(categories.toUtf8()).object()["tags"].toArray(); QMap cats; cats.insert(QString(), parent); for (const QJsonValue& obj : json) { auto label = obj.toObject(); if (label["type"].toString() == QL1S("folder")) { QString label_id = label["id"].toString(); // We have label (not "state"). auto* category = new Category(); category->setDescription(label["htmlUrl"].toString()); category->setTitle(label_id.mid(label_id.lastIndexOf(QL1C('/')) + 1)); category->setCustomId(label_id); cats.insert(category->customId(), category); parent->appendChild(category); } } json = QJsonDocument::fromJson(feeds.toUtf8()).object()["subscriptions"].toArray(); for (const QJsonValue& obj : qAsConst(json)) { auto subscription = obj.toObject(); QString id = subscription["id"].toString(); QString title = subscription["title"].toString(); QString url = subscription["htmlUrl"].toString(); QString parent_label; QJsonArray assigned_categories = subscription["categories"].toArray(); for (const QJsonValue& cat : assigned_categories) { QString potential_id = cat.toObject()["id"].toString(); if (potential_id.contains(QSL("/label/"))) { parent_label = potential_id; break; } } // We have label (not "state"). auto* feed = new Feed(); feed->setDescription(url); feed->setSource(url); feed->setTitle(title); feed->setCustomId(id); if (obtain_icons) { QString icon_url = subscription["iconUrl"].toString(); if (!icon_url.isEmpty()) { QByteArray icon_data; if (NetworkFactory::performNetworkOperation(icon_url, DOWNLOAD_TIMEOUT, QByteArray(), icon_data, QNetworkAccessManager::GetOperation).first == QNetworkReply::NoError) { // Icon downloaded, set it up. QPixmap icon_pixmap; icon_pixmap.loadFromData(icon_data); feed->setIcon(QIcon(icon_pixmap)); } } } if (cats.contains(parent_label)) { cats[parent_label]->appendChild(feed); } } return parent; } void InoreaderNetworkFactory::setOauth(OAuth2Service* oauth) { m_oauth2 = oauth; }