From d0670c22fade5909aab408fe8e49208978496fcc Mon Sep 17 00:00:00 2001 From: Martin Rotter Date: Wed, 20 Mar 2024 09:30:35 +0100 Subject: [PATCH] files --- src/librssguard-nextcloud/CMakeLists.txt | 78 ++ src/librssguard-nextcloud/plugin.json | 5 + src/librssguard-nextcloud/src/definitions.h | 13 + .../src/gui/formeditnextcloudaccount.cpp | 63 + .../src/gui/formeditnextcloudaccount.h | 30 + .../src/gui/nextcloudaccountdetails.cpp | 129 ++ .../src/gui/nextcloudaccountdetails.h | 29 + .../src/gui/nextcloudaccountdetails.ui | 201 +++ .../src/nextcloudfeed.cpp | 36 + src/librssguard-nextcloud/src/nextcloudfeed.h | 24 + .../src/nextcloudnetworkfactory.cpp | 660 +++++++++ .../src/nextcloudnetworkfactory.h | 138 ++ .../src/nextcloudserviceentrypoint.cpp | 52 + .../src/nextcloudserviceentrypoint.h | 26 + .../src/nextcloudserviceroot.cpp | 177 +++ .../src/nextcloudserviceroot.h | 48 + src/librssguard-ttrss/CMakeLists.txt | 88 ++ src/librssguard-ttrss/plugin.json | 5 + src/librssguard-ttrss/src/definitions.h | 49 + .../src/gui/formeditttrssaccount.cpp | 70 + .../src/gui/formeditttrssaccount.h | 31 + .../src/gui/formttrssfeeddetails.cpp | 74 + .../src/gui/formttrssfeeddetails.h | 32 + .../src/gui/formttrssnote.cpp | 79 ++ src/librssguard-ttrss/src/gui/formttrssnote.h | 33 + .../src/gui/formttrssnote.ui | 105 ++ .../src/gui/ttrssaccountdetails.cpp | 189 +++ .../src/gui/ttrssaccountdetails.h | 34 + .../src/gui/ttrssaccountdetails.ui | 252 ++++ .../src/gui/ttrssfeeddetails.cpp | 56 + .../src/gui/ttrssfeeddetails.h | 31 + .../src/gui/ttrssfeeddetails.ui | 68 + src/librssguard-ttrss/src/ttrssfeed.cpp | 62 + src/librssguard-ttrss/src/ttrssfeed.h | 28 + .../src/ttrssnetworkfactory.cpp | 1206 +++++++++++++++++ .../src/ttrssnetworkfactory.h | 247 ++++ .../src/ttrssnotetopublish.h | 15 + .../src/ttrssserviceentrypoint.cpp | 54 + .../src/ttrssserviceentrypoint.h | 26 + .../src/ttrssserviceroot.cpp | 423 ++++++ src/librssguard-ttrss/src/ttrssserviceroot.h | 61 + .../miscellaneous/pluginfactory.cpp | 2 +- 42 files changed, 5028 insertions(+), 1 deletion(-) create mode 100644 src/librssguard-nextcloud/CMakeLists.txt create mode 100644 src/librssguard-nextcloud/plugin.json create mode 100644 src/librssguard-nextcloud/src/definitions.h create mode 100644 src/librssguard-nextcloud/src/gui/formeditnextcloudaccount.cpp create mode 100644 src/librssguard-nextcloud/src/gui/formeditnextcloudaccount.h create mode 100644 src/librssguard-nextcloud/src/gui/nextcloudaccountdetails.cpp create mode 100644 src/librssguard-nextcloud/src/gui/nextcloudaccountdetails.h create mode 100644 src/librssguard-nextcloud/src/gui/nextcloudaccountdetails.ui create mode 100644 src/librssguard-nextcloud/src/nextcloudfeed.cpp create mode 100644 src/librssguard-nextcloud/src/nextcloudfeed.h create mode 100644 src/librssguard-nextcloud/src/nextcloudnetworkfactory.cpp create mode 100644 src/librssguard-nextcloud/src/nextcloudnetworkfactory.h create mode 100644 src/librssguard-nextcloud/src/nextcloudserviceentrypoint.cpp create mode 100644 src/librssguard-nextcloud/src/nextcloudserviceentrypoint.h create mode 100644 src/librssguard-nextcloud/src/nextcloudserviceroot.cpp create mode 100644 src/librssguard-nextcloud/src/nextcloudserviceroot.h create mode 100644 src/librssguard-ttrss/CMakeLists.txt create mode 100644 src/librssguard-ttrss/plugin.json create mode 100644 src/librssguard-ttrss/src/definitions.h create mode 100644 src/librssguard-ttrss/src/gui/formeditttrssaccount.cpp create mode 100644 src/librssguard-ttrss/src/gui/formeditttrssaccount.h create mode 100644 src/librssguard-ttrss/src/gui/formttrssfeeddetails.cpp create mode 100644 src/librssguard-ttrss/src/gui/formttrssfeeddetails.h create mode 100644 src/librssguard-ttrss/src/gui/formttrssnote.cpp create mode 100644 src/librssguard-ttrss/src/gui/formttrssnote.h create mode 100644 src/librssguard-ttrss/src/gui/formttrssnote.ui create mode 100644 src/librssguard-ttrss/src/gui/ttrssaccountdetails.cpp create mode 100644 src/librssguard-ttrss/src/gui/ttrssaccountdetails.h create mode 100644 src/librssguard-ttrss/src/gui/ttrssaccountdetails.ui create mode 100644 src/librssguard-ttrss/src/gui/ttrssfeeddetails.cpp create mode 100644 src/librssguard-ttrss/src/gui/ttrssfeeddetails.h create mode 100644 src/librssguard-ttrss/src/gui/ttrssfeeddetails.ui create mode 100644 src/librssguard-ttrss/src/ttrssfeed.cpp create mode 100644 src/librssguard-ttrss/src/ttrssfeed.h create mode 100644 src/librssguard-ttrss/src/ttrssnetworkfactory.cpp create mode 100644 src/librssguard-ttrss/src/ttrssnetworkfactory.h create mode 100644 src/librssguard-ttrss/src/ttrssnotetopublish.h create mode 100644 src/librssguard-ttrss/src/ttrssserviceentrypoint.cpp create mode 100644 src/librssguard-ttrss/src/ttrssserviceentrypoint.h create mode 100644 src/librssguard-ttrss/src/ttrssserviceroot.cpp create mode 100644 src/librssguard-ttrss/src/ttrssserviceroot.h diff --git a/src/librssguard-nextcloud/CMakeLists.txt b/src/librssguard-nextcloud/CMakeLists.txt new file mode 100644 index 000000000..d1e8f9693 --- /dev/null +++ b/src/librssguard-nextcloud/CMakeLists.txt @@ -0,0 +1,78 @@ +if(NOT DEFINED LIBRSSGUARD_BINARY_PATH) + set(LIBRSSGUARD_SOURCE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/..") +endif() + +set(SOURCES + src/definitions.h + src/gui/formeditnextcloudaccount.cpp + src/gui/formeditnextcloudaccount.h + src/gui/nextcloudaccountdetails.cpp + src/gui/nextcloudaccountdetails.h + src/nextcloudfeed.cpp + src/nextcloudfeed.h + src/nextcloudnetworkfactory.cpp + src/nextcloudnetworkfactory.h + src/nextcloudserviceentrypoint.cpp + src/nextcloudserviceentrypoint.h + src/nextcloudserviceroot.cpp + src/nextcloudserviceroot.h +) + +set(UI_FILES + src/gui/nextcloudaccountdetails.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-nextcloud SHARED ${SOURCES} ${QM_FILES}) + +# Add specific definitions. +target_compile_definitions(rssguard-nextcloud + PRIVATE + RSSGUARD_DLLSPEC=Q_DECL_IMPORT + RSSGUARD_DLLSPEC_EXPORT=Q_DECL_EXPORT +) + +target_include_directories(rssguard-nextcloud + PUBLIC + ${LIBRSSGUARD_SOURCE_PATH} +) + +# Qt. +target_link_libraries(rssguard-nextcloud 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-nextcloud DESTINATION plugins) +elseif(UNIX AND NOT APPLE AND NOT ANDROID) + include (GNUInstallDirs) + install(TARGETS rssguard-nextcloud + DESTINATION ${CMAKE_INSTALL_LIBDIR}/rssguard + ) +elseif(APPLE) + install(TARGETS rssguard-nextcloud + DESTINATION Contents/MacOS + ) +endif() diff --git a/src/librssguard-nextcloud/plugin.json b/src/librssguard-nextcloud/plugin.json new file mode 100644 index 000000000..87de3ce8b --- /dev/null +++ b/src/librssguard-nextcloud/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "Nextcloud News", + "author": "Martin Rotter", + "website": "https://github.com/martinrotter/rssguard" +} \ No newline at end of file diff --git a/src/librssguard-nextcloud/src/definitions.h b/src/librssguard-nextcloud/src/definitions.h new file mode 100644 index 000000000..157b744f8 --- /dev/null +++ b/src/librssguard-nextcloud/src/definitions.h @@ -0,0 +1,13 @@ +// For license of this file, see /LICENSE.md. + +#ifndef NEXTCLOUD_DEFINITIONS_H +#define NEXTCLOUD_DEFINITIONS_H + +#define NEXTCLOUD_CONTENT_TYPE_JSON "application/json; charset=utf-8" +#define NEXTCLOUD_API_VERSION "1.2" +#define NEXTCLOUD_API_PATH "index.php/apps/news/api/v1-2/" +#define NEXTCLOUD_MIN_VERSION "6.0.5" +#define NEXTCLOUD_UNLIMITED_BATCH_SIZE -1 +#define NEXTCLOUD_DEFAULT_BATCH_SIZE 100 + +#endif // NEXTCLOUD_DEFINITIONS_H diff --git a/src/librssguard-nextcloud/src/gui/formeditnextcloudaccount.cpp b/src/librssguard-nextcloud/src/gui/formeditnextcloudaccount.cpp new file mode 100644 index 000000000..64376d5c6 --- /dev/null +++ b/src/librssguard-nextcloud/src/gui/formeditnextcloudaccount.cpp @@ -0,0 +1,63 @@ +// For license of this file, see /LICENSE.md. + +#include "src/gui/formeditnextcloudaccount.h" + +#include "src/gui/nextcloudaccountdetails.h" +#include "src/nextcloudnetworkfactory.h" +#include "src/nextcloudserviceroot.h" + +#include + +FormEditNextcloudAccount::FormEditNextcloudAccount(QWidget* parent) + : FormAccountDetails(qApp->icons()->miscIcon(QSL("nextcloud")), parent), + m_details(new NextcloudAccountDetails(this)) { + insertCustomTab(m_details, tr("Server setup"), 0); + activateTab(0); + + connect(m_details->m_ui.m_btnTestSetup, &QPushButton::clicked, this, &FormEditNextcloudAccount::performTest); + + m_details->m_ui.m_txtUrl->setFocus(); +} + +void FormEditNextcloudAccount::apply() { + FormAccountDetails::apply(); + + bool using_another_acc = + m_details->m_ui.m_txtUsername->lineEdit()->text() != account()->network()->authUsername() || + m_details->m_ui.m_txtUrl->lineEdit()->text() != account()->network()->url(); + + account()->network()->setUrl(m_details->m_ui.m_txtUrl->lineEdit()->text()); + account()->network()->setAuthUsername(m_details->m_ui.m_txtUsername->lineEdit()->text()); + account()->network()->setAuthPassword(m_details->m_ui.m_txtPassword->lineEdit()->text()); + account()->network()->setForceServerSideUpdate(m_details->m_ui.m_checkServerSideUpdate + ->isChecked()); + account()->network()->setBatchSize(m_details->m_ui.m_spinLimitMessages->value()); + account() + ->network() + ->setDownloadOnlyUnreadMessages(m_details->m_ui.m_checkDownloadOnlyUnreadMessages->isChecked()); + + account()->saveAccountDataToDatabase(); + accept(); + + if (!m_creatingNew && using_another_acc) { + account()->completelyRemoveAllData(); + account()->start(true); + } +} + +void FormEditNextcloudAccount::loadAccountData() { + FormAccountDetails::loadAccountData(); + + NextcloudServiceRoot* existing_root = account(); + + m_details->m_ui.m_txtUsername->lineEdit()->setText(existing_root->network()->authUsername()); + m_details->m_ui.m_txtPassword->lineEdit()->setText(existing_root->network()->authPassword()); + m_details->m_ui.m_txtUrl->lineEdit()->setText(existing_root->network()->url()); + m_details->m_ui.m_checkDownloadOnlyUnreadMessages->setChecked(existing_root->network()->downloadOnlyUnreadMessages()); + m_details->m_ui.m_checkServerSideUpdate->setChecked(existing_root->network()->forceServerSideUpdate()); + m_details->m_ui.m_spinLimitMessages->setValue(existing_root->network()->batchSize()); +} + +void FormEditNextcloudAccount::performTest() { + m_details->performTest(m_proxyDetails->proxy()); +} diff --git a/src/librssguard-nextcloud/src/gui/formeditnextcloudaccount.h b/src/librssguard-nextcloud/src/gui/formeditnextcloudaccount.h new file mode 100644 index 000000000..4aa78401d --- /dev/null +++ b/src/librssguard-nextcloud/src/gui/formeditnextcloudaccount.h @@ -0,0 +1,30 @@ +// For license of this file, see /LICENSE.md. + +#ifndef FORMEDITNEXTCLOUDACCOUNT_H +#define FORMEDITNEXTCLOUDACCOUNT_H + +#include + +class NextcloudAccountDetails; +class NextcloudServiceRoot; + +class FormEditNextcloudAccount : public FormAccountDetails { + Q_OBJECT + + public: + explicit FormEditNextcloudAccount(QWidget* parent = nullptr); + + protected slots: + virtual void apply(); + + protected: + virtual void loadAccountData(); + + private slots: + void performTest(); + + private: + NextcloudAccountDetails* m_details; +}; + +#endif // FORMEDITNEXTCLOUDACCOUNT_H diff --git a/src/librssguard-nextcloud/src/gui/nextcloudaccountdetails.cpp b/src/librssguard-nextcloud/src/gui/nextcloudaccountdetails.cpp new file mode 100644 index 000000000..90f4cce35 --- /dev/null +++ b/src/librssguard-nextcloud/src/gui/nextcloudaccountdetails.cpp @@ -0,0 +1,129 @@ +// For license of this file, see /LICENSE.md. + +#include "src/gui/nextcloudaccountdetails.h" + +#include "src/definitions.h" +#include "src/nextcloudnetworkfactory.h" + +#include +#include + +NextcloudAccountDetails::NextcloudAccountDetails(QWidget* parent) : QWidget(parent) { + m_ui.setupUi(this); + + m_ui.m_lblTestResult->label()->setWordWrap(true); + m_ui.m_lblServerSideUpdateInformation + ->setHelpText(tr("Leaving this option on causes that updates " + "of feeds will be probably much slower and may time-out often."), + true); + m_ui.m_txtPassword->lineEdit()->setPlaceholderText(tr("Password for your Nextcloud account")); + m_ui.m_txtPassword->lineEdit()->setPasswordMode(true); + m_ui.m_txtUsername->lineEdit()->setPlaceholderText(tr("Username for your Nextcloud account")); + m_ui.m_txtUrl->lineEdit()->setPlaceholderText(tr("URL of your Nextcloud server, without any API path")); + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Information, + tr("No test done yet."), + tr("Here, results of connection test are shown.")); + + connect(m_ui.m_spinLimitMessages, + static_cast(&QSpinBox::valueChanged), + this, + [=](int value) { + if (value <= 0) { + m_ui.m_spinLimitMessages->setSuffix(QSL(" ") + tr("= unlimited")); + } + else { + m_ui.m_spinLimitMessages->setSuffix(QSL(" ") + tr("articles")); + } + }); + + connect(m_ui.m_txtPassword->lineEdit(), + &BaseLineEdit::textChanged, + this, + &NextcloudAccountDetails::onPasswordChanged); + connect(m_ui.m_txtUsername->lineEdit(), + &BaseLineEdit::textChanged, + this, + &NextcloudAccountDetails::onUsernameChanged); + connect(m_ui.m_txtUrl->lineEdit(), &BaseLineEdit::textChanged, this, &NextcloudAccountDetails::onUrlChanged); + + setTabOrder(m_ui.m_txtUrl->lineEdit(), m_ui.m_checkDownloadOnlyUnreadMessages); + setTabOrder(m_ui.m_checkDownloadOnlyUnreadMessages, m_ui.m_spinLimitMessages); + setTabOrder(m_ui.m_spinLimitMessages, m_ui.m_checkServerSideUpdate); + setTabOrder(m_ui.m_checkServerSideUpdate, m_ui.m_txtUsername->lineEdit()); + setTabOrder(m_ui.m_txtUsername->lineEdit(), m_ui.m_txtPassword->lineEdit()); + setTabOrder(m_ui.m_txtPassword->lineEdit(), m_ui.m_btnTestSetup); + + onPasswordChanged(); + onUsernameChanged(); + onUrlChanged(); +} + +void NextcloudAccountDetails::performTest(const QNetworkProxy& custom_proxy) { + NextcloudNetworkFactory factory; + + factory.setAuthUsername(m_ui.m_txtUsername->lineEdit()->text()); + factory.setAuthPassword(m_ui.m_txtPassword->lineEdit()->text()); + factory.setUrl(m_ui.m_txtUrl->lineEdit()->text()); + factory.setForceServerSideUpdate(m_ui.m_checkServerSideUpdate->isChecked()); + + NextcloudStatusResponse result = factory.status(custom_proxy); + + if (result.networkError() != QNetworkReply::NetworkError::NoError) { + m_ui.m_lblTestResult + ->setStatus(WidgetWithStatus::StatusType::Error, + tr("Network error: '%1'.").arg(NetworkFactory::networkErrorText(result.networkError())), + tr("Network error, have you entered correct Nextcloud endpoint and password?")); + } + else if (result.isLoaded()) { + if (!SystemFactory::isVersionEqualOrNewer(result.version(), QSL(NEXTCLOUD_MIN_VERSION))) { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error, + tr("Installed version: %1, required at least: %2.") + .arg(result.version(), QSL(NEXTCLOUD_MIN_VERSION)), + tr("Selected Nextcloud News server is running unsupported version.")); + } + else { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Ok, + tr("Installed version: %1, required at least: %2.") + .arg(result.version(), QSL(NEXTCLOUD_MIN_VERSION)), + tr("Nextcloud News server is okay.")); + } + } + else { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error, + tr("Unspecified error, did you enter correct URL?"), + tr("Unspecified error, did you enter correct URL?")); + } +} + +void NextcloudAccountDetails::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 NextcloudAccountDetails::onPasswordChanged() { + const QString password = m_ui.m_txtPassword->lineEdit()->text(); + + if (password.isEmpty()) { + m_ui.m_txtPassword->setStatus(WidgetWithStatus::StatusType::Error, tr("Password cannot be empty.")); + } + else { + m_ui.m_txtPassword->setStatus(WidgetWithStatus::StatusType::Ok, tr("Password is okay.")); + } +} + +void NextcloudAccountDetails::onUrlChanged() { + const QString url = m_ui.m_txtUrl->lineEdit()->text(); + + if (url.isEmpty()) { + m_ui.m_txtUrl->setStatus(WidgetWithStatus::StatusType::Error, tr("URL cannot be empty.")); + } + else { + m_ui.m_txtUrl->setStatus(WidgetWithStatus::StatusType::Ok, tr("URL is okay.")); + } +} diff --git a/src/librssguard-nextcloud/src/gui/nextcloudaccountdetails.h b/src/librssguard-nextcloud/src/gui/nextcloudaccountdetails.h new file mode 100644 index 000000000..af33c20ff --- /dev/null +++ b/src/librssguard-nextcloud/src/gui/nextcloudaccountdetails.h @@ -0,0 +1,29 @@ +// For license of this file, see /LICENSE.md. + +#ifndef NEXTCLOUDACCOUNTDETAILS_H +#define NEXTCLOUDACCOUNTDETAILS_H + +#include "ui_nextcloudaccountdetails.h" + +#include +#include + +class NextcloudAccountDetails : public QWidget { + Q_OBJECT + + friend class FormEditNextcloudAccount; + + public: + explicit NextcloudAccountDetails(QWidget* parent = nullptr); + + private slots: + void performTest(const QNetworkProxy& custom_proxy); + void onUsernameChanged(); + void onPasswordChanged(); + void onUrlChanged(); + + private: + Ui::NextcloudAccountDetails m_ui; +}; + +#endif // NEXTCLOUDACCOUNTDETAILS_H diff --git a/src/librssguard-nextcloud/src/gui/nextcloudaccountdetails.ui b/src/librssguard-nextcloud/src/gui/nextcloudaccountdetails.ui new file mode 100644 index 000000000..5d077e42f --- /dev/null +++ b/src/librssguard-nextcloud/src/gui/nextcloudaccountdetails.ui @@ -0,0 +1,201 @@ + + + NextcloudAccountDetails + + + + 0 + 0 + 433 + 363 + + + + + + + + + URL + + + m_txtUrl + + + + + + + + + + + + Download unread articles only + + + + + + + + + Only download newest X articles per feed + + + m_spinLimitMessages + + + + + + + + 140 + 16777215 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Some feeds require authentication, including GMail feeds. BASIC, NTLM-2 and DIGEST-MD5 authentication schemes are supported. + + + Authentication + + + false + + + false + + + + + + Username + + + m_txtUsername + + + + + + + + + + Password + + + m_txtPassword + + + + + + + + + + + + + + + &Test setup + + + + + + + + 0 + 0 + + + + Qt::RightToLeft + + + + + + + + + Qt::Vertical + + + + 409 + 35 + + + + + + + + Force execution of server-side feeds update + + + + + + + + LineEditWithStatus + QWidget +
lineeditwithstatus.h
+ 1 +
+ + LabelWithStatus + QWidget +
labelwithstatus.h
+ 1 +
+ + MessageCountSpinBox + QSpinBox +
messagecountspinbox.h
+
+ + HelpSpoiler + QWidget +
helpspoiler.h
+ 1 +
+
+ + m_checkDownloadOnlyUnreadMessages + m_checkServerSideUpdate + m_spinLimitMessages + m_btnTestSetup + + + +
diff --git a/src/librssguard-nextcloud/src/nextcloudfeed.cpp b/src/librssguard-nextcloud/src/nextcloudfeed.cpp new file mode 100644 index 000000000..abaf3ccd9 --- /dev/null +++ b/src/librssguard-nextcloud/src/nextcloudfeed.cpp @@ -0,0 +1,36 @@ +// For license of this file, see /LICENSE.md. + +#include "src/nextcloudfeed.h" + +#include "src/nextcloudnetworkfactory.h" +#include "src/nextcloudserviceroot.h" + +#include + +#include + +NextcloudFeed::NextcloudFeed(RootItem* parent) : Feed(parent) {} + +bool NextcloudFeed::canBeDeleted() const { + return true; +} + +bool NextcloudFeed::deleteItem() { + if (serviceRoot()->network()->deleteFeed(customId(), getParentServiceRoot()->networkProxy()) && removeItself()) { + serviceRoot()->requestItemRemoval(this); + return true; + } + else { + return false; + } +} + +bool NextcloudFeed::removeItself() { + QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className()); + + return DatabaseQueries::deleteFeed(database, this, serviceRoot()->accountId()); +} + +NextcloudServiceRoot* NextcloudFeed::serviceRoot() const { + return qobject_cast(getParentServiceRoot()); +} diff --git a/src/librssguard-nextcloud/src/nextcloudfeed.h b/src/librssguard-nextcloud/src/nextcloudfeed.h new file mode 100644 index 000000000..1bd554039 --- /dev/null +++ b/src/librssguard-nextcloud/src/nextcloudfeed.h @@ -0,0 +1,24 @@ +// For license of this file, see /LICENSE.md. + +#ifndef NEXTCLOUDFEED_H +#define NEXTCLOUDFEED_H + +#include + +class NextcloudServiceRoot; + +class NextcloudFeed : public Feed { + Q_OBJECT + + public: + explicit NextcloudFeed(RootItem* parent = nullptr); + + virtual bool canBeDeleted() const; + virtual bool deleteItem(); + + private: + bool removeItself(); + NextcloudServiceRoot* serviceRoot() const; +}; + +#endif // NEXTCLOUDFEED_H diff --git a/src/librssguard-nextcloud/src/nextcloudnetworkfactory.cpp b/src/librssguard-nextcloud/src/nextcloudnetworkfactory.cpp new file mode 100644 index 000000000..43bed5a4b --- /dev/null +++ b/src/librssguard-nextcloud/src/nextcloudnetworkfactory.cpp @@ -0,0 +1,660 @@ +// For license of this file, see /LICENSE.md. + +#include "src/nextcloudnetworkfactory.h" + +#include "src/definitions.h" +#include "src/nextcloudfeed.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +NextcloudNetworkFactory::NextcloudNetworkFactory() + : m_url(QString()), m_fixedUrl(QString()), m_downloadOnlyUnreadMessages(false), m_forceServerSideUpdate(false), + m_authUsername(QString()), m_authPassword(QString()), m_batchSize(NEXTCLOUD_DEFAULT_BATCH_SIZE), + m_urlUser(QString()), m_urlStatus(QString()), m_urlFolders(QString()), m_urlFeeds(QString()), + m_urlMessages(QString()), m_urlFeedsUpdate(QString()), m_urlDeleteFeed(QString()), m_urlRenameFeed(QString()) {} + +NextcloudNetworkFactory::~NextcloudNetworkFactory() = default; + +QString NextcloudNetworkFactory::url() const { + return m_url; +} + +void NextcloudNetworkFactory::setUrl(const QString& url) { + m_url = url; + + if (url.endsWith('/')) { + m_fixedUrl = url; + } + else { + m_fixedUrl = url + '/'; + } + + // Store endpoints. + m_urlUser = m_fixedUrl + NEXTCLOUD_API_PATH + "user"; + m_urlStatus = m_fixedUrl + NEXTCLOUD_API_PATH + "status"; + m_urlFolders = m_fixedUrl + NEXTCLOUD_API_PATH + "folders"; + m_urlFeeds = m_fixedUrl + NEXTCLOUD_API_PATH + "feeds"; + m_urlMessages = m_fixedUrl + NEXTCLOUD_API_PATH + "items?id=%1&batchSize=%2&type=%3&getRead=%4"; + m_urlFeedsUpdate = m_fixedUrl + NEXTCLOUD_API_PATH + "feeds/update?userId=%1&feedId=%2"; + m_urlDeleteFeed = m_fixedUrl + NEXTCLOUD_API_PATH + "feeds/%1"; + m_urlRenameFeed = m_fixedUrl + NEXTCLOUD_API_PATH + "feeds/%1/rename"; +} + +bool NextcloudNetworkFactory::forceServerSideUpdate() const { + return m_forceServerSideUpdate; +} + +void NextcloudNetworkFactory::setForceServerSideUpdate(bool force_update) { + m_forceServerSideUpdate = force_update; +} + +QString NextcloudNetworkFactory::authUsername() const { + return m_authUsername; +} + +void NextcloudNetworkFactory::setAuthUsername(const QString& auth_username) { + m_authUsername = auth_username; +} + +QString NextcloudNetworkFactory::authPassword() const { + return m_authPassword; +} + +void NextcloudNetworkFactory::setAuthPassword(const QString& auth_password) { + m_authPassword = auth_password; +} + +NextcloudStatusResponse NextcloudNetworkFactory::status(const QNetworkProxy& custom_proxy) { + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, NEXTCLOUD_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(m_urlStatus, + qApp->settings() + ->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)) + .toInt(), + QByteArray(), + result_raw, + QNetworkAccessManager::Operation::GetOperation, + headers, + false, + {}, + {}, + custom_proxy); + NextcloudStatusResponse status_response(network_reply.m_networkError, QString::fromUtf8(result_raw)); + + qDebugNN << LOGSEC_NEXTCLOUD << "Raw status data is:" << QUOTE_W_SPACE_DOT(result_raw); + + if (network_reply.m_networkError != QNetworkReply::NoError) { + qCriticalNN << LOGSEC_NEXTCLOUD << "Obtaining status info failed with error" + << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + } + + return status_response; +} + +NextcloudGetFeedsCategoriesResponse NextcloudNetworkFactory::feedsCategories(const QNetworkProxy& custom_proxy) { + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, NEXTCLOUD_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(m_urlFolders, + qApp->settings() + ->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)) + .toInt(), + QByteArray(), + result_raw, + QNetworkAccessManager::Operation::GetOperation, + headers, + false, + {}, + {}, + custom_proxy); + + if (network_reply.m_networkError != QNetworkReply::NoError) { + qCriticalNN << LOGSEC_NEXTCLOUD << "Obtaining of categories failed with error" + << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + return NextcloudGetFeedsCategoriesResponse(network_reply.m_networkError); + } + + QString content_categories = QString::fromUtf8(result_raw); + + // Now, obtain feeds. + network_reply = NetworkFactory::performNetworkOperation(m_urlFeeds, + qApp->settings() + ->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)) + .toInt(), + QByteArray(), + result_raw, + QNetworkAccessManager::Operation::GetOperation, + headers, + false, + {}, + {}, + custom_proxy); + + if (network_reply.m_networkError != QNetworkReply::NoError) { + qCriticalNN << LOGSEC_NEXTCLOUD << "Obtaining of feeds failed with error" + << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + return NextcloudGetFeedsCategoriesResponse(network_reply.m_networkError); + } + + QString content_feeds = QString::fromUtf8(result_raw); + + return NextcloudGetFeedsCategoriesResponse(network_reply.m_networkError, content_categories, content_feeds); +} + +bool NextcloudNetworkFactory::deleteFeed(const QString& feed_id, const QNetworkProxy& custom_proxy) { + QString final_url = m_urlDeleteFeed.arg(feed_id); + QByteArray raw_output; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, NEXTCLOUD_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(final_url, + qApp->settings() + ->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)) + .toInt(), + QByteArray(), + raw_output, + QNetworkAccessManager::Operation::DeleteOperation, + headers, + false, + {}, + {}, + custom_proxy); + + if (network_reply.m_networkError != QNetworkReply::NoError) { + qCriticalNN << LOGSEC_NEXTCLOUD << "Obtaining of categories failed with error" + << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + return false; + } + else { + return true; + } +} + +bool NextcloudNetworkFactory::createFeed(const QString& url, int parent_id, const QNetworkProxy& custom_proxy) { + QJsonObject json; + + json[QSL("url")] = url; + + auto nextcloud_version = status(custom_proxy).version(); + + if (SystemFactory::isVersionEqualOrNewer(nextcloud_version, QSL("15.1.0"))) { + json[QSL("folderId")] = parent_id == 0 ? QJsonValue(QJsonValue::Type::Null) : parent_id; + } + else { + json[QSL("folderId")] = parent_id; + } + + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, NEXTCLOUD_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(m_urlFeeds, + qApp->settings() + ->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)) + .toInt(), + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + custom_proxy); + + if (network_reply.m_networkError != QNetworkReply::NoError) { + qCriticalNN << LOGSEC_NEXTCLOUD << "Creating of category failed with error" + << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + return false; + } + else { + return true; + } +} + +bool NextcloudNetworkFactory::renameFeed(const QString& new_name, + const QString& custom_feed_id, + const QNetworkProxy& custom_proxy) { + QString final_url = m_urlRenameFeed.arg(custom_feed_id); + QByteArray result_raw; + QJsonObject json; + + json[QSL("feedTitle")] = new_name; + + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, NEXTCLOUD_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(final_url, + qApp->settings() + ->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)) + .toInt(), + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PutOperation, + headers, + false, + {}, + {}, + custom_proxy); + + if (network_reply.m_networkError != QNetworkReply::NetworkError::NoError) { + qCriticalNN << LOGSEC_NEXTCLOUD << "Renaming of feed failed with error" + << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + return false; + } + else { + return true; + } +} + +NextcloudGetMessagesResponse NextcloudNetworkFactory::getMessages(int feed_id, const QNetworkProxy& custom_proxy) { + if (forceServerSideUpdate()) { + triggerFeedUpdate(feed_id, custom_proxy); + } + + QString final_url = m_urlMessages.arg(QString::number(feed_id), + QString::number(batchSize() <= 0 ? -1 : batchSize()), + QString::number(0), + m_downloadOnlyUnreadMessages ? QSL("false") : QSL("true")); + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, NEXTCLOUD_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(final_url, + qApp->settings() + ->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)) + .toInt(), + QByteArray(), + result_raw, + QNetworkAccessManager::Operation::GetOperation, + headers, + false, + {}, + {}, + custom_proxy); + NextcloudGetMessagesResponse msgs_response(network_reply.m_networkError, QString::fromUtf8(result_raw)); + + if (network_reply.m_networkError != QNetworkReply::NoError) { + qCriticalNN << LOGSEC_NEXTCLOUD << "Obtaining messages failed with error" + << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + } + + return msgs_response; +} + +QNetworkReply::NetworkError NextcloudNetworkFactory::triggerFeedUpdate(int feed_id, const QNetworkProxy& custom_proxy) { + // Now, we can trigger the update. + QByteArray raw_output; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, NEXTCLOUD_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(m_urlFeedsUpdate.arg(authUsername(), QString::number(feed_id)), + qApp->settings() + ->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)) + .toInt(), + QByteArray(), + raw_output, + QNetworkAccessManager::Operation::GetOperation, + headers, + false, + {}, + {}, + custom_proxy); + + if (network_reply.m_networkError != QNetworkReply::NetworkError::NoError) { + qCriticalNN << LOGSEC_NEXTCLOUD << "Feeds update failed with error" + << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + } + + return network_reply.m_networkError; +} + +NetworkResult NextcloudNetworkFactory::markMessagesRead(RootItem::ReadStatus status, + const QStringList& custom_ids, + const QNetworkProxy& custom_proxy) { + QJsonObject json; + QJsonArray ids; + QString final_url; + + if (status == RootItem::ReadStatus::Read) { + final_url = m_fixedUrl + QSL(NEXTCLOUD_API_PATH) + QSL("items/read/multiple"); + } + else { + final_url = m_fixedUrl + QSL(NEXTCLOUD_API_PATH) + QSL("items/unread/multiple"); + } + + for (const QString& id : custom_ids) { + ids.append(QJsonValue(id.toInt())); + } + + json[QSL("items")] = ids; + + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, NEXTCLOUD_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + QByteArray output; + + return NetworkFactory::performNetworkOperation(final_url, + qApp->settings() + ->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)) + .toInt(), + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + output, + QNetworkAccessManager::Operation::PutOperation, + headers, + false, + {}, + {}, + custom_proxy); +} + +NetworkResult NextcloudNetworkFactory::markMessagesStarred(RootItem::Importance importance, + const QStringList& feed_ids, + const QStringList& guid_hashes, + const QNetworkProxy& custom_proxy) { + QJsonObject json; + QJsonArray ids; + QString final_url; + + if (importance == RootItem::Importance::Important) { + final_url = m_fixedUrl + NEXTCLOUD_API_PATH + "items/star/multiple"; + } + else { + final_url = m_fixedUrl + NEXTCLOUD_API_PATH + "items/unstar/multiple"; + } + + for (int i = 0; i < feed_ids.size(); i++) { + QJsonObject item; + + item[QSL("feedId")] = feed_ids.at(i); + item[QSL("guidHash")] = guid_hashes.at(i); + ids.append(item); + } + + json[QSL("items")] = ids; + + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, NEXTCLOUD_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + QByteArray output; + + return NetworkFactory::performNetworkOperation(final_url, + qApp->settings() + ->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)) + .toInt(), + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + output, + QNetworkAccessManager::Operation::PutOperation, + headers, + false, + {}, + {}, + custom_proxy); +} + +int NextcloudNetworkFactory::batchSize() const { + return m_batchSize; +} + +void NextcloudNetworkFactory::setBatchSize(int batch_size) { + m_batchSize = batch_size; +} + +bool NextcloudNetworkFactory::downloadOnlyUnreadMessages() const { + return m_downloadOnlyUnreadMessages; +} + +void NextcloudNetworkFactory::setDownloadOnlyUnreadMessages(bool dowload_only_unread_messages) { + m_downloadOnlyUnreadMessages = dowload_only_unread_messages; +} + +NextcloudResponse::NextcloudResponse(QNetworkReply::NetworkError response, const QString& raw_content) + : m_networkError(response), m_rawContent(QJsonDocument::fromJson(raw_content.toUtf8()).object()), + m_emptyString(raw_content.isEmpty()) {} + +NextcloudResponse::~NextcloudResponse() = default; + +bool NextcloudResponse::isLoaded() const { + return !m_emptyString && !m_rawContent.isEmpty(); +} + +QString NextcloudResponse::toString() const { + return QJsonDocument(m_rawContent).toJson(QJsonDocument::JsonFormat::Compact); +} + +QNetworkReply::NetworkError NextcloudResponse::networkError() const { + return m_networkError; +} + +NextcloudStatusResponse::NextcloudStatusResponse(QNetworkReply::NetworkError response, const QString& raw_content) + : NextcloudResponse(response, raw_content) {} + +NextcloudStatusResponse::~NextcloudStatusResponse() = default; + +QString NextcloudStatusResponse::version() const { + if (isLoaded()) { + return m_rawContent[QSL("version")].toString(); + } + else { + return QString(); + } +} + +NextcloudGetFeedsCategoriesResponse::NextcloudGetFeedsCategoriesResponse(QNetworkReply::NetworkError response, + QString raw_categories, + QString raw_feeds) + : NextcloudResponse(response), m_contentCategories(std::move(raw_categories)), m_contentFeeds(std::move(raw_feeds)) {} + +NextcloudGetFeedsCategoriesResponse::~NextcloudGetFeedsCategoriesResponse() = default; + +RootItem* NextcloudGetFeedsCategoriesResponse::feedsCategories(bool obtain_icons) const { + auto* parent = new RootItem(); + QMap cats; + + // Top-level feed have "folderId" set to "0" or JSON "null" value. + cats.insert(QSL("0"), parent); + + // Process categories first, then process feeds. + auto json_folders = QJsonDocument::fromJson(m_contentCategories.toUtf8()).object()[QSL("folders")].toArray(); + + for (const QJsonValue& cat : std::as_const(json_folders)) { + QJsonObject item = cat.toObject(); + auto* category = new Category(); + + category->setTitle(item[QSL("name")].toString()); + category->setCustomId(QString::number(item[QSL("id")].toInt())); + cats.insert(category->customId(), category); + + // All categories in Nextcloud are top-level. + parent->appendChild(category); + } + + // We have categories added, now add all feeds. + auto json_feeds = QJsonDocument::fromJson(m_contentFeeds.toUtf8()).object()[QSL("feeds")].toArray(); + + for (const QJsonValue& fed : std::as_const(json_feeds)) { + QJsonObject item = fed.toObject(); + auto* feed = new NextcloudFeed(); + + if (obtain_icons) { + QString icon_path = item[QSL("faviconLink")].toString(); + + if (!icon_path.isEmpty()) { + QByteArray icon_data; + + if (NetworkFactory::performNetworkOperation(icon_path, + DOWNLOAD_TIMEOUT, + QByteArray(), + icon_data, + QNetworkAccessManager::Operation::GetOperation) + .m_networkError == QNetworkReply::NetworkError::NoError) { + // Icon downloaded, set it up. + QPixmap icon_pixmap; + + icon_pixmap.loadFromData(icon_data); + feed->setIcon(QIcon(icon_pixmap)); + } + } + } + + feed->setCustomId(QString::number(item[QSL("id")].toInt())); + feed->setSource(item[QSL("url")].toString()); + + if (feed->source().isEmpty()) { + feed->setSource(item[QSL("link")].toString()); + } + + feed->setTitle(item[QSL("title")].toString()); + + if (feed->title().isEmpty()) { + if (feed->source().isEmpty()) { + // We cannot add feed which has no title and no url to RSS Guard!!! + qCriticalNN << LOGSEC_NEXTCLOUD << "Skipping feed with custom ID" << QUOTE_W_SPACE(feed->customId()) + << "from adding to RSS Guard because it has no title and url."; + continue; + } + else { + feed->setTitle(feed->source()); + } + } + + // NOTE: Starting with News 15.1.0, top-level feeds do not have parent folder ID 0, but JSON "null". + // Luckily, if folder ID is not convertible to int, then default 0 value is returned. + cats.value(QString::number(item[QSL("folderId")].toInt(0)))->appendChild(feed); + qDebugNN << LOGSEC_NEXTCLOUD << "Custom ID of next fetched processed feed is" + << QUOTE_W_SPACE_DOT(feed->customId()); + } + + return parent; +} + +NextcloudGetMessagesResponse::NextcloudGetMessagesResponse(QNetworkReply::NetworkError response, + const QString& raw_content) + : NextcloudResponse(response, raw_content) {} + +NextcloudGetMessagesResponse::~NextcloudGetMessagesResponse() = default; + +QList NextcloudGetMessagesResponse::messages() const { + QList msgs; + auto json_items = m_rawContent[QSL("items")].toArray(); + + for (const QJsonValue& message : std::as_const(json_items)) { + QJsonObject message_map = message.toObject(); + Message msg; + + msg.m_author = message_map[QSL("author")].toString(); + msg.m_contents = message_map[QSL("body")].toString(); + msg.m_created = TextFactory::parseDateTime(message_map[QSL("pubDate")].toDouble() * 1000); + msg.m_createdFromFeed = true; + msg.m_customId = message_map[QSL("id")].toVariant().toString(); + msg.m_customHash = message_map[QSL("guidHash")].toString(); + msg.m_rawContents = QJsonDocument(message_map).toJson(QJsonDocument::JsonFormat::Compact); + + // In case body is empty, check for content in mediaDescription if item is available. + if (msg.m_contents.isEmpty() && !message_map[QSL("mediaDescription")].isUndefined()) { + msg.m_contents = message_map[QSL("mediaDescription")].toString(); + } + + // Check for mediaThumbnail and append as first enclosure to be viewed in internal viewer. + if (!message_map[QSL("mediaThumbnail")].isUndefined()) { + Enclosure enclosure; + + enclosure.m_mimeType = QSL("image/jpg"); + enclosure.m_url = message_map[QSL("mediaThumbnail")].toString(); + + msg.m_enclosures.append(enclosure); + } + + QString enclosure_link = message_map[QSL("enclosureLink")].toString(); + + if (!enclosure_link.isEmpty()) { + Enclosure enclosure; + + enclosure.m_mimeType = message_map[QSL("enclosureMime")].toString(); + enclosure.m_url = enclosure_link; + + if (enclosure.m_mimeType.isEmpty()) { + enclosure.m_mimeType = QSL("image/png"); + } + + if (!message_map[QSL("enclosureMime")].toString().isEmpty() || + !enclosure_link.startsWith(QSL("https://www.youtube.com/v/"))) { + msg.m_enclosures.append(enclosure); + } + } + + msg.m_feedId = message_map[QSL("feedId")].toVariant().toString(); + msg.m_isImportant = message_map[QSL("starred")].toBool(); + msg.m_isRead = !message_map[QSL("unread")].toBool(); + msg.m_title = message_map[QSL("title")].toString(); + msg.m_url = message_map[QSL("url")].toString(); + + if (msg.m_title.simplified().isEmpty()) { + msg.m_title = message_map[QSL("mediaDescription")].toString(); + } + + if (msg.m_title.simplified().isEmpty()) { + msg.m_title = msg.m_url; + } + + msgs.append(msg); + } + + return msgs; +} diff --git a/src/librssguard-nextcloud/src/nextcloudnetworkfactory.h b/src/librssguard-nextcloud/src/nextcloudnetworkfactory.h new file mode 100644 index 000000000..4b6e30bf1 --- /dev/null +++ b/src/librssguard-nextcloud/src/nextcloudnetworkfactory.h @@ -0,0 +1,138 @@ +// For license of this file, see /LICENSE.md. + +#ifndef NEXTCLOUDNETWORKFACTORY_H +#define NEXTCLOUDNETWORKFACTORY_H + +#include +#include +#include + +#include +#include +#include +#include +#include + +class NextcloudResponse { + public: + explicit NextcloudResponse(QNetworkReply::NetworkError response, const QString& raw_content = QString()); + virtual ~NextcloudResponse(); + + bool isLoaded() const; + QString toString() const; + QNetworkReply::NetworkError networkError() const; + + protected: + QNetworkReply::NetworkError m_networkError; + QJsonObject m_rawContent; + bool m_emptyString; +}; + +class NextcloudGetMessagesResponse : public NextcloudResponse { + public: + explicit NextcloudGetMessagesResponse(QNetworkReply::NetworkError response, const QString& raw_content = QString()); + virtual ~NextcloudGetMessagesResponse(); + + QList messages() const; +}; + +class NextcloudStatusResponse : public NextcloudResponse { + public: + explicit NextcloudStatusResponse(QNetworkReply::NetworkError response, const QString& raw_content = QString()); + virtual ~NextcloudStatusResponse(); + + QString version() const; +}; + +class RootItem; + +class NextcloudGetFeedsCategoriesResponse : public NextcloudResponse { + public: + explicit NextcloudGetFeedsCategoriesResponse(QNetworkReply::NetworkError response, + QString raw_categories = QString(), + QString raw_feeds = QString()); + virtual ~NextcloudGetFeedsCategoriesResponse(); + + // Returns tree of feeds/categories. + // Top-level root of the tree is not needed here. + // Returned items do not have primary IDs assigned. + RootItem* feedsCategories(bool obtain_icons) const; + + private: + QString m_contentCategories; + QString m_contentFeeds; +}; + +class NextcloudNetworkFactory { + public: + explicit NextcloudNetworkFactory(); + virtual ~NextcloudNetworkFactory(); + + QString url() const; + void setUrl(const QString& url); + + bool forceServerSideUpdate() const; + void setForceServerSideUpdate(bool force_update); + + QString authUsername() const; + void setAuthUsername(const QString& auth_username); + + QString authPassword() const; + void setAuthPassword(const QString& auth_password); + + // Gets/sets the amount of messages to obtain during single feed update. + int batchSize() const; + void setBatchSize(int batch_size); + + bool downloadOnlyUnreadMessages() const; + void setDownloadOnlyUnreadMessages(bool dowload_only_unread_messages); + + // Operations. + + // Get version info. + NextcloudStatusResponse status(const QNetworkProxy& custom_proxy); + + // Get feeds & categories (used for sync-in). + NextcloudGetFeedsCategoriesResponse feedsCategories(const QNetworkProxy& custom_proxy); + + // Feed operations. + bool deleteFeed(const QString& feed_id, const QNetworkProxy& custom_proxy); + bool createFeed(const QString& url, int parent_id, const QNetworkProxy& custom_proxy); + bool renameFeed(const QString& new_name, const QString& custom_feed_id, const QNetworkProxy& custom_proxy); + + // Get messages for given feed. + NextcloudGetMessagesResponse getMessages(int feed_id, const QNetworkProxy& custom_proxy); + + // Misc methods. + QNetworkReply::NetworkError triggerFeedUpdate(int feed_id, const QNetworkProxy& custom_proxy); + + NetworkResult markMessagesRead(RootItem::ReadStatus status, + const QStringList& custom_ids, + const QNetworkProxy& custom_proxy); + + NetworkResult markMessagesStarred(RootItem::Importance importance, + const QStringList& feed_ids, + const QStringList& guid_hashes, + const QNetworkProxy& custom_proxy); + + private: + QString m_url; + QString m_fixedUrl; + bool m_downloadOnlyUnreadMessages; + bool m_forceServerSideUpdate; + QString m_authUsername; + QString m_authPassword; + int m_batchSize; + + // Endpoints. + QString m_urlUser; + QString m_urlStatus; + QString m_urlFolders; + QString m_urlFeeds; + QString m_urlMessages; + QString m_urlFeedsUpdate; + QString m_urlDeleteFeed; + QString m_urlRenameFeed; +}; + +#endif // NEXTCLOUDNETWORKFACTORY_H diff --git a/src/librssguard-nextcloud/src/nextcloudserviceentrypoint.cpp b/src/librssguard-nextcloud/src/nextcloudserviceentrypoint.cpp new file mode 100644 index 000000000..ea461ced8 --- /dev/null +++ b/src/librssguard-nextcloud/src/nextcloudserviceentrypoint.cpp @@ -0,0 +1,52 @@ +// For license of this file, see /LICENSE.md. + +#include "src/nextcloudserviceentrypoint.h" + +#include "src/definitions.h" +#include "src/gui/formeditnextcloudaccount.h" +#include "src/nextcloudserviceroot.h" + +#include +#include +#include +#include + +NextcloudServiceEntryPoint::NextcloudServiceEntryPoint(QObject* parent) : QObject(parent) {} + +NextcloudServiceEntryPoint::~NextcloudServiceEntryPoint() { + qDebugNN << LOGSEC_GMAIL << "Destructing" << QUOTE_W_SPACE(QSL(SERVICE_CODE_NEXTCLOUD)) << "plugin."; +} + +ServiceRoot* NextcloudServiceEntryPoint::createNewRoot() const { + FormEditNextcloudAccount form_acc(qApp->mainFormWidget()); + + return form_acc.addEditAccount(); +} + +QList NextcloudServiceEntryPoint::initializeSubtree() const { + QSqlDatabase database = qApp->database()->driver()->connection(QSL("NextcloudServiceEntryPoint")); + + return DatabaseQueries::getAccounts(database, code()); +} + +QString NextcloudServiceEntryPoint::name() const { + return QSL("Nextcloud News"); +} + +QString NextcloudServiceEntryPoint::code() const { + return QSL(SERVICE_CODE_NEXTCLOUD); +} + +QString NextcloudServiceEntryPoint::description() const { + return QObject::tr("The News app is an RSS/Atom feed aggregator. " + "It is part of Nextcloud suite. This plugin implements %1 API.") + .arg(QSL(NEXTCLOUD_API_VERSION)); +} + +QString NextcloudServiceEntryPoint::author() const { + return QSL(APP_AUTHOR); +} + +QIcon NextcloudServiceEntryPoint::icon() const { + return qApp->icons()->miscIcon(QSL("nextcloud")); +} diff --git a/src/librssguard-nextcloud/src/nextcloudserviceentrypoint.h b/src/librssguard-nextcloud/src/nextcloudserviceentrypoint.h new file mode 100644 index 000000000..c63bb8f5e --- /dev/null +++ b/src/librssguard-nextcloud/src/nextcloudserviceentrypoint.h @@ -0,0 +1,26 @@ +// For license of this file, see /LICENSE.md. + +#ifndef NEXTCLOUDSERVICEENTRYPOINT_H +#define NEXTCLOUDSERVICEENTRYPOINT_H + +#include + +class NextcloudServiceEntryPoint : public QObject, public ServiceEntryPoint { + Q_OBJECT + Q_PLUGIN_METADATA(IID "io.github.martinrotter.rssguard.nextcloud" FILE "plugin.json") + Q_INTERFACES(ServiceEntryPoint) + + public: + explicit NextcloudServiceEntryPoint(QObject* parent = nullptr); + virtual ~NextcloudServiceEntryPoint(); + + virtual ServiceRoot* createNewRoot() const; + virtual QList initializeSubtree() const; + virtual QString name() const; + virtual QString code() const; + virtual QString description() const; + virtual QString author() const; + virtual QIcon icon() const; +}; + +#endif // NEXTCLOUDSERVICEENTRYPOINT_H diff --git a/src/librssguard-nextcloud/src/nextcloudserviceroot.cpp b/src/librssguard-nextcloud/src/nextcloudserviceroot.cpp new file mode 100644 index 000000000..b7e03457c --- /dev/null +++ b/src/librssguard-nextcloud/src/nextcloudserviceroot.cpp @@ -0,0 +1,177 @@ +// For license of this file, see /LICENSE.md. + +#include "src/nextcloudserviceroot.h" + +#include "src/gui/formeditnextcloudaccount.h" +#include "src/nextcloudfeed.h" +#include "src/nextcloudnetworkfactory.h" +#include "src/nextcloudserviceentrypoint.h" + +#include +#include +#include +#include +#include +#include + +NextcloudServiceRoot::NextcloudServiceRoot(RootItem* parent) + : ServiceRoot(parent), m_network(new NextcloudNetworkFactory()) { + setIcon(NextcloudServiceEntryPoint().icon()); +} + +NextcloudServiceRoot::~NextcloudServiceRoot() { + delete m_network; +} + +bool NextcloudServiceRoot::isSyncable() const { + return true; +} + +bool NextcloudServiceRoot::canBeEdited() const { + return true; +} + +FormAccountDetails* NextcloudServiceRoot::accountSetupDialog() const { + return new FormEditNextcloudAccount(qApp->mainFormWidget()); +} + +void NextcloudServiceRoot::editItems(const QList& items) { + if (items.first()->kind() == RootItem::Kind::ServiceRoot) { + QScopedPointer p(qobject_cast(accountSetupDialog())); + + p->addEditAccount(this); + return; + } + + ServiceRoot::editItems(items); +} + +bool NextcloudServiceRoot::supportsFeedAdding() const { + return false; +} + +bool NextcloudServiceRoot::supportsCategoryAdding() const { + return false; +} + +void NextcloudServiceRoot::start(bool freshly_activated) { + if (!freshly_activated) { + DatabaseQueries::loadRootFromDatabase(this); + loadCacheFromFile(); + } + + updateTitle(); + + if (getSubTreeFeeds().isEmpty()) { + syncIn(); + } +} + +QString NextcloudServiceRoot::code() const { + return NextcloudServiceEntryPoint().code(); +} + +NextcloudNetworkFactory* NextcloudServiceRoot::network() const { + return m_network; +} + +void NextcloudServiceRoot::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()) { + auto res = network()->markMessagesRead(key, ids, networkProxy()); + + if (!ignore_errors && res.m_networkError != QNetworkReply::NetworkError::NoError) { + 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 feed_ids, guid_hashes; + + for (const Message& msg : messages) { + feed_ids.append(msg.m_feedId); + guid_hashes.append(msg.m_customHash); + } + + auto res = network()->markMessagesStarred(key, feed_ids, guid_hashes, networkProxy()); + + if (!ignore_errors && res.m_networkError != QNetworkReply::NetworkError::NoError) { + addMessageStatesToCache(messages, key); + } + } + } +} + +void NextcloudServiceRoot::updateTitle() { + setTitle(m_network->authUsername() + QSL(" (Nextcloud News)")); +} + +RootItem* NextcloudServiceRoot::obtainNewTreeForSyncIn() const { + NextcloudGetFeedsCategoriesResponse feed_cats_response = m_network->feedsCategories(networkProxy()); + + if (feed_cats_response.networkError() == QNetworkReply::NetworkError::NoError) { + return feed_cats_response.feedsCategories(true); + } + else { + throw NetworkException(feed_cats_response.networkError(), + tr("cannot get list of feeds, network error '%1'").arg(feed_cats_response.networkError())); + } +} + +QVariantHash NextcloudServiceRoot::customDatabaseData() const { + QVariantHash data = ServiceRoot::customDatabaseData(); + + data[QSL("auth_username")] = m_network->authUsername(); + data[QSL("auth_password")] = TextFactory::encrypt(m_network->authPassword()); + data[QSL("url")] = m_network->url(); + data[QSL("force_update")] = m_network->forceServerSideUpdate(); + data[QSL("batch_size")] = m_network->batchSize(); + data[QSL("download_only_unread")] = m_network->downloadOnlyUnreadMessages(); + + return data; +} + +void NextcloudServiceRoot::setCustomDatabaseData(const QVariantHash& data) { + ServiceRoot::setCustomDatabaseData(data); + + m_network->setAuthUsername(data[QSL("auth_username")].toString()); + m_network->setAuthPassword(TextFactory::decrypt(data[QSL("auth_password")].toString())); + m_network->setUrl(data[QSL("url")].toString()); + m_network->setForceServerSideUpdate(data[QSL("force_update")].toBool()); + m_network->setBatchSize(data[QSL("batch_size")].toInt()); + m_network->setDownloadOnlyUnreadMessages(data[QSL("download_only_unread")].toBool()); +} + +QList NextcloudServiceRoot::obtainNewMessages(Feed* feed, + const QHash& + stated_messages, + const QHash& tagged_messages) { + Q_UNUSED(stated_messages) + Q_UNUSED(tagged_messages) + + NextcloudGetMessagesResponse messages = network()->getMessages(feed->customNumericId(), networkProxy()); + + if (messages.networkError() != QNetworkReply::NetworkError::NoError) { + throw FeedFetchException(Feed::Status::NetworkError); + } + else { + return messages.messages(); + } +} diff --git a/src/librssguard-nextcloud/src/nextcloudserviceroot.h b/src/librssguard-nextcloud/src/nextcloudserviceroot.h new file mode 100644 index 000000000..e197e5134 --- /dev/null +++ b/src/librssguard-nextcloud/src/nextcloudserviceroot.h @@ -0,0 +1,48 @@ +// For license of this file, see /LICENSE.md. + +#ifndef NEXTCLOUDSERVICEROOT_H +#define NEXTCLOUDSERVICEROOT_H + +#include +#include + +#include + +class NextcloudNetworkFactory; +class Mutex; + +class NextcloudServiceRoot : public ServiceRoot, public CacheForServiceRoot { + Q_OBJECT + + public: + explicit NextcloudServiceRoot(RootItem* parent = nullptr); + virtual ~NextcloudServiceRoot(); + + virtual bool isSyncable() const; + virtual bool canBeEdited() const; + virtual void editItems(const QList& items); + virtual FormAccountDetails* accountSetupDialog() const; + virtual bool supportsFeedAdding() const; + virtual bool supportsCategoryAdding() const; + virtual void start(bool freshly_activated); + virtual QString code() const; + virtual void saveAllCachedData(bool ignore_errors); + virtual QVariantHash customDatabaseData() const; + virtual void setCustomDatabaseData(const QVariantHash& data); + virtual QList obtainNewMessages(Feed* feed, + const QHash& stated_messages, + const QHash& tagged_messages); + + NextcloudNetworkFactory* network() const; + + protected: + virtual RootItem* obtainNewTreeForSyncIn() const; + + private: + void updateTitle(); + + private: + NextcloudNetworkFactory* m_network; +}; + +#endif // NEXTCLOUDSERVICEROOT_H diff --git a/src/librssguard-ttrss/CMakeLists.txt b/src/librssguard-ttrss/CMakeLists.txt new file mode 100644 index 000000000..7e1a186ba --- /dev/null +++ b/src/librssguard-ttrss/CMakeLists.txt @@ -0,0 +1,88 @@ +if(NOT DEFINED LIBRSSGUARD_BINARY_PATH) + set(LIBRSSGUARD_SOURCE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/..") +endif() + +set(SOURCES + src/definitions.h + src/gui/formeditttrssaccount.cpp + src/gui/formeditttrssaccount.h + src/gui/formttrssfeeddetails.cpp + src/gui/formttrssfeeddetails.h + src/gui/formttrssnote.cpp + src/gui/formttrssnote.h + src/gui/ttrssaccountdetails.cpp + src/gui/ttrssaccountdetails.h + src/gui/ttrssfeeddetails.cpp + src/gui/ttrssfeeddetails.h + src/ttrssfeed.cpp + src/ttrssfeed.h + src/ttrssnetworkfactory.cpp + src/ttrssnetworkfactory.h + src/ttrssnotetopublish.h + src/ttrssserviceentrypoint.cpp + src/ttrssserviceentrypoint.h + src/ttrssserviceroot.cpp + src/ttrssserviceroot.h + ) + +set(UI_FILES + src/gui/formttrssnote.ui + src/gui/ttrssaccountdetails.ui + src/gui/ttrssfeeddetails.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-ttrss SHARED ${SOURCES} ${QM_FILES}) + +# Add specific definitions. +target_compile_definitions(rssguard-ttrss + PRIVATE + RSSGUARD_DLLSPEC=Q_DECL_IMPORT + RSSGUARD_DLLSPEC_EXPORT=Q_DECL_EXPORT +) + +target_include_directories(rssguard-ttrss + PUBLIC + ${LIBRSSGUARD_SOURCE_PATH} + src/3rd-party/richtexteditor +) + +# Qt. +target_link_libraries(rssguard-ttrss 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-ttrss DESTINATION plugins) +elseif(UNIX AND NOT APPLE AND NOT ANDROID) + include (GNUInstallDirs) + install(TARGETS rssguard-ttrss + DESTINATION ${CMAKE_INSTALL_LIBDIR}/rssguard + ) +elseif(APPLE) + install(TARGETS rssguard-ttrss + DESTINATION Contents/MacOS + ) +endif() diff --git a/src/librssguard-ttrss/plugin.json b/src/librssguard-ttrss/plugin.json new file mode 100644 index 000000000..fc52165ed --- /dev/null +++ b/src/librssguard-ttrss/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "Tiny Tiny RSS", + "author": "Martin Rotter", + "website": "https://github.com/martinrotter/rssguard" +} \ No newline at end of file diff --git a/src/librssguard-ttrss/src/definitions.h b/src/librssguard-ttrss/src/definitions.h new file mode 100644 index 000000000..b76994be2 --- /dev/null +++ b/src/librssguard-ttrss/src/definitions.h @@ -0,0 +1,49 @@ +// For license of this file, see /LICENSE.md. + +#ifndef TTRSS_DEFINITIONS_H +#define TTRSS_DEFINITIONS_H + +#define TTRSS_MINIMAL_API_LEVEL 9 +#define TTRSS_CONTENT_TYPE_JSON "application/json; charset=utf-8" + +/// +/// Errors. +/// +#define TTRSS_NOT_LOGGED_IN "NOT_LOGGED_IN" // Error when user needs to login before making an operation. +#define TTRSS_UNKNOWN_METHOD "UNKNOWN_METHOD" // Given "op" is not recognized. +#define TTRSS_INCORRECT_USAGE "INCORRECT_USAGE" // Given "op" was used with bad parameters. + +// Limitations +#define TTRSS_DEFAULT_MESSAGES 100 +#define TTRSS_MAX_MESSAGES 200 + +// General return status codes. +#define TTRSS_API_STATUS_OK 0 +#define TTRSS_API_STATUS_ERR 1 +#define TTRSS_CONTENT_NOT_LOADED -1 + +// Login. +#define TTRSS_API_DISABLED "API_DISABLED" // API is not enabled. +#define TTRSS_LOGIN_ERROR "LOGIN_ERROR" // Incorrect password/username. + +// Get feed tree. +#define TTRSS_GFT_TYPE_CATEGORY "category" + +// "Published" feed/label. +#define TTRSS_PUBLISHED_LABEL_ID -2 +#define TTRSS_PUBLISHED_FEED_ID 0 + +// Subscribe to feed. +#define STF_UNKNOWN -1 +#define STF_EXISTS 0 +#define STF_INVALID_URL 2 +#define STF_UNREACHABLE_URL 5 +#define STF_URL_NO_FEED 3 +#define STF_URL_MANY_FEEDS 4 +#define STF_INSERTED 1 + +// Unsubscribe from feed. +#define UFF_FEED_NOT_FOUND "FEED_NOT_FOUND" +#define UFF_OK "OK" + +#endif // TTRSS_DEFINITIONS_H diff --git a/src/librssguard-ttrss/src/gui/formeditttrssaccount.cpp b/src/librssguard-ttrss/src/gui/formeditttrssaccount.cpp new file mode 100644 index 000000000..8b85d46e8 --- /dev/null +++ b/src/librssguard-ttrss/src/gui/formeditttrssaccount.cpp @@ -0,0 +1,70 @@ +// For license of this file, see /LICENSE.md. + +#include "src/gui/formeditttrssaccount.h" + +#include "src/gui/ttrssaccountdetails.h" +#include "src/ttrssnetworkfactory.h" +#include "src/ttrssserviceroot.h" + +#include + +FormEditTtRssAccount::FormEditTtRssAccount(QWidget* parent) + : FormAccountDetails(qApp->icons()->miscIcon(QSL("tt-rss")), parent), m_details(new TtRssAccountDetails(this)) { + insertCustomTab(m_details, tr("Server setup"), 0); + activateTab(0); + + connect(m_details->m_ui.m_btnTestSetup, &QPushButton::clicked, this, &FormEditTtRssAccount::performTest); + m_details->m_ui.m_txtUrl->setFocus(); +} + +void FormEditTtRssAccount::apply() { + FormAccountDetails::apply(); + + bool using_another_acc = + m_details->m_ui.m_txtUsername->lineEdit()->text() != account()->network()->username() || + m_details->m_ui.m_txtUrl->lineEdit()->text() != account()->network()->url(); + + account()->network()->logout(m_account->networkProxy()); + account()->network()->setUrl(m_details->m_ui.m_txtUrl->lineEdit()->text()); + account()->network()->setUsername(m_details->m_ui.m_txtUsername->lineEdit()->text()); + account()->network()->setPassword(m_details->m_ui.m_txtPassword->lineEdit()->text()); + account()->network()->setAuthIsUsed(m_details->m_ui.m_gbHttpAuthentication->isChecked()); + account()->network()->setAuthUsername(m_details->m_ui.m_txtHttpUsername->lineEdit()->text()); + account()->network()->setAuthPassword(m_details->m_ui.m_txtHttpPassword->lineEdit()->text()); + account()->network()->setBatchSize(m_details->m_ui.m_spinLimitMessages->value()); + account()->network()->setIntelligentSynchronization(m_details->m_ui.m_cbNewAlgorithm->isChecked()); + account()->network()->setForceServerSideUpdate(m_details->m_ui.m_checkServerSideUpdate + ->isChecked()); + account() + ->network() + ->setDownloadOnlyUnreadMessages(m_details->m_ui.m_checkDownloadOnlyUnreadMessages->isChecked()); + + account()->saveAccountDataToDatabase(); + accept(); + + if (!m_creatingNew && using_another_acc) { + account()->completelyRemoveAllData(); + account()->start(true); + } +} + +void FormEditTtRssAccount::loadAccountData() { + FormAccountDetails::loadAccountData(); + + TtRssServiceRoot* existing_root = account(); + + m_details->m_ui.m_gbHttpAuthentication->setChecked(existing_root->network()->authIsUsed()); + m_details->m_ui.m_txtHttpPassword->lineEdit()->setText(existing_root->network()->authPassword()); + m_details->m_ui.m_txtHttpUsername->lineEdit()->setText(existing_root->network()->authUsername()); + m_details->m_ui.m_txtUsername->lineEdit()->setText(existing_root->network()->username()); + m_details->m_ui.m_txtPassword->lineEdit()->setText(existing_root->network()->password()); + m_details->m_ui.m_txtUrl->lineEdit()->setText(existing_root->network()->url()); + m_details->m_ui.m_spinLimitMessages->setValue(existing_root->network()->batchSize()); + m_details->m_ui.m_checkServerSideUpdate->setChecked(existing_root->network()->forceServerSideUpdate()); + m_details->m_ui.m_checkDownloadOnlyUnreadMessages->setChecked(existing_root->network()->downloadOnlyUnreadMessages()); + m_details->m_ui.m_cbNewAlgorithm->setChecked(existing_root->network()->intelligentSynchronization()); +} + +void FormEditTtRssAccount::performTest() { + m_details->performTest(m_proxyDetails->proxy()); +} diff --git a/src/librssguard-ttrss/src/gui/formeditttrssaccount.h b/src/librssguard-ttrss/src/gui/formeditttrssaccount.h new file mode 100644 index 000000000..4dd0faff1 --- /dev/null +++ b/src/librssguard-ttrss/src/gui/formeditttrssaccount.h @@ -0,0 +1,31 @@ +// For license of this file, see /LICENSE.md. + +#ifndef FORMEDITACCOUNT_H +#define FORMEDITACCOUNT_H + +#include + +class RootItem; +class TtRssServiceRoot; +class TtRssAccountDetails; + +class FormEditTtRssAccount : public FormAccountDetails { + Q_OBJECT + + public: + explicit FormEditTtRssAccount(QWidget* parent = nullptr); + + protected slots: + virtual void apply(); + + protected: + virtual void loadAccountData(); + + private slots: + void performTest(); + + private: + TtRssAccountDetails* m_details; +}; + +#endif // FORMEDITACCOUNT_H diff --git a/src/librssguard-ttrss/src/gui/formttrssfeeddetails.cpp b/src/librssguard-ttrss/src/gui/formttrssfeeddetails.cpp new file mode 100644 index 000000000..dc1fa1d09 --- /dev/null +++ b/src/librssguard-ttrss/src/gui/formttrssfeeddetails.cpp @@ -0,0 +1,74 @@ +// For license of this file, see /LICENSE.md. + +#include "src/gui/formttrssfeeddetails.h" + +#include "src/definitions.h" +#include "src/gui/ttrssfeeddetails.h" +#include "src/ttrssnetworkfactory.h" +#include "src/ttrssserviceroot.h" + +#include +#include +#include + +#include +#include + +FormTtRssFeedDetails::FormTtRssFeedDetails(ServiceRoot* service_root, + RootItem* parent_to_select, + const QString& url, + QWidget* parent) + : FormFeedDetails(service_root, parent), m_feedDetails(new TtRssFeedDetails(this)), + m_authDetails(new AuthenticationDetails(true, this)), m_parentToSelect(parent_to_select), m_urlToProcess(url) {} + +void FormTtRssFeedDetails::apply() { + if (!m_creatingNew) { + // NOTE: We can only edit base properties, therefore + // base method is fine. + FormFeedDetails::apply(); + } + else { + RootItem* parent = m_feedDetails->ui.m_cmbParentCategory->currentData().value(); + auto* root = qobject_cast(parent->getParentServiceRoot()); + const int category_id = parent->kind() == RootItem::Kind::ServiceRoot ? 0 : parent->customNumericId(); + const TtRssSubscribeToFeedResponse response = + root->network()->subscribeToFeed(m_feedDetails->ui.m_txtUrl->lineEdit()->text(), + category_id, + m_serviceRoot->networkProxy(), + m_authDetails->authenticationType() == + NetworkFactory::NetworkAuthentication::Basic, + m_authDetails->username(), + m_authDetails->password()); + + if (response.code() == STF_INSERTED) { + // Feed was added online. + qApp->showGuiMessage(Notification::Event::GeneralEvent, + {tr("Feed added"), + tr("Feed was added, obtaining new tree of feeds now."), + QSystemTrayIcon::MessageIcon::Information}); + QTimer::singleShot(300, root, &TtRssServiceRoot::syncIn); + } + else { + throw ApplicationException(tr("API returned error code %1").arg(QString::number(response.code()))); + } + } +} + +void FormTtRssFeedDetails::loadFeedData() { + FormFeedDetails::loadFeedData(); + + if (m_creatingNew) { + insertCustomTab(m_feedDetails, tr("General"), 0); + insertCustomTab(m_authDetails, tr("Network"), 1); + activateTab(0); + + m_feedDetails->loadCategories(m_serviceRoot->getSubTreeCategories(), m_serviceRoot, m_parentToSelect); + + if (!m_urlToProcess.isEmpty()) { + m_feedDetails->ui.m_txtUrl->lineEdit()->setText(m_urlToProcess); + } + + m_feedDetails->ui.m_txtUrl->lineEdit()->selectAll(); + m_feedDetails->ui.m_txtUrl->setFocus(); + } +} diff --git a/src/librssguard-ttrss/src/gui/formttrssfeeddetails.h b/src/librssguard-ttrss/src/gui/formttrssfeeddetails.h new file mode 100644 index 000000000..e89dd115d --- /dev/null +++ b/src/librssguard-ttrss/src/gui/formttrssfeeddetails.h @@ -0,0 +1,32 @@ +// For license of this file, see /LICENSE.md. + +#ifndef FORMTTRSSFEEDDETAILS_H +#define FORMTTRSSFEEDDETAILS_H + +#include + +class TtRssFeed; +class TtRssFeedDetails; +class AuthenticationDetails; + +class FormTtRssFeedDetails : public FormFeedDetails { + public: + explicit FormTtRssFeedDetails(ServiceRoot* service_root, + RootItem* parent_to_select = nullptr, + const QString& url = QString(), + QWidget* parent = nullptr); + + protected slots: + virtual void apply(); + + private: + virtual void loadFeedData(); + + private: + TtRssFeedDetails* m_feedDetails; + AuthenticationDetails* m_authDetails; + RootItem* m_parentToSelect; + QString m_urlToProcess; +}; + +#endif // FORMTTRSSFEEDDETAILS_H diff --git a/src/librssguard-ttrss/src/gui/formttrssnote.cpp b/src/librssguard-ttrss/src/gui/formttrssnote.cpp new file mode 100644 index 000000000..eee0f8270 --- /dev/null +++ b/src/librssguard-ttrss/src/gui/formttrssnote.cpp @@ -0,0 +1,79 @@ +// For license of this file, see /LICENSE.md. + +#include "src/gui/formttrssnote.h" + +#include "src/definitions.h" +#include "src/ttrssnetworkfactory.h" +#include "src/ttrssnotetopublish.h" +#include "src/ttrssserviceroot.h" + +#include +#include +#include +#include + +#include + +FormTtRssNote::FormTtRssNote(TtRssServiceRoot* root) + : QDialog(qApp->mainFormWidget()), m_root(root), m_titleOk(false), m_urlOk(false) { + m_ui.setupUi(this); + + GuiUtilities::applyDialogProperties(*this, + qApp->icons()->fromTheme(QSL("emblem-shared")), + tr("Share note to \"Published\" feed")); + + setTabOrder(m_ui.m_txtTitle->lineEdit(), m_ui.m_txtUrl->lineEdit()); + setTabOrder(m_ui.m_txtUrl->lineEdit(), m_ui.m_txtContent); + setTabOrder(m_ui.m_txtContent, m_ui.m_btnBox); + + connect(m_ui.m_txtTitle->lineEdit(), &BaseLineEdit::textChanged, this, &FormTtRssNote::onTitleChanged); + connect(m_ui.m_txtUrl->lineEdit(), &BaseLineEdit::textChanged, this, &FormTtRssNote::onUrlChanged); + connect(m_ui.m_btnBox, &QDialogButtonBox::accepted, this, &FormTtRssNote::sendNote); + + emit m_ui.m_txtTitle->lineEdit()->textChanged({}); + emit m_ui.m_txtUrl->lineEdit()->textChanged({}); +} + +void FormTtRssNote::sendNote() { + TtRssNoteToPublish note; + + note.m_content = m_ui.m_txtContent->toPlainText(); + note.m_url = m_ui.m_txtUrl->lineEdit()->text(); + note.m_title = m_ui.m_txtTitle->lineEdit()->text(); + + auto res = m_root->network()->shareToPublished(note, m_root->networkProxy()); + + if (res.status() == TTRSS_API_STATUS_OK) { + accept(); + } + else { + MsgBox::show({}, + QMessageBox::Icon::Critical, + tr("Cannot share note"), + tr("There was an error, when trying to send your custom note."), + {}, + res.error()); + } +} + +void FormTtRssNote::onTitleChanged(const QString& text) { + m_titleOk = !text.simplified().isEmpty(); + + m_ui.m_txtTitle->setStatus(m_titleOk ? WidgetWithStatus::StatusType::Ok : WidgetWithStatus::StatusType::Error, + tr("Enter non-empty title.")); + + updateOkButton(); +} + +void FormTtRssNote::onUrlChanged(const QString& text) { + m_urlOk = text.startsWith(URI_SCHEME_HTTPS) || text.startsWith(URI_SCHEME_HTTP); + + m_ui.m_txtUrl->setStatus(m_urlOk ? WidgetWithStatus::StatusType::Ok : WidgetWithStatus::StatusType::Error, + tr("Enter valid URL.")); + + updateOkButton(); +} + +void FormTtRssNote::updateOkButton() { + m_ui.m_btnBox->button(QDialogButtonBox::StandardButton::Ok)->setEnabled(m_urlOk && m_titleOk); +} diff --git a/src/librssguard-ttrss/src/gui/formttrssnote.h b/src/librssguard-ttrss/src/gui/formttrssnote.h new file mode 100644 index 000000000..fa8d478a0 --- /dev/null +++ b/src/librssguard-ttrss/src/gui/formttrssnote.h @@ -0,0 +1,33 @@ +// For license of this file, see /LICENSE.md. + +#ifndef FORMTTRSSNOTE_H +#define FORMTTRSSNOTE_H + +#include "ui_formttrssnote.h" + +#include + +class TtRssServiceRoot; + +class FormTtRssNote : public QDialog { + Q_OBJECT + + public: + explicit FormTtRssNote(TtRssServiceRoot* root); + + private slots: + void sendNote(); + void onTitleChanged(const QString& text); + void onUrlChanged(const QString& text); + + private: + void updateOkButton(); + + private: + Ui::FormTtRssNote m_ui; + TtRssServiceRoot* m_root; + bool m_titleOk; + bool m_urlOk; +}; + +#endif // FORMTTRSSNOTE_H diff --git a/src/librssguard-ttrss/src/gui/formttrssnote.ui b/src/librssguard-ttrss/src/gui/formttrssnote.ui new file mode 100644 index 000000000..e84325232 --- /dev/null +++ b/src/librssguard-ttrss/src/gui/formttrssnote.ui @@ -0,0 +1,105 @@ + + + FormTtRssNote + + + + 0 + 0 + 400 + 340 + + + + + + + Title + + + m_txtTitle + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + URL + + + m_txtUrl + + + + + + + Content + + + m_txtContent + + + + + + + + + + + LineEditWithStatus + QWidget +
lineeditwithstatus.h
+ 1 +
+
+ + + + m_btnBox + rejected() + FormTtRssNote + reject() + + + 295 + 327 + + + 286 + 274 + + + + +
diff --git a/src/librssguard-ttrss/src/gui/ttrssaccountdetails.cpp b/src/librssguard-ttrss/src/gui/ttrssaccountdetails.cpp new file mode 100644 index 000000000..627f922dd --- /dev/null +++ b/src/librssguard-ttrss/src/gui/ttrssaccountdetails.cpp @@ -0,0 +1,189 @@ +// For license of this file, see /LICENSE.md. + +#include "src/gui/ttrssaccountdetails.h" + +#include "src/definitions.h" +#include "src/ttrssnetworkfactory.h" + +#include +#include + +TtRssAccountDetails::TtRssAccountDetails(QWidget* parent) : QWidget(parent) { + m_ui.setupUi(this); + + m_ui.m_lblTestResult->label()->setWordWrap(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.

" + "Also, make sure to install api_newsplus TT-RSS " + "plugin to your server instance."), + true, + true); + m_ui.m_lblServerSideUpdateInformation + ->setHelpText(tr("Leaving this option on causes that updates " + "of feeds will be probably much slower and may time-out often."), + true); + m_ui.m_txtHttpUsername->lineEdit()->setPlaceholderText(tr("HTTP authentication username")); + m_ui.m_txtHttpPassword->lineEdit()->setPlaceholderText(tr("HTTP authentication password")); + m_ui.m_txtPassword->lineEdit()->setPlaceholderText(tr("Password for your TT-RSS account")); + m_ui.m_txtUsername->lineEdit()->setPlaceholderText(tr("Username for your TT-RSS account")); + m_ui.m_txtUrl->lineEdit()->setPlaceholderText(tr("URL of your TT-RSS instance WITHOUT trailing \"/api/\" string")); + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Information, + tr("No test done yet."), + tr("Here, results of connection test are shown.")); + + setTabOrder(m_ui.m_txtUrl->lineEdit(), m_ui.m_checkDownloadOnlyUnreadMessages); + setTabOrder(m_ui.m_checkDownloadOnlyUnreadMessages, m_ui.m_spinLimitMessages); + setTabOrder(m_ui.m_spinLimitMessages, m_ui.m_cbNewAlgorithm); + setTabOrder(m_ui.m_cbNewAlgorithm, m_ui.m_checkServerSideUpdate); + setTabOrder(m_ui.m_checkServerSideUpdate, m_ui.m_txtUsername->lineEdit()); + setTabOrder(m_ui.m_txtUsername->lineEdit(), m_ui.m_txtPassword->lineEdit()); + setTabOrder(m_ui.m_txtPassword->lineEdit(), m_ui.m_gbHttpAuthentication); + setTabOrder(m_ui.m_gbHttpAuthentication, m_ui.m_txtHttpUsername->lineEdit()); + setTabOrder(m_ui.m_txtHttpUsername->lineEdit(), m_ui.m_txtHttpPassword->lineEdit()); + setTabOrder(m_ui.m_txtHttpPassword->lineEdit(), m_ui.m_btnTestSetup); + + m_ui.m_txtHttpPassword->lineEdit()->setPasswordMode(true); + m_ui.m_txtPassword->lineEdit()->setPasswordMode(true); + + connect(m_ui.m_txtPassword->lineEdit(), &BaseLineEdit::textChanged, this, &TtRssAccountDetails::onPasswordChanged); + connect(m_ui.m_txtUsername->lineEdit(), &BaseLineEdit::textChanged, this, &TtRssAccountDetails::onUsernameChanged); + connect(m_ui.m_txtHttpPassword->lineEdit(), + &BaseLineEdit::textChanged, + this, + &TtRssAccountDetails::onHttpPasswordChanged); + connect(m_ui.m_txtHttpUsername->lineEdit(), + &BaseLineEdit::textChanged, + this, + &TtRssAccountDetails::onHttpUsernameChanged); + connect(m_ui.m_txtUrl->lineEdit(), &BaseLineEdit::textChanged, this, &TtRssAccountDetails::onUrlChanged); + connect(m_ui.m_gbHttpAuthentication, &QGroupBox::toggled, this, &TtRssAccountDetails::onHttpPasswordChanged); + connect(m_ui.m_gbHttpAuthentication, &QGroupBox::toggled, this, &TtRssAccountDetails::onHttpUsernameChanged); + + onPasswordChanged(); + onUsernameChanged(); + onUrlChanged(); + onHttpPasswordChanged(); + onHttpUsernameChanged(); +} + +void TtRssAccountDetails::performTest(const QNetworkProxy& proxy) { + TtRssNetworkFactory factory; + + factory.setUsername(m_ui.m_txtUsername->lineEdit()->text()); + factory.setPassword(m_ui.m_txtPassword->lineEdit()->text()); + factory.setUrl(m_ui.m_txtUrl->lineEdit()->text()); + factory.setAuthIsUsed(m_ui.m_gbHttpAuthentication->isChecked()); + factory.setAuthUsername(m_ui.m_txtHttpUsername->lineEdit()->text()); + factory.setAuthPassword(m_ui.m_txtHttpPassword->lineEdit()->text()); + factory.setForceServerSideUpdate(m_ui.m_checkServerSideUpdate->isChecked()); + factory.setBatchSize(m_ui.m_spinLimitMessages->value()); + + TtRssLoginResponse result = factory.login(proxy); + + if (result.isLoaded()) { + if (result.hasError()) { + QString error = result.error(); + + if (error == QSL(TTRSS_API_DISABLED)) { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error, + tr("API access on selected server is not enabled."), + tr("API access on selected server is not enabled.")); + } + else if (error == QSL(TTRSS_LOGIN_ERROR)) { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error, + tr("Entered credentials are incorrect."), + tr("Entered credentials are incorrect.")); + } + else { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error, + tr("Other error occurred, contact developers."), + tr("Other error occurred, contact developers.")); + } + } + else if (result.apiLevel() < TTRSS_MINIMAL_API_LEVEL) { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error, + tr("Installed version: %1, required at least: %2.") + .arg(QString::number(result.apiLevel()), + QString::number(TTRSS_MINIMAL_API_LEVEL)), + tr("Selected Tiny Tiny RSS server is running unsupported version of API.")); + } + else { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Ok, + tr("Installed version: %1, required at least: %2.") + .arg(QString::number(result.apiLevel()), + QString::number(TTRSS_MINIMAL_API_LEVEL)), + tr("Tiny Tiny RSS server is okay.")); + } + } + else if (factory.lastError() != QNetworkReply::NoError) { + m_ui.m_lblTestResult + ->setStatus(WidgetWithStatus::StatusType::Error, + tr("Network error: '%1'.").arg(NetworkFactory::networkErrorText(factory.lastError())), + tr("Network error, have you entered correct Tiny Tiny RSS API endpoint and password?")); + } + else { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error, + tr("Unspecified error, did you enter correct URL?"), + tr("Unspecified error, did you enter correct URL?")); + } +} + +void TtRssAccountDetails::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 TtRssAccountDetails::onPasswordChanged() { + const QString password = m_ui.m_txtPassword->lineEdit()->text(); + + if (password.isEmpty()) { + m_ui.m_txtPassword->setStatus(WidgetWithStatus::StatusType::Error, tr("Password cannot be empty.")); + } + else { + m_ui.m_txtPassword->setStatus(WidgetWithStatus::StatusType::Ok, tr("Password is okay.")); + } +} + +void TtRssAccountDetails::onHttpUsernameChanged() { + const bool is_username_ok = + !m_ui.m_gbHttpAuthentication->isChecked() || !m_ui.m_txtHttpUsername->lineEdit()->text().isEmpty(); + + m_ui.m_txtHttpUsername->setStatus(is_username_ok ? LineEditWithStatus::StatusType::Ok + : LineEditWithStatus::StatusType::Warning, + is_username_ok ? tr("Username is ok or it is not needed.") + : tr("Username is empty.")); +} + +void TtRssAccountDetails::onHttpPasswordChanged() { + const bool is_username_ok = + !m_ui.m_gbHttpAuthentication->isChecked() || !m_ui.m_txtHttpPassword->lineEdit()->text().isEmpty(); + + m_ui.m_txtHttpPassword->setStatus(is_username_ok ? LineEditWithStatus::StatusType::Ok + : LineEditWithStatus::StatusType::Warning, + is_username_ok ? tr("Password is ok or it is not needed.") + : tr("Password is empty.")); +} + +void TtRssAccountDetails::onUrlChanged() { + const QString url = m_ui.m_txtUrl->lineEdit()->text(); + + if (url.isEmpty()) { + m_ui.m_txtUrl->setStatus(WidgetWithStatus::StatusType::Error, tr("URL cannot be empty.")); + } + else if (url.endsWith(QL1S("/api/")) || url.endsWith(QL1S("/api"))) { + m_ui.m_txtUrl->setStatus(WidgetWithStatus::StatusType::Warning, tr("URL should NOT end with \"/api/\".")); + } + else { + m_ui.m_txtUrl->setStatus(WidgetWithStatus::StatusType::Ok, tr("URL is okay.")); + } +} diff --git a/src/librssguard-ttrss/src/gui/ttrssaccountdetails.h b/src/librssguard-ttrss/src/gui/ttrssaccountdetails.h new file mode 100644 index 000000000..279db27dd --- /dev/null +++ b/src/librssguard-ttrss/src/gui/ttrssaccountdetails.h @@ -0,0 +1,34 @@ +// For license of this file, see /LICENSE.md. + +#ifndef TTRSSACCOUNTDETAILS_H +#define TTRSSACCOUNTDETAILS_H + +#include "ui_ttrssaccountdetails.h" + +#include +#include + +class TtRssServiceRoot; + +class TtRssAccountDetails : public QWidget { + Q_OBJECT + + friend class FormEditTtRssAccount; + + public: + explicit TtRssAccountDetails(QWidget* parent = nullptr); + + private slots: + void performTest(const QNetworkProxy& proxy); + + void onUsernameChanged(); + void onPasswordChanged(); + void onHttpUsernameChanged(); + void onHttpPasswordChanged(); + void onUrlChanged(); + + private: + Ui::TtRssAccountDetails m_ui; +}; + +#endif // TTRSSACCOUNTDETAILS_H diff --git a/src/librssguard-ttrss/src/gui/ttrssaccountdetails.ui b/src/librssguard-ttrss/src/gui/ttrssaccountdetails.ui new file mode 100644 index 000000000..2692e93f5 --- /dev/null +++ b/src/librssguard-ttrss/src/gui/ttrssaccountdetails.ui @@ -0,0 +1,252 @@ + + + TtRssAccountDetails + + + + 0 + 0 + 432 + 396 + + + + + + + Qt::Vertical + + + + 408 + 30 + + + + + + + + + + URL + + + m_txtUrl + + + + + + + + + + + + + + Only download newest X articles per feed + + + m_spinLimitMessages + + + + + + + + 140 + 16777215 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Download unread articles only + + + + + + + Intelligent synchronization algorithm + + + + + + + Force execution of server-side feeds update + + + + + + + Some feeds require authentication, including GMail feeds. BASIC, NTLM-2 and DIGEST-MD5 authentication schemes are supported. + + + Authentication + + + false + + + false + + + + + + Username + + + m_txtUsername + + + + + + + + + + Password + + + m_txtPassword + + + + + + + + + + + + + Some feeds require authentication, including GMail feeds. BASIC, NTLM-2 and DIGEST-MD5 authentication schemes are supported. + + + Requires HTTP authentication + + + false + + + true + + + false + + + + + + Username + + + m_txtHttpUsername + + + + + + + + + + Password + + + m_txtHttpPassword + + + + + + + + + + + + + + + &Test setup + + + + + + + + 0 + 0 + + + + Qt::RightToLeft + + + + + + + + + + + + + + + + LabelWithStatus + QWidget +
labelwithstatus.h
+ 1 +
+ + LineEditWithStatus + QWidget +
lineeditwithstatus.h
+ 1 +
+ + MessageCountSpinBox + QSpinBox +
messagecountspinbox.h
+
+ + HelpSpoiler + QWidget +
helpspoiler.h
+ 1 +
+
+ + +
diff --git a/src/librssguard-ttrss/src/gui/ttrssfeeddetails.cpp b/src/librssguard-ttrss/src/gui/ttrssfeeddetails.cpp new file mode 100644 index 000000000..54f6580a8 --- /dev/null +++ b/src/librssguard-ttrss/src/gui/ttrssfeeddetails.cpp @@ -0,0 +1,56 @@ +// For license of this file, see /LICENSE.md. + +#include "src/gui/ttrssfeeddetails.h" + +#include +#include + +TtRssFeedDetails::TtRssFeedDetails(QWidget* parent) : QWidget(parent) { + ui.setupUi(this); + + ui.m_txtUrl->lineEdit()->setPlaceholderText(tr("Full feed URL including scheme")); + ui.m_txtUrl->lineEdit()->setToolTip(tr("Provide URL for your feed.")); + + connect(ui.m_txtUrl->lineEdit(), &BaseLineEdit::textChanged, this, &TtRssFeedDetails::onUrlChanged); + onUrlChanged(QString()); +} + +void TtRssFeedDetails::onUrlChanged(const QString& new_url) { + if (QRegularExpression(QSL(URL_REGEXP)).match(new_url).hasMatch()) { + // New url is well-formed. + ui.m_txtUrl->setStatus(LineEditWithStatus::StatusType::Ok, tr("The URL is ok.")); + } + else if (!new_url.simplified().isEmpty()) { + // New url is not well-formed but is not empty on the other hand. + ui.m_txtUrl->setStatus( + LineEditWithStatus::StatusType::Warning, + tr(R"(The URL does not meet standard pattern. Does your URL start with "http://" or "https://" prefix.)")); + } + else { + // New url is empty. + ui.m_txtUrl->setStatus(LineEditWithStatus::StatusType::Error, tr("The URL is empty.")); + } +} + +void TtRssFeedDetails::loadCategories(const QList& categories, + RootItem* root_item, + RootItem* parent_to_select) { + ui.m_cmbParentCategory->addItem(root_item->fullIcon(), root_item->title(), QVariant::fromValue(root_item)); + + for (Category* category : categories) { + ui.m_cmbParentCategory->addItem(category->fullIcon(), category->title(), QVariant::fromValue(category)); + } + + if (parent_to_select != nullptr) { + if (parent_to_select->kind() == RootItem::Kind::Category) { + ui.m_cmbParentCategory->setCurrentIndex(ui.m_cmbParentCategory->findData(QVariant::fromValue(parent_to_select))); + } + else if (parent_to_select->kind() == RootItem::Kind::Feed) { + int target_item = ui.m_cmbParentCategory->findData(QVariant::fromValue(parent_to_select->parent())); + + if (target_item >= 0) { + ui.m_cmbParentCategory->setCurrentIndex(target_item); + } + } + } +} diff --git a/src/librssguard-ttrss/src/gui/ttrssfeeddetails.h b/src/librssguard-ttrss/src/gui/ttrssfeeddetails.h new file mode 100644 index 000000000..d6eadb785 --- /dev/null +++ b/src/librssguard-ttrss/src/gui/ttrssfeeddetails.h @@ -0,0 +1,31 @@ +// For license of this file, see /LICENSE.md. + +#ifndef TTRSSFEEDDETAILS_H +#define TTRSSFEEDDETAILS_H + +#include "ui_ttrssfeeddetails.h" + +#include + +class Category; +class RootItem; + +class TtRssFeedDetails : public QWidget { + Q_OBJECT + + friend class FormTtRssFeedDetails; + + public: + explicit TtRssFeedDetails(QWidget* parent = nullptr); + + private slots: + void onUrlChanged(const QString& new_url); + + private: + void loadCategories(const QList& categories, RootItem* root_item, RootItem* parent_to_select = nullptr); + + private: + Ui::TtRssFeedDetails ui; +}; + +#endif // TTRSSFEEDDETAILS_H diff --git a/src/librssguard-ttrss/src/gui/ttrssfeeddetails.ui b/src/librssguard-ttrss/src/gui/ttrssfeeddetails.ui new file mode 100644 index 000000000..6e3c75e55 --- /dev/null +++ b/src/librssguard-ttrss/src/gui/ttrssfeeddetails.ui @@ -0,0 +1,68 @@ + + + TtRssFeedDetails + + + + 0 + 0 + 367 + 202 + + + + Form + + + + + + Parent folder + + + m_cmbParentCategory + + + + + + + Select parent item for your feed. + + + + 12 + 12 + + + + true + + + + + + + URL + + + m_txtUrl + + + + + + + + + + + LineEditWithStatus + QWidget +
lineeditwithstatus.h
+ 1 +
+
+ + +
diff --git a/src/librssguard-ttrss/src/ttrssfeed.cpp b/src/librssguard-ttrss/src/ttrssfeed.cpp new file mode 100644 index 000000000..220dbb1a9 --- /dev/null +++ b/src/librssguard-ttrss/src/ttrssfeed.cpp @@ -0,0 +1,62 @@ +// For license of this file, see /LICENSE.md. + +#include "src/ttrssfeed.h" + +#include "src/definitions.h" +#include "src/ttrssnetworkfactory.h" +#include "src/ttrssserviceroot.h" + +#include +#include +#include +#include + +#include + +TtRssFeed::TtRssFeed(RootItem* parent) : Feed(parent), m_actionShareToPublished(nullptr) {} + +TtRssServiceRoot* TtRssFeed::serviceRoot() const { + return qobject_cast(getParentServiceRoot()); +} + +bool TtRssFeed::canBeDeleted() const { + return true; +} + +bool TtRssFeed::deleteItem() { + TtRssUnsubscribeFeedResponse response = + serviceRoot()->network()->unsubscribeFeed(customNumericId(), getParentServiceRoot()->networkProxy()); + + if (response.code() == QSL(UFF_OK) && removeItself()) { + serviceRoot()->requestItemRemoval(this); + return true; + } + else { + qWarningNN << LOGSEC_TTRSS + << "Unsubscribing from feed failed, received JSON:" << QUOTE_W_SPACE_DOT(response.toString()); + return false; + } +} + +QList TtRssFeed::contextMenuFeedsList() { + auto menu = Feed::contextMenuFeedsList(); + + if (customNumericId() == TTRSS_PUBLISHED_FEED_ID) { + if (m_actionShareToPublished == nullptr) { + m_actionShareToPublished = + new QAction(qApp->icons()->fromTheme(QSL("emblem-shared")), tr("Share to published"), this); + + connect(m_actionShareToPublished, &QAction::triggered, serviceRoot(), &TtRssServiceRoot::shareToPublished); + } + + menu.append(m_actionShareToPublished); + } + + return menu; +} + +bool TtRssFeed::removeItself() { + QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className()); + + return DatabaseQueries::deleteFeed(database, this, serviceRoot()->accountId()); +} diff --git a/src/librssguard-ttrss/src/ttrssfeed.h b/src/librssguard-ttrss/src/ttrssfeed.h new file mode 100644 index 000000000..722268d4c --- /dev/null +++ b/src/librssguard-ttrss/src/ttrssfeed.h @@ -0,0 +1,28 @@ +// For license of this file, see /LICENSE.md. + +#ifndef TTRSSFEED_H +#define TTRSSFEED_H + +#include + +class TtRssServiceRoot; + +class TtRssFeed : public Feed { + Q_OBJECT + + public: + explicit TtRssFeed(RootItem* parent = nullptr); + + virtual bool canBeDeleted() const; + virtual bool deleteItem(); + virtual QList contextMenuFeedsList(); + + private: + TtRssServiceRoot* serviceRoot() const; + bool removeItself(); + + private: + QAction* m_actionShareToPublished; +}; + +#endif // TTRSSFEED_H diff --git a/src/librssguard-ttrss/src/ttrssnetworkfactory.cpp b/src/librssguard-ttrss/src/ttrssnetworkfactory.cpp new file mode 100644 index 000000000..946d4d65e --- /dev/null +++ b/src/librssguard-ttrss/src/ttrssnetworkfactory.cpp @@ -0,0 +1,1206 @@ +// For license of this file, see /LICENSE.md. + +#include "src/ttrssnetworkfactory.h" + +#include "src/definitions.h" +#include "src/ttrssfeed.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +TtRssNetworkFactory::TtRssNetworkFactory() + : m_bareUrl(QString()), m_fullUrl(QString()), m_username(QString()), m_password(QString()), + m_batchSize(TTRSS_DEFAULT_MESSAGES), m_forceServerSideUpdate(false), m_intelligentSynchronization(false), + m_authIsUsed(false), m_authUsername(QString()), m_authPassword(QString()), m_sessionId(QString()), + m_lastError(QNetworkReply::NetworkError::NoError) {} + +QString TtRssNetworkFactory::url() const { + return m_bareUrl; +} + +void TtRssNetworkFactory::setUrl(const QString& url) { + m_bareUrl = url; + + if (!m_bareUrl.endsWith(QSL("/"))) { + m_bareUrl = m_bareUrl + QSL("/"); + } + + if (!m_bareUrl.endsWith(QSL("api/"))) { + m_fullUrl = m_bareUrl + QSL("api/"); + } + else { + m_fullUrl = m_bareUrl; + } +} + +QString TtRssNetworkFactory::username() const { + return m_username; +} + +void TtRssNetworkFactory::setUsername(const QString& username) { + m_username = username; +} + +QString TtRssNetworkFactory::password() const { + return m_password; +} + +void TtRssNetworkFactory::setPassword(const QString& password) { + m_password = password; +} + +QDateTime TtRssNetworkFactory::lastLoginTime() const { + return m_lastLoginTime; +} + +QNetworkReply::NetworkError TtRssNetworkFactory::lastError() const { + return m_lastError; +} + +TtRssLoginResponse TtRssNetworkFactory::login(const QNetworkProxy& proxy) { + if (!m_sessionId.isEmpty()) { + qWarningNN << LOGSEC_TTRSS << "Session ID is not empty before login, logging out first."; + logout(proxy); + } + + QJsonObject json; + + json[QSL("op")] = QSL("login"); + json[QSL("user")] = m_username; + json[QSL("password")] = m_password; + + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, TTRSS_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + qApp->settings() + ->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)) + .toInt(), + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + TtRssLoginResponse login_response(QString::fromUtf8(result_raw)); + + if (network_reply.m_networkError == QNetworkReply::NoError) { + m_sessionId = login_response.sessionId(); + m_lastLoginTime = QDateTime::currentDateTime(); + } + else { + qWarningNN << LOGSEC_TTRSS << "Login failed with error:" << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + } + + m_lastError = network_reply.m_networkError; + return login_response; +} + +TtRssResponse TtRssNetworkFactory::logout(const QNetworkProxy& proxy) { + if (!m_sessionId.isEmpty()) { + QJsonObject json; + + json[QSL("op")] = QSL("logout"); + json[QSL("sid")] = m_sessionId; + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, TTRSS_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + qApp->settings() + ->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)) + .toInt(), + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + + m_lastError = network_reply.m_networkError; + + if (m_lastError == QNetworkReply::NetworkError::NoError) { + m_sessionId.clear(); + } + else { + qWarningNN << LOGSEC_TTRSS << "Logout failed with error:" << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + } + + return TtRssResponse(QString::fromUtf8(result_raw)); + } + else { + qWarningNN << LOGSEC_TTRSS << "Cannot logout because session ID is empty."; + m_lastError = QNetworkReply::NetworkError::NoError; + return TtRssResponse(); + } +} + +TtRssGetLabelsResponse TtRssNetworkFactory::getLabels(const QNetworkProxy& proxy) { + QJsonObject json; + + json[QSL("op")] = QSL("getLabels"); + json[QSL("sid")] = m_sessionId; + + const int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, TTRSS_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + TtRssGetLabelsResponse result(QString::fromUtf8(result_raw)); + + if (result.isNotLoggedIn()) { + // We are not logged in. + login(proxy); + json[QSL("sid")] = m_sessionId; + network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + result = TtRssGetLabelsResponse(QString::fromUtf8(result_raw)); + } + + if (network_reply.m_networkError != QNetworkReply::NoError) { + qWarningNN << LOGSEC_TTRSS << "getLabels failed with error:" << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + } + + m_lastError = network_reply.m_networkError; + return result; +} + +TtRssResponse TtRssNetworkFactory::shareToPublished(const TtRssNoteToPublish& note, const QNetworkProxy& proxy) { + QJsonObject json; + + json[QSL("op")] = QSL("shareToPublished"); + json[QSL("sid")] = m_sessionId; + json[QSL("title")] = note.m_title; + json[QSL("url")] = note.m_url; + json[QSL("content")] = note.m_content; + + const int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, TTRSS_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + TtRssResponse result(QString::fromUtf8(result_raw)); + + if (result.isNotLoggedIn()) { + // We are not logged in. + login(proxy); + json[QSL("sid")] = m_sessionId; + network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + result = TtRssResponse(QString::fromUtf8(result_raw)); + } + + if (network_reply.m_networkError != QNetworkReply::NoError) { + qWarningNN << LOGSEC_TTRSS + << "shareToPublished failed with error:" << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + } + + m_lastError = network_reply.m_networkError; + return result; +} + +TtRssGetFeedsCategoriesResponse TtRssNetworkFactory::getFeedsCategories(const QNetworkProxy& proxy) { + QJsonObject json; + + json[QSL("op")] = QSL("getFeedTree"); + json[QSL("sid")] = m_sessionId; + json[QSL("include_empty")] = true; + const int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, TTRSS_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + TtRssGetFeedsCategoriesResponse result(QString::fromUtf8(result_raw)); + + if (result.isNotLoggedIn()) { + // We are not logged in. + login(proxy); + json[QSL("sid")] = m_sessionId; + network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + result = TtRssGetFeedsCategoriesResponse(QString::fromUtf8(result_raw)); + } + + if (network_reply.m_networkError != QNetworkReply::NoError) { + qWarningNN << LOGSEC_TTRSS << "getFeedTree failed with error:" << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + } + + m_lastError = network_reply.m_networkError; + return result; +} + +TtRssGetCompactHeadlinesResponse TtRssNetworkFactory::getCompactHeadlines(int feed_id, + int limit, + int skip, + const QString& view_mode, + const QNetworkProxy& proxy) { + QJsonObject json; + + json[QSL("op")] = QSL("getCompactHeadlines"); + json[QSL("sid")] = m_sessionId; + json[QSL("feed_id")] = feed_id; + json[QSL("limit")] = limit; + // json[QSL("skip")] = skip; + json[QSL("view_mode")] = view_mode; + + const int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, TTRSS_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + TtRssGetCompactHeadlinesResponse result(QString::fromUtf8(result_raw)); + + if (result.isUnknownMethod()) { + qCriticalNN << LOGSEC_TTRSS << "'getCompactHeadlines' method is not installed."; + + throw FeedFetchException(Feed::Status::OtherError, + QSL("'getCompactHeadlines' method is not installed on your TT-RSS instance. Install " + "'api_newsplus' plugin.")); + } + else if (result.isNotLoggedIn()) { + // We are not logged in. + login(proxy); + json[QSL("sid")] = m_sessionId; + network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + result = TtRssGetCompactHeadlinesResponse(QString::fromUtf8(result_raw)); + } + + if (network_reply.m_networkError != QNetworkReply::NetworkError::NoError) { + qWarningNN << LOGSEC_TTRSS + << "getCompactHeadlines failed with error:" << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + } + + m_lastError = network_reply.m_networkError; + + return result; +} + +TtRssGetHeadlinesResponse TtRssNetworkFactory::getArticle(const QStringList& article_ids, const QNetworkProxy& proxy) { + QJsonObject json; + + json[QSL("op")] = QSL("getArticle"); + json[QSL("sid")] = m_sessionId; + json[QSL("article_id")] = article_ids.join(QL1C(',')); + + const int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, TTRSS_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + TtRssGetHeadlinesResponse result(QString::fromUtf8(result_raw)); + + if (result.isNotLoggedIn()) { + // We are not logged in. + login(proxy); + json[QSL("sid")] = m_sessionId; + network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + result = TtRssGetHeadlinesResponse(QString::fromUtf8(result_raw)); + } + + if (network_reply.m_networkError != QNetworkReply::NetworkError::NoError) { + qWarningNN << LOGSEC_TTRSS << "getArticle failed with error:" << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + } + + m_lastError = network_reply.m_networkError; + return result; +} + +TtRssGetHeadlinesResponse TtRssNetworkFactory::getHeadlines(int feed_id, + int limit, + int skip, + bool show_content, + bool include_attachments, + bool sanitize, + bool unread_only, + const QNetworkProxy& proxy) { + QJsonObject json; + + json[QSL("op")] = QSL("getHeadlines"); + json[QSL("sid")] = m_sessionId; + json[QSL("feed_id")] = feed_id; + json[QSL("force_update")] = m_forceServerSideUpdate; + json[QSL("limit")] = limit; + json[QSL("skip")] = skip; + json[QSL("view_mode")] = unread_only ? QSL("unread") : QSL("all_articles"); + json[QSL("show_content")] = show_content; + json[QSL("include_attachments")] = include_attachments; + json[QSL("sanitize")] = sanitize; + + const int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, TTRSS_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + TtRssGetHeadlinesResponse result(QString::fromUtf8(result_raw)); + + if (result.isNotLoggedIn()) { + // We are not logged in. + login(proxy); + json[QSL("sid")] = m_sessionId; + network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + result = TtRssGetHeadlinesResponse(QString::fromUtf8(result_raw)); + } + + if (network_reply.m_networkError != QNetworkReply::NoError) { + qWarningNN << LOGSEC_TTRSS << "getHeadlines failed with error:" << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + } + + m_lastError = network_reply.m_networkError; + return result; +} + +TtRssResponse TtRssNetworkFactory::setArticleLabel(const QStringList& article_ids, + const QString& label_custom_id, + bool assign, + const QNetworkProxy& proxy) { + QJsonObject json; + + json[QSL("op")] = QSL("setArticleLabel"); + json[QSL("sid")] = m_sessionId; + json[QSL("article_ids")] = article_ids.join(QSL(",")); + json[QSL("label_id")] = label_custom_id.toInt(); + json[QSL("assign")] = assign; + + const int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, TTRSS_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + TtRssResponse result(QString::fromUtf8(result_raw)); + + if (result.isNotLoggedIn()) { + // We are not logged in. + login(proxy); + json[QSL("sid")] = m_sessionId; + network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + result = TtRssResponse(QString::fromUtf8(result_raw)); + } + + if (network_reply.m_networkError != QNetworkReply::NoError) { + qWarningNN << LOGSEC_TTRSS << "setArticleLabel failed with error" + << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + } + + m_lastError = network_reply.m_networkError; + return result; +} + +TtRssUpdateArticleResponse TtRssNetworkFactory::updateArticles(const QStringList& ids, + UpdateArticle::OperatingField field, + UpdateArticle::Mode mode, + const QNetworkProxy& proxy) { + QJsonObject json; + + json[QSL("op")] = QSL("updateArticle"); + json[QSL("sid")] = m_sessionId; + json[QSL("article_ids")] = ids.join(QSL(",")); + json[QSL("mode")] = int(mode); + json[QSL("field")] = int(field); + + const int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, TTRSS_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + TtRssUpdateArticleResponse result(QString::fromUtf8(result_raw)); + + if (result.isNotLoggedIn()) { + // We are not logged in. + login(proxy); + json[QSL("sid")] = m_sessionId; + network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + result = TtRssUpdateArticleResponse(QString::fromUtf8(result_raw)); + } + + if (network_reply.m_networkError != QNetworkReply::NoError) { + qWarningNN << LOGSEC_TTRSS << "updateArticle failed with error" << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + } + + m_lastError = network_reply.m_networkError; + return result; +} + +TtRssSubscribeToFeedResponse TtRssNetworkFactory::subscribeToFeed(const QString& url, + int category_id, + const QNetworkProxy& proxy, + bool protectd, + const QString& username, + const QString& password) { + QJsonObject json; + + json[QSL("op")] = QSL("subscribeToFeed"); + json[QSL("sid")] = m_sessionId; + json[QSL("feed_url")] = url; + json[QSL("category_id")] = category_id; + + if (protectd) { + json[QSL("login")] = username; + json[QSL("password")] = password; + } + + const int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, TTRSS_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + TtRssSubscribeToFeedResponse result(QString::fromUtf8(result_raw)); + + if (result.isNotLoggedIn()) { + // We are not logged in. + login(proxy); + json[QSL("sid")] = m_sessionId; + network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + result = TtRssSubscribeToFeedResponse(QString::fromUtf8(result_raw)); + } + + if (network_reply.m_networkError != QNetworkReply::NoError) { + qWarningNN << LOGSEC_TTRSS << "updateArticle failed with error" << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + } + + m_lastError = network_reply.m_networkError; + return result; +} + +TtRssUnsubscribeFeedResponse TtRssNetworkFactory::unsubscribeFeed(int feed_id, const QNetworkProxy& proxy) { + QJsonObject json; + + json[QSL("op")] = QSL("unsubscribeFeed"); + json[QSL("sid")] = m_sessionId; + json[QSL("feed_id")] = feed_id; + const int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + QByteArray result_raw; + QList> headers; + + headers << QPair(HTTP_HEADERS_CONTENT_TYPE, TTRSS_CONTENT_TYPE_JSON); + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + m_authUsername, + m_authPassword); + + NetworkResult network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + TtRssUnsubscribeFeedResponse result(QString::fromUtf8(result_raw)); + + if (result.isNotLoggedIn()) { + // We are not logged in. + login(proxy); + json[QSL("sid")] = m_sessionId; + network_reply = + NetworkFactory::performNetworkOperation(m_fullUrl, + timeout, + QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact), + result_raw, + QNetworkAccessManager::Operation::PostOperation, + headers, + false, + {}, + {}, + proxy); + result = TtRssUnsubscribeFeedResponse(QString::fromUtf8(result_raw)); + } + + if (network_reply.m_networkError != QNetworkReply::NoError) { + qWarningNN << LOGSEC_TTRSS << "getFeeds failed with error" << QUOTE_W_SPACE_DOT(network_reply.m_networkError); + } + + m_lastError = network_reply.m_networkError; + return result; +} + +int TtRssNetworkFactory::batchSize() const { + return m_batchSize; +} + +void TtRssNetworkFactory::setBatchSize(int batch_size) { + m_batchSize = batch_size; +} + +bool TtRssNetworkFactory::intelligentSynchronization() const { + return m_intelligentSynchronization; +} + +void TtRssNetworkFactory::setIntelligentSynchronization(bool intelligent_synchronization) { + m_intelligentSynchronization = intelligent_synchronization; +} + +bool TtRssNetworkFactory::downloadOnlyUnreadMessages() const { + return m_downloadOnlyUnreadMessages; +} + +void TtRssNetworkFactory::setDownloadOnlyUnreadMessages(bool download_only_unread_messages) { + m_downloadOnlyUnreadMessages = download_only_unread_messages; +} + +bool TtRssNetworkFactory::forceServerSideUpdate() const { + return m_forceServerSideUpdate; +} + +void TtRssNetworkFactory::setForceServerSideUpdate(bool force_server_side_update) { + m_forceServerSideUpdate = force_server_side_update; +} + +bool TtRssNetworkFactory::authIsUsed() const { + return m_authIsUsed; +} + +void TtRssNetworkFactory::setAuthIsUsed(bool auth_is_used) { + m_authIsUsed = auth_is_used; +} + +QString TtRssNetworkFactory::authUsername() const { + return m_authUsername; +} + +void TtRssNetworkFactory::setAuthUsername(const QString& auth_username) { + m_authUsername = auth_username; +} + +QString TtRssNetworkFactory::authPassword() const { + return m_authPassword; +} + +void TtRssNetworkFactory::setAuthPassword(const QString& auth_password) { + m_authPassword = auth_password; +} + +TtRssResponse::TtRssResponse(const QString& raw_content) { + m_rawContent = QJsonDocument::fromJson(raw_content.toUtf8()).object(); +} + +TtRssResponse::~TtRssResponse() = default; + +bool TtRssResponse::isLoaded() const { + return !m_rawContent.isEmpty(); +} + +int TtRssResponse::seq() const { + if (!isLoaded()) { + return TTRSS_CONTENT_NOT_LOADED; + } + else { + return m_rawContent[QSL("seq")].toInt(); + } +} + +int TtRssResponse::status() const { + if (!isLoaded()) { + return TTRSS_CONTENT_NOT_LOADED; + } + else { + return m_rawContent[QSL("status")].toInt(); + } +} + +bool TtRssResponse::isNotLoggedIn() const { + return status() == TTRSS_API_STATUS_ERR && hasError() && error() == QSL(TTRSS_NOT_LOGGED_IN); +} + +bool TtRssResponse::isUnknownMethod() const { + return status() == TTRSS_API_STATUS_ERR && hasError() && error() == QSL(TTRSS_UNKNOWN_METHOD); +} + +QString TtRssResponse::toString() const { + return QJsonDocument(m_rawContent).toJson(QJsonDocument::JsonFormat::Compact); +} + +TtRssLoginResponse::TtRssLoginResponse(const QString& raw_content) : TtRssResponse(raw_content) {} + +TtRssLoginResponse::~TtRssLoginResponse() = default; +int TtRssLoginResponse::apiLevel() const { + if (!isLoaded()) { + return TTRSS_CONTENT_NOT_LOADED; + } + else { + return m_rawContent[QSL("content")].toObject()[QSL("api_level")].toInt(); + } +} + +QString TtRssLoginResponse::sessionId() const { + if (!isLoaded()) { + return QString(); + } + else { + return m_rawContent[QSL("content")].toObject()[QSL("session_id")].toString(); + } +} + +QString TtRssResponse::error() const { + if (!isLoaded()) { + return QString(); + } + else { + return m_rawContent[QSL("content")].toObject()[QSL("error")].toString(); + } +} + +bool TtRssResponse::hasError() const { + if (!isLoaded()) { + return false; + } + else { + return m_rawContent[QSL("content")].toObject().contains(QSL("error")); + } +} + +TtRssGetFeedsCategoriesResponse::TtRssGetFeedsCategoriesResponse(const QString& raw_content) + : TtRssResponse(raw_content) {} + +TtRssGetFeedsCategoriesResponse::~TtRssGetFeedsCategoriesResponse() = default; + +RootItem* TtRssGetFeedsCategoriesResponse::feedsCategories(TtRssNetworkFactory* network, + bool obtain_icons, + const QNetworkProxy& proxy, + const QString& base_address) const { + auto* parent = new RootItem(); + + // Chop the "api/" from the end of the address. + qDebugNN << LOGSEC_TTRSS << "Base address to get feed icons is" << QUOTE_W_SPACE_DOT(base_address); + + if (status() == TTRSS_API_STATUS_OK) { + // We have data, construct object tree according to data. + QJsonArray items_to_process = + m_rawContent[QSL("content")].toObject()[QSL("categories")].toObject()[QSL("items")].toArray(); + QVector> pairs; + pairs.reserve(items_to_process.size()); + + for (const QJsonValue& item : items_to_process) { + pairs.append(QPair(parent, item)); + } + + while (!pairs.isEmpty()) { + QPair pair = pairs.takeFirst(); + RootItem* act_parent = pair.first; + QJsonObject item = pair.second.toObject(); + int item_id = item[QSL("bare_id")].toInt(); + bool is_category = item.contains(QSL("type")) && item[QSL("type")].toString() == QSL(TTRSS_GFT_TYPE_CATEGORY); + + if (item_id >= 0) { + if (is_category) { + if (item_id == 0) { + // This is "Uncategorized" category, all its feeds belong to top-level root. + if (item.contains(QSL("items"))) { + auto ite = item[QSL("items")].toArray(); + + for (const QJsonValue& child_feed : std::as_const(ite)) { + pairs.append(QPair(parent, child_feed)); + } + } + } + else { + auto* category = new Category(); + + category->setTitle(item[QSL("name")].toString()); + category->setCustomId(QString::number(item_id)); + act_parent->appendChild(category); + + if (item.contains(QSL("items"))) { + auto ite = item[QSL("items")].toArray(); + + for (const QJsonValue& child : std::as_const(ite)) { + pairs.append(QPair(category, child)); + } + } + } + } + else { + // We have feed. + auto* feed = new TtRssFeed(); + + if (obtain_icons) { + QString icon_path = + item[QSL("icon")].type() == QJsonValue::Type::String ? item[QSL("icon")].toString() : QString(); + + if (!icon_path.isEmpty()) { + QString full_icon_address = QUrl(base_address).resolved(icon_path).toString(); + QPixmap icon; + QList> headers; + + if (network->authIsUsed()) { + headers << NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic, + network->authUsername(), + network->authPassword()); + } + + auto res = + NetworkFactory::downloadIcon({{full_icon_address, true}}, DOWNLOAD_TIMEOUT, icon, headers, proxy); + + if (res == QNetworkReply::NetworkError::NoError) { + feed->setIcon(icon); + } + else { + qWarningNN << LOGSEC_TTRSS << "Failed to download icon with error" << QUOTE_W_SPACE_DOT(res); + } + } + } + + feed->setTitle(item[QSL("name")].toString()); + feed->setCustomId(QString::number(item_id)); + + act_parent->appendChild(feed); + } + } + } + + // Append special "published" feed to hold "notes" created by user + // via "shareToPublished" method. These "notes" are not normal articles + // because they do not belong to any feed. + // We have feed. + auto* published_feed = new TtRssFeed(); + + published_feed->setTitle(QSL("[SYSTEM] ") + QObject::tr("User-published articles")); + published_feed->setCustomId(QString::number(0)); + published_feed->setKeepOnTop(true); + + parent->appendChild(published_feed); + } + + return parent; +} + +TtRssGetHeadlinesResponse::TtRssGetHeadlinesResponse(const QString& raw_content) : TtRssResponse(raw_content) {} + +TtRssGetHeadlinesResponse::~TtRssGetHeadlinesResponse() = default; + +TtRssGetArticleResponse::TtRssGetArticleResponse(const QString& raw_content) : TtRssResponse(raw_content) {} + +QList TtRssGetArticleResponse::messages(ServiceRoot* root) const { + return {}; +} + +TtRssGetArticleResponse::~TtRssGetArticleResponse() = default; + +QList TtRssGetHeadlinesResponse::messages(ServiceRoot* root) const { + QList messages; + auto active_labels = root->labelsNode() != nullptr ? root->labelsNode()->labels() : QList(); + auto json_msgs = m_rawContent[QSL("content")].toArray(); + auto* published_lbl = boolinq::from(active_labels).firstOrDefault([](const Label* lbl) { + return lbl->customNumericId() == TTRSS_PUBLISHED_LABEL_ID; + }); + + for (const QJsonValue& item : std::as_const(json_msgs)) { + QJsonObject mapped = item.toObject(); + Message message; + + message.m_author = mapped[QSL("author")].toString(); + message.m_isRead = !mapped[QSL("unread")].toBool(); + message.m_isImportant = mapped[QSL("marked")].toBool(); + message.m_contents = mapped[QSL("content")].toString(); + message.m_rawContents = QJsonDocument(mapped).toJson(QJsonDocument::JsonFormat::Compact); + + if (published_lbl != nullptr && mapped[QSL("published")].toBool()) { + // Article is published, set label. + message.m_assignedLabels.append(published_lbl); + } + + auto json_labels = mapped[QSL("labels")].toArray(); + + for (const QJsonValue& lbl_val : std::as_const(json_labels)) { + QString lbl_custom_id = QString::number(lbl_val.toArray().at(0).toInt()); + Label* label = + boolinq::from(active_labels.begin(), active_labels.end()).firstOrDefault([lbl_custom_id](Label* lbl) { + return lbl->customId() == lbl_custom_id; + }); + + if (label != nullptr) { + message.m_assignedLabels.append(label); + } + else { + qWarningNN << LOGSEC_TTRSS << "Label with custom ID" << QUOTE_W_SPACE(lbl_custom_id) + << "was not found. Maybe you need to perform sync-in to download it from server."; + } + } + + // Multiply by 1000 because Tiny Tiny RSS API does not include miliseconds in Unix + // date/time number. + const qint64 t = static_cast(mapped[QSL("updated")].toDouble()) * 1000; + + message.m_created = TextFactory::parseDateTime(t); + message.m_createdFromFeed = true; + message.m_customId = QString::number(mapped[QSL("id")].toInt()); + message.m_feedId = mapped[QSL("feed_id")].type() == QJsonValue::Type::Double + ? QString::number(mapped[QSL("feed_id")].toInt()) + : mapped[QSL("feed_id")].toString(); + message.m_title = mapped[QSL("title")].toString(); + message.m_url = mapped[QSL("link")].toString(); + + if (mapped.contains(QSL("attachments"))) { + // Process enclosures. + auto json_att = mapped[QSL("attachments")].toArray(); + + for (const QJsonValue& attachment : std::as_const(json_att)) { + QJsonObject mapped_attachemnt = attachment.toObject(); + Enclosure enclosure; + + enclosure.m_mimeType = mapped_attachemnt[QSL("content_type")].toString(); + enclosure.m_url = mapped_attachemnt[QSL("content_url")].toString(); + message.m_enclosures.append(enclosure); + } + } + + messages.append(message); + } + + return messages; +} + +TtRssUpdateArticleResponse::TtRssUpdateArticleResponse(const QString& raw_content) : TtRssResponse(raw_content) {} + +TtRssUpdateArticleResponse::~TtRssUpdateArticleResponse() = default; + +QString TtRssUpdateArticleResponse::updateStatus() const { + if (m_rawContent.contains(QSL("content"))) { + return m_rawContent[QSL("content")].toObject()[QSL("status")].toString(); + } + else { + return QString(); + } +} + +int TtRssUpdateArticleResponse::articlesUpdated() const { + if (m_rawContent.contains(QSL("content"))) { + return m_rawContent[QSL("content")].toObject()[QSL("updated")].toInt(); + } + else { + return 0; + } +} + +TtRssSubscribeToFeedResponse::TtRssSubscribeToFeedResponse(const QString& raw_content) : TtRssResponse(raw_content) {} + +TtRssSubscribeToFeedResponse::~TtRssSubscribeToFeedResponse() = default; +int TtRssSubscribeToFeedResponse::code() const { + if (m_rawContent.contains(QSL("content"))) { + return m_rawContent[QSL("content")].toObject()[QSL("status")].toObject()[QSL("code")].toInt(); + } + else { + return STF_UNKNOWN; + } +} + +TtRssUnsubscribeFeedResponse::TtRssUnsubscribeFeedResponse(const QString& raw_content) : TtRssResponse(raw_content) {} + +TtRssUnsubscribeFeedResponse::~TtRssUnsubscribeFeedResponse() = default; +QString TtRssUnsubscribeFeedResponse::code() const { + if (m_rawContent.contains(QSL("content"))) { + QJsonObject map = m_rawContent[QSL("content")].toObject(); + + if (map.contains(QSL("error"))) { + return map[QSL("error")].toString(); + } + else if (map.contains(QSL("status"))) { + return map[QSL("status")].toString(); + } + } + + return QString(); +} + +TtRssGetLabelsResponse::TtRssGetLabelsResponse(const QString& raw_content) : TtRssResponse(raw_content) {} + +QList TtRssGetLabelsResponse::labels() const { + QList labels; + auto json_labels = m_rawContent[QSL("content")].toArray(); + + // Add "Published" label. + // + // NOTE: In TT-RSS there is a problem with "published" feature: + // 1. If user has article in existing feed, he can mark it as "published" and in + // that case, the "published" behaves more like a label. + // 2. If user uses feature "shareToPublished", he essentially creates new textual + // note, which is then assigned to "Published feed" but can be also assigned label from 1). + // + // This label solves situation 1). 2) is solved in other way (creating static system feed). + QString published_caption = QSL("[SYSTEM] ") + QObject::tr("Published articles"); + auto* published_lbl = new Label(published_caption, TextFactory::generateColorFromText(published_caption)); + + published_lbl->setKeepOnTop(true); + published_lbl->setCustomId(QString::number(TTRSS_PUBLISHED_LABEL_ID)); + labels.append(published_lbl); + + for (const QJsonValue& lbl_val : std::as_const(json_labels)) { + QJsonObject lbl_obj = lbl_val.toObject(); + Label* lbl = new Label(lbl_obj[QSL("caption")].toString(), QColor(lbl_obj[QSL("fg_color")].toString())); + + lbl->setCustomId(QString::number(lbl_obj[QSL("id")].toInt())); + labels.append(lbl); + } + + return labels; +} + +TtRssGetCompactHeadlinesResponse::TtRssGetCompactHeadlinesResponse(const QString& raw_content) + : TtRssResponse(raw_content) {} + +TtRssGetCompactHeadlinesResponse::~TtRssGetCompactHeadlinesResponse() = default; + +QStringList TtRssGetCompactHeadlinesResponse::ids() const { + auto json_ids = m_rawContent[QSL("content")].toArray(); + QStringList msg_ids; + + for (const QJsonValue& id_val : std::as_const(json_ids)) { + msg_ids.append(QString::number(id_val.toObject()[QSL("id")].toInt())); + } + + return msg_ids; +} diff --git a/src/librssguard-ttrss/src/ttrssnetworkfactory.h b/src/librssguard-ttrss/src/ttrssnetworkfactory.h new file mode 100644 index 000000000..579cbb73d --- /dev/null +++ b/src/librssguard-ttrss/src/ttrssnetworkfactory.h @@ -0,0 +1,247 @@ +// For license of this file, see /LICENSE.md. + +#ifndef TTRSSNETWORKFACTORY_H +#define TTRSSNETWORKFACTORY_H + +#include "src/ttrssnotetopublish.h" + +#include + +#include +#include +#include +#include + +class RootItem; +class TtRssFeed; +class Label; + +class TtRssResponse { + public: + explicit TtRssResponse(const QString& raw_content = QString()); + virtual ~TtRssResponse(); + + bool isLoaded() const; + + int seq() const; + int status() const; + QString error() const; + bool hasError() const; + bool isNotLoggedIn() const; + bool isUnknownMethod() const; + QString toString() const; + + protected: + QJsonObject m_rawContent; +}; + +class TtRssLoginResponse : public TtRssResponse { + public: + explicit TtRssLoginResponse(const QString& raw_content = QString()); + virtual ~TtRssLoginResponse(); + + int apiLevel() const; + QString sessionId() const; +}; + +class TtRssGetLabelsResponse : public TtRssResponse { + public: + explicit TtRssGetLabelsResponse(const QString& raw_content = QString()); + + QList labels() const; +}; + +class TtRssNetworkFactory; + +class TtRssGetFeedsCategoriesResponse : public TtRssResponse { + public: + explicit TtRssGetFeedsCategoriesResponse(const QString& raw_content = QString()); + virtual ~TtRssGetFeedsCategoriesResponse(); + + // Returns tree of feeds/categories. + // Top-level root of the tree is not needed here. + // Returned items do not have primary IDs assigned. + RootItem* feedsCategories(TtRssNetworkFactory* network, + bool obtain_icons, + const QNetworkProxy& proxy, + const QString& base_address = QString()) const; +}; + +class ServiceRoot; + +class TtRssGetHeadlinesResponse : public TtRssResponse { + public: + explicit TtRssGetHeadlinesResponse(const QString& raw_content = QString()); + virtual ~TtRssGetHeadlinesResponse(); + + QList messages(ServiceRoot* root) const; +}; + +class TtRssGetArticleResponse : public TtRssResponse { + public: + explicit TtRssGetArticleResponse(const QString& raw_content = QString()); + virtual ~TtRssGetArticleResponse(); + + QList messages(ServiceRoot* root) const; +}; + +class TtRssGetCompactHeadlinesResponse : public TtRssResponse { + public: + explicit TtRssGetCompactHeadlinesResponse(const QString& raw_content = QString()); + virtual ~TtRssGetCompactHeadlinesResponse(); + + QStringList ids() const; +}; + +class TtRssUpdateArticleResponse : public TtRssResponse { + public: + explicit TtRssUpdateArticleResponse(const QString& raw_content = QString()); + virtual ~TtRssUpdateArticleResponse(); + + QString updateStatus() const; + int articlesUpdated() const; +}; + +class TtRssSubscribeToFeedResponse : public TtRssResponse { + public: + explicit TtRssSubscribeToFeedResponse(const QString& raw_content = QString()); + virtual ~TtRssSubscribeToFeedResponse(); + + int code() const; +}; + +class TtRssUnsubscribeFeedResponse : public TtRssResponse { + public: + explicit TtRssUnsubscribeFeedResponse(const QString& raw_content = QString()); + virtual ~TtRssUnsubscribeFeedResponse(); + + QString code() const; +}; + +namespace UpdateArticle { + enum class Mode { + SetToFalse = 0, + SetToTrue = 1, + Togggle = 2 + }; + + enum class OperatingField { + Starred = 0, + Published = 1, + Unread = 2 + }; + +} // namespace UpdateArticle + +class TtRssNetworkFactory { + public: + explicit TtRssNetworkFactory(); + + QString url() const; + void setUrl(const QString& url); + + QString username() const; + void setUsername(const QString& username); + + QString password() const; + void setPassword(const QString& password); + + bool authIsUsed() const; + void setAuthIsUsed(bool auth_is_used); + + QString authUsername() const; + void setAuthUsername(const QString& auth_username); + + QString authPassword() const; + void setAuthPassword(const QString& auth_password); + + bool forceServerSideUpdate() const; + void setForceServerSideUpdate(bool force_server_side_update); + + bool downloadOnlyUnreadMessages() const; + void setDownloadOnlyUnreadMessages(bool download_only_unread_messages); + + // Metadata. + QDateTime lastLoginTime() const; + QNetworkReply::NetworkError lastError() const; + + // Operations. + + // Logs user in. + TtRssLoginResponse login(const QNetworkProxy& proxy); + + // Logs user out. + TtRssResponse logout(const QNetworkProxy& proxy); + + // Gets list of labels from the server. + TtRssGetLabelsResponse getLabels(const QNetworkProxy& proxy); + + // Shares new item to "published" feed. + TtRssResponse shareToPublished(const TtRssNoteToPublish& note, const QNetworkProxy& proxy); + + // Gets feeds from the server. + TtRssGetFeedsCategoriesResponse getFeedsCategories(const QNetworkProxy& proxy); + + // Gets message IDs from the server. + TtRssGetCompactHeadlinesResponse getCompactHeadlines(int feed_id, + int limit, + int skip, + const QString& view_mode, + const QNetworkProxy& proxy); + + TtRssGetHeadlinesResponse getArticle(const QStringList& article_ids, const QNetworkProxy& proxy); + + // Gets headlines (messages) from the server. + TtRssGetHeadlinesResponse getHeadlines(int feed_id, + int limit, + int skip, + bool show_content, + bool include_attachments, + bool sanitize, + bool unread_only, + const QNetworkProxy& proxy); + + TtRssResponse setArticleLabel(const QStringList& article_ids, + const QString& label_custom_id, + bool assign, + const QNetworkProxy& proxy); + + TtRssUpdateArticleResponse updateArticles(const QStringList& ids, + UpdateArticle::OperatingField field, + UpdateArticle::Mode mode, + const QNetworkProxy& proxy); + + TtRssSubscribeToFeedResponse subscribeToFeed(const QString& url, + int category_id, + const QNetworkProxy& proxy, + bool protectd = false, + const QString& username = QString(), + const QString& password = QString()); + + TtRssUnsubscribeFeedResponse unsubscribeFeed(int feed_id, const QNetworkProxy& proxy); + + int batchSize() const; + void setBatchSize(int batch_size); + + bool intelligentSynchronization() const; + void setIntelligentSynchronization(bool intelligent_synchronization); + + private: + QString m_bareUrl; + QString m_fullUrl; + QString m_username; + QString m_password; + int m_batchSize; + bool m_forceServerSideUpdate; + bool m_downloadOnlyUnreadMessages; + bool m_intelligentSynchronization; + bool m_authIsUsed; + QString m_authUsername; + QString m_authPassword; + QString m_sessionId; + QDateTime m_lastLoginTime; + + QNetworkReply::NetworkError m_lastError; +}; + +#endif // TTRSSNETWORKFACTORY_H diff --git a/src/librssguard-ttrss/src/ttrssnotetopublish.h b/src/librssguard-ttrss/src/ttrssnotetopublish.h new file mode 100644 index 000000000..077ed239f --- /dev/null +++ b/src/librssguard-ttrss/src/ttrssnotetopublish.h @@ -0,0 +1,15 @@ +// For license of this file, see /LICENSE.md. + +#ifndef TTRSSNOTETOPUBLISH_H +#define TTRSSNOTETOPUBLISH_H + +#include + +struct TtRssNoteToPublish { + public: + QString m_title; + QString m_url; + QString m_content; +}; + +#endif // TTRSSNOTETOPUBLISH_H diff --git a/src/librssguard-ttrss/src/ttrssserviceentrypoint.cpp b/src/librssguard-ttrss/src/ttrssserviceentrypoint.cpp new file mode 100644 index 000000000..adb45635d --- /dev/null +++ b/src/librssguard-ttrss/src/ttrssserviceentrypoint.cpp @@ -0,0 +1,54 @@ +// For license of this file, see /LICENSE.md. + +#include "src/ttrssserviceentrypoint.h" + +#include "src/definitions.h" +#include "src/gui/formeditttrssaccount.h" +#include "src/ttrssserviceroot.h" + +#include +#include +#include + +TtRssServiceEntryPoint::TtRssServiceEntryPoint(QObject* parent) : QObject(parent) {} + +TtRssServiceEntryPoint::~TtRssServiceEntryPoint() { + qDebugNN << LOGSEC_GMAIL << "Destructing" << QUOTE_W_SPACE(QSL(SERVICE_CODE_TT_RSS)) << "plugin."; +} + +QString TtRssServiceEntryPoint::name() const { + return QSL("Tiny Tiny RSS"); +} + +QString TtRssServiceEntryPoint::description() const { + return QObject::tr("This service offers integration with Tiny Tiny RSS.\n\n" + "Tiny Tiny RSS is an open source web-based news feed (RSS/Atom) reader and aggregator, " + "designed to allow you to read news from any location, while feeling as close to a real " + "desktop application as possible.\n\nAt least API level %1 is required.") + .arg(TTRSS_MINIMAL_API_LEVEL); +} + +QString TtRssServiceEntryPoint::author() const { + return QSL(APP_AUTHOR); +} + +QIcon TtRssServiceEntryPoint::icon() const { + return qApp->icons()->miscIcon(QSL("tt-rss")); +} + +QString TtRssServiceEntryPoint::code() const { + return QSL(SERVICE_CODE_TT_RSS); +} + +ServiceRoot* TtRssServiceEntryPoint::createNewRoot() const { + FormEditTtRssAccount form_acc(qApp->mainFormWidget()); + + return form_acc.addEditAccount(); +} + +QList TtRssServiceEntryPoint::initializeSubtree() const { + // Check DB if standard account is enabled. + QSqlDatabase database = qApp->database()->driver()->connection(QSL("TtRssServiceEntryPoint")); + + return DatabaseQueries::getAccounts(database, code()); +} diff --git a/src/librssguard-ttrss/src/ttrssserviceentrypoint.h b/src/librssguard-ttrss/src/ttrssserviceentrypoint.h new file mode 100644 index 000000000..0452663c1 --- /dev/null +++ b/src/librssguard-ttrss/src/ttrssserviceentrypoint.h @@ -0,0 +1,26 @@ +// For license of this file, see /LICENSE.md. + +#ifndef TTRSSSERVICEENTRYPOINT_H +#define TTRSSSERVICEENTRYPOINT_H + +#include + +class TtRssServiceEntryPoint : public QObject, public ServiceEntryPoint { + Q_OBJECT + Q_PLUGIN_METADATA(IID "io.github.martinrotter.rssguard.ttrss" FILE "plugin.json") + Q_INTERFACES(ServiceEntryPoint) + + public: + explicit TtRssServiceEntryPoint(QObject* parent = nullptr); + virtual ~TtRssServiceEntryPoint(); + + virtual QString name() const; + virtual QString description() const; + virtual QString author() const; + virtual QIcon icon() const; + virtual QString code() const; + virtual ServiceRoot* createNewRoot() const; + virtual QList initializeSubtree() const; +}; + +#endif // TTRSSSERVICEENTRYPOINT_H diff --git a/src/librssguard-ttrss/src/ttrssserviceroot.cpp b/src/librssguard-ttrss/src/ttrssserviceroot.cpp new file mode 100644 index 000000000..3866d9a37 --- /dev/null +++ b/src/librssguard-ttrss/src/ttrssserviceroot.cpp @@ -0,0 +1,423 @@ +// For license of this file, see /LICENSE.md. + +#include "src/ttrssserviceroot.h" + +#include "src/definitions.h" +#include "src/gui/formeditttrssaccount.h" +#include "src/gui/formttrssfeeddetails.h" +#include "src/gui/formttrssnote.h" +#include "src/ttrssfeed.h" +#include "src/ttrssnetworkfactory.h" +#include "src/ttrssserviceentrypoint.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +TtRssServiceRoot::TtRssServiceRoot(RootItem* parent) : ServiceRoot(parent), m_network(new TtRssNetworkFactory()) { + setIcon(TtRssServiceEntryPoint().icon()); +} + +TtRssServiceRoot::~TtRssServiceRoot() { + delete m_network; +} + +ServiceRoot::LabelOperation TtRssServiceRoot::supportedLabelOperations() const { + return ServiceRoot::LabelOperation::Synchronised; +} + +void TtRssServiceRoot::start(bool freshly_activated) { + if (!freshly_activated) { + DatabaseQueries::loadRootFromDatabase(this); + loadCacheFromFile(); + + auto lbls = labelsNode()->labels(); + + boolinq::from(lbls).for_each([](Label* lbl) { + if (lbl->customNumericId() == TTRSS_PUBLISHED_LABEL_ID) { + lbl->setKeepOnTop(true); + } + }); + + boolinq::from(childItems()).for_each([](RootItem* child) { + if (child->kind() == RootItem::Kind::Feed && child->customNumericId() == TTRSS_PUBLISHED_FEED_ID) { + child->setKeepOnTop(true); + } + }); + } + + updateTitle(); + + if (getSubTreeFeeds().isEmpty()) { + syncIn(); + } +} + +void TtRssServiceRoot::stop() { + m_network->logout(networkProxy()); + qDebugNN << LOGSEC_TTRSS << "Stopping Tiny Tiny RSS account, logging out with result" + << QUOTE_W_SPACE_DOT(m_network->lastError()); +} + +QString TtRssServiceRoot::code() const { + return TtRssServiceEntryPoint().code(); +} + +bool TtRssServiceRoot::isSyncable() const { + return true; +} + +FormAccountDetails* TtRssServiceRoot::accountSetupDialog() const { + return new FormEditTtRssAccount(qApp->mainFormWidget()); +} + +void TtRssServiceRoot::editItems(const QList& items) { + if (items.first()->kind() == RootItem::Kind::ServiceRoot) { + QScopedPointer p(qobject_cast(accountSetupDialog())); + + p->addEditAccount(this); + return; + } + + ServiceRoot::editItems(items); +} + +bool TtRssServiceRoot::supportsFeedAdding() const { + return true; +} + +bool TtRssServiceRoot::supportsCategoryAdding() const { + return false; +} + +void TtRssServiceRoot::addNewFeed(RootItem* selected_item, const QString& url) { + if (!qApp->feedUpdateLock()->tryLock()) { + // Lock was not obtained because + // it is used probably by feed updater or application + // is quitting. + qApp->showGuiMessage(Notification::Event::GeneralEvent, + {tr("Cannot add item"), + tr("Cannot add feed because another critical operation is ongoing."), + QSystemTrayIcon::MessageIcon::Warning}); + + return; + } + + QScopedPointer form_pointer(new FormTtRssFeedDetails(this, + selected_item, + url, + qApp->mainFormWidget())); + + form_pointer->addEditFeed(); + qApp->feedUpdateLock()->unlock(); +} + +bool TtRssServiceRoot::canBeEdited() const { + return true; +} + +void TtRssServiceRoot::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()) { + auto res = network()->updateArticles(ids, + UpdateArticle::OperatingField::Unread, + key == RootItem::ReadStatus::Unread ? UpdateArticle::Mode::SetToTrue + : UpdateArticle::Mode::SetToFalse, + networkProxy()); + + if (!ignore_errors && (network()->lastError() != QNetworkReply::NetworkError::NoError || res.hasError())) { + 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); + auto res = network()->updateArticles(ids, + UpdateArticle::OperatingField::Starred, + key == RootItem::Importance::Important ? UpdateArticle::Mode::SetToTrue + : UpdateArticle::Mode::SetToFalse, + networkProxy()); + + if (!ignore_errors && (network()->lastError() != QNetworkReply::NetworkError::NoError || res.hasError())) { + 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()) { + TtRssResponse res; + + if (label_custom_id.toInt() == TTRSS_PUBLISHED_LABEL_ID) { + // "published" label must be added in other method. + res = network()->updateArticles(messages, + UpdateArticle::OperatingField::Published, + UpdateArticle::Mode::SetToTrue, + networkProxy()); + } + else { + res = network()->setArticleLabel(messages, label_custom_id, true, networkProxy()); + } + + if (!ignore_errors && (network()->lastError() != QNetworkReply::NetworkError::NoError || res.hasError())) { + 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()) { + TtRssResponse res; + + if (label_custom_id.toInt() == TTRSS_PUBLISHED_LABEL_ID) { + // "published" label must be removed in other method. + res = network()->updateArticles(messages, + UpdateArticle::OperatingField::Published, + UpdateArticle::Mode::SetToFalse, + networkProxy()); + } + else { + res = network()->setArticleLabel(messages, label_custom_id, false, networkProxy()); + } + + if (!ignore_errors && (network()->lastError() != QNetworkReply::NetworkError::NoError || res.hasError())) { + addLabelsAssignmentsToCache(messages, label_custom_id, false); + } + } + } +} + +QVariantHash TtRssServiceRoot::customDatabaseData() const { + QVariantHash data = ServiceRoot::customDatabaseData(); + + data[QSL("username")] = m_network->username(); + data[QSL("password")] = TextFactory::encrypt(m_network->password()); + data[QSL("auth_protected")] = m_network->authIsUsed(); + data[QSL("auth_username")] = m_network->authUsername(); + data[QSL("auth_password")] = TextFactory::encrypt(m_network->authPassword()); + data[QSL("url")] = m_network->url(); + data[QSL("force_update")] = m_network->forceServerSideUpdate(); + 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 TtRssServiceRoot::setCustomDatabaseData(const QVariantHash& data) { + ServiceRoot::setCustomDatabaseData(data); + + m_network->setUsername(data[QSL("username")].toString()); + m_network->setPassword(TextFactory::decrypt(data[QSL("password")].toString())); + m_network->setAuthIsUsed(data[QSL("auth_protected")].toBool()); + m_network->setAuthUsername(data[QSL("auth_username")].toString()); + m_network->setAuthPassword(TextFactory::decrypt(data[QSL("auth_password")].toString())); + m_network->setUrl(data[QSL("url")].toString()); + m_network->setForceServerSideUpdate(data[QSL("force_update")].toBool()); + 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 TtRssServiceRoot::obtainNewMessages(Feed* feed, + const QHash& + stated_messages, + const QHash& tagged_messages) { + Q_UNUSED(tagged_messages) + + if (m_network->intelligentSynchronization()) { + return obtainMessagesIntelligently(feed, stated_messages); + } + else { + return obtainMessagesViaHeadlines(feed); + } +} + +QList TtRssServiceRoot::obtainMessagesIntelligently(Feed* feed, + const QHash& stated_messages) { + // 1. Get unread IDs for a feed. + // 2. Get read IDs for a feed. + // 3. Get starred IDs for a feed. + // 4. Determine IDs needed to download. + // 5. Download needed articles. + const QStringList remote_all_ids_list = + m_network->downloadOnlyUnreadMessages() + ? QStringList() + : m_network->getCompactHeadlines(feed->customNumericId(), 1000000, 0, QSL("all_articles"), networkProxy()).ids(); + const QStringList remote_unread_ids_list = + m_network->getCompactHeadlines(feed->customNumericId(), 1000000, 0, QSL("unread"), networkProxy()).ids(); + const QStringList remote_starred_ids_list = + m_network->getCompactHeadlines(feed->customNumericId(), 1000000, 0, QSL("marked"), networkProxy()).ids(); + + const QSet remote_all_ids = FROM_LIST_TO_SET(QSet, remote_all_ids_list); + + // 1. + auto local_unread_ids_list = stated_messages.value(ServiceRoot::BagOfMessages::Unread); + const QSet remote_unread_ids = FROM_LIST_TO_SET(QSet, remote_unread_ids_list); + const QSet local_unread_ids = FROM_LIST_TO_SET(QSet, local_unread_ids_list); + + // 2. + const auto local_read_ids_list = stated_messages.value(ServiceRoot::BagOfMessages::Read); + const QSet remote_read_ids = remote_all_ids - remote_unread_ids; + const QSet local_read_ids = FROM_LIST_TO_SET(QSet, local_read_ids_list); + + // 3. + const auto local_starred_ids_list = stated_messages.value(ServiceRoot::BagOfMessages::Starred); + const QSet remote_starred_ids = FROM_LIST_TO_SET(QSet, remote_starred_ids_list); + const QSet local_starred_ids = FROM_LIST_TO_SET(QSet, local_starred_ids_list); + + // 4. + QSet to_download; + + if (!m_network->downloadOnlyUnreadMessages()) { + to_download += remote_all_ids - local_read_ids - local_unread_ids; + } + else { + to_download += remote_unread_ids - local_read_ids - local_unread_ids; + } + + auto moved_read = local_read_ids & remote_unread_ids; + + to_download += moved_read; + + if (!m_network->downloadOnlyUnreadMessages()) { + auto moved_unread = local_unread_ids & remote_read_ids; + + to_download += moved_unread; + } + + auto moved_starred = (local_starred_ids + remote_starred_ids) - (local_starred_ids & remote_starred_ids); + + to_download += moved_starred; + + // 5. + auto msgs = m_network->getArticle(to_download.values(), networkProxy()); + + return msgs.messages(this); +} + +QList TtRssServiceRoot::obtainMessagesViaHeadlines(Feed* feed) { + QList messages; + int newly_added_messages = 0; + int limit = network()->batchSize() <= 0 ? TTRSS_MAX_MESSAGES : network()->batchSize(); + int skip = 0; + + do { + TtRssGetHeadlinesResponse headlines = network()->getHeadlines(feed->customNumericId(), + limit, + skip, + true, + true, + false, + network()->downloadOnlyUnreadMessages(), + networkProxy()); + + if (network()->lastError() != QNetworkReply::NetworkError::NoError) { + throw FeedFetchException(Feed::Status::NetworkError, headlines.error()); + } + else { + QList new_messages = headlines.messages(this); + + messages << new_messages; + newly_added_messages = new_messages.size(); + skip += newly_added_messages; + } + } + while (newly_added_messages > 0 && (network()->batchSize() <= 0 || messages.size() < network()->batchSize())); + + return messages; +} + +QString TtRssServiceRoot::additionalTooltip() const { + return ServiceRoot::additionalTooltip() + QSL("\n") + + tr("Username: %1\nServer: %2\n" + "Last error: %3\nLast login on: %4") + .arg(m_network->username(), + m_network->url(), + NetworkFactory::networkErrorText(m_network->lastError()), + m_network->lastLoginTime().isValid() + ? QLocale().toString(m_network->lastLoginTime(), QLocale::FormatType::ShortFormat) + : QSL("-")); +} + +TtRssNetworkFactory* TtRssServiceRoot::network() const { + return m_network; +} + +void TtRssServiceRoot::shareToPublished() { + FormTtRssNote(this).exec(); +} + +void TtRssServiceRoot::updateTitle() { + QString host = QUrl(m_network->url()).host(); + + if (host.isEmpty()) { + host = m_network->url(); + } + + setTitle(TextFactory::extractUsernameFromEmail(m_network->username()) + QSL(" (Tiny Tiny RSS)")); +} + +RootItem* TtRssServiceRoot::obtainNewTreeForSyncIn() const { + TtRssGetFeedsCategoriesResponse feed_cats = m_network->getFeedsCategories(networkProxy()); + TtRssGetLabelsResponse labels = m_network->getLabels(networkProxy()); + + auto lst_error = m_network->lastError(); + + if (lst_error == QNetworkReply::NoError) { + auto* tree = feed_cats.feedsCategories(m_network, true, networkProxy(), m_network->url()); + auto* lblroot = new LabelsNode(tree); + + lblroot->setChildItems(labels.labels()); + tree->appendChild(lblroot); + + return tree; + } + else { + throw NetworkException(lst_error, tr("cannot get list of feeds, network error '%1'").arg(lst_error)); + } +} + +bool TtRssServiceRoot::wantsBaggedIdsOfExistingMessages() const { + return m_network->intelligentSynchronization(); +} diff --git a/src/librssguard-ttrss/src/ttrssserviceroot.h b/src/librssguard-ttrss/src/ttrssserviceroot.h new file mode 100644 index 000000000..407d93f38 --- /dev/null +++ b/src/librssguard-ttrss/src/ttrssserviceroot.h @@ -0,0 +1,61 @@ +// For license of this file, see /LICENSE.md. + +#ifndef TTRSSSERVICEROOT_H +#define TTRSSSERVICEROOT_H + +#include +#include + +#include + +class TtRssCategory; +class TtRssFeed; +class TtRssNetworkFactory; + +class TtRssServiceRoot : public ServiceRoot, public CacheForServiceRoot { + Q_OBJECT + + public: + explicit TtRssServiceRoot(RootItem* parent = nullptr); + virtual ~TtRssServiceRoot(); + + virtual bool wantsBaggedIdsOfExistingMessages() const; + virtual LabelOperation supportedLabelOperations() const; + virtual void start(bool freshly_activated); + virtual void stop(); + virtual QString code() const; + virtual bool isSyncable() const; + virtual bool canBeEdited() const; + virtual void editItems(const QList& items); + virtual FormAccountDetails* accountSetupDialog() const; + virtual bool supportsFeedAdding() const; + virtual bool supportsCategoryAdding() const; + virtual void addNewFeed(RootItem* selected_item, const QString& url = QString()); + virtual QString additionalTooltip() const; + virtual void saveAllCachedData(bool ignore_errors); + virtual QVariantHash customDatabaseData() const; + virtual void setCustomDatabaseData(const QVariantHash& data); + virtual QList obtainNewMessages(Feed* feed, + const QHash& stated_messages, + const QHash& tagged_messages); + + // Access to network. + TtRssNetworkFactory* network() const; + + public slots: + void shareToPublished(); + + protected: + virtual RootItem* obtainNewTreeForSyncIn() const; + + private: + void updateTitle(); + QList obtainMessagesIntelligently(Feed* feed, + const QHash& stated_messages); + QList obtainMessagesViaHeadlines(Feed* feed); + + private: + TtRssNetworkFactory* m_network; +}; + +#endif // TTRSSSERVICEROOT_H diff --git a/src/librssguard/miscellaneous/pluginfactory.cpp b/src/librssguard/miscellaneous/pluginfactory.cpp index 123099408..7f0937a78 100644 --- a/src/librssguard/miscellaneous/pluginfactory.cpp +++ b/src/librssguard/miscellaneous/pluginfactory.cpp @@ -67,7 +67,7 @@ QStringList PluginFactory::pluginPaths() const { paths << QCoreApplication::applicationDirPath(); #endif -#if !defined(NDEBUG) +#if defined(NDEBUG) paths << QCoreApplication::applicationDirPath() + QDir::separator() + QL1S("..") + QDir::separator(); #endif