diff --git a/CMakeLists.txt b/CMakeLists.txt index be7b35df6..8a0c260c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -325,6 +325,7 @@ add_subdirectory(localization) # Plugins. add_subdirectory(src/librssguard-standard) add_subdirectory(src/librssguard-feedly) +add_subdirectory(src/librssguard-gmail) # GUI executable. add_subdirectory(src/rssguard) diff --git a/src/librssguard-feedly/CMakeLists.txt b/src/librssguard-feedly/CMakeLists.txt new file mode 100644 index 000000000..96b664ae4 --- /dev/null +++ b/src/librssguard-feedly/CMakeLists.txt @@ -0,0 +1,76 @@ +if(NOT DEFINED LIBRSSGUARD_BINARY_PATH) + set(LIBRSSGUARD_SOURCE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/..") +endif() + +set(SOURCES + src/definitions.h + src/feedlyentrypoint.cpp + src/feedlyentrypoint.h + src/feedlynetwork.cpp + src/feedlynetwork.h + src/feedlyserviceroot.cpp + src/feedlyserviceroot.h + src/gui/feedlyaccountdetails.cpp + src/gui/feedlyaccountdetails.h + src/gui/formeditfeedlyaccount.cpp + src/gui/formeditfeedlyaccount.h +) + +set(UI_FILES + src/gui/feedlyaccountdetails.ui +) + +# Deal with .ui files. +qt_wrap_ui(SOURCES ${UI_FILES}) + +# Bundle version info. +if(WIN32) + enable_language("RC") + list(APPEND SOURCES "${CMAKE_BINARY_DIR}/rssguard.rc") +endif() + +add_library(rssguard-feedly SHARED ${SOURCES} ${QM_FILES}) + +# Add specific definitions. +target_compile_definitions(rssguard-feedly + PRIVATE + RSSGUARD_DLLSPEC=Q_DECL_IMPORT + RSSGUARD_DLLSPEC_EXPORT=Q_DECL_EXPORT +) + +target_include_directories(rssguard-feedly + PUBLIC + ${LIBRSSGUARD_SOURCE_PATH} +) + +# Qt. +target_link_libraries(rssguard-feedly PUBLIC + rssguard + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Network + Qt${QT_VERSION_MAJOR}::Qml + Qt${QT_VERSION_MAJOR}::Sql + Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::Xml + Qt${QT_VERSION_MAJOR}::Concurrent +) + +if(QT_VERSION_MAJOR EQUAL 6) + target_link_libraries(rssguard-feedly PUBLIC + Qt${QT_VERSION_MAJOR}::Core5Compat + ) +endif() + +if(WIN32 OR OS2) + install(TARGETS rssguard-feedly DESTINATION plugins) +elseif(UNIX AND NOT APPLE AND NOT ANDROID) + include (GNUInstallDirs) + install(TARGETS rssguard-feedly + DESTINATION ${CMAKE_INSTALL_LIBDIR}/rssguard + ) +elseif(APPLE) + install(TARGETS rssguard-feedly + DESTINATION Contents/MacOS + ) +endif() diff --git a/src/librssguard-feedly/plugin.json b/src/librssguard-feedly/plugin.json new file mode 100644 index 000000000..1c174f508 --- /dev/null +++ b/src/librssguard-feedly/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "Feedly", + "author": "Martin Rotter", + "website": "https://github.com/martinrotter/rssguard" +} \ No newline at end of file diff --git a/src/librssguard-feedly/src/definitions.h b/src/librssguard-feedly/src/definitions.h new file mode 100644 index 000000000..ea6c7ae7f --- /dev/null +++ b/src/librssguard-feedly/src/definitions.h @@ -0,0 +1,35 @@ +#ifndef FEEDLY_DEFINITIONS_H +#define FEEDLY_DEFINITIONS_H + +#define FEEDLY_DEFAULT_BATCH_SIZE 100 +#define FEEDLY_MAX_BATCH_SIZE 500 +#define FEEDLY_MAX_TOTAL_SIZE 5000 +#define FEEDLY_UNTAG_BATCH_SIZE 100 + +#define FEEDLY_GENERATE_DAT "https://feedly.com/v3/auth/dev" + +#define FEEDLY_API_REDIRECT_URI_PORT 14466 +#define FEEDLY_API_SCOPE "https://cloud.feedly.com/subscriptions" + +// #define FEEDLY_API_URL_BASE "https://sandbox7.feedly.com/v3/" +#define FEEDLY_API_URL_BASE "https://cloud.feedly.com/v3/" + +#define FEEDLY_API_SYSTEM_TAG_READ "global.read" +#define FEEDLY_API_SYSTEM_TAG_SAVED "global.saved" + +#define FEEDLY_MARKERS_READ "markAsRead" +#define FEEDLY_MARKERS_UNREAD "keepUnread" +#define FEEDLY_MARKERS_IMPORTANT "markAsSaved" +#define FEEDLY_MARKERS_UNIMPORTANT "markAsUnsaved" + +#define FEEDLY_API_URL_AUTH "auth/auth" +#define FEEDLY_API_URL_TOKEN "auth/token" +#define FEEDLY_API_URL_PROFILE "profile" +#define FEEDLY_API_URL_COLLETIONS "collections" +#define FEEDLY_API_URL_TAGS "tags" +#define FEEDLY_API_URL_STREAM_CONTENTS "streams/contents?streamId=%1" +#define FEEDLY_API_URL_STREAM_IDS "streams/%1/ids" +#define FEEDLY_API_URL_MARKERS "markers" +#define FEEDLY_API_URL_ENTRIES "entries/.mget" + +#endif // FEEDLY_DEFINITIONS_H diff --git a/src/librssguard-feedly/src/feedlyentrypoint.cpp b/src/librssguard-feedly/src/feedlyentrypoint.cpp new file mode 100644 index 000000000..9d00def77 --- /dev/null +++ b/src/librssguard-feedly/src/feedlyentrypoint.cpp @@ -0,0 +1,55 @@ +// For license of this file, see /LICENSE.md. + +#include "src/feedlyentrypoint.h" + +#include "src/feedlyserviceroot.h" +#include "src/gui/formeditfeedlyaccount.h" + +#include +#include +#include +#include + +FeedlyEntryPoint::FeedlyEntryPoint(QObject* parent) : QObject(parent) {} + +FeedlyEntryPoint::~FeedlyEntryPoint() { + qDebugNN << LOGSEC_CORE << "Destructing" << QUOTE_W_SPACE(QSL(SERVICE_CODE_FEEDLY)) << "plugin."; +} + +ServiceRoot* FeedlyEntryPoint::createNewRoot() const { + FormEditFeedlyAccount form_acc(qApp->mainFormWidget()); + + return form_acc.addEditAccount(); +} + +QList FeedlyEntryPoint::initializeSubtree() const { + QSqlDatabase database = qApp->database()->driver()->connection(QSL("FeedlyEntryPoint")); + + return DatabaseQueries::getAccounts(database, code()); +} + +QString FeedlyEntryPoint::name() const { + return QSL("Feedly"); +} + +QString FeedlyEntryPoint::code() const { + return QSL(SERVICE_CODE_FEEDLY); +} + +QString FeedlyEntryPoint::description() const { + return QObject::tr("Keep up with the topics and trends you care about, without the overwhelm.\n\n" + "Feedly is a secure space where you can privately organize and research the " + "topics and trends that matter to you."); +} + +QString FeedlyEntryPoint::author() const { + return QSL(APP_AUTHOR); +} + +QIcon FeedlyEntryPoint::icon() const { + return qApp->icons()->miscIcon(QSL("feedly")); +} + +bool FeedlyEntryPoint::isDynamicallyLoaded() const { + return true; +} diff --git a/src/librssguard-feedly/src/feedlyentrypoint.h b/src/librssguard-feedly/src/feedlyentrypoint.h new file mode 100644 index 000000000..48de55750 --- /dev/null +++ b/src/librssguard-feedly/src/feedlyentrypoint.h @@ -0,0 +1,27 @@ +// For license of this file, see /LICENSE.md. + +#ifndef FEEDLYENTRYPOINT_H +#define FEEDLYENTRYPOINT_H + +#include + +class FeedlyEntryPoint : public QObject, public ServiceEntryPoint { + Q_OBJECT + Q_PLUGIN_METADATA(IID "io.github.martinrotter.rssguard.feedly" FILE "plugin.json") + Q_INTERFACES(ServiceEntryPoint) + + public: + explicit FeedlyEntryPoint(QObject* parent = nullptr); + virtual ~FeedlyEntryPoint(); + + 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; + virtual bool isDynamicallyLoaded() const; +}; + +#endif // FEEDLYENTRYPOINT_H diff --git a/src/librssguard-feedly/src/feedlynetwork.cpp b/src/librssguard-feedly/src/feedlynetwork.cpp new file mode 100644 index 000000000..0415f5708 --- /dev/null +++ b/src/librssguard-feedly/src/feedlynetwork.cpp @@ -0,0 +1,822 @@ +// For license of this file, see /LICENSE.md. + +#include "src/feedlynetwork.h" + +#include "src/definitions.h" +#include "src/feedlyserviceroot.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(FEEDLY_OFFICIAL_SUPPORT) +#include +#endif + +#include +#include +#include + +FeedlyNetwork::FeedlyNetwork(QObject* parent) + : QObject(parent), m_service(nullptr), +#if defined(FEEDLY_OFFICIAL_SUPPORT) + m_oauth(new OAuth2Service(QSL(FEEDLY_API_URL_BASE) + QSL(FEEDLY_API_URL_AUTH), + QSL(FEEDLY_API_URL_BASE) + QSL(FEEDLY_API_URL_TOKEN), + TextFactory::decrypt(QSL(FEEDLY_CLIENT_ID), OAUTH_DECRYPTION_KEY), + TextFactory::decrypt(QSL(FEEDLY_CLIENT_SECRET), OAUTH_DECRYPTION_KEY), + QSL(FEEDLY_API_SCOPE), + this)), +#endif + m_username(QString()), m_developerAccessToken(QString()), m_batchSize(FEEDLY_DEFAULT_BATCH_SIZE), + m_downloadOnlyUnreadMessages(false), m_intelligentSynchronization(true) { + +#if defined(FEEDLY_OFFICIAL_SUPPORT) + m_oauth->setRedirectUrl(QSL(OAUTH_REDIRECT_URI) + QL1C(':') + QString::number(FEEDLY_API_REDIRECT_URI_PORT), true); + + connect(m_oauth, &OAuth2Service::tokensRetrieveError, this, &FeedlyNetwork::onTokensError); + connect(m_oauth, &OAuth2Service::authFailed, this, &FeedlyNetwork::onAuthFailed); + connect(m_oauth, &OAuth2Service::tokensRetrieved, this, &FeedlyNetwork::onTokensRetrieved); +#endif +} + +QList FeedlyNetwork::messages(const QString& stream_id, + const QHash& stated_messages) { + if (!m_intelligentSynchronization) { + return streamContents(stream_id); + } + + // 1. Get unread IDs for a feed. + // 2. Get read IDs for a feed. + // 3. Download messages/contents for missing or changed IDs. + QStringList remote_all_ids_list, remote_unread_ids_list; + + remote_unread_ids_list = streamIds(stream_id, true, batchSize()); + + if (!downloadOnlyUnreadMessages()) { + remote_all_ids_list = streamIds(stream_id, false, batchSize()); + } + + // 1. + auto local_unread_ids_list = stated_messages.value(ServiceRoot::BagOfMessages::Unread); + QSet local_unread_ids = FROM_LIST_TO_SET(QSet, local_unread_ids_list); + QSet remote_unread_ids = FROM_LIST_TO_SET(QSet, remote_unread_ids_list); + + // 2. + auto local_read_ids_list = stated_messages.value(ServiceRoot::BagOfMessages::Read); + QSet local_read_ids = FROM_LIST_TO_SET(QSet, local_read_ids_list); + QSet remote_read_ids = FROM_LIST_TO_SET(QSet, remote_all_ids_list) - remote_unread_ids; + + // 3. + QSet to_download; + + // Undownloaded unread articles. + to_download += remote_unread_ids - local_unread_ids; + + // Undownloaded read articles. + if (!m_downloadOnlyUnreadMessages) { + to_download += remote_read_ids - local_read_ids; + } + + // Read articles newly marked as unread in service. + auto moved_read = local_read_ids.intersect(remote_unread_ids); + + to_download += moved_read; + + // Unread articles newly marked as read in service. + if (!m_downloadOnlyUnreadMessages) { + auto moved_unread = local_unread_ids.intersect(remote_read_ids); + + to_download += moved_unread; + } + + qDebugNN << LOGSEC_FEEDLY << "Will download" << QUOTE_W_SPACE(to_download.size()) << "articles."; + + if (to_download.isEmpty()) { + return {}; + } + else { + return entries(QStringList(to_download.values())); + } +} + +void FeedlyNetwork::untagEntries(const QString& tag_id, const QStringList& msg_custom_ids) { + if (msg_custom_ids.isEmpty()) { + return; + } + + QString bear = bearer(); + + if (bear.isEmpty()) { + qCriticalNN << LOGSEC_FEEDLY << "Cannot untag entries, because bearer is empty."; + throw NetworkException(QNetworkReply::NetworkError::AuthenticationRequiredError); + } + + QString target_url = fullUrl(Service::TagEntries) + QSL("/%1/").arg(QString(QUrl::toPercentEncoding(tag_id))); + int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray output; + int i = 0; + + do { + auto msg_batch = msg_custom_ids.mid(i, FEEDLY_UNTAG_BATCH_SIZE); + + i += FEEDLY_UNTAG_BATCH_SIZE; + + auto ids = boolinq::from(msg_batch) + .select([](const QString& msg_id) { + return QString(QUrl::toPercentEncoding(msg_id)); + }) + .toStdList(); + QString final_url = target_url + FROM_STD_LIST(QStringList, ids).join(','); + auto result = NetworkFactory::performNetworkOperation(final_url, + timeout, + {}, + output, + QNetworkAccessManager::Operation::DeleteOperation, + {bearerHeader(bear)}, + false, + {}, + {}, + m_service->networkProxy()); + + if (result.m_networkError != QNetworkReply::NetworkError::NoError) { + throw NetworkException(result.m_networkError, output); + } + } + while (i < msg_custom_ids.size()); +} + +void FeedlyNetwork::tagEntries(const QString& tag_id, const QStringList& msg_custom_ids) { + if (msg_custom_ids.isEmpty()) { + return; + } + + QString bear = bearer(); + + if (bear.isEmpty()) { + qCriticalNN << LOGSEC_FEEDLY << "Cannot tag entries, because bearer is empty."; + throw NetworkException(QNetworkReply::NetworkError::AuthenticationRequiredError); + } + + QString target_url = fullUrl(Service::TagEntries) + QSL("/%1").arg(QString(QUrl::toPercentEncoding(tag_id))); + int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray output; + QByteArray input_data; + QJsonObject input; + + input[QSL("entryIds")] = QJsonArray::fromStringList(msg_custom_ids); + input_data = QJsonDocument(input).toJson(QJsonDocument::JsonFormat::Compact); + + auto result = + NetworkFactory::performNetworkOperation(target_url, + timeout, + input_data, + output, + QNetworkAccessManager::Operation::PutOperation, + {bearerHeader(bear), {HTTP_HEADERS_CONTENT_TYPE, "application/json"}}, + false, + {}, + {}, + m_service->networkProxy()); + + if (result.m_networkError != QNetworkReply::NetworkError::NoError) { + throw NetworkException(result.m_networkError, output); + } +} + +void FeedlyNetwork::markers(const QString& action, const QStringList& msg_custom_ids) { + if (msg_custom_ids.isEmpty()) { + return; + } + + QString bear = bearer(); + + if (bear.isEmpty()) { + qCriticalNN << LOGSEC_FEEDLY << "Cannot mark entries, because bearer is empty."; + throw NetworkException(QNetworkReply::NetworkError::AuthenticationRequiredError); + } + + QString target_url = fullUrl(Service::Markers); + int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray output; + + for (int i = 0; i < msg_custom_ids.size(); i += 500) { + QJsonObject input; + + input[QSL("action")] = action; + input[QSL("type")] = QSL("entries"); + input[QSL("entryIds")] = QJsonArray::fromStringList(msg_custom_ids.mid(i, 500)); + + QByteArray input_data = QJsonDocument(input).toJson(QJsonDocument::JsonFormat::Compact); + auto result = + NetworkFactory::performNetworkOperation(target_url, + timeout, + input_data, + output, + QNetworkAccessManager::Operation::PostOperation, + {bearerHeader(bear), {HTTP_HEADERS_CONTENT_TYPE, "application/json"}}, + false, + {}, + {}, + m_service->networkProxy()); + + if (result.m_networkError != QNetworkReply::NetworkError::NoError) { + throw NetworkException(result.m_networkError, output); + } + } +} + +QList FeedlyNetwork::entries(const QStringList& ids) { + const QString bear = bearer(); + + if (bear.isEmpty()) { + qCriticalNN << LOGSEC_FEEDLY << "Cannot obtain personal collections, because bearer is empty."; + throw NetworkException(QNetworkReply::NetworkError::AuthenticationRequiredError); + } + + QList msgs; + int next_message = 0; + QString continuation; + const QString target_url = fullUrl(Service::Entries); + const int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + + do { + QJsonArray json; + + for (int window = next_message + 1000; next_message < window && next_message < ids.size(); next_message++) { + json.append(QJsonValue(ids.at(next_message))); + } + + QByteArray output; + auto result = + NetworkFactory::performNetworkOperation(target_url, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + output, + QNetworkAccessManager::Operation::PostOperation, + {bearerHeader(bear)}, + false, + {}, + {}, + m_service->networkProxy()); + + if (result.m_networkError != QNetworkReply::NetworkError::NoError) { + throw NetworkException(result.m_networkError, output); + } + + msgs += decodeStreamContents(output, false, continuation); + } + while (next_message < ids.size()); + + return msgs; +} + +QList FeedlyNetwork::streamContents(const QString& stream_id) { + QString bear = bearer(); + + if (bear.isEmpty()) { + qCriticalNN << LOGSEC_FEEDLY << "Cannot obtain personal collections, because bearer is empty."; + throw NetworkException(QNetworkReply::NetworkError::AuthenticationRequiredError); + } + + int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray output; + QString continuation; + QList messages; + + // We download in batches. + do { + QString target_url = fullUrl(Service::StreamContents).arg(QString(QUrl::toPercentEncoding(stream_id))); + + if (m_downloadOnlyUnreadMessages) { + target_url += QSL("&unreadOnly=true"); + } + + if (!continuation.isEmpty()) { + target_url += QSL("&continuation=%1").arg(continuation); + } + + if (m_batchSize > 0) { + target_url += QSL("&count=%1").arg(QString::number(m_batchSize)); + } + else { + // User wants to download all messages. Make sure we use large batches + // to limit network requests. + target_url += QSL("&count=%1").arg(QString::number(FEEDLY_MAX_BATCH_SIZE)); + } + + auto result = NetworkFactory::performNetworkOperation(target_url, + timeout, + {}, + output, + QNetworkAccessManager::Operation::GetOperation, + {bearerHeader(bear)}, + false, + {}, + {}, + m_service->networkProxy()); + + if (result.m_networkError != QNetworkReply::NetworkError::NoError) { + throw NetworkException(result.m_networkError, output); + } + + messages += decodeStreamContents(output, true, continuation); + } + while (!continuation.isEmpty() && (m_batchSize <= 0 || messages.size() < m_batchSize) && + messages.size() <= FEEDLY_MAX_TOTAL_SIZE); + + return messages; +} + +QStringList FeedlyNetwork::streamIds(const QString& stream_id, bool unread_only, int batch_size) { + QString bear = bearer(); + + if (bear.isEmpty()) { + qCriticalNN << LOGSEC_FEEDLY << "Cannot obtain stream IDs, because bearer is empty."; + throw NetworkException(QNetworkReply::NetworkError::AuthenticationRequiredError); + } + + int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray output; + QString continuation; + QStringList messages; + + // We download in batches. + do { + QString target_url = fullUrl(Service::StreamIds).arg(QString(QUrl::toPercentEncoding(stream_id))); + + if (batch_size > 0) { + target_url += QSL("?count=%1").arg(QString::number(batch_size)); + } + else { + // User wants to download all messages. Make sure we use large batches + // to limit network requests. + target_url += QSL("?count=%1").arg(QString::number(10000)); + } + + if (unread_only) { + target_url += QSL("&unreadOnly=true"); + } + + if (!continuation.isEmpty()) { + target_url += QSL("&continuation=%1").arg(continuation); + } + + auto result = NetworkFactory::performNetworkOperation(target_url, + timeout, + {}, + output, + QNetworkAccessManager::Operation::GetOperation, + {bearerHeader(bear)}, + false, + {}, + {}, + m_service->networkProxy()); + + if (result.m_networkError != QNetworkReply::NetworkError::NoError) { + throw NetworkException(result.m_networkError, output); + } + + messages += decodeStreamIds(output, continuation); + } + while (!continuation.isEmpty() && (batch_size <= 0 || messages.size() < batch_size)); + + return messages; +} + +QStringList FeedlyNetwork::decodeStreamIds(const QByteArray& stream_ids, QString& continuation) const { + QStringList messages; + QJsonDocument json = QJsonDocument::fromJson(stream_ids); + + continuation = json.object()[QSL("continuation")].toString(); + + for (const QJsonValue& id_val : json.object()[QSL("ids")].toArray()) { + messages << id_val.toString(); + } + + return messages; +} + +QList FeedlyNetwork::decodeStreamContents(const QByteArray& stream_contents, + bool nested_items, + QString& continuation) const { + QList messages; + QJsonDocument json = QJsonDocument::fromJson(stream_contents); + auto active_labels = m_service->labelsNode() != nullptr ? m_service->labelsNode()->labels() : QList(); + + continuation = json.object()[QSL("continuation")].toString(); + + auto items = nested_items ? json.object()[QSL("items")].toArray() : json.array(); + + for (const QJsonValue& entry : std::as_const(items)) { + const QJsonObject& entry_obj = entry.toObject(); + Message message; + + message.m_feedId = entry_obj[QSL("origin")].toObject()[QSL("streamId")].toString(); + message.m_title = entry_obj[QSL("title")].toString(); + message.m_author = entry_obj[QSL("author")].toString(); + message.m_contents = entry_obj[QSL("content")].toObject()[QSL("content")].toString(); + message.m_rawContents = QJsonDocument(entry_obj).toJson(QJsonDocument::JsonFormat::Compact); + + if (message.m_contents.isEmpty()) { + message.m_contents = entry_obj[QSL("summary")].toObject()[QSL("content")].toString(); + } + + message.m_createdFromFeed = true; + message.m_created = + QDateTime::fromMSecsSinceEpoch(entry_obj[QSL("published")].toVariant().toLongLong(), Qt::TimeSpec::UTC); + message.m_customId = entry_obj[QSL("id")].toString(); + message.m_isRead = !entry_obj[QSL("unread")].toBool(); + message.m_url = entry_obj[QSL("canonicalUrl")].toString(); + + if (message.m_url.isEmpty()) { + auto canonical_arr = entry_obj[QSL("canonical")].toArray(); + + if (!canonical_arr.isEmpty()) { + message.m_url = canonical_arr.first().toObject()[QSL("href")].toString(); + } + else { + auto alternate_arr = entry_obj[QSL("alternate")].toArray(); + + if (!alternate_arr.isEmpty()) { + message.m_url = alternate_arr.first().toObject()[QSL("href")].toString(); + } + } + } + + auto enclosures = entry_obj[QSL("enclosure")].toArray(); + + for (const QJsonValue& enc : std::as_const(enclosures)) { + const QJsonObject& enc_obj = enc.toObject(); + const QString& enc_href = enc_obj[QSL("href")].toString(); + + if (!boolinq::from(message.m_enclosures).any([enc_href](const Enclosure& existing_enclosure) { + return existing_enclosure.m_url == enc_href; + })) { + message.m_enclosures.append(Enclosure(enc_href, enc_obj[QSL("type")].toString())); + } + } + + auto tags = entry_obj[QSL("tags")].toArray(); + + for (const QJsonValue& tag : std::as_const(tags)) { + const QJsonObject& tag_obj = tag.toObject(); + const QString& tag_id = tag_obj[QSL("id")].toString(); + + if (tag_id.endsWith(FEEDLY_API_SYSTEM_TAG_SAVED)) { + message.m_isImportant = true; + } + else if (tag_id.endsWith(FEEDLY_API_SYSTEM_TAG_READ)) { + // NOTE: We don't do anything with "global read" tag. + } + else { + Label* label = boolinq::from(active_labels.begin(), active_labels.end()).firstOrDefault([tag_id](Label* lbl) { + return lbl->customId() == tag_id; + }); + + if (label != nullptr) { + message.m_assignedLabels.append(label); + } + else { + qCriticalNN << LOGSEC_FEEDLY << "Failed to find live Label object for tag" << QUOTE_W_SPACE_DOT(tag_id); + } + } + } + + messages.append(message); + } + + return messages; +} + +RootItem* FeedlyNetwork::collections(bool obtain_icons) { + QString bear = bearer(); + + if (bear.isEmpty()) { + qCriticalNN << LOGSEC_FEEDLY << "Cannot obtain personal collections, because bearer is empty."; + throw NetworkException(QNetworkReply::NetworkError::AuthenticationRequiredError); + } + + QString target_url = fullUrl(Service::Collections); + int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray output; + auto result = NetworkFactory::performNetworkOperation(target_url, + timeout, + {}, + output, + QNetworkAccessManager::Operation::GetOperation, + {bearerHeader(bear)}, + false, + {}, + {}, + m_service->networkProxy()); + + if (result.m_networkError != QNetworkReply::NetworkError::NoError) { + throw NetworkException(result.m_networkError, output); + } + + return decodeCollections(output, obtain_icons, m_service->networkProxy(), timeout); +} + +RootItem* FeedlyNetwork::decodeCollections(const QByteArray& json, + bool obtain_icons, + const QNetworkProxy& proxy, + int timeout) const { + QJsonDocument doc = QJsonDocument::fromJson(json); + auto* parent = new RootItem(); + QList used_feeds; + auto coll = doc.array(); + + for (const QJsonValue& cat : std::as_const(coll)) { + QJsonObject cat_obj = cat.toObject(); + auto* category = new Category(parent); + + category->setTitle(cat_obj[QSL("label")].toString()); + category->setCustomId(cat_obj[QSL("id")].toString()); + + auto feeds = cat[QSL("feeds")].toArray(); + + for (const QJsonValue& fee : std::as_const(feeds)) { + QJsonObject fee_obj = fee.toObject(); + + if (used_feeds.contains(fee_obj[QSL("id")].toString())) { + qWarningNN << LOGSEC_FEEDLY << "Feed" << QUOTE_W_SPACE(fee_obj[QSL("id")].toString()) + << "is already decoded and cannot be placed under several categories."; + continue; + } + + auto* feed = new Feed(category); + + feed->setSource(fee_obj[QSL("website")].toString()); + feed->setTitle(fee_obj[QSL("title")].toString()); + feed->setDescription(qApp->web()->stripTags(fee_obj[QSL("description")].toString())); + feed->setCustomId(fee_obj[QSL("id")].toString()); + + if (feed->title().isEmpty()) { + feed->setTitle(feed->description()); + } + + if (feed->title().isEmpty()) { + feed->setTitle(feed->source()); + } + + if (feed->title().isEmpty()) { + feed->setTitle(feed->customId()); + qWarningNN << LOGSEC_FEEDLY + << "Some feed does not have nor title, neither description. Using its ID for its title."; + } + + if (obtain_icons) { + QPixmap icon; + auto result = NetworkFactory::downloadIcon({{fee_obj[QSL("iconUrl")].toString(), true}, + {fee_obj[QSL("website")].toString(), false}, + {fee_obj[QSL("logo")].toString(), true}}, + timeout, + icon, + {}, + proxy); + + if (result == QNetworkReply::NetworkError::NoError && !icon.isNull()) { + feed->setIcon(icon); + } + } + + used_feeds.append(feed->customId()); + category->appendChild(feed); + } + + if (category->childCount() == 0) { + delete category; + } + else { + parent->appendChild(category); + } + } + + return parent; +} + +QVariantHash FeedlyNetwork::profile(const QNetworkProxy& network_proxy) { + QString bear = bearer(); + + if (bear.isEmpty()) { + qCriticalNN << LOGSEC_FEEDLY << "Cannot obtain profile information, because bearer is empty."; + throw NetworkException(QNetworkReply::NetworkError::AuthenticationRequiredError); + } + + QString target_url = fullUrl(Service::Profile); + int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray output; + + // This method uses proxy via parameter, + // not via "m_service" field. + auto result = NetworkFactory::performNetworkOperation(target_url, + timeout, + {}, + output, + QNetworkAccessManager::Operation::GetOperation, + {bearerHeader(bear)}, + false, + {}, + {}, + network_proxy); + + if (result.m_networkError != QNetworkReply::NetworkError::NoError) { + throw NetworkException(result.m_networkError, output); + } + + return QJsonDocument::fromJson(output).object().toVariantHash(); +} + +QList FeedlyNetwork::tags() { + QString bear = bearer(); + + if (bear.isEmpty()) { + qCriticalNN << LOGSEC_FEEDLY << "Cannot obtain tags, because bearer is empty."; + throw NetworkException(QNetworkReply::NetworkError::AuthenticationRequiredError); + } + + QString target_url = fullUrl(Service::Tags); + int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray output; + auto result = NetworkFactory::performNetworkOperation(target_url, + timeout, + {}, + output, + QNetworkAccessManager::Operation::GetOperation, + {bearerHeader(bear)}, + false, + {}, + {}, + m_service->networkProxy()); + + if (result.m_networkError != QNetworkReply::NetworkError::NoError) { + throw NetworkException(result.m_networkError, output); + } + + QJsonDocument json = QJsonDocument::fromJson(output); + QList lbls; + auto tags = json.array(); + + for (const QJsonValue& tag : std::as_const(tags)) { + const QJsonObject& tag_obj = tag.toObject(); + QString name_id = tag_obj[QSL("id")].toString(); + + if (name_id.endsWith(FEEDLY_API_SYSTEM_TAG_READ) || name_id.endsWith(FEEDLY_API_SYSTEM_TAG_SAVED)) { + continue; + } + + QString plain_name = tag_obj[QSL("label")].toString(); + auto* new_lbl = new Label(plain_name, TextFactory::generateColorFromText(name_id)); + + new_lbl->setCustomId(name_id); + lbls.append(new_lbl); + } + + return lbls; +} + +QString FeedlyNetwork::username() const { + return m_username; +} + +void FeedlyNetwork::setUsername(const QString& username) { + m_username = username; +} + +QString FeedlyNetwork::developerAccessToken() const { + return m_developerAccessToken; +} + +void FeedlyNetwork::setDeveloperAccessToken(const QString& dev_acc_token) { + m_developerAccessToken = dev_acc_token; +} + +int FeedlyNetwork::batchSize() const { + return m_batchSize; +} + +void FeedlyNetwork::setBatchSize(int batch_size) { + m_batchSize = batch_size; +} + +#if defined(FEEDLY_OFFICIAL_SUPPORT) + +void FeedlyNetwork::onTokensError(const QString& error, const QString& error_description) { + Q_UNUSED(error) + + qApp->showGuiMessage(Notification::Event::LoginFailure, + {tr("Feedly: authentication error"), + tr("Click this to login again. Error is: '%1'").arg(error_description), + QSystemTrayIcon::MessageIcon::Critical}, + {}, + {tr("Login"), [this]() { + m_oauth->setAccessToken(QString()); + m_oauth->setRefreshToken(QString()); + + // m_oauth->logout(false); + m_oauth->login(); + }}); +} + +void FeedlyNetwork::onAuthFailed() { + qApp->showGuiMessage(Notification::Event::LoginFailure, + {tr("Feedly: authorization denied"), + tr("Click this to login again."), + QSystemTrayIcon::MessageIcon::Critical}, + {}, + {tr("Login"), [this]() { + // m_oauth->logout(false); + m_oauth->login(); + }}); +} + +void FeedlyNetwork::onTokensRetrieved(const QString& access_token, const 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()); + } +} + +OAuth2Service* FeedlyNetwork::oauth() const { + return m_oauth; +} + +void FeedlyNetwork::setOauth(OAuth2Service* oauth) { + m_oauth = oauth; +} + +#endif + +QString FeedlyNetwork::fullUrl(FeedlyNetwork::Service service) const { + switch (service) { + case Service::Profile: + return QSL(FEEDLY_API_URL_BASE) + QSL(FEEDLY_API_URL_PROFILE); + + case Service::Collections: + return QSL(FEEDLY_API_URL_BASE) + QSL(FEEDLY_API_URL_COLLETIONS); + + case Service::Tags: + case Service::TagEntries: + return QSL(FEEDLY_API_URL_BASE) + QSL(FEEDLY_API_URL_TAGS); + + case Service::StreamContents: + return QSL(FEEDLY_API_URL_BASE) + QSL(FEEDLY_API_URL_STREAM_CONTENTS); + + case Service::StreamIds: + return QSL(FEEDLY_API_URL_BASE) + QSL(FEEDLY_API_URL_STREAM_IDS); + + case Service::Entries: + return QSL(FEEDLY_API_URL_BASE) + QSL(FEEDLY_API_URL_ENTRIES); + + case Service::Markers: + return QSL(FEEDLY_API_URL_BASE) + QSL(FEEDLY_API_URL_MARKERS); + + default: + return QSL(FEEDLY_API_URL_BASE); + } +} + +QString FeedlyNetwork::bearer() const { +#if defined(FEEDLY_OFFICIAL_SUPPORT) + if (m_developerAccessToken.simplified().isEmpty()) { + return m_oauth->bearer().toLocal8Bit(); + } +#endif + + return QSL("Bearer %1").arg(m_developerAccessToken); +} + +QPair FeedlyNetwork::bearerHeader(const QString& bearer) const { + return {QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), bearer.toLocal8Bit()}; +} + +void FeedlyNetwork::setIntelligentSynchronization(bool intelligent_sync) { + m_intelligentSynchronization = intelligent_sync; +} + +bool FeedlyNetwork::intelligentSynchronization() const { + return m_intelligentSynchronization; +} + +bool FeedlyNetwork::downloadOnlyUnreadMessages() const { + return m_downloadOnlyUnreadMessages; +} + +void FeedlyNetwork::setDownloadOnlyUnreadMessages(bool download_only_unread_messages) { + m_downloadOnlyUnreadMessages = download_only_unread_messages; +} + +void FeedlyNetwork::setService(FeedlyServiceRoot* service) { + m_service = service; +} diff --git a/src/librssguard-feedly/src/feedlynetwork.h b/src/librssguard-feedly/src/feedlynetwork.h new file mode 100644 index 000000000..25024516c --- /dev/null +++ b/src/librssguard-feedly/src/feedlynetwork.h @@ -0,0 +1,110 @@ +// For license of this file, see /LICENSE.md. + +#ifndef FEEDLYNETWORK_H +#define FEEDLYNETWORK_H + +#include +#include +#include + +#include + +#if defined(FEEDLY_OFFICIAL_SUPPORT) +class OAuth2Service; +#endif + +class FeedlyServiceRoot; + +class FeedlyNetwork : public QObject { + Q_OBJECT + + public: + explicit FeedlyNetwork(QObject* parent = nullptr); + + QList messages(const QString& stream_id, + const QHash& stated_messages); + + // API operations. + void untagEntries(const QString& tag_id, const QStringList& msg_custom_ids); + void tagEntries(const QString& tag_id, const QStringList& msg_custom_ids); + void markers(const QString& action, const QStringList& msg_custom_ids); + QList entries(const QStringList& ids); + QList streamContents(const QString& stream_id); + QStringList streamIds(const QString& stream_id, bool unread_only, int batch_size); + QVariantHash profile(const QNetworkProxy& network_proxy); + QList tags(); + RootItem* collections(bool obtain_icons); + + // Getters and setters. + QString username() const; + void setUsername(const QString& username); + + QString developerAccessToken() const; + void setDeveloperAccessToken(const QString& dev_acc_token); + + bool downloadOnlyUnreadMessages() const; + void setDownloadOnlyUnreadMessages(bool download_only_unread_messages); + + bool intelligentSynchronization() const; + void setIntelligentSynchronization(bool intelligent_sync); + + int batchSize() const; + void setBatchSize(int batch_size); + + void setService(FeedlyServiceRoot* service); + +#if defined(FEEDLY_OFFICIAL_SUPPORT) + OAuth2Service* oauth() const; + void setOauth(OAuth2Service* oauth); + + private slots: + void onTokensError(const QString& error, const QString& error_description); + void onAuthFailed(); + void onTokensRetrieved(const QString& access_token, const QString& refresh_token, int expires_in); +#endif + + private: + enum class Service { + Profile, + Collections, + Tags, + StreamContents, + Markers, + TagEntries, + StreamIds, + Entries + }; + + QString fullUrl(Service service) const; + QString bearer() const; + QStringList decodeStreamIds(const QByteArray& stream_ids, QString& continuation) const; + QList decodeStreamContents(const QByteArray& stream_contents, + bool nested_items, + QString& continuation) const; + RootItem* decodeCollections(const QByteArray& json, + bool obtain_icons, + const QNetworkProxy& proxy, + int timeout = 0) const; + QPair bearerHeader(const QString& bearer) const; + + private: + FeedlyServiceRoot* m_service; + +#if defined(FEEDLY_OFFICIAL_SUPPORT) + OAuth2Service* m_oauth; +#endif + + QString m_username; + QString m_developerAccessToken; + + // Only download N newest messages per feed. + int m_batchSize; + + // Only download unread messages. + bool m_downloadOnlyUnreadMessages; + + // Better synchronization algorithm. + bool m_intelligentSynchronization; +}; + +#endif // FEEDLYNETWORK_H diff --git a/src/librssguard-feedly/src/feedlyserviceroot.cpp b/src/librssguard-feedly/src/feedlyserviceroot.cpp new file mode 100644 index 000000000..3b6ff79e0 --- /dev/null +++ b/src/librssguard-feedly/src/feedlyserviceroot.cpp @@ -0,0 +1,250 @@ +// For license of this file, see /LICENSE.md. + +#include "src/feedlyserviceroot.h" + +#include "src/definitions.h" +#include "src/feedlyentrypoint.h" +#include "src/feedlynetwork.h" +#include "src/gui/formeditfeedlyaccount.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(FEEDLY_OFFICIAL_SUPPORT) +#include +#endif + +FeedlyServiceRoot::FeedlyServiceRoot(RootItem* parent) : ServiceRoot(parent), m_network(new FeedlyNetwork(this)) { + setIcon(FeedlyEntryPoint().icon()); + m_network->setService(this); +} + +bool FeedlyServiceRoot::isSyncable() const { + return true; +} + +bool FeedlyServiceRoot::canBeEdited() const { + return true; +} + +FormAccountDetails* FeedlyServiceRoot::accountSetupDialog() const { + return new FormEditFeedlyAccount(qApp->mainFormWidget()); +} + +void FeedlyServiceRoot::editItems(const QList& items) { + if (items.first()->kind() == RootItem::Kind::ServiceRoot) { + QScopedPointer p(qobject_cast(accountSetupDialog())); + + p->addEditAccount(this); + return; + } + + ServiceRoot::editItems(items); +} + +QVariantHash FeedlyServiceRoot::customDatabaseData() const { + QVariantHash data = ServiceRoot::customDatabaseData(); + + data[QSL("username")] = m_network->username(); + data[QSL("dat")] = m_network->developerAccessToken(); + +#if defined(FEEDLY_OFFICIAL_SUPPORT) + data[QSL("refresh_token")] = m_network->oauth()->refreshToken(); +#endif + + data[QSL("batch_size")] = m_network->batchSize(); + data[QSL("download_only_unread")] = m_network->downloadOnlyUnreadMessages(); + data[QSL("intelligent_synchronization")] = m_network->intelligentSynchronization(); + + return data; +} + +void FeedlyServiceRoot::setCustomDatabaseData(const QVariantHash& data) { + ServiceRoot::setCustomDatabaseData(data); + + m_network->setUsername(data[QSL("username")].toString()); + m_network->setDeveloperAccessToken(data[QSL("dat")].toString()); + +#if defined(FEEDLY_OFFICIAL_SUPPORT) + m_network->oauth()->setRefreshToken(data[QSL("refresh_token")].toString()); +#endif + + m_network->setBatchSize(data[QSL("batch_size")].toInt()); + m_network->setDownloadOnlyUnreadMessages(data[QSL("download_only_unread")].toBool()); + m_network->setIntelligentSynchronization(data[QSL("intelligent_synchronization")].toBool()); +} + +QList FeedlyServiceRoot::obtainNewMessages(Feed* feed, + const QHash& + stated_messages, + const QHash& tagged_messages) { + Q_UNUSED(tagged_messages) + + try { + return m_network->messages(feed->customId(), stated_messages); + } + catch (const ApplicationException& ex) { + throw FeedFetchException(Feed::Status::NetworkError, ex.message()); + } +} + +void FeedlyServiceRoot::start(bool freshly_activated) { + if (!freshly_activated) { + DatabaseQueries::loadRootFromDatabase(this); + loadCacheFromFile(); + } + + updateTitle(); + + if (getSubTreeFeeds().isEmpty()) { +#if defined(FEEDLY_OFFICIAL_SUPPORT) + m_network->oauth()->login([this]() { + syncIn(); + }); +#else + syncIn(); +#endif + } + +#if defined(FEEDLY_OFFICIAL_SUPPORT) + else { + m_network->oauth()->login(); + } +#endif +} + +QString FeedlyServiceRoot::code() const { + return FeedlyEntryPoint().code(); +} + +void FeedlyServiceRoot::saveAllCachedData(bool ignore_errors) { + auto msg_cache = takeMessageCache(); + QMapIterator i(msg_cache.m_cachedStatesRead); + + // Save the actual data read/unread. + while (i.hasNext()) { + i.next(); + auto key = i.key(); + QStringList ids = i.value(); + + if (!ids.isEmpty()) { + try { + network()->markers(key == RootItem::ReadStatus::Read ? QSL(FEEDLY_MARKERS_READ) : QSL(FEEDLY_MARKERS_UNREAD), + ids); + } + catch (const NetworkException& net_ex) { + qCriticalNN << LOGSEC_FEEDLY + << "Failed to synchronize read/unread state with error:" << QUOTE_W_SPACE(net_ex.message()) + << "and HTTP code" << QUOTE_W_SPACE_DOT(net_ex.networkError()); + + if (!ignore_errors) { + addMessageStatesToCache(ids, key); + } + } + } + } + + QMapIterator> j(msg_cache.m_cachedStatesImportant); + + // Save the actual data important/not important. + while (j.hasNext()) { + j.next(); + auto key = j.key(); + QList messages = j.value(); + + if (!messages.isEmpty()) { + QStringList ids = customIDsOfMessages(messages); + + try { + network()->markers(key == RootItem::Importance::Important ? FEEDLY_MARKERS_IMPORTANT + : FEEDLY_MARKERS_UNIMPORTANT, + ids); + } + catch (const NetworkException& net_ex) { + qCriticalNN << LOGSEC_FEEDLY << "Failed to synchronize important/unimportant state with error:" + << QUOTE_W_SPACE(net_ex.message()) << "and HTTP code" << QUOTE_W_SPACE_DOT(net_ex.networkError()); + + if (!ignore_errors) { + addMessageStatesToCache(messages, key); + } + } + } + } + + QMapIterator k(msg_cache.m_cachedLabelAssignments); + + // Assign label for these messages. + while (k.hasNext()) { + k.next(); + auto label_custom_id = k.key(); + QStringList messages = k.value(); + + if (!messages.isEmpty()) { + try { + network()->tagEntries(label_custom_id, messages); + } + catch (const NetworkException& net_ex) { + qCriticalNN << LOGSEC_FEEDLY + << "Failed to synchronize tag assignments with error:" << QUOTE_W_SPACE(net_ex.message()) + << "and HTTP code" << QUOTE_W_SPACE_DOT(net_ex.networkError()); + + if (!ignore_errors) { + addLabelsAssignmentsToCache(messages, label_custom_id, true); + } + } + } + } + + QMapIterator l(msg_cache.m_cachedLabelDeassignments); + + // Remove label from these messages. + while (l.hasNext()) { + l.next(); + auto label_custom_id = l.key(); + QStringList messages = l.value(); + + if (!messages.isEmpty()) { + try { + network()->untagEntries(label_custom_id, messages); + } + catch (const NetworkException& net_ex) { + qCriticalNN << LOGSEC_FEEDLY + << "Failed to synchronize tag DEassignments with error:" << QUOTE_W_SPACE(net_ex.message()) + << "and HTTP code" << QUOTE_W_SPACE_DOT(net_ex.networkError()); + + if (!ignore_errors) { + addLabelsAssignmentsToCache(messages, label_custom_id, false); + } + } + } + } +} + +ServiceRoot::LabelOperation FeedlyServiceRoot::supportedLabelOperations() const { + return ServiceRoot::LabelOperation::Synchronised; +} + +void FeedlyServiceRoot::updateTitle() { + setTitle(QSL("%1 (Feedly)").arg(TextFactory::extractUsernameFromEmail(m_network->username()))); +} + +RootItem* FeedlyServiceRoot::obtainNewTreeForSyncIn() const { + auto tree = m_network->collections(true); + auto* lblroot = new LabelsNode(tree); + auto labels = m_network->tags(); + + lblroot->setChildItems(labels); + tree->appendChild(lblroot); + + return tree; +} + +bool FeedlyServiceRoot::wantsBaggedIdsOfExistingMessages() const { + return m_network->intelligentSynchronization(); +} diff --git a/src/librssguard-feedly/src/feedlyserviceroot.h b/src/librssguard-feedly/src/feedlyserviceroot.h new file mode 100644 index 000000000..b4d2e32e6 --- /dev/null +++ b/src/librssguard-feedly/src/feedlyserviceroot.h @@ -0,0 +1,48 @@ +// For license of this file, see /LICENSE.md. + +#ifndef FEEDLYSERVICEROOT_H +#define FEEDLYSERVICEROOT_H + +#include +#include + +class FeedlyNetwork; + +class FeedlyServiceRoot : public ServiceRoot, public CacheForServiceRoot { + Q_OBJECT + + public: + explicit FeedlyServiceRoot(RootItem* parent = nullptr); + + virtual bool isSyncable() const; + virtual bool canBeEdited() const; + virtual void editItems(const QList& items); + virtual FormAccountDetails* accountSetupDialog() const; + virtual void start(bool freshly_activated); + virtual QString code() const; + virtual void saveAllCachedData(bool ignore_errors); + virtual LabelOperation supportedLabelOperations() const; + virtual QVariantHash customDatabaseData() const; + virtual void setCustomDatabaseData(const QVariantHash& data); + virtual bool wantsBaggedIdsOfExistingMessages() const; + virtual QList obtainNewMessages(Feed* feed, + const QHash& stated_messages, + const QHash& tagged_messages); + + FeedlyNetwork* network() const; + + protected: + virtual RootItem* obtainNewTreeForSyncIn() const; + + private: + void updateTitle(); + + private: + FeedlyNetwork* m_network; +}; + +inline FeedlyNetwork* FeedlyServiceRoot::network() const { + return m_network; +} + +#endif // FEEDLYSERVICEROOT_H diff --git a/src/librssguard-feedly/src/gui/feedlyaccountdetails.cpp b/src/librssguard-feedly/src/gui/feedlyaccountdetails.cpp new file mode 100644 index 000000000..ad29b3bc6 --- /dev/null +++ b/src/librssguard-feedly/src/gui/feedlyaccountdetails.cpp @@ -0,0 +1,184 @@ +// For license of this file, see /LICENSE.md. + +#include "src/gui/feedlyaccountdetails.h" + +#include "src/definitions.h" +#include "src/feedlynetwork.h" + +#include +#include +#include +#include + +#if defined(FEEDLY_OFFICIAL_SUPPORT) +#include +#endif + +#include + +FeedlyAccountDetails::FeedlyAccountDetails(QWidget* parent) : QWidget(parent), m_lastProxy({}) { +#if defined(FEEDLY_OFFICIAL_SUPPORT) + m_oauth = nullptr; +#endif + + m_ui.setupUi(this); + + m_ui.m_lblTestResult->label()->setWordWrap(true); + m_ui.m_txtUsername->lineEdit()->setPlaceholderText(tr("Username for your account")); + m_ui.m_txtDeveloperAccessToken->lineEdit()->setPlaceholderText(tr("Developer access token")); + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Information, + tr("No test done yet."), + tr("Here, results of connection test are shown.")); + +#if defined(FEEDLY_OFFICIAL_SUPPORT) + m_ui.m_lblInfo->setHelpText(tr("Your %1 build has official Feedly support. You do not have to use \"developer access " + "token\". You can therefore leave corresponding field empty.") + .arg(QSL(APP_NAME)), + false); +#else + m_ui.m_lblInfo->setHelpText(tr("Your %1 does not offer official Feedly support, thus you must " + "authorize via special authorization code called \"developer access token\". " + "These tokens are usually valid only for 1 month and allow only 250 API calls " + "each day.") + .arg(QSL(APP_NAME)), + true); +#endif + + m_ui.m_lblLimitMessagesInfo->setHelpText(tr("Beware of downloading too many articles, because " + "Feedly permanently caches ALL articles of the feed, so you might " + "end up with thousands of articles which you will never read anyway."), + true); + + m_ui.m_lblNewAlgorithm->setHelpText(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, but " + "first feed fetching could be slow anyway if your feed contains " + "huge number of articles."), + false); + + connect(m_ui.m_btnGetToken, &QPushButton::clicked, this, &FeedlyAccountDetails::getDeveloperAccessToken); + connect(m_ui.m_txtUsername->lineEdit(), &BaseLineEdit::textChanged, this, &FeedlyAccountDetails::onUsernameChanged); + connect(m_ui.m_txtDeveloperAccessToken->lineEdit(), + &BaseLineEdit::textChanged, + this, + &FeedlyAccountDetails::onDeveloperAccessTokenChanged); + + setTabOrder(m_ui.m_txtUsername->lineEdit(), m_ui.m_btnGetToken); + setTabOrder(m_ui.m_btnGetToken, m_ui.m_txtDeveloperAccessToken->lineEdit()); + setTabOrder(m_ui.m_txtDeveloperAccessToken->lineEdit(), m_ui.m_checkDownloadOnlyUnreadMessages); + setTabOrder(m_ui.m_checkDownloadOnlyUnreadMessages, m_ui.m_cbNewAlgorithm); + setTabOrder(m_ui.m_cbNewAlgorithm, m_ui.m_spinLimitMessages); + setTabOrder(m_ui.m_spinLimitMessages, m_ui.m_btnTestSetup); + + onDeveloperAccessTokenChanged(); + onUsernameChanged(); + +#if defined(FEEDLY_OFFICIAL_SUPPORT) + hookNetwork(); +#endif +} + +void FeedlyAccountDetails::getDeveloperAccessToken() { + qApp->web()->openUrlInExternalBrowser(QSL(FEEDLY_GENERATE_DAT)); +} + +#if defined(FEEDLY_OFFICIAL_SUPPORT) + +void FeedlyAccountDetails::hookNetwork() { + connect(m_oauth, &OAuth2Service::tokensRetrieved, this, &FeedlyAccountDetails::onAuthGranted); + connect(m_oauth, &OAuth2Service::tokensRetrieveError, this, &FeedlyAccountDetails::onAuthError); + connect(m_oauth, &OAuth2Service::authFailed, this, &FeedlyAccountDetails::onAuthFailed); +} + +void FeedlyAccountDetails::onAuthFailed() { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error, + tr("You did not grant access."), + tr("There was error during testing.")); +} + +void FeedlyAccountDetails::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 FeedlyAccountDetails::onAuthGranted() { + FeedlyNetwork factory; + + factory.setOauth(m_oauth); + + try { + auto prof = factory.profile(m_lastProxy); + + m_ui.m_txtUsername->lineEdit()->setText(prof[QSL("email")].toString()); + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Ok, + tr("Tested successfully. You may be prompted to login once more."), + tr("Your access was approved.")); + } + catch (const ApplicationException& ex) { + qCriticalNN << LOGSEC_FEEDLY << "Failed to obtain profile with error:" << QUOTE_W_SPACE_DOT(ex.message()); + } +} + +#endif + +void FeedlyAccountDetails::performTest(const QNetworkProxy& custom_proxy) { + m_lastProxy = custom_proxy; + +#if defined(FEEDLY_OFFICIAL_SUPPORT) + m_oauth->logout(false); + + if (m_ui.m_txtDeveloperAccessToken->lineEdit()->text().simplified().isEmpty()) { + m_oauth->login(); + return; + } +#endif + + FeedlyNetwork factory; + + factory.setDeveloperAccessToken(m_ui.m_txtDeveloperAccessToken->lineEdit()->text()); + + try { + auto prof = factory.profile(custom_proxy); + + m_ui.m_txtUsername->lineEdit()->setText(prof[QSL("email")].toString()); + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Ok, + tr("Login was successful."), + tr("Access granted.")); + } + catch (const NetworkException& ex) { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error, + tr("Error: '%1'").arg(ex.message()), + tr("Some problems.")); + } +} + +void FeedlyAccountDetails::onUsernameChanged() { + const QString username = m_ui.m_txtUsername->lineEdit()->text(); + + if (username.isEmpty()) { + m_ui.m_txtUsername->setStatus(WidgetWithStatus::StatusType::Error, tr("Username cannot be empty.")); + } + else { + m_ui.m_txtUsername->setStatus(WidgetWithStatus::StatusType::Ok, tr("Username is okay.")); + } +} + +void FeedlyAccountDetails::onDeveloperAccessTokenChanged() { + const QString token = m_ui.m_txtDeveloperAccessToken->lineEdit()->text(); + + if (token.isEmpty()) { +#if defined(FEEDLY_OFFICIAL_SUPPORT) + WidgetWithStatus::StatusType stat = WidgetWithStatus::StatusType::Ok; +#else + WidgetWithStatus::StatusType stat = WidgetWithStatus::StatusType::Error; +#endif + + m_ui.m_txtDeveloperAccessToken->setStatus(stat, tr("Access token is empty.")); + } + else { + m_ui.m_txtDeveloperAccessToken->setStatus(WidgetWithStatus::StatusType::Ok, tr("Access token is okay.")); + } +} diff --git a/src/librssguard-feedly/src/gui/feedlyaccountdetails.h b/src/librssguard-feedly/src/gui/feedlyaccountdetails.h new file mode 100644 index 000000000..93be136af --- /dev/null +++ b/src/librssguard-feedly/src/gui/feedlyaccountdetails.h @@ -0,0 +1,51 @@ +// For license of this file, see /LICENSE.md. + +#ifndef FEEDLYACCOUNTDETAILS_H +#define FEEDLYACCOUNTDETAILS_H + +#include "src/feedlyserviceroot.h" + +#include "ui_feedlyaccountdetails.h" + +#include +#include + +#if defined(FEEDLY_OFFICIAL_SUPPORT) +class OAuth2Service; +#endif + +class FeedlyAccountDetails : public QWidget { + Q_OBJECT + + friend class FormEditFeedlyAccount; + + public: + explicit FeedlyAccountDetails(QWidget* parent = nullptr); + + private slots: + void getDeveloperAccessToken(); + void performTest(const QNetworkProxy& custom_proxy); + void onUsernameChanged(); + void onDeveloperAccessTokenChanged(); + +#if defined(FEEDLY_OFFICIAL_SUPPORT) + private slots: + void onAuthFailed(); + void onAuthError(const QString& error, const QString& detailed_description); + void onAuthGranted(); + + private: + void hookNetwork(); +#endif + + private: + Ui::FeedlyAccountDetails m_ui; + +#if defined(FEEDLY_OFFICIAL_SUPPORT) + OAuth2Service* m_oauth; +#endif + + QNetworkProxy m_lastProxy; +}; + +#endif // FEEDLYACCOUNTDETAILS_H diff --git a/src/librssguard-feedly/src/gui/feedlyaccountdetails.ui b/src/librssguard-feedly/src/gui/feedlyaccountdetails.ui new file mode 100644 index 000000000..45bdac98a --- /dev/null +++ b/src/librssguard-feedly/src/gui/feedlyaccountdetails.ui @@ -0,0 +1,159 @@ + + + FeedlyAccountDetails + + + + 0 + 0 + 421 + 321 + + + + + + + Username + + + + + + + + + + Developer access token + + + + + + + + + Get token + + + + + + + + + + + + + + + Download unread articles only + + + + + + + Intelligent synchronization algorithm + + + + + + + + + + + + Only download newest X articles per feed + + + m_spinLimitMessages + + + + + + + + 140 + 16777215 + + + + + + + + + + + + + Qt::Vertical + + + + 400 + 86 + + + + + + + + + + &Login + + + + + + + Qt::RightToLeft + + + + + + + + + + LabelWithStatus + QWidget +
labelwithstatus.h
+ 1 +
+ + LineEditWithStatus + QWidget +
lineeditwithstatus.h
+ 1 +
+ + MessageCountSpinBox + QSpinBox +
messagecountspinbox.h
+
+ + HelpSpoiler + QWidget +
helpspoiler.h
+ 1 +
+
+ + m_btnGetToken + m_checkDownloadOnlyUnreadMessages + m_cbNewAlgorithm + m_spinLimitMessages + m_btnTestSetup + + + +
diff --git a/src/librssguard-feedly/src/gui/formeditfeedlyaccount.cpp b/src/librssguard-feedly/src/gui/formeditfeedlyaccount.cpp new file mode 100644 index 000000000..1797f4f01 --- /dev/null +++ b/src/librssguard-feedly/src/gui/formeditfeedlyaccount.cpp @@ -0,0 +1,74 @@ +// For license of this file, see /LICENSE.md. + +#include "src/gui/formeditfeedlyaccount.h" + +#include "src/feedlynetwork.h" +#include "src/feedlyserviceroot.h" +#include "src/gui/feedlyaccountdetails.h" + +#include + +#if defined(FEEDLY_OFFICIAL_SUPPORT) +#include +#endif + +FormEditFeedlyAccount::FormEditFeedlyAccount(QWidget* parent) + : FormAccountDetails(qApp->icons()->miscIcon(QSL("feedly")), parent), m_details(new FeedlyAccountDetails(this)) { + insertCustomTab(m_details, tr("Service setup"), 0); + activateTab(0); + + connect(m_details->m_ui.m_btnTestSetup, &QPushButton::clicked, this, &FormEditFeedlyAccount::performTest); + m_details->m_ui.m_txtUsername->setFocus(); +} + +void FormEditFeedlyAccount::apply() { + FormAccountDetails::apply(); + +#if defined(FEEDLY_OFFICIAL_SUPPORT) + account()->network()->oauth()->logout(false); +#endif + + bool using_another_acc = + m_details->m_ui.m_txtUsername->lineEdit()->text() != account()->network()->username(); + + account()->network()->setUsername(m_details->m_ui.m_txtUsername->lineEdit()->text()); + account() + ->network() + ->setDownloadOnlyUnreadMessages(m_details->m_ui.m_checkDownloadOnlyUnreadMessages->isChecked()); + account()->network()->setBatchSize(m_details->m_ui.m_spinLimitMessages->value()); + account()->network()->setDeveloperAccessToken(m_details->m_ui.m_txtDeveloperAccessToken->lineEdit() + ->text()); + account()->network()->setIntelligentSynchronization(m_details->m_ui.m_cbNewAlgorithm->isChecked()); + + account()->saveAccountDataToDatabase(); + accept(); + + if (!m_creatingNew) { + if (using_another_acc) { + account()->completelyRemoveAllData(); + } + + account()->start(true); + } +} + +void FormEditFeedlyAccount::loadAccountData() { + FormAccountDetails::loadAccountData(); + +#if defined(FEEDLY_OFFICIAL_SUPPORT) + m_details->m_oauth = account()->network()->oauth(); + m_details->hookNetwork(); +#endif + + m_details->m_ui.m_txtUsername->lineEdit()->setText(account()->network()->username()); + m_details->m_ui.m_txtDeveloperAccessToken->lineEdit() + ->setText(account()->network()->developerAccessToken()); + m_details->m_ui.m_checkDownloadOnlyUnreadMessages + ->setChecked(account()->network()->downloadOnlyUnreadMessages()); + m_details->m_ui.m_spinLimitMessages->setValue(account()->network()->batchSize()); + m_details->m_ui.m_cbNewAlgorithm->setChecked(account()->network()->intelligentSynchronization()); +} + +void FormEditFeedlyAccount::performTest() { + m_details->performTest(m_proxyDetails->proxy()); +} diff --git a/src/librssguard-feedly/src/gui/formeditfeedlyaccount.h b/src/librssguard-feedly/src/gui/formeditfeedlyaccount.h new file mode 100644 index 000000000..e69d5707d --- /dev/null +++ b/src/librssguard-feedly/src/gui/formeditfeedlyaccount.h @@ -0,0 +1,30 @@ +// For license of this file, see /LICENSE.md. + +#ifndef FORMEDITFEEDLYACCOUNT_H +#define FORMEDITFEEDLYACCOUNT_H + +#include + +class FeedlyAccountDetails; +class FeedlyServiceRoot; + +class FormEditFeedlyAccount : public FormAccountDetails { + Q_OBJECT + + public: + explicit FormEditFeedlyAccount(QWidget* parent = nullptr); + + protected slots: + virtual void apply(); + + protected: + virtual void loadAccountData(); + + private slots: + void performTest(); + + private: + FeedlyAccountDetails* m_details; +}; + +#endif // FORMEDITFEEDLYACCOUNT_H diff --git a/src/librssguard-gmail/CMakeLists.txt b/src/librssguard-gmail/CMakeLists.txt new file mode 100644 index 000000000..5c68b304c --- /dev/null +++ b/src/librssguard-gmail/CMakeLists.txt @@ -0,0 +1,96 @@ +if(NOT DEFINED LIBRSSGUARD_BINARY_PATH) + set(LIBRSSGUARD_SOURCE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/..") +endif() + +set(SOURCES + src/definitions.h + src/gmailentrypoint.cpp + src/gmailentrypoint.h + src/gmailnetworkfactory.cpp + src/gmailnetworkfactory.h + src/gmailserviceroot.cpp + src/gmailserviceroot.h + src/gui/emailpreviewer.cpp + src/gui/emailpreviewer.h + src/gui/emailrecipientcontrol.cpp + src/gui/emailrecipientcontrol.h + src/gui/formaddeditemail.cpp + src/gui/formaddeditemail.h + src/gui/formeditgmailaccount.cpp + src/gui/formeditgmailaccount.h + src/gui/gmailaccountdetails.cpp + src/gui/gmailaccountdetails.h + + src/3rd-party/mimesis/mimesis.cpp + src/3rd-party/mimesis/mimesis.hpp + src/3rd-party/mimesis/quoted-printable.cpp + src/3rd-party/mimesis/quoted-printable.hpp + + src/3rd-party/richtexteditor/mrichtextedit.cpp + src/3rd-party/richtexteditor/mrichtextedit.h + src/3rd-party/richtexteditor/mtextedit.cpp + src/3rd-party/richtexteditor/mtextedit.h +) + +set(UI_FILES + src/gui/emailpreviewer.ui + src/gui/formaddeditemail.ui + src/gui/gmailaccountdetails.ui + src/3rd-party/richtexteditor/mrichtextedit.ui +) + +# Deal with .ui files. +qt_wrap_ui(SOURCES ${UI_FILES}) + +# Bundle version info. +if(WIN32) + enable_language("RC") + list(APPEND SOURCES "${CMAKE_BINARY_DIR}/rssguard.rc") +endif() + +add_library(rssguard-gmail SHARED ${SOURCES} ${QM_FILES}) + +# Add specific definitions. +target_compile_definitions(rssguard-gmail + PRIVATE + RSSGUARD_DLLSPEC=Q_DECL_IMPORT + RSSGUARD_DLLSPEC_EXPORT=Q_DECL_EXPORT +) + +target_include_directories(rssguard-gmail + PUBLIC + ${LIBRSSGUARD_SOURCE_PATH} + src/3rd-party/richtexteditor +) + +# Qt. +target_link_libraries(rssguard-gmail PUBLIC + rssguard + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Network + Qt${QT_VERSION_MAJOR}::Qml + Qt${QT_VERSION_MAJOR}::Sql + Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::Xml + Qt${QT_VERSION_MAJOR}::Concurrent +) + +#if(QT_VERSION_MAJOR EQUAL 6) +# target_link_libraries(rssguard-feedly PUBLIC +# Qt${QT_VERSION_MAJOR}::Core5Compat +# ) +#endif() + +if(WIN32 OR OS2) + install(TARGETS rssguard-gmail DESTINATION plugins) +elseif(UNIX AND NOT APPLE AND NOT ANDROID) + include (GNUInstallDirs) + install(TARGETS rssguard-gmail + DESTINATION ${CMAKE_INSTALL_LIBDIR}/rssguard + ) +elseif(APPLE) + install(TARGETS rssguard-gmail + DESTINATION Contents/MacOS + ) +endif() diff --git a/src/librssguard-gmail/plugin.json b/src/librssguard-gmail/plugin.json new file mode 100644 index 000000000..000a24bbf --- /dev/null +++ b/src/librssguard-gmail/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "Gmail", + "author": "Martin Rotter", + "website": "https://github.com/martinrotter/rssguard" +} \ No newline at end of file diff --git a/src/librssguard/3rd-party/mimesis/mimesis.cpp b/src/librssguard-gmail/src/3rd-party/mimesis/mimesis.cpp similarity index 100% rename from src/librssguard/3rd-party/mimesis/mimesis.cpp rename to src/librssguard-gmail/src/3rd-party/mimesis/mimesis.cpp diff --git a/src/librssguard/3rd-party/mimesis/mimesis.hpp b/src/librssguard-gmail/src/3rd-party/mimesis/mimesis.hpp similarity index 100% rename from src/librssguard/3rd-party/mimesis/mimesis.hpp rename to src/librssguard-gmail/src/3rd-party/mimesis/mimesis.hpp diff --git a/src/librssguard/3rd-party/mimesis/quoted-printable.cpp b/src/librssguard-gmail/src/3rd-party/mimesis/quoted-printable.cpp similarity index 100% rename from src/librssguard/3rd-party/mimesis/quoted-printable.cpp rename to src/librssguard-gmail/src/3rd-party/mimesis/quoted-printable.cpp diff --git a/src/librssguard/3rd-party/mimesis/quoted-printable.hpp b/src/librssguard-gmail/src/3rd-party/mimesis/quoted-printable.hpp similarity index 100% rename from src/librssguard/3rd-party/mimesis/quoted-printable.hpp rename to src/librssguard-gmail/src/3rd-party/mimesis/quoted-printable.hpp diff --git a/src/librssguard/gui/richtexteditor/mrichtextedit.cpp b/src/librssguard-gmail/src/3rd-party/richtexteditor/mrichtextedit.cpp similarity index 99% rename from src/librssguard/gui/richtexteditor/mrichtextedit.cpp rename to src/librssguard-gmail/src/3rd-party/richtexteditor/mrichtextedit.cpp index fc6b4a85f..90f1d4a9b 100644 --- a/src/librssguard/gui/richtexteditor/mrichtextedit.cpp +++ b/src/librssguard-gmail/src/3rd-party/richtexteditor/mrichtextedit.cpp @@ -25,11 +25,11 @@ ** $QT_END_LICENSE$ */ -#include "gui/richtexteditor/mrichtextedit.h" +#include "src/3rd-party/richtexteditor/mrichtextedit.h" -#include "definitions/definitions.h" -#include "miscellaneous/application.h" -#include "miscellaneous/iconfactory.h" +#include +#include +#include #include #include diff --git a/src/librssguard/gui/richtexteditor/mrichtextedit.h b/src/librssguard-gmail/src/3rd-party/richtexteditor/mrichtextedit.h similarity index 100% rename from src/librssguard/gui/richtexteditor/mrichtextedit.h rename to src/librssguard-gmail/src/3rd-party/richtexteditor/mrichtextedit.h diff --git a/src/librssguard/gui/richtexteditor/mrichtextedit.ui b/src/librssguard-gmail/src/3rd-party/richtexteditor/mrichtextedit.ui similarity index 100% rename from src/librssguard/gui/richtexteditor/mrichtextedit.ui rename to src/librssguard-gmail/src/3rd-party/richtexteditor/mrichtextedit.ui diff --git a/src/librssguard/gui/richtexteditor/mtextedit.cpp b/src/librssguard-gmail/src/3rd-party/richtexteditor/mtextedit.cpp similarity index 96% rename from src/librssguard/gui/richtexteditor/mtextedit.cpp rename to src/librssguard-gmail/src/3rd-party/richtexteditor/mtextedit.cpp index c99b70816..aa0a8090a 100644 --- a/src/librssguard/gui/richtexteditor/mtextedit.cpp +++ b/src/librssguard-gmail/src/3rd-party/richtexteditor/mtextedit.cpp @@ -2,10 +2,9 @@ // // For license of this file, see /resources/text/COPYING_GNU_LGPL_21. -#include "gui/richtexteditor/mtextedit.h" - -#include "definitions/definitions.h" +#include "src/3rd-party/richtexteditor/mtextedit.h" +#include #include #include diff --git a/src/librssguard/gui/richtexteditor/mtextedit.h b/src/librssguard-gmail/src/3rd-party/richtexteditor/mtextedit.h similarity index 100% rename from src/librssguard/gui/richtexteditor/mtextedit.h rename to src/librssguard-gmail/src/3rd-party/richtexteditor/mtextedit.h diff --git a/src/librssguard/services/gmail/definitions.h b/src/librssguard-gmail/src/definitions.h similarity index 100% rename from src/librssguard/services/gmail/definitions.h rename to src/librssguard-gmail/src/definitions.h diff --git a/src/librssguard/services/gmail/gmailentrypoint.cpp b/src/librssguard-gmail/src/gmailentrypoint.cpp similarity index 61% rename from src/librssguard/services/gmail/gmailentrypoint.cpp rename to src/librssguard-gmail/src/gmailentrypoint.cpp index 8871457cd..d26e250df 100644 --- a/src/librssguard/services/gmail/gmailentrypoint.cpp +++ b/src/librssguard-gmail/src/gmailentrypoint.cpp @@ -1,16 +1,23 @@ // For license of this file, see /LICENSE.md. -#include "services/gmail/gmailentrypoint.h" +#include "src/gmailentrypoint.h" -#include "database/databasequeries.h" -#include "definitions/definitions.h" -#include "miscellaneous/application.h" -#include "miscellaneous/iconfactory.h" -#include "services/gmail/gmailserviceroot.h" -#include "services/gmail/gui/formeditgmailaccount.h" +#include "src/gmailserviceroot.h" +#include "src/gui/formeditgmailaccount.h" + +#include +#include +#include +#include #include +GmailEntryPoint::GmailEntryPoint(QObject* parent) : QObject(parent) {} + +GmailEntryPoint::~GmailEntryPoint() { + qDebugNN << LOGSEC_GMAIL << "Destructing" << QUOTE_W_SPACE(QSL(SERVICE_CODE_GMAIL)) << "plugin."; +} + ServiceRoot* GmailEntryPoint::createNewRoot() const { FormEditGmailAccount form_acc(qApp->mainFormWidget()); @@ -42,3 +49,7 @@ QString GmailEntryPoint::author() const { QIcon GmailEntryPoint::icon() const { return qApp->icons()->miscIcon(QSL("gmail")); } + +bool GmailEntryPoint::isDynamicallyLoaded() const { + return true; +} diff --git a/src/librssguard/services/gmail/gmailentrypoint.h b/src/librssguard-gmail/src/gmailentrypoint.h similarity index 52% rename from src/librssguard/services/gmail/gmailentrypoint.h rename to src/librssguard-gmail/src/gmailentrypoint.h index d07af7898..770079795 100644 --- a/src/librssguard/services/gmail/gmailentrypoint.h +++ b/src/librssguard-gmail/src/gmailentrypoint.h @@ -3,10 +3,17 @@ #ifndef GMAILENTRYPOINT_H #define GMAILENTRYPOINT_H -#include "services/abstract/serviceentrypoint.h" +#include + +class GmailEntryPoint : public QObject, public ServiceEntryPoint { + Q_OBJECT + Q_PLUGIN_METADATA(IID "io.github.martinrotter.rssguard.gmail" FILE "plugin.json") + Q_INTERFACES(ServiceEntryPoint) -class GmailEntryPoint : public ServiceEntryPoint { public: + explicit GmailEntryPoint(QObject* parent = nullptr); + virtual ~GmailEntryPoint(); + virtual ServiceRoot* createNewRoot() const; virtual QList initializeSubtree() const; virtual QString name() const; @@ -14,6 +21,7 @@ class GmailEntryPoint : public ServiceEntryPoint { virtual QString description() const; virtual QString author() const; virtual QIcon icon() const; + virtual bool isDynamicallyLoaded() const; }; #endif // GMAILENTRYPOINT_H diff --git a/src/librssguard/services/gmail/gmailnetworkfactory.cpp b/src/librssguard-gmail/src/gmailnetworkfactory.cpp similarity index 95% rename from src/librssguard/services/gmail/gmailnetworkfactory.cpp rename to src/librssguard-gmail/src/gmailnetworkfactory.cpp index bb02e6f8c..17836ef90 100644 --- a/src/librssguard/services/gmail/gmailnetworkfactory.cpp +++ b/src/librssguard-gmail/src/gmailnetworkfactory.cpp @@ -1,20 +1,21 @@ // For license of this file, see /LICENSE.md. -#include "services/gmail/gmailnetworkfactory.h" +#include "src/gmailnetworkfactory.h" -#include "3rd-party/boolinq/boolinq.h" -#include "database/databasequeries.h" -#include "definitions/definitions.h" -#include "exceptions/applicationexception.h" -#include "exceptions/networkexception.h" -#include "miscellaneous/application.h" -#include "miscellaneous/settings.h" -#include "miscellaneous/textfactory.h" -#include "network-web/networkfactory.h" -#include "network-web/oauth2service.h" -#include "services/abstract/labelsnode.h" -#include "services/gmail/definitions.h" -#include "services/gmail/gmailserviceroot.h" +#include "src/definitions.h" +#include "src/gmailserviceroot.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include diff --git a/src/librssguard/services/gmail/gmailnetworkfactory.h b/src/librssguard-gmail/src/gmailnetworkfactory.h similarity index 91% rename from src/librssguard/services/gmail/gmailnetworkfactory.h rename to src/librssguard-gmail/src/gmailnetworkfactory.h index 9d2c247dc..d4bdf226d 100644 --- a/src/librssguard/services/gmail/gmailnetworkfactory.h +++ b/src/librssguard-gmail/src/gmailnetworkfactory.h @@ -3,11 +3,12 @@ #ifndef GMAILNETWORKFACTORY_H #define GMAILNETWORKFACTORY_H -#include "3rd-party/mimesis/mimesis.hpp" -#include "core/message.h" -#include "services/abstract/feed.h" -#include "services/abstract/rootitem.h" -#include "services/abstract/serviceroot.h" +#include "src/3rd-party/mimesis/mimesis.hpp" + +#include +#include +#include +#include #include #include diff --git a/src/librssguard/services/gmail/gmailserviceroot.cpp b/src/librssguard-gmail/src/gmailserviceroot.cpp similarity index 91% rename from src/librssguard/services/gmail/gmailserviceroot.cpp rename to src/librssguard-gmail/src/gmailserviceroot.cpp index 610cd535f..579588c43 100644 --- a/src/librssguard/services/gmail/gmailserviceroot.cpp +++ b/src/librssguard-gmail/src/gmailserviceroot.cpp @@ -1,19 +1,20 @@ // For license of this file, see /LICENSE.md. -#include "services/gmail/gmailserviceroot.h" +#include "src/gmailserviceroot.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/labelsnode.h" -#include "services/gmail/definitions.h" -#include "services/gmail/gmailentrypoint.h" -#include "services/gmail/gmailnetworkfactory.h" -#include "services/gmail/gui/emailpreviewer.h" -#include "services/gmail/gui/formaddeditemail.h" -#include "services/gmail/gui/formeditgmailaccount.h" +#include "src/definitions.h" +#include "src/gmailentrypoint.h" +#include "src/gmailnetworkfactory.h" +#include "src/gui/emailpreviewer.h" +#include "src/gui/formaddeditemail.h" +#include "src/gui/formeditgmailaccount.h" + +#include +#include +#include +#include +#include +#include #include diff --git a/src/librssguard/services/gmail/gmailserviceroot.h b/src/librssguard-gmail/src/gmailserviceroot.h similarity index 90% rename from src/librssguard/services/gmail/gmailserviceroot.h rename to src/librssguard-gmail/src/gmailserviceroot.h index e7eb60115..6694c134b 100644 --- a/src/librssguard/services/gmail/gmailserviceroot.h +++ b/src/librssguard-gmail/src/gmailserviceroot.h @@ -3,9 +3,10 @@ #ifndef GMAILSERVICEROOT_H #define GMAILSERVICEROOT_H -#include "services/abstract/cacheforserviceroot.h" -#include "services/abstract/serviceroot.h" -#include "services/gmail/gui/emailpreviewer.h" +#include "src/gui/emailpreviewer.h" + +#include +#include class GmailNetworkFactory; diff --git a/src/librssguard/services/gmail/gui/emailpreviewer.cpp b/src/librssguard-gmail/src/gui/emailpreviewer.cpp similarity index 89% rename from src/librssguard/services/gmail/gui/emailpreviewer.cpp rename to src/librssguard-gmail/src/gui/emailpreviewer.cpp index c6435e12a..7f015de4a 100644 --- a/src/librssguard/services/gmail/gui/emailpreviewer.cpp +++ b/src/librssguard-gmail/src/gui/emailpreviewer.cpp @@ -1,15 +1,16 @@ // For license of this file, see /LICENSE.md. -#include "services/gmail/gui/emailpreviewer.h" +#include "src/gui/emailpreviewer.h" -#include "exceptions/networkexception.h" -#include "gui/messagebox.h" -#include "miscellaneous/application.h" -#include "miscellaneous/iconfactory.h" -#include "services/gmail/definitions.h" -#include "services/gmail/gmailnetworkfactory.h" -#include "services/gmail/gmailserviceroot.h" -#include "services/gmail/gui/formaddeditemail.h" +#include "src/definitions.h" +#include "src/gmailnetworkfactory.h" +#include "src/gmailserviceroot.h" +#include "src/gui/formaddeditemail.h" + +#include +#include +#include +#include #include diff --git a/src/librssguard/services/gmail/gui/emailpreviewer.h b/src/librssguard-gmail/src/gui/emailpreviewer.h similarity index 84% rename from src/librssguard/services/gmail/gui/emailpreviewer.h rename to src/librssguard-gmail/src/gui/emailpreviewer.h index 8751fb75b..fc53d3a0b 100644 --- a/src/librssguard/services/gmail/gui/emailpreviewer.h +++ b/src/librssguard-gmail/src/gui/emailpreviewer.h @@ -3,11 +3,11 @@ #ifndef EMAILPREVIEWER_H #define EMAILPREVIEWER_H -#include "gui/webbrowser.h" -#include "services/abstract/gui/custommessagepreviewer.h" - #include "ui_emailpreviewer.h" +#include +#include + #include class GmailServiceRoot; diff --git a/src/librssguard/services/gmail/gui/emailpreviewer.ui b/src/librssguard-gmail/src/gui/emailpreviewer.ui similarity index 100% rename from src/librssguard/services/gmail/gui/emailpreviewer.ui rename to src/librssguard-gmail/src/gui/emailpreviewer.ui diff --git a/src/librssguard/services/gmail/gui/emailrecipientcontrol.cpp b/src/librssguard-gmail/src/gui/emailrecipientcontrol.cpp similarity index 88% rename from src/librssguard/services/gmail/gui/emailrecipientcontrol.cpp rename to src/librssguard-gmail/src/gui/emailrecipientcontrol.cpp index 69f9afc1e..8087fc890 100644 --- a/src/librssguard/services/gmail/gui/emailrecipientcontrol.cpp +++ b/src/librssguard-gmail/src/gui/emailrecipientcontrol.cpp @@ -1,11 +1,12 @@ // For license of this file, see /LICENSE.md. -#include "services/gmail/gui/emailrecipientcontrol.h" +#include "src/gui/emailrecipientcontrol.h" -#include "gui/reusable/plaintoolbutton.h" -#include "miscellaneous/application.h" -#include "miscellaneous/iconfactory.h" -#include "services/gmail/definitions.h" +#include "src/definitions.h" + +#include +#include +#include #include #include diff --git a/src/librssguard/services/gmail/gui/emailrecipientcontrol.h b/src/librssguard-gmail/src/gui/emailrecipientcontrol.h similarity index 90% rename from src/librssguard/services/gmail/gui/emailrecipientcontrol.h rename to src/librssguard-gmail/src/gui/emailrecipientcontrol.h index 720e2b3ff..4bc442b2b 100644 --- a/src/librssguard/services/gmail/gui/emailrecipientcontrol.h +++ b/src/librssguard-gmail/src/gui/emailrecipientcontrol.h @@ -3,7 +3,7 @@ #ifndef EMAILRECIPIENTCONTROL_H #define EMAILRECIPIENTCONTROL_H -#include "services/gmail/definitions.h" +#include "src/definitions.h" #include diff --git a/src/librssguard/services/gmail/gui/formaddeditemail.cpp b/src/librssguard-gmail/src/gui/formaddeditemail.cpp similarity index 89% rename from src/librssguard/services/gmail/gui/formaddeditemail.cpp rename to src/librssguard-gmail/src/gui/formaddeditemail.cpp index ee3b1da40..315c4f1ef 100644 --- a/src/librssguard/services/gmail/gui/formaddeditemail.cpp +++ b/src/librssguard-gmail/src/gui/formaddeditemail.cpp @@ -1,17 +1,18 @@ // For license of this file, see /LICENSE.md. -#include "services/gmail/gui/formaddeditemail.h" +#include "src/gui/formaddeditemail.h" -#include "3rd-party/mimesis/mimesis.hpp" -#include "database/databasequeries.h" -#include "exceptions/applicationexception.h" -#include "gui/guiutilities.h" -#include "gui/messagebox.h" -#include "miscellaneous/application.h" -#include "miscellaneous/iconfactory.h" -#include "services/gmail/gmailnetworkfactory.h" -#include "services/gmail/gmailserviceroot.h" -#include "services/gmail/gui/emailrecipientcontrol.h" +#include "src/3rd-party/mimesis/mimesis.hpp" +#include "src/gmailnetworkfactory.h" +#include "src/gmailserviceroot.h" +#include "src/gui/emailrecipientcontrol.h" + +#include +#include +#include +#include +#include +#include #include #include diff --git a/src/librssguard/services/gmail/gui/formaddeditemail.h b/src/librssguard-gmail/src/gui/formaddeditemail.h similarity index 91% rename from src/librssguard/services/gmail/gui/formaddeditemail.h rename to src/librssguard-gmail/src/gui/formaddeditemail.h index 5ccac8e13..f1b9799ac 100644 --- a/src/librssguard/services/gmail/gui/formaddeditemail.h +++ b/src/librssguard-gmail/src/gui/formaddeditemail.h @@ -7,10 +7,6 @@ #include -namespace Ui { - class FormAddEditEmail; -} - class GmailServiceRoot; class Message; class EmailRecipientControl; diff --git a/src/librssguard/services/gmail/gui/formaddeditemail.ui b/src/librssguard-gmail/src/gui/formaddeditemail.ui similarity index 100% rename from src/librssguard/services/gmail/gui/formaddeditemail.ui rename to src/librssguard-gmail/src/gui/formaddeditemail.ui diff --git a/src/librssguard/services/gmail/gui/formeditgmailaccount.cpp b/src/librssguard-gmail/src/gui/formeditgmailaccount.cpp similarity index 89% rename from src/librssguard/services/gmail/gui/formeditgmailaccount.cpp rename to src/librssguard-gmail/src/gui/formeditgmailaccount.cpp index c722cad8c..a7a0cadf0 100644 --- a/src/librssguard/services/gmail/gui/formeditgmailaccount.cpp +++ b/src/librssguard-gmail/src/gui/formeditgmailaccount.cpp @@ -1,12 +1,13 @@ // For license of this file, see /LICENSE.md. -#include "services/gmail/gui/formeditgmailaccount.h" +#include "src/gui/formeditgmailaccount.h" -#include "miscellaneous/application.h" -#include "miscellaneous/iconfactory.h" -#include "network-web/oauth2service.h" -#include "services/gmail/gmailserviceroot.h" -#include "services/gmail/gui/gmailaccountdetails.h" +#include "src/gmailserviceroot.h" +#include "src/gui/gmailaccountdetails.h" + +#include +#include +#include FormEditGmailAccount::FormEditGmailAccount(QWidget* parent) : FormAccountDetails(qApp->icons()->miscIcon(QSL("gmail")), parent), m_details(new GmailAccountDetails(this)) { diff --git a/src/librssguard/services/gmail/gui/formeditgmailaccount.h b/src/librssguard-gmail/src/gui/formeditgmailaccount.h similarity index 79% rename from src/librssguard/services/gmail/gui/formeditgmailaccount.h rename to src/librssguard-gmail/src/gui/formeditgmailaccount.h index f3d768d9a..aa2e5d238 100644 --- a/src/librssguard/services/gmail/gui/formeditgmailaccount.h +++ b/src/librssguard-gmail/src/gui/formeditgmailaccount.h @@ -3,8 +3,9 @@ #ifndef FORMEDITINOREADERACCOUNT_H #define FORMEDITINOREADERACCOUNT_H -#include "services/abstract/gui/formaccountdetails.h" -#include "services/gmail/gmailnetworkfactory.h" +#include "src/gmailnetworkfactory.h" + +#include class GmailServiceRoot; class GmailAccountDetails; diff --git a/src/librssguard/services/gmail/gui/gmailaccountdetails.cpp b/src/librssguard-gmail/src/gui/gmailaccountdetails.cpp similarity index 92% rename from src/librssguard/services/gmail/gui/gmailaccountdetails.cpp rename to src/librssguard-gmail/src/gui/gmailaccountdetails.cpp index c513abf3b..bba825428 100644 --- a/src/librssguard/services/gmail/gui/gmailaccountdetails.cpp +++ b/src/librssguard-gmail/src/gui/gmailaccountdetails.cpp @@ -1,13 +1,14 @@ // For license of this file, see /LICENSE.md. -#include "services/gmail/gui/gmailaccountdetails.h" +#include "src/gui/gmailaccountdetails.h" -#include "exceptions/applicationexception.h" -#include "miscellaneous/application.h" -#include "network-web/oauth2service.h" -#include "network-web/webfactory.h" -#include "services/gmail/definitions.h" -#include "services/gmail/gmailnetworkfactory.h" +#include "src/definitions.h" +#include "src/gmailnetworkfactory.h" + +#include +#include +#include +#include GmailAccountDetails::GmailAccountDetails(QWidget* parent) : QWidget(parent), m_oauth(nullptr), m_lastProxy({}) { m_ui.setupUi(this); diff --git a/src/librssguard/services/gmail/gui/gmailaccountdetails.h b/src/librssguard-gmail/src/gui/gmailaccountdetails.h similarity index 100% rename from src/librssguard/services/gmail/gui/gmailaccountdetails.h rename to src/librssguard-gmail/src/gui/gmailaccountdetails.h diff --git a/src/librssguard/services/gmail/gui/gmailaccountdetails.ui b/src/librssguard-gmail/src/gui/gmailaccountdetails.ui similarity index 100% rename from src/librssguard/services/gmail/gui/gmailaccountdetails.ui rename to src/librssguard-gmail/src/gui/gmailaccountdetails.ui diff --git a/src/librssguard/CMakeLists.txt b/src/librssguard/CMakeLists.txt index d834e8b49..1fdd827ad 100644 --- a/src/librssguard/CMakeLists.txt +++ b/src/librssguard/CMakeLists.txt @@ -159,10 +159,6 @@ set(SOURCES gui/reusable/treeviewcolumnsmenu.h gui/reusable/widgetwithstatus.cpp gui/reusable/widgetwithstatus.h - gui/richtexteditor/mrichtextedit.cpp - gui/richtexteditor/mrichtextedit.h - gui/richtexteditor/mtextedit.cpp - gui/richtexteditor/mtextedit.h gui/settings/settingsbrowsermail.cpp gui/settings/settingsbrowsermail.h gui/settings/settingsdatabase.cpp @@ -328,23 +324,6 @@ set(SOURCES services/abstract/serviceroot.h services/abstract/unreadnode.cpp services/abstract/unreadnode.h - services/gmail/definitions.h - services/gmail/gmailentrypoint.cpp - services/gmail/gmailentrypoint.h - services/gmail/gmailnetworkfactory.cpp - services/gmail/gmailnetworkfactory.h - services/gmail/gmailserviceroot.cpp - services/gmail/gmailserviceroot.h - services/gmail/gui/emailpreviewer.cpp - services/gmail/gui/emailpreviewer.h - services/gmail/gui/emailrecipientcontrol.cpp - services/gmail/gui/emailrecipientcontrol.h - services/gmail/gui/formaddeditemail.cpp - services/gmail/gui/formaddeditemail.h - services/gmail/gui/formeditgmailaccount.cpp - services/gmail/gui/formeditgmailaccount.h - services/gmail/gui/gmailaccountdetails.cpp - services/gmail/gui/gmailaccountdetails.h services/greader/definitions.h services/greader/greaderentrypoint.cpp services/greader/greaderentrypoint.h @@ -431,7 +410,6 @@ set(UI_FILES gui/reusable/articleamountcontrol.ui gui/reusable/networkproxydetails.ui gui/reusable/searchtextwidget.ui - gui/richtexteditor/mrichtextedit.ui gui/settings/settingsbrowsermail.ui gui/settings/settingsdatabase.ui gui/settings/settingsdownloads.ui @@ -454,9 +432,6 @@ set(UI_FILES services/abstract/gui/formaddeditprobe.ui services/abstract/gui/formcategorydetails.ui services/abstract/gui/formfeeddetails.ui - services/gmail/gui/emailpreviewer.ui - services/gmail/gui/formaddeditemail.ui - services/gmail/gui/gmailaccountdetails.ui services/greader/gui/greaderaccountdetails.ui services/greader/gui/greaderfeeddetails.ui services/owncloud/gui/owncloudaccountdetails.ui @@ -538,14 +513,6 @@ list(APPEND SOURCES gui/webviewers/qtextbrowser/textbrowserviewer.cpp ) -# Add mimesis. -list(APPEND SOURCES - 3rd-party/mimesis/mimesis.cpp - 3rd-party/mimesis/mimesis.hpp - 3rd-party/mimesis/quoted-printable.cpp - 3rd-party/mimesis/quoted-printable.hpp -) - # Add boolinq. list(APPEND SOURCES 3rd-party/boolinq/boolinq.h diff --git a/src/librssguard/gui/reusable/colortoolbutton.h b/src/librssguard/gui/reusable/colortoolbutton.h index b631cf851..453b9270f 100644 --- a/src/librssguard/gui/reusable/colortoolbutton.h +++ b/src/librssguard/gui/reusable/colortoolbutton.h @@ -5,7 +5,7 @@ #include -class ColorToolButton : public QToolButton { +class RSSGUARD_DLLSPEC ColorToolButton : public QToolButton { Q_OBJECT public: diff --git a/src/librssguard/gui/reusable/plaintoolbutton.h b/src/librssguard/gui/reusable/plaintoolbutton.h index 818efcee1..bbc0d1f86 100644 --- a/src/librssguard/gui/reusable/plaintoolbutton.h +++ b/src/librssguard/gui/reusable/plaintoolbutton.h @@ -5,7 +5,7 @@ #include -class PlainToolButton : public QToolButton { +class RSSGUARD_DLLSPEC PlainToolButton : public QToolButton { Q_OBJECT public: diff --git a/src/librssguard/gui/webbrowser.h b/src/librssguard/gui/webbrowser.h index 8f3bf2b0f..5692cc326 100644 --- a/src/librssguard/gui/webbrowser.h +++ b/src/librssguard/gui/webbrowser.h @@ -23,7 +23,7 @@ class WebViewer; class LocationLineEdit; class SearchTextWidget; -class WebBrowser : public TabContent { +class RSSGUARD_DLLSPEC WebBrowser : public TabContent { Q_OBJECT friend class WebEngineViewer; diff --git a/src/librssguard/miscellaneous/feedreader.cpp b/src/librssguard/miscellaneous/feedreader.cpp index be235c33f..5f5b080c8 100644 --- a/src/librssguard/miscellaneous/feedreader.cpp +++ b/src/librssguard/miscellaneous/feedreader.cpp @@ -16,7 +16,6 @@ #include "miscellaneous/settings.h" #include "services/abstract/cacheforserviceroot.h" #include "services/abstract/serviceroot.h" -#include "services/gmail/gmailentrypoint.h" #include "services/greader/greaderentrypoint.h" #include "services/owncloud/owncloudserviceentrypoint.h" #include "services/reddit/redditentrypoint.h" @@ -68,7 +67,6 @@ FeedReader::~FeedReader() { QList FeedReader::feedServices() { if (m_feedServices.isEmpty()) { - m_feedServices.append(new GmailEntryPoint()); m_feedServices.append(new GreaderEntryPoint()); m_feedServices.append(new OwnCloudServiceEntryPoint()); diff --git a/src/librssguard/network-web/downloadmanager.h b/src/librssguard/network-web/downloadmanager.h index 90a7e6be8..263c7a539 100644 --- a/src/librssguard/network-web/downloadmanager.h +++ b/src/librssguard/network-web/downloadmanager.h @@ -24,7 +24,7 @@ namespace Ui { class DownloadManager; } -class DownloadItem : public QWidget { +class RSSGUARD_DLLSPEC DownloadItem : public QWidget { Q_OBJECT friend class DownloadManager; @@ -89,7 +89,7 @@ class DownloadItem : public QWidget { class WebBrowser; class SilentNetworkAccessManager; -class DownloadManager : public TabContent { +class RSSGUARD_DLLSPEC DownloadManager : public TabContent { Q_OBJECT Q_PROPERTY(RemovePolicy removePolicy READ removePolicy WRITE setRemovePolicy NOTIFY removePolicyChanged) diff --git a/src/librssguard/network-web/httpresponse.h b/src/librssguard/network-web/httpresponse.h index d6aa9f1a8..0028a6696 100644 --- a/src/librssguard/network-web/httpresponse.h +++ b/src/librssguard/network-web/httpresponse.h @@ -8,7 +8,7 @@ typedef QPair HttpHeader; -class HttpResponse { +class RSSGUARD_DLLSPEC HttpResponse { public: explicit HttpResponse(); diff --git a/src/librssguard/network-web/oauth2service.h b/src/librssguard/network-web/oauth2service.h index 70e2fff8b..65dc9a54b 100644 --- a/src/librssguard/network-web/oauth2service.h +++ b/src/librssguard/network-web/oauth2service.h @@ -33,7 +33,7 @@ #include #include -class OAuth2Service : public QObject { +class RSSGUARD_DLLSPEC OAuth2Service : public QObject { Q_OBJECT public: diff --git a/src/librssguard/services/reddit/redditnetworkfactory.h b/src/librssguard/services/reddit/redditnetworkfactory.h index 2c94882a3..4ce33b940 100644 --- a/src/librssguard/services/reddit/redditnetworkfactory.h +++ b/src/librssguard/services/reddit/redditnetworkfactory.h @@ -3,7 +3,6 @@ #ifndef REDDITNETWORKFACTORY_H #define REDDITNETWORKFACTORY_H -#include "3rd-party/mimesis/mimesis.hpp" #include "core/message.h" #include "services/abstract/feed.h" #include "services/abstract/rootitem.h"