This commit is contained in:
Martin Rotter 2024-03-20 09:30:35 +01:00 committed by martinrotter
parent 4318929bb0
commit d0670c22fa
42 changed files with 5028 additions and 1 deletions

View File

@ -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()

View File

@ -0,0 +1,5 @@
{
"name": "Nextcloud News",
"author": "Martin Rotter",
"website": "https://github.com/martinrotter/rssguard"
}

View File

@ -0,0 +1,13 @@
// For license of this file, see <project-root-folder>/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

View File

@ -0,0 +1,63 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/gui/formeditnextcloudaccount.h"
#include "src/gui/nextcloudaccountdetails.h"
#include "src/nextcloudnetworkfactory.h"
#include "src/nextcloudserviceroot.h"
#include <librssguard/miscellaneous/iconfactory.h>
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<NextcloudServiceRoot>()->network()->authUsername() ||
m_details->m_ui.m_txtUrl->lineEdit()->text() != account<NextcloudServiceRoot>()->network()->url();
account<NextcloudServiceRoot>()->network()->setUrl(m_details->m_ui.m_txtUrl->lineEdit()->text());
account<NextcloudServiceRoot>()->network()->setAuthUsername(m_details->m_ui.m_txtUsername->lineEdit()->text());
account<NextcloudServiceRoot>()->network()->setAuthPassword(m_details->m_ui.m_txtPassword->lineEdit()->text());
account<NextcloudServiceRoot>()->network()->setForceServerSideUpdate(m_details->m_ui.m_checkServerSideUpdate
->isChecked());
account<NextcloudServiceRoot>()->network()->setBatchSize(m_details->m_ui.m_spinLimitMessages->value());
account<NextcloudServiceRoot>()
->network()
->setDownloadOnlyUnreadMessages(m_details->m_ui.m_checkDownloadOnlyUnreadMessages->isChecked());
account<NextcloudServiceRoot>()->saveAccountDataToDatabase();
accept();
if (!m_creatingNew && using_another_acc) {
account<NextcloudServiceRoot>()->completelyRemoveAllData();
account<NextcloudServiceRoot>()->start(true);
}
}
void FormEditNextcloudAccount::loadAccountData() {
FormAccountDetails::loadAccountData();
NextcloudServiceRoot* existing_root = account<NextcloudServiceRoot>();
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());
}

View File

@ -0,0 +1,30 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef FORMEDITNEXTCLOUDACCOUNT_H
#define FORMEDITNEXTCLOUDACCOUNT_H
#include <librssguard/services/abstract/gui/formaccountdetails.h>
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

View File

@ -0,0 +1,129 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/gui/nextcloudaccountdetails.h"
#include "src/definitions.h"
#include "src/nextcloudnetworkfactory.h"
#include <librssguard/definitions/definitions.h>
#include <librssguard/miscellaneous/systemfactory.h>
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<void (QSpinBox::*)(int)>(&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."));
}
}

View File

@ -0,0 +1,29 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef NEXTCLOUDACCOUNTDETAILS_H
#define NEXTCLOUDACCOUNTDETAILS_H
#include "ui_nextcloudaccountdetails.h"
#include <QNetworkProxy>
#include <QWidget>
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

View File

@ -0,0 +1,201 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NextcloudAccountDetails</class>
<widget class="QWidget" name="NextcloudAccountDetails">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>433</width>
<height>363</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="m_lblTitle">
<property name="text">
<string>URL</string>
</property>
<property name="buddy">
<cstring>m_txtUrl</cstring>
</property>
</widget>
</item>
<item>
<widget class="LineEditWithStatus" name="m_txtUrl" native="true"/>
</item>
</layout>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="m_checkDownloadOnlyUnreadMessages">
<property name="text">
<string>Download unread articles only</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Only download newest X articles per feed</string>
</property>
<property name="buddy">
<cstring>m_spinLimitMessages</cstring>
</property>
</widget>
</item>
<item>
<widget class="MessageCountSpinBox" name="m_spinLimitMessages">
<property name="maximumSize">
<size>
<width>140</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="4" column="0" colspan="2">
<widget class="HelpSpoiler" name="m_lblServerSideUpdateInformation" native="true"/>
</item>
<item row="6" column="0" colspan="2">
<widget class="QGroupBox" name="m_gbAuthentication">
<property name="toolTip">
<string>Some feeds require authentication, including GMail feeds. BASIC, NTLM-2 and DIGEST-MD5 authentication schemes are supported.</string>
</property>
<property name="title">
<string>Authentication</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Username</string>
</property>
<property name="buddy">
<cstring>m_txtUsername</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="LineEditWithStatus" name="m_txtUsername" native="true"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Password</string>
</property>
<property name="buddy">
<cstring>m_txtPassword</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="LineEditWithStatus" name="m_txtPassword" native="true"/>
</item>
</layout>
</widget>
</item>
<item row="7" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="m_btnTestSetup">
<property name="text">
<string>&amp;Test setup</string>
</property>
</widget>
</item>
<item>
<widget class="LabelWithStatus" name="m_lblTestResult" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="8" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>409</width>
<height>35</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="m_checkServerSideUpdate">
<property name="text">
<string>Force execution of server-side feeds update</string>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LineEditWithStatus</class>
<extends>QWidget</extends>
<header>lineeditwithstatus.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>LabelWithStatus</class>
<extends>QWidget</extends>
<header>labelwithstatus.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>MessageCountSpinBox</class>
<extends>QSpinBox</extends>
<header>messagecountspinbox.h</header>
</customwidget>
<customwidget>
<class>HelpSpoiler</class>
<extends>QWidget</extends>
<header>helpspoiler.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>m_checkDownloadOnlyUnreadMessages</tabstop>
<tabstop>m_checkServerSideUpdate</tabstop>
<tabstop>m_spinLimitMessages</tabstop>
<tabstop>m_btnTestSetup</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,36 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/nextcloudfeed.h"
#include "src/nextcloudnetworkfactory.h"
#include "src/nextcloudserviceroot.h"
#include <librssguard/database/databasequeries.h>
#include <QPointer>
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<NextcloudServiceRoot*>(getParentServiceRoot());
}

View File

@ -0,0 +1,24 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef NEXTCLOUDFEED_H
#define NEXTCLOUDFEED_H
#include <librssguard/services/abstract/feed.h>
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

View File

@ -0,0 +1,660 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/nextcloudnetworkfactory.h"
#include "src/definitions.h"
#include "src/nextcloudfeed.h"
#include <librssguard/miscellaneous/application.h>
#include <librssguard/miscellaneous/settings.h>
#include <librssguard/miscellaneous/textfactory.h>
#include <librssguard/network-web/networkfactory.h>
#include <librssguard/services/abstract/category.h>
#include <librssguard/services/abstract/rootitem.h>
#include <utility>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPixmap>
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<QPair<QByteArray, QByteArray>> headers;
headers << QPair<QByteArray, QByteArray>(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<QPair<QByteArray, QByteArray>> headers;
headers << QPair<QByteArray, QByteArray>(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<QPair<QByteArray, QByteArray>> headers;
headers << QPair<QByteArray, QByteArray>(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<QPair<QByteArray, QByteArray>> headers;
headers << QPair<QByteArray, QByteArray>(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<QPair<QByteArray, QByteArray>> headers;
headers << QPair<QByteArray, QByteArray>(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<QPair<QByteArray, QByteArray>> headers;
headers << QPair<QByteArray, QByteArray>(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<QPair<QByteArray, QByteArray>> headers;
headers << QPair<QByteArray, QByteArray>(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<QPair<QByteArray, QByteArray>> headers;
headers << QPair<QByteArray, QByteArray>(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<QPair<QByteArray, QByteArray>> headers;
headers << QPair<QByteArray, QByteArray>(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<QString, RootItem*> 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<Message> NextcloudGetMessagesResponse::messages() const {
QList<Message> 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;
}

View File

@ -0,0 +1,138 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef NEXTCLOUDNETWORKFACTORY_H
#define NEXTCLOUDNETWORKFACTORY_H
#include <librssguard/core/message.h>
#include <librssguard/network-web/networkfactory.h>
#include <librssguard/services/abstract/rootitem.h>
#include <QDateTime>
#include <QIcon>
#include <QJsonObject>
#include <QNetworkReply>
#include <QString>
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<Message> 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

View File

@ -0,0 +1,52 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/nextcloudserviceentrypoint.h"
#include "src/definitions.h"
#include "src/gui/formeditnextcloudaccount.h"
#include "src/nextcloudserviceroot.h"
#include <librssguard/database/databasequeries.h>
#include <librssguard/definitions/definitions.h>
#include <librssguard/miscellaneous/application.h>
#include <librssguard/miscellaneous/iconfactory.h>
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<NextcloudServiceRoot>();
}
QList<ServiceRoot*> NextcloudServiceEntryPoint::initializeSubtree() const {
QSqlDatabase database = qApp->database()->driver()->connection(QSL("NextcloudServiceEntryPoint"));
return DatabaseQueries::getAccounts<NextcloudServiceRoot>(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"));
}

View File

@ -0,0 +1,26 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef NEXTCLOUDSERVICEENTRYPOINT_H
#define NEXTCLOUDSERVICEENTRYPOINT_H
#include <librssguard/services/abstract/serviceentrypoint.h>
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<ServiceRoot*> 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

View File

@ -0,0 +1,177 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/nextcloudserviceroot.h"
#include "src/gui/formeditnextcloudaccount.h"
#include "src/nextcloudfeed.h"
#include "src/nextcloudnetworkfactory.h"
#include "src/nextcloudserviceentrypoint.h"
#include <librssguard/database/databasequeries.h>
#include <librssguard/definitions/definitions.h>
#include <librssguard/exceptions/feedfetchexception.h>
#include <librssguard/exceptions/networkexception.h>
#include <librssguard/miscellaneous/application.h>
#include <librssguard/miscellaneous/textfactory.h>
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<RootItem*>& items) {
if (items.first()->kind() == RootItem::Kind::ServiceRoot) {
QScopedPointer<FormEditNextcloudAccount> p(qobject_cast<FormEditNextcloudAccount*>(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<Category, NextcloudFeed>(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<RootItem::ReadStatus, QStringList> 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<RootItem::Importance, QList<Message>> j(msg_cache.m_cachedStatesImportant);
// Save the actual data important/not important.
while (j.hasNext()) {
j.next();
auto key = j.key();
QList<Message> 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<Message> NextcloudServiceRoot::obtainNewMessages(Feed* feed,
const QHash<ServiceRoot::BagOfMessages, QStringList>&
stated_messages,
const QHash<QString, QStringList>& 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();
}
}

View File

@ -0,0 +1,48 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef NEXTCLOUDSERVICEROOT_H
#define NEXTCLOUDSERVICEROOT_H
#include <librssguard/services/abstract/cacheforserviceroot.h>
#include <librssguard/services/abstract/serviceroot.h>
#include <QMap>
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<RootItem*>& 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<Message> obtainNewMessages(Feed* feed,
const QHash<ServiceRoot::BagOfMessages, QStringList>& stated_messages,
const QHash<QString, QStringList>& tagged_messages);
NextcloudNetworkFactory* network() const;
protected:
virtual RootItem* obtainNewTreeForSyncIn() const;
private:
void updateTitle();
private:
NextcloudNetworkFactory* m_network;
};
#endif // NEXTCLOUDSERVICEROOT_H

View File

@ -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()

View File

@ -0,0 +1,5 @@
{
"name": "Tiny Tiny RSS",
"author": "Martin Rotter",
"website": "https://github.com/martinrotter/rssguard"
}

View File

@ -0,0 +1,49 @@
// For license of this file, see <project-root-folder>/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

View File

@ -0,0 +1,70 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/gui/formeditttrssaccount.h"
#include "src/gui/ttrssaccountdetails.h"
#include "src/ttrssnetworkfactory.h"
#include "src/ttrssserviceroot.h"
#include <librssguard/miscellaneous/iconfactory.h>
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<TtRssServiceRoot>()->network()->username() ||
m_details->m_ui.m_txtUrl->lineEdit()->text() != account<TtRssServiceRoot>()->network()->url();
account<TtRssServiceRoot>()->network()->logout(m_account->networkProxy());
account<TtRssServiceRoot>()->network()->setUrl(m_details->m_ui.m_txtUrl->lineEdit()->text());
account<TtRssServiceRoot>()->network()->setUsername(m_details->m_ui.m_txtUsername->lineEdit()->text());
account<TtRssServiceRoot>()->network()->setPassword(m_details->m_ui.m_txtPassword->lineEdit()->text());
account<TtRssServiceRoot>()->network()->setAuthIsUsed(m_details->m_ui.m_gbHttpAuthentication->isChecked());
account<TtRssServiceRoot>()->network()->setAuthUsername(m_details->m_ui.m_txtHttpUsername->lineEdit()->text());
account<TtRssServiceRoot>()->network()->setAuthPassword(m_details->m_ui.m_txtHttpPassword->lineEdit()->text());
account<TtRssServiceRoot>()->network()->setBatchSize(m_details->m_ui.m_spinLimitMessages->value());
account<TtRssServiceRoot>()->network()->setIntelligentSynchronization(m_details->m_ui.m_cbNewAlgorithm->isChecked());
account<TtRssServiceRoot>()->network()->setForceServerSideUpdate(m_details->m_ui.m_checkServerSideUpdate
->isChecked());
account<TtRssServiceRoot>()
->network()
->setDownloadOnlyUnreadMessages(m_details->m_ui.m_checkDownloadOnlyUnreadMessages->isChecked());
account<TtRssServiceRoot>()->saveAccountDataToDatabase();
accept();
if (!m_creatingNew && using_another_acc) {
account<TtRssServiceRoot>()->completelyRemoveAllData();
account<TtRssServiceRoot>()->start(true);
}
}
void FormEditTtRssAccount::loadAccountData() {
FormAccountDetails::loadAccountData();
TtRssServiceRoot* existing_root = account<TtRssServiceRoot>();
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());
}

View File

@ -0,0 +1,31 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef FORMEDITACCOUNT_H
#define FORMEDITACCOUNT_H
#include <librssguard/services/abstract/gui/formaccountdetails.h>
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

View File

@ -0,0 +1,74 @@
// For license of this file, see <project-root-folder>/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 <librssguard/exceptions/applicationexception.h>
#include <librssguard/miscellaneous/application.h>
#include <librssguard/services/abstract/gui/authenticationdetails.h>
#include <QMimeData>
#include <QTimer>
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<RootItem*>();
auto* root = qobject_cast<TtRssServiceRoot*>(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();
}
}

View File

@ -0,0 +1,32 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef FORMTTRSSFEEDDETAILS_H
#define FORMTTRSSFEEDDETAILS_H
#include <librssguard/services/abstract/gui/formfeeddetails.h>
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

View File

@ -0,0 +1,79 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/gui/formttrssnote.h"
#include "src/definitions.h"
#include "src/ttrssnetworkfactory.h"
#include "src/ttrssnotetopublish.h"
#include "src/ttrssserviceroot.h"
#include <librssguard/gui/guiutilities.h>
#include <librssguard/gui/messagebox.h>
#include <librssguard/miscellaneous/application.h>
#include <librssguard/miscellaneous/iconfactory.h>
#include <QPushButton>
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);
}

View File

@ -0,0 +1,33 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef FORMTTRSSNOTE_H
#define FORMTTRSSNOTE_H
#include "ui_formttrssnote.h"
#include <QDialog>
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

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FormTtRssNote</class>
<widget class="QDialog" name="FormTtRssNote">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>340</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Title</string>
</property>
<property name="buddy">
<cstring>m_txtTitle</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="LineEditWithStatus" name="m_txtTitle" native="true"/>
</item>
<item row="1" column="1">
<widget class="LineEditWithStatus" name="m_txtUrl" native="true"/>
</item>
<item row="3" column="0" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="0" colspan="2">
<widget class="QDialogButtonBox" name="m_btnBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>URL</string>
</property>
<property name="buddy">
<cstring>m_txtUrl</cstring>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Content</string>
</property>
<property name="buddy">
<cstring>m_txtContent</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPlainTextEdit" name="m_txtContent"/>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LineEditWithStatus</class>
<extends>QWidget</extends>
<header>lineeditwithstatus.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>m_btnBox</sender>
<signal>rejected()</signal>
<receiver>FormTtRssNote</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>295</x>
<y>327</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,189 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/gui/ttrssaccountdetails.h"
#include "src/definitions.h"
#include "src/ttrssnetworkfactory.h"
#include <librssguard/definitions/definitions.h>
#include <librssguard/network-web/networkfactory.h>
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.<br/><br/>"
"Also, make sure to install <a href=\"https://www.google.com\">api_newsplus</a> 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."));
}
}

View File

@ -0,0 +1,34 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef TTRSSACCOUNTDETAILS_H
#define TTRSSACCOUNTDETAILS_H
#include "ui_ttrssaccountdetails.h"
#include <QNetworkProxy>
#include <QWidget>
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

View File

@ -0,0 +1,252 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TtRssAccountDetails</class>
<widget class="QWidget" name="TtRssAccountDetails">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>432</width>
<height>396</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="10" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>408</width>
<height>30</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="m_lblTitle">
<property name="text">
<string>URL</string>
</property>
<property name="buddy">
<cstring>m_txtUrl</cstring>
</property>
</widget>
</item>
<item>
<widget class="LineEditWithStatus" name="m_txtUrl" native="true"/>
</item>
</layout>
</item>
<item row="2" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Only download newest X articles per feed</string>
</property>
<property name="buddy">
<cstring>m_spinLimitMessages</cstring>
</property>
</widget>
</item>
<item>
<widget class="MessageCountSpinBox" name="m_spinLimitMessages">
<property name="maximumSize">
<size>
<width>140</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="m_checkDownloadOnlyUnreadMessages">
<property name="text">
<string>Download unread articles only</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="m_cbNewAlgorithm">
<property name="text">
<string>Intelligent synchronization algorithm</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="m_checkServerSideUpdate">
<property name="text">
<string>Force execution of server-side feeds update</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QGroupBox" name="m_gbAuthentication">
<property name="toolTip">
<string>Some feeds require authentication, including GMail feeds. BASIC, NTLM-2 and DIGEST-MD5 authentication schemes are supported.</string>
</property>
<property name="title">
<string>Authentication</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Username</string>
</property>
<property name="buddy">
<cstring>m_txtUsername</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="LineEditWithStatus" name="m_txtUsername" native="true"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Password</string>
</property>
<property name="buddy">
<cstring>m_txtPassword</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="LineEditWithStatus" name="m_txtPassword" native="true"/>
</item>
</layout>
</widget>
</item>
<item row="8" column="0" colspan="2">
<widget class="QGroupBox" name="m_gbHttpAuthentication">
<property name="toolTip">
<string>Some feeds require authentication, including GMail feeds. BASIC, NTLM-2 and DIGEST-MD5 authentication schemes are supported.</string>
</property>
<property name="title">
<string>Requires HTTP authentication</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
<layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Username</string>
</property>
<property name="buddy">
<cstring>m_txtHttpUsername</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="LineEditWithStatus" name="m_txtHttpUsername" native="true"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Password</string>
</property>
<property name="buddy">
<cstring>m_txtHttpPassword</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="LineEditWithStatus" name="m_txtHttpPassword" native="true"/>
</item>
</layout>
</widget>
</item>
<item row="9" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="m_btnTestSetup">
<property name="text">
<string>&amp;Test setup</string>
</property>
</widget>
</item>
<item>
<widget class="LabelWithStatus" name="m_lblTestResult" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0" colspan="2">
<widget class="HelpSpoiler" name="m_lblNewAlgorithm" native="true"/>
</item>
<item row="6" column="0" colspan="2">
<widget class="HelpSpoiler" name="m_lblServerSideUpdateInformation" native="true"/>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LabelWithStatus</class>
<extends>QWidget</extends>
<header>labelwithstatus.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>LineEditWithStatus</class>
<extends>QWidget</extends>
<header>lineeditwithstatus.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>MessageCountSpinBox</class>
<extends>QSpinBox</extends>
<header>messagecountspinbox.h</header>
</customwidget>
<customwidget>
<class>HelpSpoiler</class>
<extends>QWidget</extends>
<header>helpspoiler.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,56 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/gui/ttrssfeeddetails.h"
#include <librssguard/definitions/definitions.h>
#include <librssguard/services/abstract/category.h>
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<Category*>& 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);
}
}
}
}

View File

@ -0,0 +1,31 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef TTRSSFEEDDETAILS_H
#define TTRSSFEEDDETAILS_H
#include "ui_ttrssfeeddetails.h"
#include <QWidget>
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<Category*>& categories, RootItem* root_item, RootItem* parent_to_select = nullptr);
private:
Ui::TtRssFeedDetails ui;
};
#endif // TTRSSFEEDDETAILS_H

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TtRssFeedDetails</class>
<widget class="QWidget" name="TtRssFeedDetails">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>367</width>
<height>202</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="m_lblParentCategory">
<property name="text">
<string>Parent folder</string>
</property>
<property name="buddy">
<cstring>m_cmbParentCategory</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="m_cmbParentCategory">
<property name="toolTip">
<string>Select parent item for your feed.</string>
</property>
<property name="iconSize">
<size>
<width>12</width>
<height>12</height>
</size>
</property>
<property name="frame">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>URL</string>
</property>
<property name="buddy">
<cstring>m_txtUrl</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="LineEditWithStatus" name="m_txtUrl" native="true"/>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LineEditWithStatus</class>
<extends>QWidget</extends>
<header>lineeditwithstatus.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,62 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/ttrssfeed.h"
#include "src/definitions.h"
#include "src/ttrssnetworkfactory.h"
#include "src/ttrssserviceroot.h"
#include <librssguard/database/databasequeries.h>
#include <librssguard/definitions/definitions.h>
#include <librssguard/miscellaneous/application.h>
#include <librssguard/miscellaneous/iconfactory.h>
#include <QPointer>
TtRssFeed::TtRssFeed(RootItem* parent) : Feed(parent), m_actionShareToPublished(nullptr) {}
TtRssServiceRoot* TtRssFeed::serviceRoot() const {
return qobject_cast<TtRssServiceRoot*>(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<QAction*> 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());
}

View File

@ -0,0 +1,28 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef TTRSSFEED_H
#define TTRSSFEED_H
#include <librssguard/services/abstract/feed.h>
class TtRssServiceRoot;
class TtRssFeed : public Feed {
Q_OBJECT
public:
explicit TtRssFeed(RootItem* parent = nullptr);
virtual bool canBeDeleted() const;
virtual bool deleteItem();
virtual QList<QAction*> contextMenuFeedsList();
private:
TtRssServiceRoot* serviceRoot() const;
bool removeItself();
private:
QAction* m_actionShareToPublished;
};
#endif // TTRSSFEED_H

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,247 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef TTRSSNETWORKFACTORY_H
#define TTRSSNETWORKFACTORY_H
#include "src/ttrssnotetopublish.h"
#include <librssguard/core/message.h>
#include <QJsonObject>
#include <QNetworkReply>
#include <QPair>
#include <QString>
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<RootItem*> 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<Message> messages(ServiceRoot* root) const;
};
class TtRssGetArticleResponse : public TtRssResponse {
public:
explicit TtRssGetArticleResponse(const QString& raw_content = QString());
virtual ~TtRssGetArticleResponse();
QList<Message> 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

View File

@ -0,0 +1,15 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef TTRSSNOTETOPUBLISH_H
#define TTRSSNOTETOPUBLISH_H
#include <QString>
struct TtRssNoteToPublish {
public:
QString m_title;
QString m_url;
QString m_content;
};
#endif // TTRSSNOTETOPUBLISH_H

View File

@ -0,0 +1,54 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/ttrssserviceentrypoint.h"
#include "src/definitions.h"
#include "src/gui/formeditttrssaccount.h"
#include "src/ttrssserviceroot.h"
#include <librssguard/database/databasequeries.h>
#include <librssguard/definitions/definitions.h>
#include <librssguard/miscellaneous/iconfactory.h>
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<TtRssServiceRoot>();
}
QList<ServiceRoot*> TtRssServiceEntryPoint::initializeSubtree() const {
// Check DB if standard account is enabled.
QSqlDatabase database = qApp->database()->driver()->connection(QSL("TtRssServiceEntryPoint"));
return DatabaseQueries::getAccounts<TtRssServiceRoot>(database, code());
}

View File

@ -0,0 +1,26 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef TTRSSSERVICEENTRYPOINT_H
#define TTRSSSERVICEENTRYPOINT_H
#include <librssguard/services/abstract/serviceentrypoint.h>
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<ServiceRoot*> initializeSubtree() const;
};
#endif // TTRSSSERVICEENTRYPOINT_H

View File

@ -0,0 +1,423 @@
// For license of this file, see <project-root-folder>/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 <librssguard/3rd-party/boolinq/boolinq.h>
#include <librssguard/database/databasequeries.h>
#include <librssguard/exceptions/feedfetchexception.h>
#include <librssguard/exceptions/networkexception.h>
#include <librssguard/miscellaneous/application.h>
#include <librssguard/miscellaneous/mutex.h>
#include <librssguard/miscellaneous/textfactory.h>
#include <librssguard/network-web/networkfactory.h>
#include <librssguard/services/abstract/labelsnode.h>
#include <QPair>
#include <QSqlTableModel>
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<Category, TtRssFeed>(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<RootItem*>& items) {
if (items.first()->kind() == RootItem::Kind::ServiceRoot) {
QScopedPointer<FormEditTtRssAccount> p(qobject_cast<FormEditTtRssAccount*>(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<FormTtRssFeedDetails> form_pointer(new FormTtRssFeedDetails(this,
selected_item,
url,
qApp->mainFormWidget()));
form_pointer->addEditFeed<TtRssFeed>();
qApp->feedUpdateLock()->unlock();
}
bool TtRssServiceRoot::canBeEdited() const {
return true;
}
void TtRssServiceRoot::saveAllCachedData(bool ignore_errors) {
auto msg_cache = takeMessageCache();
QMapIterator<RootItem::ReadStatus, QStringList> 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<RootItem::Importance, QList<Message>> j(msg_cache.m_cachedStatesImportant);
// Save the actual data important/not important.
while (j.hasNext()) {
j.next();
auto key = j.key();
QList<Message> 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<QString, QStringList> 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<QString, QStringList> 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<Message> TtRssServiceRoot::obtainNewMessages(Feed* feed,
const QHash<ServiceRoot::BagOfMessages, QStringList>&
stated_messages,
const QHash<QString, QStringList>& tagged_messages) {
Q_UNUSED(tagged_messages)
if (m_network->intelligentSynchronization()) {
return obtainMessagesIntelligently(feed, stated_messages);
}
else {
return obtainMessagesViaHeadlines(feed);
}
}
QList<Message> TtRssServiceRoot::obtainMessagesIntelligently(Feed* feed,
const QHash<BagOfMessages, QStringList>& 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<QString> remote_all_ids = FROM_LIST_TO_SET(QSet<QString>, remote_all_ids_list);
// 1.
auto local_unread_ids_list = stated_messages.value(ServiceRoot::BagOfMessages::Unread);
const QSet<QString> remote_unread_ids = FROM_LIST_TO_SET(QSet<QString>, remote_unread_ids_list);
const QSet<QString> local_unread_ids = FROM_LIST_TO_SET(QSet<QString>, local_unread_ids_list);
// 2.
const auto local_read_ids_list = stated_messages.value(ServiceRoot::BagOfMessages::Read);
const QSet<QString> remote_read_ids = remote_all_ids - remote_unread_ids;
const QSet<QString> local_read_ids = FROM_LIST_TO_SET(QSet<QString>, local_read_ids_list);
// 3.
const auto local_starred_ids_list = stated_messages.value(ServiceRoot::BagOfMessages::Starred);
const QSet<QString> remote_starred_ids = FROM_LIST_TO_SET(QSet<QString>, remote_starred_ids_list);
const QSet<QString> local_starred_ids = FROM_LIST_TO_SET(QSet<QString>, local_starred_ids_list);
// 4.
QSet<QString> 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<Message> TtRssServiceRoot::obtainMessagesViaHeadlines(Feed* feed) {
QList<Message> 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<Message> 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();
}

View File

@ -0,0 +1,61 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef TTRSSSERVICEROOT_H
#define TTRSSSERVICEROOT_H
#include <librssguard/services/abstract/cacheforserviceroot.h>
#include <librssguard/services/abstract/serviceroot.h>
#include <QCoreApplication>
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<RootItem*>& 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<Message> obtainNewMessages(Feed* feed,
const QHash<ServiceRoot::BagOfMessages, QStringList>& stated_messages,
const QHash<QString, QStringList>& tagged_messages);
// Access to network.
TtRssNetworkFactory* network() const;
public slots:
void shareToPublished();
protected:
virtual RootItem* obtainNewTreeForSyncIn() const;
private:
void updateTitle();
QList<Message> obtainMessagesIntelligently(Feed* feed,
const QHash<ServiceRoot::BagOfMessages, QStringList>& stated_messages);
QList<Message> obtainMessagesViaHeadlines(Feed* feed);
private:
TtRssNetworkFactory* m_network;
};
#endif // TTRSSSERVICEROOT_H

View File

@ -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