diff --git a/localization/rssguard_cs.ts b/localization/rssguard_cs.ts index 8e9470ea0..ceeb36472 100644 --- a/localization/rssguard_cs.ts +++ b/localization/rssguard_cs.ts @@ -22,7 +22,7 @@ Cannot enable AdBlock - + AdBlock nelze povolit @@ -32,25 +32,27 @@ OK! - + OK! There is error, check application log for more details and head to online documentation. - + Došlo k chybě, zkontrolujte log programu a případně prověřte dokumentaci. There is error, check application log for more details and head to online documentation. Also make sure that Node.js is installed. Error: %1 - + Došlo k chybě, zkontrolujte log programu a případně prověřte dokumentaci. Ujistětě se, že máte nakonfigurovaný Node.js. + +Chyba: %1 ERROR! - + CHYBA! @@ -58,12 +60,12 @@ Error: %1 No additional info. - + Žádné další info. It seems your AdBlock runs fine, but wait few seconds to be sure. - + Vypadá to, že AdBlock běží dobře, ale počkejme pár vteřin. @@ -78,7 +80,7 @@ Error: %1 Filter lists - + Filtrovací seznamy @@ -114,7 +116,7 @@ Error: %1 failed to download filter list '%1' - + nepodařilo se stáhnout filter '%1' @@ -167,22 +169,22 @@ Error: %1 Packages %1 were updated. - + Balíčky %1 byly aktualizovány. Unread articles fetched - + Staženy nepřečtené zprávy Go to changelog - + Zobrazit seznam změn AdBlock needs to be configured - + Je třeba nastavit AdBlock @@ -202,7 +204,7 @@ Error: %1 Welcome - + Vítejte @@ -218,7 +220,7 @@ na tuto bublinu. Already running - + Již běží @@ -286,7 +288,7 @@ na tuto bublinu. Show/hide the password - + Zobrazit/skrýt heslo @@ -327,32 +329,32 @@ na tuto bublinu. Removing read articles... - + Mažu přečtené zprávy... Read articles purged... - + Přečtené zprávy smazány... Removing old articles... - + Mažu staré zprávy... Old articles purged... - + Staré zprávy smazány... Removing starred articles... - + Mažu důležité zprávy... Starred articles purged... - + Důležité zprávy smazány... @@ -393,7 +395,7 @@ na tuto bublinu. Not supported by account - + Není podporováno @@ -483,7 +485,7 @@ Klikněte sem pro otevření nadřazeného adresáře. Open folder - + Otevřít složku @@ -612,39 +614,39 @@ Stav: %3 does not use auto-fetching of articles Describes feed auto-update status. - + nestahuje zprávy automaticky uses global settings (%n minute(s) to next auto-fetch of articles) Describes feed auto-update status. - + používá globální nastavení (%n minuta do dalšího stahování zpráv)používá globální nastavení (%n minuty do dalšího stahování zpráv)používá globální nastavení (%n minut do dalšího stahování zpráv)používá globální nastavení (%n minut do dalšího stahování zpráv) uses global settings (global auto-fetching of articles is disabled) - + používá globální nastavení (globální stahování zpráv je zakázáno) uses specific settings (%n minute(s) to next auto-fetching of new articles) Describes feed auto-update status. - + používá specifické nastavení (%n minuta do dalšího automatického stažení zpráv)používá specifické nastavení (%n minuty do dalšího automatického stažení zpráv)používá specifické nastavení (%n minut do dalšího automatického stažení zpráv)používá specifické nastavení (%n minut do dalšího automatického stažení zpráv) has new articles - + má nové zprávy parsing error - + špatný formát kanálu error - + chyba @@ -657,7 +659,7 @@ Stav: %3 Toolbar for articles - + Nástrojová lišta pro zprávy @@ -665,22 +667,22 @@ Stav: %3 Starting auto-download of some feeds' articles - + Zahajuji automatické stažení zpráv pro některé kanály I will auto-download new articles for %n feed(s). - + Budou se stahovat zprávy pro %n kanálBudou se stahovat zprávy pro %n kanályBudou se stahovat zprávy pro %n kanálůBudou se stahovat zprávy pro %n kanálů Cannot fetch articles at this point - + V tuto chvíli nelze stáhnout zprávy You cannot fetch new articles now because another critical operation is ongoing. - + V tuto chvíli nelze zahájit stahování zpráv, protože běží jiná důležitá operace. @@ -704,7 +706,7 @@ Stav: %3 Only download newest X articles per feed - + Stáhnout pouze X nejnovějších zpráv pro každý kanál @@ -714,7 +716,7 @@ Stav: %3 Download unread articles only - + Stahovat pouze nepřečtené zprávy @@ -770,17 +772,17 @@ Stav: %3 Login was successful. - + Přihlášení bylo úspěšné. Your %1 build has official Feedly support. You do not have to use "developer access token". You can therefore leave corresponding field empty. - + Vaše verze programu %1 má oficiální podporu Feedly. Nemusíte tedy používat DAC, a tak můžete nechat odpovídající políčko prázdné. Some problems. - + Nějaké problémy. @@ -795,22 +797,22 @@ Stav: %3 Access token is empty. - + Přístupový token je prázdný. Access token is okay. - + Přístupový token je v pořádku. Error: '%1' - + Chyba: '%1' Beware of downloading too many articles, because Feedly permanently caches ALL articles of the feed, so you might end up with thousands of articles which you will never read anyway. - + Zvažte, zda je dobrá věc stahovat příliš mnoho zpráv. Feedly permantně cachuje všechny dřívější zprávy z kanálu, takže můžete skončit s tisícovkami zpráv, které stejně nebudete číst. @@ -818,7 +820,7 @@ Stav: %3 Feedly: authentication error - + Feedly: chyba autentizace @@ -828,7 +830,7 @@ Stav: %3 Feedly: authorization denied - + Feedly: přihlašovací údaje zamítnuty @@ -1006,13 +1008,13 @@ or this functionality is not implemented yet. Context menu for important articles - + Kontextové menu pro důležité zprávy Not supported by account - + Není podporováno @@ -1082,7 +1084,7 @@ or this functionality is not implemented yet. GNU LGPL License (applies to Breeze source code) - + GNU LGPL Licence (pro kód komponenty Breeze) @@ -1122,7 +1124,7 @@ or this functionality is not implemented yet. Database location - + Umístění databáze @@ -1135,7 +1137,7 @@ or this functionality is not implemented yet. Network proxy - + Síťová proxy @@ -1145,7 +1147,7 @@ or this functionality is not implemented yet. Edit account "%1" - + Upravit účet "%1" @@ -1431,12 +1433,12 @@ or this functionality is not implemented yet. Edit "%1" - + Upravit "%1" Parent folder - + Nadřazený uzel @@ -1506,32 +1508,32 @@ or this functionality is not implemented yet. Cleanup settings - + Nastavení čištění Optimize database file - + Optimalizovat databázi Remove all read articles - + Smazat přečtené zprávy Remove all articles from recycle bin - + Smazat zprávy z košů Remove all articles older than - + Smazat zprávy starší než Remove all starred articles - + Smazat důležité zprávy @@ -1541,7 +1543,7 @@ or this functionality is not implemented yet. Total data size - + Velikost dat @@ -1572,7 +1574,7 @@ or this functionality is not implemented yet. Service setup - + Nastavení služby @@ -1580,7 +1582,7 @@ or this functionality is not implemented yet. Server setup - + Nastavení serveru @@ -1588,7 +1590,7 @@ or this functionality is not implemented yet. Server setup - + Nastavení serveru @@ -1596,7 +1598,7 @@ or this functionality is not implemented yet. Server setup - + Nastavení serveru @@ -1604,7 +1606,7 @@ or this functionality is not implemented yet. Server setup - + Nastavení serveru @@ -1612,7 +1614,7 @@ or this functionality is not implemented yet. Server setup - + Nastavení serveru @@ -1625,62 +1627,62 @@ or this functionality is not implemented yet. Cannot save changes: %1 - + Změny nelze uložit: %1 Edit "%1" - + Upravit "%1" Fetch articles using global interval - + Stahovat zprávy dle hlavního nastavení Fetch articles every - + Stahovat zprávy každých Disable auto-fetching of articles - + Zakázat automatické stahování zpráv Cannot save feed properties - + Nelze uložit vlastnosti kanálu Articles - + Zprávy Auto-downloading of articles - + Automatické stahování zpráv Select the auto-download strategy for messages of this feed. Default auto-download strategy means that new messges of this feed will be downloaded in time intervals set in application settings. - + Zvolte strategii auto-aktualizací zpráv tohoto kanálu. Výchozí strategorie auto-aktualizace znamená, že kanál bude aktualizován v intervalech udaných v nastavení aplikace. Open articles via their URL automatically - + Otevírat zdrojové URL zpráv automaticky Miscellaneous - + Různé Disable this feed - + Vypnout kanál @@ -1703,7 +1705,7 @@ or this functionality is not implemented yet. Open main menu - + Otevřít hlavní menu @@ -1738,12 +1740,12 @@ or this functionality is not implemented yet. F&eeds - + &Kanály &Add item - + &Přidat položku @@ -3149,7 +3151,7 @@ Nyní ho můžete nainstalovat. Only download newest X articles per feed - + Stáhnout pouze X nejnovějších zpráv pro každý kanál @@ -3159,7 +3161,7 @@ Nyní ho můžete nainstalovat. Download unread articles only - + Stahovat pouze nepřečtené zprávy @@ -3413,7 +3415,7 @@ Tokeny vyprší: %2 Download unread articles only - + Stahovat pouze nepřečtené zprávy @@ -3428,7 +3430,7 @@ Tokeny vyprší: %2 Only download newest X articles per feed - + Stáhnout pouze X nejnovějších zpráv pro každý kanál @@ -4434,12 +4436,12 @@ Tokeny vyprší: %2 Download unread articles only - + Stahovat pouze nepřečtené zprávy Only download newest X articles per feed - + Stáhnout pouze X nejnovějších zpráv pro každý kanál @@ -4835,7 +4837,7 @@ List of supported readers: Only download newest X articles per feed - + Stáhnout pouze X nejnovějších zpráv pro každý kanál @@ -4845,7 +4847,7 @@ List of supported readers: Download unread articles only - + Stahovat pouze nepřečtené zprávy @@ -5160,7 +5162,7 @@ File filter for external e-mail selection dialog. Network proxy - + Síťová proxy @@ -5469,7 +5471,7 @@ Authors of this application are NOT responsible for lost data. Articles - + Zprávy @@ -6309,7 +6311,7 @@ Also, you can post-process generated feed data with yet another script if you wi Parent folder - + Nadřazený uzel @@ -6798,12 +6800,12 @@ Nepřečtené zprávy: %2 Download unread articles only - + Stahovat pouze nepřečtené zprávy Only download newest X articles per feed - + Stáhnout pouze X nejnovějších zpráv pro každý kanál @@ -6887,7 +6889,7 @@ Nepřečtené zprávy: %2 Parent folder - + Nadřazený uzel diff --git a/resources/desktop/com.github.rssguard.appdata.xml b/resources/desktop/com.github.rssguard.appdata.xml index 3c194cc98..cb71a2436 100644 --- a/resources/desktop/com.github.rssguard.appdata.xml +++ b/resources/desktop/com.github.rssguard.appdata.xml @@ -26,7 +26,7 @@ https://github.com/sponsors/martinrotter - + none diff --git a/resources/graphics/misc/newsblur.png b/resources/graphics/misc/newsblur.png new file mode 100755 index 000000000..658ffe1f1 Binary files /dev/null and b/resources/graphics/misc/newsblur.png differ diff --git a/resources/rssguard.qrc b/resources/rssguard.qrc index e8b7d4baa..a269d096f 100644 --- a/resources/rssguard.qrc +++ b/resources/rssguard.qrc @@ -34,6 +34,7 @@ graphics/misc/google.png graphics/misc/image-placeholder.png graphics/misc/inoreader.png + graphics/misc/newsblur.png graphics/misc/nextcloud.png graphics/misc/reddit.png graphics/misc/reedah.png diff --git a/src/librssguard/CMakeLists.txt b/src/librssguard/CMakeLists.txt index eaadab8de..1be67a101 100644 --- a/src/librssguard/CMakeLists.txt +++ b/src/librssguard/CMakeLists.txt @@ -305,6 +305,17 @@ set(SOURCES services/greader/gui/formeditgreaderaccount.h services/greader/gui/greaderaccountdetails.cpp services/greader/gui/greaderaccountdetails.h + services/newsblur/definitions.h + services/newsblur/newsblurentrypoint.cpp + services/newsblur/newsblurentrypoint.h + services/newsblur/newsblurnetwork.cpp + services/newsblur/newsblurnetwork.h + services/newsblur/newsblurserviceroot.cpp + services/newsblur/newsblurserviceroot.h + services/newsblur/gui/formeditnewsbluraccount.cpp + services/newsblur/gui/formeditnewsbluraccount.h + services/newsblur/gui/newsbluraccountdetails.cpp + services/newsblur/gui/newsbluraccountdetails.h services/owncloud/definitions.h services/owncloud/gui/formeditowncloudaccount.cpp services/owncloud/gui/formeditowncloudaccount.h @@ -447,6 +458,7 @@ set(UI_FILES services/gmail/gui/formdownloadattachment.ui services/gmail/gui/gmailaccountdetails.ui services/greader/gui/greaderaccountdetails.ui + services/newsblur/gui/newsbluraccountdetails.ui services/owncloud/gui/owncloudaccountdetails.ui services/reddit/gui/redditaccountdetails.ui services/standard/gui/formstandardimportexport.ui diff --git a/src/librssguard/definitions/definitions.h b/src/librssguard/definitions/definitions.h index 1aa656654..fcc1c65fd 100644 --- a/src/librssguard/definitions/definitions.h +++ b/src/librssguard/definitions/definitions.h @@ -14,6 +14,7 @@ #define SERVICE_CODE_INOREADER "inoreader" #define SERVICE_CODE_GMAIL "gmail" #define SERVICE_CODE_REDDIT "reddit" +#define SERVICE_CODE_NEWSBLUR "newsblur" #define ADBLOCK_SERVER_PORT 48484 #define ADBLOCK_HOWTO "https://github.com/martinrotter/rssguard/blob/master/resources/docs/Documentation.md#adbl" @@ -139,6 +140,7 @@ #define LOGSEC_GMAIL "gmail: " #define LOGSEC_OAUTH "oauth: " #define LOGSEC_REDDIT "reddit: " +#define LOGSEC_NEWSBLUR "newsblur: " #define MAX_ZOOM_FACTOR 5.0f #define MIN_ZOOM_FACTOR 0.25f diff --git a/src/librssguard/miscellaneous/application.cpp b/src/librssguard/miscellaneous/application.cpp index b17cb52b0..d0c420a3b 100644 --- a/src/librssguard/miscellaneous/application.cpp +++ b/src/librssguard/miscellaneous/application.cpp @@ -850,7 +850,7 @@ void Application::parseCmdArgumentsFromMyInstance() { void Application::onNodeJsPackageUpdateError(const QList& pkgs, const QString& error) { qApp->showGuiMessage(Notification::Event::NodePackageFailedToUpdate, { {}, - tr("Packages %1 were NOT updated because of error: %3.").arg(NodeJs::packagesToString(pkgs), + tr("Packages %1 were NOT updated because of error: %2.").arg(NodeJs::packagesToString(pkgs), error), QSystemTrayIcon::MessageIcon::Critical }); } diff --git a/src/librssguard/miscellaneous/feedreader.cpp b/src/librssguard/miscellaneous/feedreader.cpp index 0e4a0d764..cf31d3333 100644 --- a/src/librssguard/miscellaneous/feedreader.cpp +++ b/src/librssguard/miscellaneous/feedreader.cpp @@ -17,6 +17,7 @@ #include "services/feedly/feedlyentrypoint.h" #include "services/gmail/gmailentrypoint.h" #include "services/greader/greaderentrypoint.h" +#include "services/newsblur/newsblurentrypoint.h" #include "services/owncloud/owncloudserviceentrypoint.h" #include "services/reddit/redditentrypoint.h" #include "services/standard/standardserviceentrypoint.h" @@ -60,6 +61,7 @@ QList FeedReader::feedServices() { m_feedServices.append(new FeedlyEntryPoint()); m_feedServices.append(new GmailEntryPoint()); m_feedServices.append(new GreaderEntryPoint()); + m_feedServices.append(new NewsBlurEntryPoint()); m_feedServices.append(new OwnCloudServiceEntryPoint()); #if defined(DEBUG) diff --git a/src/librssguard/network-web/downloader.cpp b/src/librssguard/network-web/downloader.cpp index c31da6327..c1a94280b 100644 --- a/src/librssguard/network-web/downloader.cpp +++ b/src/librssguard/network-web/downloader.cpp @@ -171,6 +171,17 @@ void Downloader::finished() { m_lastOutputMultipartData = decodeMultipartAnswer(reply); } + QVariant set_cookies_header = reply->header(QNetworkRequest::SetCookieHeader); + + if (set_cookies_header.isValid()) { + QList cookies = set_cookies_header.value>(); + + m_lastCookies = cookies; + } + else { + m_lastCookies = {}; + } + m_lastContentType = reply->header(QNetworkRequest::ContentTypeHeader); m_lastOutputError = reply->error(); m_activeReply->deleteLater(); @@ -292,6 +303,10 @@ void Downloader::runGetRequest(const QNetworkRequest& request) { connect(m_activeReply, &QNetworkReply::finished, this, &Downloader::finished); } +QList Downloader::lastCookies() const { + return m_lastCookies; +} + QVariant Downloader::lastContentType() const { return m_lastContentType; } diff --git a/src/librssguard/network-web/downloader.h b/src/librssguard/network-web/downloader.h index f0421bb4a..a275fe14d 100644 --- a/src/librssguard/network-web/downloader.h +++ b/src/librssguard/network-web/downloader.h @@ -28,6 +28,7 @@ class Downloader : public QObject { QNetworkReply::NetworkError lastOutputError() const; QList lastOutputMultipartData() const; QVariant lastContentType() const; + QList lastCookies() const; void setProxy(const QNetworkProxy& proxy); @@ -98,6 +99,7 @@ class Downloader : public QObject { QNetworkReply::NetworkError m_lastOutputError; QVariant m_lastContentType; + QList m_lastCookies; }; #endif // DOWNLOADER_H diff --git a/src/librssguard/network-web/networkfactory.cpp b/src/librssguard/network-web/networkfactory.cpp index 8c7d308d6..010d60536 100644 --- a/src/librssguard/network-web/networkfactory.cpp +++ b/src/librssguard/network-web/networkfactory.cpp @@ -229,11 +229,15 @@ QNetworkReply::NetworkError NetworkFactory::downloadIcon(const QList>& additional_headers, bool protected_contents, - const QString& username, const QString& password, + const QString& username, + const QString& password, const QNetworkProxy& custom_proxy) { Downloader downloader; QEventLoop loop; diff --git a/src/librssguard/services/newsblur/definitions.h b/src/librssguard/services/newsblur/definitions.h new file mode 100755 index 000000000..d68b61fbf --- /dev/null +++ b/src/librssguard/services/newsblur/definitions.h @@ -0,0 +1,15 @@ +#ifndef NEWSBLUR_DEFINITIONS_H +#define NEWSBLUR_DEFINITIONS_H + +// Misc. +#define NEWSBLUR_DEFAULT_BATCH_SIZE 500 + +// URLs. +#define NEWSBLUR_URL "https://newsblur.com" + +// API. +#define NEWSBLUR_API_LOGIN "api/login" +#define NEWSBLUR_API_LOGOUT "api/logout" +#define NEWSBLUR_API_SIGNUP "api/signup" + +#endif // NEWSBLUR_DEFINITIONS_H diff --git a/src/librssguard/services/newsblur/gui/formeditnewsbluraccount.cpp b/src/librssguard/services/newsblur/gui/formeditnewsbluraccount.cpp new file mode 100755 index 000000000..d1aca3b6b --- /dev/null +++ b/src/librssguard/services/newsblur/gui/formeditnewsbluraccount.cpp @@ -0,0 +1,63 @@ +// For license of this file, see /LICENSE.md. + +#include "services/newsblur/gui/formeditnewsbluraccount.h" + +#include "gui/guiutilities.h" +#include "miscellaneous/iconfactory.h" +#include "network-web/networkfactory.h" +#include "services/newsblur/definitions.h" +#include "services/newsblur/gui/newsbluraccountdetails.h" +#include "services/newsblur/newsblurnetwork.h" +#include "services/newsblur/newsblurserviceroot.h" + +FormEditNewsBlurAccount::FormEditNewsBlurAccount(QWidget* parent) + : FormAccountDetails(qApp->icons()->miscIcon(QSL("newsblur")), parent), m_details(new NewsBlurAccountDetails(this)) { + insertCustomTab(m_details, tr("Server setup"), 0); + activateTab(0); + + connect(m_details->m_ui.m_btnTestSetup, &QPushButton::clicked, this, &FormEditNewsBlurAccount::performTest); + + m_details->m_ui.m_txtUrl->setFocus(); +} + +void FormEditNewsBlurAccount::apply() { + FormAccountDetails::apply(); + + NewsBlurServiceRoot* existing_root = account(); + bool using_another_acc = + m_details->m_ui.m_txtUsername->lineEdit()->text() != existing_root->network()->username() || + m_details->m_ui.m_txtUrl->lineEdit()->text() != existing_root->network()->baseUrl(); + + existing_root->network()->setBaseUrl(m_details->m_ui.m_txtUrl->lineEdit()->text()); + existing_root->network()->setUsername(m_details->m_ui.m_txtUsername->lineEdit()->text()); + existing_root->network()->setPassword(m_details->m_ui.m_txtPassword->lineEdit()->text()); + existing_root->network()->setBatchSize(m_details->m_ui.m_spinLimitMessages->value()); + existing_root->network()->setDownloadOnlyUnreadMessages(m_details->m_ui.m_cbDownloadOnlyUnreadMessages->isChecked()); + + existing_root->saveAccountDataToDatabase(); + accept(); + + if (!m_creatingNew) { + if (using_another_acc) { + existing_root->completelyRemoveAllData(); + } + + existing_root->start(true); + } +} + +void FormEditNewsBlurAccount::loadAccountData() { + FormAccountDetails::loadAccountData(); + + NewsBlurServiceRoot* existing_root = account(); + + 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()->baseUrl()); + m_details->m_ui.m_spinLimitMessages->setValue(existing_root->network()->batchSize()); + m_details->m_ui.m_cbDownloadOnlyUnreadMessages->setChecked(existing_root->network()->downloadOnlyUnreadMessages()); +} + +void FormEditNewsBlurAccount::performTest() { + m_details->performTest(m_proxyDetails->proxy()); +} diff --git a/src/librssguard/services/newsblur/gui/formeditnewsbluraccount.h b/src/librssguard/services/newsblur/gui/formeditnewsbluraccount.h new file mode 100755 index 000000000..1871f8295 --- /dev/null +++ b/src/librssguard/services/newsblur/gui/formeditnewsbluraccount.h @@ -0,0 +1,30 @@ +// For license of this file, see /LICENSE.md. + +#ifndef FORMEDITNEWSBLURACCOUNT_H +#define FORMEDITNEWSBLURACCOUNT_H + +#include "services/abstract/gui/formaccountdetails.h" + +class NewsBlurAccountDetails; +class NewsBlurServiceRoot; + +class FormEditNewsBlurAccount : public FormAccountDetails { + Q_OBJECT + + public: + explicit FormEditNewsBlurAccount(QWidget* parent = nullptr); + + protected slots: + virtual void apply(); + + protected: + virtual void loadAccountData(); + + private slots: + void performTest(); + + private: + NewsBlurAccountDetails* m_details; +}; + +#endif // FORMEDITNEWSBLURACCOUNT_H diff --git a/src/librssguard/services/newsblur/gui/newsbluraccountdetails.cpp b/src/librssguard/services/newsblur/gui/newsbluraccountdetails.cpp new file mode 100755 index 000000000..bbd894b1f --- /dev/null +++ b/src/librssguard/services/newsblur/gui/newsbluraccountdetails.cpp @@ -0,0 +1,114 @@ +// For license of this file, see /LICENSE.md. + +#include "services/newsblur/gui/newsbluraccountdetails.h" + +#include "definitions/definitions.h" +#include "exceptions/applicationexception.h" +#include "exceptions/networkexception.h" +#include "gui/guiutilities.h" +#include "miscellaneous/application.h" +#include "miscellaneous/systemfactory.h" +#include "network-web/webfactory.h" +#include "services/newsblur/definitions.h" +#include "services/newsblur/newsblurnetwork.h" + +#include + +NewsBlurAccountDetails::NewsBlurAccountDetails(QWidget* parent) : QWidget(parent), m_lastProxy({}) { + m_ui.setupUi(this); + + m_ui.m_lblTestResult->label()->setWordWrap(true); + m_ui.m_txtPassword->lineEdit()->setPasswordMode(true); + m_ui.m_txtPassword->lineEdit()->setPlaceholderText(tr("Password for your account")); + m_ui.m_txtUsername->lineEdit()->setPlaceholderText(tr("Username for your account")); + m_ui.m_txtUrl->lineEdit()->setPlaceholderText(tr("URL of your server, without any service-specific path")); + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Information, + tr("No test done yet."), + tr("Here, results of connection test are shown.")); + + m_ui.m_lblLimitMessages->setHelpText(tr("Some feeds might contain tens of thousands of articles " + "and downloading all of them could take great amount of time, " + "so sometimes it is good to download " + "only certain amount of newest messages."), + true); + + connect(m_ui.m_txtPassword->lineEdit(), &BaseLineEdit::textChanged, this, &NewsBlurAccountDetails::onPasswordChanged); + connect(m_ui.m_txtUsername->lineEdit(), &BaseLineEdit::textChanged, this, &NewsBlurAccountDetails::onUsernameChanged); + connect(m_ui.m_txtUrl->lineEdit(), &BaseLineEdit::textChanged, this, &NewsBlurAccountDetails::onUrlChanged); + + setTabOrder(m_ui.m_txtUrl->lineEdit(), m_ui.m_cbDownloadOnlyUnreadMessages); + setTabOrder(m_ui.m_cbDownloadOnlyUnreadMessages, m_ui.m_spinLimitMessages); + setTabOrder(m_ui.m_spinLimitMessages, 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 NewsBlurAccountDetails::performTest(const QNetworkProxy& custom_proxy) { + m_lastProxy = custom_proxy; + + NewsBlurNetwork factory; + + factory.setUsername(m_ui.m_txtUsername->lineEdit()->text()); + factory.setPassword(m_ui.m_txtPassword->lineEdit()->text()); + factory.setBaseUrl(m_ui.m_txtUrl->lineEdit()->text()); + + try { + LoginResult result = factory.login(custom_proxy); + + if (result.m_authenticated && !result.m_sessiodId.isEmpty()) { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Ok, + tr("You are good to go!"), + tr("Yeah.")); + } + else { + throw ApplicationException(result.m_errors.join(QSL(", "))); + } + } + catch (const NetworkException& netEx) { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error, + tr("Network error: '%1'.").arg(NetworkFactory::networkErrorText(netEx.networkError())), + tr("Network error, have you entered correct username and password?")); + } + catch (const ApplicationException& ex) { + m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error, + tr("Error: '%1'.").arg(ex.message()), + tr("Error, have you entered correct Nextcloud endpoint and password?")); + } +} + +void NewsBlurAccountDetails::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 NewsBlurAccountDetails::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 NewsBlurAccountDetails::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/services/newsblur/gui/newsbluraccountdetails.h b/src/librssguard/services/newsblur/gui/newsbluraccountdetails.h new file mode 100755 index 000000000..18fdeb3fa --- /dev/null +++ b/src/librssguard/services/newsblur/gui/newsbluraccountdetails.h @@ -0,0 +1,33 @@ +// For license of this file, see /LICENSE.md. + +#ifndef NEWSBLURACCOUNTDETAILS_H +#define NEWSBLURACCOUNTDETAILS_H + +#include + +#include "ui_newsbluraccountdetails.h" + +#include "services/newsblur/newsblurserviceroot.h" + +#include + +class NewsBlurAccountDetails : public QWidget { + Q_OBJECT + + friend class FormEditNewsBlurAccount; + + public: + explicit NewsBlurAccountDetails(QWidget* parent = nullptr); + + private slots: + void performTest(const QNetworkProxy& custom_proxy); + void onUsernameChanged(); + void onPasswordChanged(); + void onUrlChanged(); + + private: + Ui::NewsBlurAccountDetails m_ui; + QNetworkProxy m_lastProxy; +}; + +#endif // NEWSBLURACCOUNTDETAILS_H diff --git a/src/librssguard/services/newsblur/gui/newsbluraccountdetails.ui b/src/librssguard/services/newsblur/gui/newsbluraccountdetails.ui new file mode 100755 index 000000000..51378af47 --- /dev/null +++ b/src/librssguard/services/newsblur/gui/newsbluraccountdetails.ui @@ -0,0 +1,163 @@ + + + NewsBlurAccountDetails + + + + 0 + 0 + 430 + 281 + + + + + + + URL + + + m_txtUrl + + + + + + + + + + Download unread articles only + + + + + + + + + Only download newest X articles per feed + + + m_spinLimitMessages + + + + + + + + + + + + + + + + 0 + 0 + + + + Authentication + + + + + + Username + + + m_txtUsername + + + + + + + + + + Password + + + m_txtPassword + + + + + + + + + + + + + + + &Test setup + + + + + + + + 0 + 0 + + + + + + + + + + Qt::Vertical + + + + 409 + 60 + + + + + + + + + LabelWithStatus + QWidget +
labelwithstatus.h
+ 1 +
+ + LineEditWithStatus + QWidget +
lineeditwithstatus.h
+ 1 +
+ + MessageCountSpinBox + QSpinBox +
messagecountspinbox.h
+
+ + HelpSpoiler + QWidget +
helpspoiler.h
+ 1 +
+
+ + m_cbDownloadOnlyUnreadMessages + m_spinLimitMessages + m_btnTestSetup + + + +
diff --git a/src/librssguard/services/newsblur/newsblurentrypoint.cpp b/src/librssguard/services/newsblur/newsblurentrypoint.cpp new file mode 100755 index 000000000..898deb464 --- /dev/null +++ b/src/librssguard/services/newsblur/newsblurentrypoint.cpp @@ -0,0 +1,42 @@ +// For license of this file, see /LICENSE.md. + +#include "services/newsblur/newsblurentrypoint.h" + +#include "database/databasequeries.h" +#include "definitions/definitions.h" +#include "miscellaneous/application.h" +#include "miscellaneous/iconfactory.h" +#include "services/newsblur/gui/formeditnewsbluraccount.h" +#include "services/newsblur/newsblurserviceroot.h" + +ServiceRoot* NewsBlurEntryPoint::createNewRoot() const { + FormEditNewsBlurAccount form_acc(qApp->mainFormWidget()); + + return form_acc.addEditAccount(); +} + +QList NewsBlurEntryPoint::initializeSubtree() const { + QSqlDatabase database = qApp->database()->driver()->connection(QSL("NewsBlurEntryPoint")); + + return DatabaseQueries::getAccounts(database, code()); +} + +QString NewsBlurEntryPoint::name() const { + return QSL("NewsBlur"); +} + +QString NewsBlurEntryPoint::code() const { + return QSL(SERVICE_CODE_NEWSBLUR); +} + +QString NewsBlurEntryPoint::description() const { + return QObject::tr("Personal news reader bringing people together to talk about the world."); +} + +QString NewsBlurEntryPoint::author() const { + return QSL(APP_AUTHOR); +} + +QIcon NewsBlurEntryPoint::icon() const { + return qApp->icons()->miscIcon(QSL("newsblur")); +} diff --git a/src/librssguard/services/newsblur/newsblurentrypoint.h b/src/librssguard/services/newsblur/newsblurentrypoint.h new file mode 100755 index 000000000..6e96a3422 --- /dev/null +++ b/src/librssguard/services/newsblur/newsblurentrypoint.h @@ -0,0 +1,19 @@ +// For license of this file, see /LICENSE.md. + +#ifndef NEWSBLURENTRYPOINT_H +#define NEWSBLURENTRYPOINT_H + +#include "services/abstract/serviceentrypoint.h" + +class NewsBlurEntryPoint : public ServiceEntryPoint { + public: + virtual ServiceRoot* createNewRoot() const; + virtual QList initializeSubtree() const; + virtual QString name() const; + virtual QString code() const; + virtual QString description() const; + virtual QString author() const; + virtual QIcon icon() const; +}; + +#endif // NEWSBLURENTRYPOINT_H diff --git a/src/librssguard/services/newsblur/newsblurnetwork.cpp b/src/librssguard/services/newsblur/newsblurnetwork.cpp new file mode 100755 index 000000000..44064edee --- /dev/null +++ b/src/librssguard/services/newsblur/newsblurnetwork.cpp @@ -0,0 +1,177 @@ +// For license of this file, see /LICENSE.md. + +#include "services/newsblur/newsblurnetwork.h" + +#include "3rd-party/boolinq/boolinq.h" +#include "database/databasequeries.h" +#include "exceptions/applicationexception.h" +#include "exceptions/feedfetchexception.h" +#include "exceptions/networkexception.h" +#include "miscellaneous/application.h" +#include "network-web/networkfactory.h" +#include "network-web/webfactory.h" +#include "services/abstract/category.h" +#include "services/abstract/label.h" +#include "services/abstract/labelsnode.h" +#include "services/newsblur/definitions.h" + +#include +#include +#include + +NewsBlurNetwork::NewsBlurNetwork(QObject* parent) + : QObject(parent), m_root(nullptr), m_username(QString()), m_password(QString()), m_baseUrl(QSL(NEWSBLUR_URL)), + m_batchSize(NEWSBLUR_DEFAULT_BATCH_SIZE), + m_downloadOnlyUnreadMessages(false) { + clearCredentials(); +} + +LoginResult NewsBlurNetwork::login(const QNetworkProxy& proxy) { + const QString full_url = generateFullUrl(Operations::Login); + const auto timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); + const QString data = QSL("username=%1&password=%2").arg(m_username, m_password); + QByteArray output; + auto network_result = NetworkFactory::performNetworkOperation(full_url, + timeout, + data.toUtf8(), + output, + QNetworkAccessManager::Operation::PostOperation, + { { + QSL(HTTP_HEADERS_CONTENT_TYPE).toLocal8Bit(), + QSL("application/x-www-form-urlencoded").toLocal8Bit() + } }, + false, + {}, + {}, + proxy); + + if (network_result.first == QNetworkReply::NetworkError::NoError) { + QJsonParseError err; + QJsonDocument doc = QJsonDocument::fromJson(output, &err); + + if (err.error != QJsonParseError::ParseError::NoError) { + throw ApplicationException(err.errorString()); + } + + LoginResult res; + + res.decodeBaseResponse(doc); + res.m_userId = doc.object()["user_id"].toInt(); + + return res; + } + else { + throw NetworkException(network_result.first, output); + } +} + +QString NewsBlurNetwork::username() const { + return m_username; +} + +void NewsBlurNetwork::setUsername(const QString& username) { + m_username = username; +} + +QString NewsBlurNetwork::password() const { + return m_password; +} + +void NewsBlurNetwork::setPassword(const QString& password) { + m_password = password; +} + +QString NewsBlurNetwork::baseUrl() const { + return m_baseUrl; +} + +void NewsBlurNetwork::setBaseUrl(const QString& base_url) { + m_baseUrl = base_url; +} + +QPair NewsBlurNetwork::authHeader() const { + return { QSL("Cookie").toLocal8Bit(), + QSL("newsblur_sessionid=%1").arg(m_authSid).toLocal8Bit() }; +} + +void NewsBlurNetwork::ensureLogin(const QNetworkProxy& proxy) { + if (m_authSid.isEmpty()) { + try { + auto log = login(proxy); + + if (log.m_authenticated && !log.m_sessiodId.isEmpty()) { + m_authSid = log.m_sessiodId; + } + else { + throw ApplicationException(log.m_errors.join(QSL(", "))); + } + } + catch (const NetworkException& ex) { + throw ex; + } + catch (const ApplicationException& ex) { + throw ex; + } + } +} + +int NewsBlurNetwork::batchSize() const { + return m_batchSize; +} + +void NewsBlurNetwork::setBatchSize(int batch_size) { + m_batchSize = batch_size; +} + +void NewsBlurNetwork::clearCredentials() { + m_authSid = {}; + m_userId = {}; +} + +QString NewsBlurNetwork::sanitizedBaseUrl() const { + QString base_url = m_baseUrl; + + if (!base_url.endsWith('/')) { + base_url = base_url + QL1C('/'); + } + + return base_url; +} + +QString NewsBlurNetwork::generateFullUrl(NewsBlurNetwork::Operations operation) const { + switch (operation) { + case Operations::Login: + return sanitizedBaseUrl() + QSL(NEWSBLUR_API_LOGIN); + + default: + return sanitizedBaseUrl(); + } +} + +void NewsBlurNetwork::setRoot(NewsBlurServiceRoot* root) { + m_root = root; +} + +bool NewsBlurNetwork::downloadOnlyUnreadMessages() const { + return m_downloadOnlyUnreadMessages; +} + +void NewsBlurNetwork::setDownloadOnlyUnreadMessages(bool download_only_unread) { + m_downloadOnlyUnreadMessages = download_only_unread; +} + +void ApiResult::decodeBaseResponse(const QJsonDocument& doc) { + m_authenticated = doc.object()["authenticated"].toBool(); + m_code = doc.object()["code"].toInt(); + + QStringList errs; + QJsonObject obj_errs = doc.object()["errors"].toObject(); + + for (const QString& key : obj_errs.keys()) { + for (const QJsonValue& val: obj_errs.value(key).toArray()) { + errs << val.toString(); + } + } + + m_errors = errs; +} diff --git a/src/librssguard/services/newsblur/newsblurnetwork.h b/src/librssguard/services/newsblur/newsblurnetwork.h new file mode 100755 index 000000000..c6a130fe0 --- /dev/null +++ b/src/librssguard/services/newsblur/newsblurnetwork.h @@ -0,0 +1,77 @@ +// For license of this file, see /LICENSE.md. + +#ifndef NEWSBLURNETWORK_H +#define NEWSBLURNETWORK_H + +#include + +#include "network-web/networkfactory.h" +#include "services/abstract/feed.h" +#include "services/newsblur/newsblurserviceroot.h" + +struct ApiResult { + bool m_authenticated; + int m_code; + QStringList m_errors; + + void decodeBaseResponse(const QJsonDocument& doc); +}; + +struct LoginResult : ApiResult { + QString m_sessiodId; + int m_userId; +}; + +class NewsBlurNetwork : public QObject { + Q_OBJECT + + public: + enum class Operations { + Login + }; + + explicit NewsBlurNetwork(QObject* parent = nullptr); + + void clearCredentials(); + + QString username() const; + void setUsername(const QString& username); + + QString password() const; + void setPassword(const QString& password); + + QString baseUrl() const; + void setBaseUrl(const QString& base_url); + + int batchSize() const; + void setBatchSize(int batch_size); + + bool downloadOnlyUnreadMessages() const; + void setDownloadOnlyUnreadMessages(bool download_only_unread); + + void setRoot(NewsBlurServiceRoot* root); + + // API methods. + LoginResult login(const QNetworkProxy& proxy); + + private: + QPair authHeader() const; + + // Make sure we are logged in and if we are not, throw exception. + void ensureLogin(const QNetworkProxy& proxy); + + QString sanitizedBaseUrl() const; + QString generateFullUrl(Operations operation) const; + + private: + NewsBlurServiceRoot* m_root; + QString m_username; + QString m_password; + QString m_baseUrl; + int m_batchSize; + bool m_downloadOnlyUnreadMessages; + QString m_authSid; + int m_userId; +}; + +#endif // NEWSBLURNETWORK_H diff --git a/src/librssguard/services/newsblur/newsblurserviceroot.cpp b/src/librssguard/services/newsblur/newsblurserviceroot.cpp new file mode 100755 index 000000000..0d4c1712f --- /dev/null +++ b/src/librssguard/services/newsblur/newsblurserviceroot.cpp @@ -0,0 +1,145 @@ +// For license of this file, see /LICENSE.md. + +#include "services/newsblur/newsblurserviceroot.h" + +#include "database/databasequeries.h" +#include "definitions/definitions.h" +#include "exceptions/feedfetchexception.h" +#include "miscellaneous/application.h" +#include "miscellaneous/iconfactory.h" +#include "miscellaneous/mutex.h" +#include "miscellaneous/textfactory.h" +#include "network-web/oauth2service.h" +#include "services/abstract/importantnode.h" +#include "services/abstract/recyclebin.h" +#include "services/newsblur/definitions.h" +#include "services/newsblur/gui/formeditnewsbluraccount.h" +#include "services/newsblur/newsblurentrypoint.h" +#include "services/newsblur/newsblurnetwork.h" + +NewsBlurServiceRoot::NewsBlurServiceRoot(RootItem* parent) + : ServiceRoot(parent), m_network(new NewsBlurNetwork(this)) { + m_network->setRoot(this); + setIcon(NewsBlurEntryPoint().icon()); +} + +bool NewsBlurServiceRoot::isSyncable() const { + return true; +} + +bool NewsBlurServiceRoot::canBeEdited() const { + return true; +} + +bool NewsBlurServiceRoot::editViaGui() { + FormEditNewsBlurAccount form_pointer(qApp->mainFormWidget()); + + form_pointer.addEditAccount(this); + return true; +} + +QVariantHash NewsBlurServiceRoot::customDatabaseData() const { + QVariantHash data; + + data[QSL("username")] = m_network->username(); + data[QSL("password")] = TextFactory::encrypt(m_network->password()); + data[QSL("url")] = m_network->baseUrl(); + + return data; +} + +void NewsBlurServiceRoot::setCustomDatabaseData(const QVariantHash& data) { + m_network->setUsername(data[QSL("username")].toString()); + m_network->setPassword(TextFactory::decrypt(data[QSL("password")].toString())); + m_network->setBaseUrl(data[QSL("url")].toString()); +} + +QList NewsBlurServiceRoot::obtainNewMessages(Feed* feed, + const QHash& stated_messages, + const QHash& tagged_messages) { + Feed::Status error = Feed::Status::Normal; + QList msgs; + + // TODO:: + + if (error != Feed::Status::NewMessages && error != Feed::Status::Normal) { + throw FeedFetchException(error); + } + else { + return msgs; + } +} + +void NewsBlurServiceRoot::start(bool freshly_activated) { + if (!freshly_activated) { + DatabaseQueries::loadFromDatabase(this); + loadCacheFromFile(); + } + + updateTitleIcon(); + + if (getSubTreeFeeds().isEmpty()) { + syncIn(); + } +} + +QString NewsBlurServiceRoot::code() const { + return NewsBlurEntryPoint().code(); +} + +void NewsBlurServiceRoot::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()) { + if (network()->markMessagesRead(key, ids, networkProxy()) != QNetworkReply::NetworkError::NoError && + !ignore_errors) { + addMessageStatesToCache(ids, key); + } + } + } + + QMapIterator> j(msg_cache.m_cachedStatesImportant); + + // Save the actual data important/not important. + while (j.hasNext()) { + j.next(); + auto key = j.key(); + QList messages = j.value(); + + if (!messages.isEmpty()) { + QStringList custom_ids; custom_ids.reserve(messages.size()); + + for (const Message& msg : messages) { + custom_ids.append(msg.m_customId); + } + + if (network()->markMessagesStarred(key, custom_ids, networkProxy()) != QNetworkReply::NetworkError::NoError && + !ignore_errors) { + addMessageStatesToCache(messages, key); + } + } + } + */ +} + +ServiceRoot::LabelOperation NewsBlurServiceRoot::supportedLabelOperations() const { + return ServiceRoot::LabelOperation::Synchronised; +} + +void NewsBlurServiceRoot::updateTitleIcon() { + setTitle(QSL("%1 (%2)").arg(m_network->username(), NewsBlurEntryPoint().name())); +} + +RootItem* NewsBlurServiceRoot::obtainNewTreeForSyncIn() const { + return nullptr; + + //return m_network->categoriesFeedsLabelsTree(true, networkProxy()); +} diff --git a/src/librssguard/services/newsblur/newsblurserviceroot.h b/src/librssguard/services/newsblur/newsblurserviceroot.h new file mode 100755 index 000000000..60ec2fc46 --- /dev/null +++ b/src/librssguard/services/newsblur/newsblurserviceroot.h @@ -0,0 +1,46 @@ +// For license of this file, see /LICENSE.md. + +#ifndef NEWSBLURSERVICEROOT_H +#define NEWSBLURSERVICEROOT_H + +#include "services/abstract/cacheforserviceroot.h" +#include "services/abstract/serviceroot.h" + +class NewsBlurNetwork; + +class NewsBlurServiceRoot : public ServiceRoot, public CacheForServiceRoot { + Q_OBJECT + + public: + explicit NewsBlurServiceRoot(RootItem* parent = nullptr); + + virtual bool isSyncable() const; + virtual bool canBeEdited() const; + virtual bool editViaGui(); + virtual void start(bool freshly_activated); + virtual QString code() const; + virtual void saveAllCachedData(bool ignore_errors); + virtual LabelOperation supportedLabelOperations() const; + virtual QVariantHash customDatabaseData() const; + virtual void setCustomDatabaseData(const QVariantHash& data); + virtual QList obtainNewMessages(Feed* feed, + const QHash& stated_messages, + const QHash& tagged_messages); + + NewsBlurNetwork* network() const; + + protected: + virtual RootItem* obtainNewTreeForSyncIn() const; + + private: + void updateTitleIcon(); + + private: + NewsBlurNetwork* m_network; +}; + +inline NewsBlurNetwork* NewsBlurServiceRoot::network() const { + return m_network; +} + +#endif // NEWSBLURSERVICEROOT_H