nextcloud

This commit is contained in:
Martin Rotter 2024-03-20 09:23:18 +01:00
parent b9cb1471a5
commit e7a388bb26
57 changed files with 4 additions and 6099 deletions

View File

@ -327,6 +327,8 @@ add_subdirectory(src/librssguard-standard)
add_subdirectory(src/librssguard-feedly) add_subdirectory(src/librssguard-feedly)
add_subdirectory(src/librssguard-gmail) add_subdirectory(src/librssguard-gmail)
add_subdirectory(src/librssguard-greader) add_subdirectory(src/librssguard-greader)
add_subdirectory(src/librssguard-ttrss)
add_subdirectory(src/librssguard-nextcloud)
# GUI executable. # GUI executable.
add_subdirectory(src/rssguard) add_subdirectory(src/rssguard)

View File

@ -324,54 +324,6 @@ set(SOURCES
services/abstract/serviceroot.h services/abstract/serviceroot.h
services/abstract/unreadnode.cpp services/abstract/unreadnode.cpp
services/abstract/unreadnode.h services/abstract/unreadnode.h
services/owncloud/definitions.h
services/owncloud/gui/formeditowncloudaccount.cpp
services/owncloud/gui/formeditowncloudaccount.h
services/owncloud/gui/owncloudaccountdetails.cpp
services/owncloud/gui/owncloudaccountdetails.h
services/owncloud/owncloudfeed.cpp
services/owncloud/owncloudfeed.h
services/owncloud/owncloudnetworkfactory.cpp
services/owncloud/owncloudnetworkfactory.h
services/owncloud/owncloudserviceentrypoint.cpp
services/owncloud/owncloudserviceentrypoint.h
services/owncloud/owncloudserviceroot.cpp
services/owncloud/owncloudserviceroot.h
services/reddit/definitions.h
services/reddit/gui/formeditredditaccount.cpp
services/reddit/gui/formeditredditaccount.h
services/reddit/gui/redditaccountdetails.cpp
services/reddit/gui/redditaccountdetails.h
services/reddit/redditcategory.cpp
services/reddit/redditcategory.h
services/reddit/redditentrypoint.cpp
services/reddit/redditentrypoint.h
services/reddit/redditnetworkfactory.cpp
services/reddit/redditnetworkfactory.h
services/reddit/redditserviceroot.cpp
services/reddit/redditserviceroot.h
services/reddit/redditsubscription.cpp
services/reddit/redditsubscription.h
services/tt-rss/definitions.h
services/tt-rss/gui/formeditttrssaccount.cpp
services/tt-rss/gui/formeditttrssaccount.h
services/tt-rss/gui/formttrssfeeddetails.cpp
services/tt-rss/gui/formttrssfeeddetails.h
services/tt-rss/gui/formttrssnote.cpp
services/tt-rss/gui/formttrssnote.h
services/tt-rss/gui/ttrssaccountdetails.cpp
services/tt-rss/gui/ttrssaccountdetails.h
services/tt-rss/gui/ttrssfeeddetails.cpp
services/tt-rss/gui/ttrssfeeddetails.h
services/tt-rss/ttrssfeed.cpp
services/tt-rss/ttrssfeed.h
services/tt-rss/ttrssnetworkfactory.cpp
services/tt-rss/ttrssnetworkfactory.h
services/tt-rss/ttrssnotetopublish.h
services/tt-rss/ttrssserviceentrypoint.cpp
services/tt-rss/ttrssserviceentrypoint.h
services/tt-rss/ttrssserviceroot.cpp
services/tt-rss/ttrssserviceroot.h
) )
set(UI_FILES set(UI_FILES
@ -415,11 +367,6 @@ set(UI_FILES
services/abstract/gui/formaddeditprobe.ui services/abstract/gui/formaddeditprobe.ui
services/abstract/gui/formcategorydetails.ui services/abstract/gui/formcategorydetails.ui
services/abstract/gui/formfeeddetails.ui services/abstract/gui/formfeeddetails.ui
services/owncloud/gui/owncloudaccountdetails.ui
services/reddit/gui/redditaccountdetails.ui
services/tt-rss/gui/formttrssnote.ui
services/tt-rss/gui/ttrssaccountdetails.ui
services/tt-rss/gui/ttrssfeeddetails.ui
) )
if(ENABLE_MEDIAPLAYER) if(ENABLE_MEDIAPLAYER)

View File

@ -8,7 +8,7 @@
#define SERVICE_CODE_STD_RSS "std-rss" #define SERVICE_CODE_STD_RSS "std-rss"
#define SERVICE_CODE_TT_RSS "tt-rss" #define SERVICE_CODE_TT_RSS "tt-rss"
#define SERVICE_CODE_OWNCLOUD "owncloud" #define SERVICE_CODE_NEXTCLOUD "nextcloud"
#define SERVICE_CODE_GREADER "greader" #define SERVICE_CODE_GREADER "greader"
#define SERVICE_CODE_FEEDLY "feedly" #define SERVICE_CODE_FEEDLY "feedly"
#define SERVICE_CODE_INOREADER "inoreader" #define SERVICE_CODE_INOREADER "inoreader"

View File

@ -15,10 +15,8 @@
#include "miscellaneous/pluginfactory.h" #include "miscellaneous/pluginfactory.h"
#include "miscellaneous/settings.h" #include "miscellaneous/settings.h"
#include "services/abstract/cacheforserviceroot.h" #include "services/abstract/cacheforserviceroot.h"
#include "services/abstract/serviceentrypoint.h"
#include "services/abstract/serviceroot.h" #include "services/abstract/serviceroot.h"
#include "services/owncloud/owncloudserviceentrypoint.h"
#include "services/reddit/redditentrypoint.h"
#include "services/tt-rss/ttrssserviceentrypoint.h"
#include <QThread> #include <QThread>
#include <QTimer> #include <QTimer>
@ -66,14 +64,6 @@ FeedReader::~FeedReader() {
QList<ServiceEntryPoint*> FeedReader::feedServices() { QList<ServiceEntryPoint*> FeedReader::feedServices() {
if (m_feedServices.isEmpty()) { if (m_feedServices.isEmpty()) {
m_feedServices.append(new OwnCloudServiceEntryPoint());
#if !defined(NDEBUG)
m_feedServices.append(new RedditEntryPoint());
#endif
m_feedServices.append(new TtRssServiceEntryPoint());
PluginFactory plugin_loader; PluginFactory plugin_loader;
// Add dynamically loaded plugins. // Add dynamically loaded plugins.

View File

@ -1,13 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef OWNCLOUD_DEFINITIONS_H
#define OWNCLOUD_DEFINITIONS_H
#define OWNCLOUD_CONTENT_TYPE_JSON "application/json; charset=utf-8"
#define OWNCLOUD_API_VERSION "1.2"
#define OWNCLOUD_API_PATH "index.php/apps/news/api/v1-2/"
#define OWNCLOUD_MIN_VERSION "6.0.5"
#define OWNCLOUD_UNLIMITED_BATCH_SIZE -1
#define OWNCLOUD_DEFAULT_BATCH_SIZE 100
#endif // OWNCLOUD_DEFINITIONS_H

View File

@ -1,61 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/owncloud/gui/formeditowncloudaccount.h"
#include "miscellaneous/iconfactory.h"
#include "services/owncloud/gui/owncloudaccountdetails.h"
#include "services/owncloud/owncloudnetworkfactory.h"
#include "services/owncloud/owncloudserviceroot.h"
FormEditOwnCloudAccount::FormEditOwnCloudAccount(QWidget* parent)
: FormAccountDetails(qApp->icons()->miscIcon(QSL("nextcloud")), parent), m_details(new OwnCloudAccountDetails(this)) {
insertCustomTab(m_details, tr("Server setup"), 0);
activateTab(0);
connect(m_details->m_ui.m_btnTestSetup, &QPushButton::clicked, this, &FormEditOwnCloudAccount::performTest);
m_details->m_ui.m_txtUrl->setFocus();
}
void FormEditOwnCloudAccount::apply() {
FormAccountDetails::apply();
bool using_another_acc =
m_details->m_ui.m_txtUsername->lineEdit()->text() != account<OwnCloudServiceRoot>()->network()->authUsername() ||
m_details->m_ui.m_txtUrl->lineEdit()->text() != account<OwnCloudServiceRoot>()->network()->url();
account<OwnCloudServiceRoot>()->network()->setUrl(m_details->m_ui.m_txtUrl->lineEdit()->text());
account<OwnCloudServiceRoot>()->network()->setAuthUsername(m_details->m_ui.m_txtUsername->lineEdit()->text());
account<OwnCloudServiceRoot>()->network()->setAuthPassword(m_details->m_ui.m_txtPassword->lineEdit()->text());
account<OwnCloudServiceRoot>()->network()->setForceServerSideUpdate(m_details->m_ui.m_checkServerSideUpdate
->isChecked());
account<OwnCloudServiceRoot>()->network()->setBatchSize(m_details->m_ui.m_spinLimitMessages->value());
account<OwnCloudServiceRoot>()
->network()
->setDownloadOnlyUnreadMessages(m_details->m_ui.m_checkDownloadOnlyUnreadMessages->isChecked());
account<OwnCloudServiceRoot>()->saveAccountDataToDatabase();
accept();
if (!m_creatingNew && using_another_acc) {
account<OwnCloudServiceRoot>()->completelyRemoveAllData();
account<OwnCloudServiceRoot>()->start(true);
}
}
void FormEditOwnCloudAccount::loadAccountData() {
FormAccountDetails::loadAccountData();
OwnCloudServiceRoot* existing_root = account<OwnCloudServiceRoot>();
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 FormEditOwnCloudAccount::performTest() {
m_details->performTest(m_proxyDetails->proxy());
}

View File

@ -1,30 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef FORMEDITOWNCLOUDACCOUNT_H
#define FORMEDITOWNCLOUDACCOUNT_H
#include "services/abstract/gui/formaccountdetails.h"
class OwnCloudAccountDetails;
class OwnCloudServiceRoot;
class FormEditOwnCloudAccount : public FormAccountDetails {
Q_OBJECT
public:
explicit FormEditOwnCloudAccount(QWidget* parent = nullptr);
protected slots:
virtual void apply();
protected:
virtual void loadAccountData();
private slots:
void performTest();
private:
OwnCloudAccountDetails* m_details;
};
#endif // FORMEDITOWNCLOUDACCOUNT_H

View File

@ -1,122 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/owncloud/gui/owncloudaccountdetails.h"
#include "definitions/definitions.h"
#include "miscellaneous/systemfactory.h"
#include "services/owncloud/definitions.h"
#include "services/owncloud/owncloudnetworkfactory.h"
OwnCloudAccountDetails::OwnCloudAccountDetails(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, &OwnCloudAccountDetails::onPasswordChanged);
connect(m_ui.m_txtUsername->lineEdit(), &BaseLineEdit::textChanged, this, &OwnCloudAccountDetails::onUsernameChanged);
connect(m_ui.m_txtUrl->lineEdit(), &BaseLineEdit::textChanged, this, &OwnCloudAccountDetails::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 OwnCloudAccountDetails::performTest(const QNetworkProxy& custom_proxy) {
OwnCloudNetworkFactory 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());
OwnCloudStatusResponse 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(OWNCLOUD_MIN_VERSION))) {
m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error,
tr("Installed version: %1, required at least: %2.")
.arg(result.version(), QSL(OWNCLOUD_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(OWNCLOUD_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 OwnCloudAccountDetails::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 OwnCloudAccountDetails::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 OwnCloudAccountDetails::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

@ -1,29 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef OWNCLOUDACCOUNTDETAILS_H
#define OWNCLOUDACCOUNTDETAILS_H
#include "ui_owncloudaccountdetails.h"
#include <QNetworkProxy>
#include <QWidget>
class OwnCloudAccountDetails : public QWidget {
Q_OBJECT
friend class FormEditOwnCloudAccount;
public:
explicit OwnCloudAccountDetails(QWidget* parent = nullptr);
private slots:
void performTest(const QNetworkProxy& custom_proxy);
void onUsernameChanged();
void onPasswordChanged();
void onUrlChanged();
private:
Ui::OwnCloudAccountDetails m_ui;
};
#endif // OWNCLOUDACCOUNTDETAILS_H

View File

@ -1,201 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OwnCloudAccountDetails</class>
<widget class="QWidget" name="OwnCloudAccountDetails">
<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

@ -1,35 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/owncloud/owncloudfeed.h"
#include "database/databasequeries.h"
#include "services/owncloud/owncloudnetworkfactory.h"
#include "services/owncloud/owncloudserviceroot.h"
#include <QPointer>
OwnCloudFeed::OwnCloudFeed(RootItem* parent) : Feed(parent) {}
bool OwnCloudFeed::canBeDeleted() const {
return true;
}
bool OwnCloudFeed::deleteItem() {
if (serviceRoot()->network()->deleteFeed(customId(), getParentServiceRoot()->networkProxy()) && removeItself()) {
serviceRoot()->requestItemRemoval(this);
return true;
}
else {
return false;
}
}
bool OwnCloudFeed::removeItself() {
QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
return DatabaseQueries::deleteFeed(database, this, serviceRoot()->accountId());
}
OwnCloudServiceRoot* OwnCloudFeed::serviceRoot() const {
return qobject_cast<OwnCloudServiceRoot*>(getParentServiceRoot());
}

View File

@ -1,24 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef OWNCLOUDFEED_H
#define OWNCLOUDFEED_H
#include "services/abstract/feed.h"
class OwnCloudServiceRoot;
class OwnCloudFeed : public Feed {
Q_OBJECT
public:
explicit OwnCloudFeed(RootItem* parent = nullptr);
virtual bool canBeDeleted() const;
virtual bool deleteItem();
private:
bool removeItself();
OwnCloudServiceRoot* serviceRoot() const;
};
#endif // OWNCLOUDFEED_H

View File

@ -1,660 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/owncloud/owncloudnetworkfactory.h"
#include "miscellaneous/application.h"
#include "miscellaneous/settings.h"
#include "miscellaneous/textfactory.h"
#include "network-web/networkfactory.h"
#include "services/abstract/category.h"
#include "services/abstract/rootitem.h"
#include "services/owncloud/definitions.h"
#include "services/owncloud/owncloudfeed.h"
#include <utility>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPixmap>
OwnCloudNetworkFactory::OwnCloudNetworkFactory()
: m_url(QString()), m_fixedUrl(QString()), m_downloadOnlyUnreadMessages(false), m_forceServerSideUpdate(false),
m_authUsername(QString()), m_authPassword(QString()), m_batchSize(OWNCLOUD_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()) {}
OwnCloudNetworkFactory::~OwnCloudNetworkFactory() = default;
QString OwnCloudNetworkFactory::url() const {
return m_url;
}
void OwnCloudNetworkFactory::setUrl(const QString& url) {
m_url = url;
if (url.endsWith('/')) {
m_fixedUrl = url;
}
else {
m_fixedUrl = url + '/';
}
// Store endpoints.
m_urlUser = m_fixedUrl + OWNCLOUD_API_PATH + "user";
m_urlStatus = m_fixedUrl + OWNCLOUD_API_PATH + "status";
m_urlFolders = m_fixedUrl + OWNCLOUD_API_PATH + "folders";
m_urlFeeds = m_fixedUrl + OWNCLOUD_API_PATH + "feeds";
m_urlMessages = m_fixedUrl + OWNCLOUD_API_PATH + "items?id=%1&batchSize=%2&type=%3&getRead=%4";
m_urlFeedsUpdate = m_fixedUrl + OWNCLOUD_API_PATH + "feeds/update?userId=%1&feedId=%2";
m_urlDeleteFeed = m_fixedUrl + OWNCLOUD_API_PATH + "feeds/%1";
m_urlRenameFeed = m_fixedUrl + OWNCLOUD_API_PATH + "feeds/%1/rename";
}
bool OwnCloudNetworkFactory::forceServerSideUpdate() const {
return m_forceServerSideUpdate;
}
void OwnCloudNetworkFactory::setForceServerSideUpdate(bool force_update) {
m_forceServerSideUpdate = force_update;
}
QString OwnCloudNetworkFactory::authUsername() const {
return m_authUsername;
}
void OwnCloudNetworkFactory::setAuthUsername(const QString& auth_username) {
m_authUsername = auth_username;
}
QString OwnCloudNetworkFactory::authPassword() const {
return m_authPassword;
}
void OwnCloudNetworkFactory::setAuthPassword(const QString& auth_password) {
m_authPassword = auth_password;
}
OwnCloudStatusResponse OwnCloudNetworkFactory::status(const QNetworkProxy& custom_proxy) {
QByteArray result_raw;
QList<QPair<QByteArray, QByteArray>> headers;
headers << QPair<QByteArray, QByteArray>(HTTP_HEADERS_CONTENT_TYPE, OWNCLOUD_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);
OwnCloudStatusResponse 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;
}
OwnCloudGetFeedsCategoriesResponse OwnCloudNetworkFactory::feedsCategories(const QNetworkProxy& custom_proxy) {
QByteArray result_raw;
QList<QPair<QByteArray, QByteArray>> headers;
headers << QPair<QByteArray, QByteArray>(HTTP_HEADERS_CONTENT_TYPE, OWNCLOUD_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 OwnCloudGetFeedsCategoriesResponse(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 OwnCloudGetFeedsCategoriesResponse(network_reply.m_networkError);
}
QString content_feeds = QString::fromUtf8(result_raw);
return OwnCloudGetFeedsCategoriesResponse(network_reply.m_networkError, content_categories, content_feeds);
}
bool OwnCloudNetworkFactory::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, OWNCLOUD_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 OwnCloudNetworkFactory::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, OWNCLOUD_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 OwnCloudNetworkFactory::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, OWNCLOUD_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;
}
}
OwnCloudGetMessagesResponse OwnCloudNetworkFactory::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, OWNCLOUD_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);
OwnCloudGetMessagesResponse 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 OwnCloudNetworkFactory::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, OWNCLOUD_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 OwnCloudNetworkFactory::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(OWNCLOUD_API_PATH) + QSL("items/read/multiple");
}
else {
final_url = m_fixedUrl + QSL(OWNCLOUD_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, OWNCLOUD_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 OwnCloudNetworkFactory::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 + OWNCLOUD_API_PATH + "items/star/multiple";
}
else {
final_url = m_fixedUrl + OWNCLOUD_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, OWNCLOUD_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 OwnCloudNetworkFactory::batchSize() const {
return m_batchSize;
}
void OwnCloudNetworkFactory::setBatchSize(int batch_size) {
m_batchSize = batch_size;
}
bool OwnCloudNetworkFactory::downloadOnlyUnreadMessages() const {
return m_downloadOnlyUnreadMessages;
}
void OwnCloudNetworkFactory::setDownloadOnlyUnreadMessages(bool dowload_only_unread_messages) {
m_downloadOnlyUnreadMessages = dowload_only_unread_messages;
}
OwnCloudResponse::OwnCloudResponse(QNetworkReply::NetworkError response, const QString& raw_content)
: m_networkError(response), m_rawContent(QJsonDocument::fromJson(raw_content.toUtf8()).object()),
m_emptyString(raw_content.isEmpty()) {}
OwnCloudResponse::~OwnCloudResponse() = default;
bool OwnCloudResponse::isLoaded() const {
return !m_emptyString && !m_rawContent.isEmpty();
}
QString OwnCloudResponse::toString() const {
return QJsonDocument(m_rawContent).toJson(QJsonDocument::JsonFormat::Compact);
}
QNetworkReply::NetworkError OwnCloudResponse::networkError() const {
return m_networkError;
}
OwnCloudStatusResponse::OwnCloudStatusResponse(QNetworkReply::NetworkError response, const QString& raw_content)
: OwnCloudResponse(response, raw_content) {}
OwnCloudStatusResponse::~OwnCloudStatusResponse() = default;
QString OwnCloudStatusResponse::version() const {
if (isLoaded()) {
return m_rawContent[QSL("version")].toString();
}
else {
return QString();
}
}
OwnCloudGetFeedsCategoriesResponse::OwnCloudGetFeedsCategoriesResponse(QNetworkReply::NetworkError response,
QString raw_categories,
QString raw_feeds)
: OwnCloudResponse(response), m_contentCategories(std::move(raw_categories)), m_contentFeeds(std::move(raw_feeds)) {}
OwnCloudGetFeedsCategoriesResponse::~OwnCloudGetFeedsCategoriesResponse() = default;
RootItem* OwnCloudGetFeedsCategoriesResponse::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 OwnCloudFeed();
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;
}
OwnCloudGetMessagesResponse::OwnCloudGetMessagesResponse(QNetworkReply::NetworkError response,
const QString& raw_content)
: OwnCloudResponse(response, raw_content) {}
OwnCloudGetMessagesResponse::~OwnCloudGetMessagesResponse() = default;
QList<Message> OwnCloudGetMessagesResponse::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

@ -1,138 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef OWNCLOUDNETWORKFACTORY_H
#define OWNCLOUDNETWORKFACTORY_H
#include "core/message.h"
#include "network-web/networkfactory.h"
#include "services/abstract/rootitem.h"
#include <QDateTime>
#include <QIcon>
#include <QJsonObject>
#include <QNetworkReply>
#include <QString>
class OwnCloudResponse {
public:
explicit OwnCloudResponse(QNetworkReply::NetworkError response, const QString& raw_content = QString());
virtual ~OwnCloudResponse();
bool isLoaded() const;
QString toString() const;
QNetworkReply::NetworkError networkError() const;
protected:
QNetworkReply::NetworkError m_networkError;
QJsonObject m_rawContent;
bool m_emptyString;
};
class OwnCloudGetMessagesResponse : public OwnCloudResponse {
public:
explicit OwnCloudGetMessagesResponse(QNetworkReply::NetworkError response, const QString& raw_content = QString());
virtual ~OwnCloudGetMessagesResponse();
QList<Message> messages() const;
};
class OwnCloudStatusResponse : public OwnCloudResponse {
public:
explicit OwnCloudStatusResponse(QNetworkReply::NetworkError response, const QString& raw_content = QString());
virtual ~OwnCloudStatusResponse();
QString version() const;
};
class RootItem;
class OwnCloudGetFeedsCategoriesResponse : public OwnCloudResponse {
public:
explicit OwnCloudGetFeedsCategoriesResponse(QNetworkReply::NetworkError response,
QString raw_categories = QString(),
QString raw_feeds = QString());
virtual ~OwnCloudGetFeedsCategoriesResponse();
// 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 OwnCloudNetworkFactory {
public:
explicit OwnCloudNetworkFactory();
virtual ~OwnCloudNetworkFactory();
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.
OwnCloudStatusResponse status(const QNetworkProxy& custom_proxy);
// Get feeds & categories (used for sync-in).
OwnCloudGetFeedsCategoriesResponse 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.
OwnCloudGetMessagesResponse 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 // OWNCLOUDNETWORKFACTORY_H

View File

@ -1,45 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/owncloud/owncloudserviceentrypoint.h"
#include "database/databasequeries.h"
#include "definitions/definitions.h"
#include "miscellaneous/application.h"
#include "miscellaneous/iconfactory.h"
#include "services/owncloud/definitions.h"
#include "services/owncloud/gui/formeditowncloudaccount.h"
#include "services/owncloud/owncloudserviceroot.h"
ServiceRoot* OwnCloudServiceEntryPoint::createNewRoot() const {
FormEditOwnCloudAccount form_acc(qApp->mainFormWidget());
return form_acc.addEditAccount<OwnCloudServiceRoot>();
}
QList<ServiceRoot*> OwnCloudServiceEntryPoint::initializeSubtree() const {
QSqlDatabase database = qApp->database()->driver()->connection(QSL("OwnCloudServiceEntryPoint"));
return DatabaseQueries::getAccounts<OwnCloudServiceRoot>(database, code());
}
QString OwnCloudServiceEntryPoint::name() const {
return QSL("Nextcloud News");
}
QString OwnCloudServiceEntryPoint::code() const {
return QSL(SERVICE_CODE_OWNCLOUD);
}
QString OwnCloudServiceEntryPoint::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(OWNCLOUD_API_VERSION));
}
QString OwnCloudServiceEntryPoint::author() const {
return QSL(APP_AUTHOR);
}
QIcon OwnCloudServiceEntryPoint::icon() const {
return qApp->icons()->miscIcon(QSL("nextcloud"));
}

View File

@ -1,19 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef OWNCLOUDSERVICEENTRYPOINT_H
#define OWNCLOUDSERVICEENTRYPOINT_H
#include "services/abstract/serviceentrypoint.h"
class OwnCloudServiceEntryPoint : public ServiceEntryPoint {
public:
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 // OWNCLOUDSERVICEENTRYPOINT_H

View File

@ -1,176 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/owncloud/owncloudserviceroot.h"
#include "database/databasequeries.h"
#include "definitions/definitions.h"
#include "exceptions/feedfetchexception.h"
#include "exceptions/networkexception.h"
#include "miscellaneous/application.h"
#include "miscellaneous/textfactory.h"
#include "services/owncloud/gui/formeditowncloudaccount.h"
#include "services/owncloud/owncloudfeed.h"
#include "services/owncloud/owncloudnetworkfactory.h"
#include "services/owncloud/owncloudserviceentrypoint.h"
OwnCloudServiceRoot::OwnCloudServiceRoot(RootItem* parent)
: ServiceRoot(parent), m_network(new OwnCloudNetworkFactory()) {
setIcon(OwnCloudServiceEntryPoint().icon());
}
OwnCloudServiceRoot::~OwnCloudServiceRoot() {
delete m_network;
}
bool OwnCloudServiceRoot::isSyncable() const {
return true;
}
bool OwnCloudServiceRoot::canBeEdited() const {
return true;
}
FormAccountDetails* OwnCloudServiceRoot::accountSetupDialog() const {
return new FormEditOwnCloudAccount(qApp->mainFormWidget());
}
void OwnCloudServiceRoot::editItems(const QList<RootItem*>& items) {
if (items.first()->kind() == RootItem::Kind::ServiceRoot) {
QScopedPointer<FormEditOwnCloudAccount> p(qobject_cast<FormEditOwnCloudAccount*>(accountSetupDialog()));
p->addEditAccount(this);
return;
}
ServiceRoot::editItems(items);
}
bool OwnCloudServiceRoot::supportsFeedAdding() const {
return false;
}
bool OwnCloudServiceRoot::supportsCategoryAdding() const {
return false;
}
void OwnCloudServiceRoot::start(bool freshly_activated) {
if (!freshly_activated) {
DatabaseQueries::loadRootFromDatabase<Category, OwnCloudFeed>(this);
loadCacheFromFile();
}
updateTitle();
if (getSubTreeFeeds().isEmpty()) {
syncIn();
}
}
QString OwnCloudServiceRoot::code() const {
return OwnCloudServiceEntryPoint().code();
}
OwnCloudNetworkFactory* OwnCloudServiceRoot::network() const {
return m_network;
}
void OwnCloudServiceRoot::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 OwnCloudServiceRoot::updateTitle() {
setTitle(m_network->authUsername() + QSL(" (Nextcloud News)"));
}
RootItem* OwnCloudServiceRoot::obtainNewTreeForSyncIn() const {
OwnCloudGetFeedsCategoriesResponse 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 OwnCloudServiceRoot::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 OwnCloudServiceRoot::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> OwnCloudServiceRoot::obtainNewMessages(Feed* feed,
const QHash<ServiceRoot::BagOfMessages, QStringList>&
stated_messages,
const QHash<QString, QStringList>& tagged_messages) {
Q_UNUSED(stated_messages)
Q_UNUSED(tagged_messages)
OwnCloudGetMessagesResponse messages = network()->getMessages(feed->customNumericId(), networkProxy());
if (messages.networkError() != QNetworkReply::NetworkError::NoError) {
throw FeedFetchException(Feed::Status::NetworkError);
}
else {
return messages.messages();
}
}

View File

@ -1,48 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef OWNCLOUDSERVICEROOT_H
#define OWNCLOUDSERVICEROOT_H
#include "services/abstract/cacheforserviceroot.h"
#include "services/abstract/serviceroot.h"
#include <QMap>
class OwnCloudNetworkFactory;
class Mutex;
class OwnCloudServiceRoot : public ServiceRoot, public CacheForServiceRoot {
Q_OBJECT
public:
explicit OwnCloudServiceRoot(RootItem* parent = nullptr);
virtual ~OwnCloudServiceRoot();
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);
OwnCloudNetworkFactory* network() const;
protected:
virtual RootItem* obtainNewTreeForSyncIn() const;
private:
void updateTitle();
private:
OwnCloudNetworkFactory* m_network;
};
#endif // OWNCLOUDSERVICEROOT_H

View File

@ -1,23 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef REDDIT_DEFINITIONS_H
#define REDDIT_DEFINITIONS_H
#define REDDIT_OAUTH_REDIRECT_URI_PORT 14499
#define REDDIT_OAUTH_AUTH_URL "https://www.reddit.com/api/v1/authorize"
#define REDDIT_OAUTH_TOKEN_URL "https://www.reddit.com/api/v1/access_token"
#define REDDIT_OAUTH_SCOPE "identity mysubreddits read"
#define REDDIT_REG_API_URL "https://www.reddit.com/prefs/apps"
#define REDDIT_API_GET_PROFILE "https://oauth.reddit.com/api/v1/me"
#define REDDIT_API_SUBREDDITS "https://oauth.reddit.com/subreddits/mine/subscriber?limit=%1"
#define REDDIT_API_HOT "https://oauth.reddit.com%1hot?limit=%2&count=%3&g=%4"
#define REDDIT_DEFAULT_BATCH_SIZE 100
#define REDDIT_MAX_BATCH_SIZE 999
#define REDDIT_CONTENT_TYPE_HTTP "application/http"
#define REDDIT_CONTENT_TYPE_JSON "application/json"
#endif // REDDIT_DEFINITIONS_H

View File

@ -1,67 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/reddit/gui/formeditredditaccount.h"
#include "miscellaneous/application.h"
#include "miscellaneous/iconfactory.h"
#include "network-web/oauth2service.h"
#include "services/reddit/gui/redditaccountdetails.h"
#include "services/reddit/redditserviceroot.h"
FormEditRedditAccount::FormEditRedditAccount(QWidget* parent)
: FormAccountDetails(qApp->icons()->miscIcon(QSL("reddit")), parent), m_details(new RedditAccountDetails(this)) {
insertCustomTab(m_details, tr("Server setup"), 0);
activateTab(0);
m_details->m_ui.m_txtUsername->setFocus();
connect(m_details->m_ui.m_btnTestSetup, &QPushButton::clicked, this, [this]() {
m_details->testSetup(m_proxyDetails->proxy());
});
}
void FormEditRedditAccount::apply() {
FormAccountDetails::apply();
bool using_another_acc =
m_details->m_ui.m_txtUsername->lineEdit()->text() != account<RedditServiceRoot>()->network()->username();
// Make sure that the data copied from GUI are used for brand new login.
account<RedditServiceRoot>()->network()->oauth()->logout(false);
account<RedditServiceRoot>()->network()->oauth()->setClientId(m_details->m_ui.m_txtAppId->lineEdit()->text());
account<RedditServiceRoot>()->network()->oauth()->setClientSecret(m_details->m_ui.m_txtAppKey->lineEdit()->text());
account<RedditServiceRoot>()->network()->oauth()->setRedirectUrl(m_details->m_ui.m_txtRedirectUrl->lineEdit()->text(),
true);
account<RedditServiceRoot>()->network()->setUsername(m_details->m_ui.m_txtUsername->lineEdit()->text());
account<RedditServiceRoot>()->network()->setBatchSize(m_details->m_ui.m_spinLimitMessages->value());
account<RedditServiceRoot>()->network()->setDownloadOnlyUnreadMessages(m_details->m_ui.m_cbDownloadOnlyUnreadMessages
->isChecked());
account<RedditServiceRoot>()->saveAccountDataToDatabase();
accept();
if (!m_creatingNew) {
if (using_another_acc) {
account<RedditServiceRoot>()->completelyRemoveAllData();
}
account<RedditServiceRoot>()->start(true);
}
}
void FormEditRedditAccount::loadAccountData() {
FormAccountDetails::loadAccountData();
m_details->m_oauth = account<RedditServiceRoot>()->network()->oauth();
m_details->hookNetwork();
// Setup the GUI.
m_details->m_ui.m_txtAppId->lineEdit()->setText(m_details->m_oauth->clientId());
m_details->m_ui.m_txtAppKey->lineEdit()->setText(m_details->m_oauth->clientSecret());
m_details->m_ui.m_txtRedirectUrl->lineEdit()->setText(m_details->m_oauth->redirectUrl());
m_details->m_ui.m_txtUsername->lineEdit()->setText(account<RedditServiceRoot>()->network()->username());
m_details->m_ui.m_spinLimitMessages->setValue(account<RedditServiceRoot>()->network()->batchSize());
m_details->m_ui.m_cbDownloadOnlyUnreadMessages
->setChecked(account<RedditServiceRoot>()->network()->downloadOnlyUnreadMessages());
}

View File

@ -1,28 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef FORMEDITREDDITACCOUNT_H
#define FORMEDITREDDITACCOUNT_H
#include "services/abstract/gui/formaccountdetails.h"
#include "services/reddit/redditnetworkfactory.h"
class RedditServiceRoot;
class RedditAccountDetails;
class FormEditRedditAccount : public FormAccountDetails {
Q_OBJECT
public:
explicit FormEditRedditAccount(QWidget* parent = nullptr);
protected slots:
virtual void apply();
protected:
virtual void loadAccountData();
private:
RedditAccountDetails* m_details;
};
#endif // FORMEDITREDDITACCOUNT_H

View File

@ -1,120 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/reddit/gui/redditaccountdetails.h"
#include "exceptions/applicationexception.h"
#include "miscellaneous/application.h"
#include "network-web/oauth2service.h"
#include "network-web/webfactory.h"
#include "services/reddit/definitions.h"
#include "services/reddit/redditnetworkfactory.h"
RedditAccountDetails::RedditAccountDetails(QWidget* parent) : QWidget(parent), m_oauth(nullptr), m_lastProxy({}) {
m_ui.setupUi(this);
m_ui.m_lblInfo->setHelpText(tr("You have to fill in your client ID/secret and also fill in correct redirect URL."),
true);
m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Information,
tr("Not tested yet."),
tr("Not tested yet."));
m_ui.m_lblTestResult->label()->setWordWrap(true);
m_ui.m_txtUsername->lineEdit()->setPlaceholderText(tr("User-visible username"));
setTabOrder(m_ui.m_txtUsername->lineEdit(), m_ui.m_txtAppId);
setTabOrder(m_ui.m_txtAppId, m_ui.m_txtAppKey);
setTabOrder(m_ui.m_txtAppKey, m_ui.m_txtRedirectUrl);
setTabOrder(m_ui.m_txtRedirectUrl, m_ui.m_spinLimitMessages);
setTabOrder(m_ui.m_spinLimitMessages, m_ui.m_btnTestSetup);
connect(m_ui.m_txtAppId->lineEdit(), &BaseLineEdit::textChanged, this, &RedditAccountDetails::checkOAuthValue);
connect(m_ui.m_txtAppKey->lineEdit(), &BaseLineEdit::textChanged, this, &RedditAccountDetails::checkOAuthValue);
connect(m_ui.m_txtRedirectUrl->lineEdit(), &BaseLineEdit::textChanged, this, &RedditAccountDetails::checkOAuthValue);
connect(m_ui.m_txtUsername->lineEdit(), &BaseLineEdit::textChanged, this, &RedditAccountDetails::checkUsername);
connect(m_ui.m_btnRegisterApi, &QPushButton::clicked, this, &RedditAccountDetails::registerApi);
emit m_ui.m_txtUsername->lineEdit()->textChanged(m_ui.m_txtUsername->lineEdit()->text());
emit m_ui.m_txtAppId->lineEdit()->textChanged(m_ui.m_txtAppId->lineEdit()->text());
emit m_ui.m_txtAppKey->lineEdit()->textChanged(m_ui.m_txtAppKey->lineEdit()->text());
emit m_ui.m_txtRedirectUrl->lineEdit()->textChanged(m_ui.m_txtAppKey->lineEdit()->text());
hookNetwork();
}
void RedditAccountDetails::testSetup(const QNetworkProxy& custom_proxy) {
m_oauth->logout(true);
m_oauth->setClientId(m_ui.m_txtAppId->lineEdit()->text());
m_oauth->setClientSecret(m_ui.m_txtAppKey->lineEdit()->text());
m_oauth->setRedirectUrl(m_ui.m_txtRedirectUrl->lineEdit()->text(), true);
m_lastProxy = custom_proxy;
m_oauth->login();
}
void RedditAccountDetails::checkUsername(const QString& username) {
if (username.isEmpty()) {
m_ui.m_txtUsername->setStatus(WidgetWithStatus::StatusType::Error, tr("No username entered."));
}
else {
m_ui.m_txtUsername->setStatus(WidgetWithStatus::StatusType::Ok, tr("Some username entered."));
}
}
void RedditAccountDetails::onAuthFailed() {
m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error,
tr("You did not grant access."),
tr("There was error during testing."));
}
void RedditAccountDetails::onAuthError(const QString& error, const QString& detailed_description) {
Q_UNUSED(error)
m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Error,
tr("There is error: %1").arg(detailed_description),
tr("There was error during testing."));
}
void RedditAccountDetails::onAuthGranted() {
m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Ok,
tr("Tested successfully. You may be prompted to login once more."),
tr("Your access was approved."));
try {
RedditNetworkFactory fac;
fac.setOauth(m_oauth);
auto resp = fac.me(m_lastProxy);
m_ui.m_txtUsername->lineEdit()->setText(resp[QSL("name")].toString());
}
catch (const ApplicationException& ex) {
qCriticalNN << LOGSEC_REDDIT << "Failed to obtain profile with error:" << QUOTE_W_SPACE_DOT(ex.message());
}
}
void RedditAccountDetails::hookNetwork() {
connect(m_oauth, &OAuth2Service::tokensRetrieved, this, &RedditAccountDetails::onAuthGranted);
connect(m_oauth, &OAuth2Service::tokensRetrieveError, this, &RedditAccountDetails::onAuthError);
connect(m_oauth, &OAuth2Service::authFailed, this, &RedditAccountDetails::onAuthFailed);
}
void RedditAccountDetails::registerApi() {
qApp->web()->openUrlInExternalBrowser(QSL(REDDIT_REG_API_URL));
}
void RedditAccountDetails::checkOAuthValue(const QString& value) {
auto* line_edit = qobject_cast<LineEditWithStatus*>(sender()->parent());
if (line_edit != nullptr) {
if (value.isEmpty()) {
#if defined(REDDIT_OFFICIAL_SUPPORT)
line_edit->setStatus(WidgetWithStatus::StatusType::Ok, tr("Preconfigured client ID/secret will be used."));
#else
line_edit->setStatus(WidgetWithStatus::StatusType::Error, tr("Empty value is entered."));
#endif
}
else {
line_edit->setStatus(WidgetWithStatus::StatusType::Ok, tr("Some value is entered."));
}
}
}

View File

@ -1,43 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef REDDITACCOUNTDETAILS_H
#define REDDITACCOUNTDETAILS_H
#include "ui_redditaccountdetails.h"
#include <QNetworkProxy>
#include <QWidget>
class OAuth2Service;
class RedditAccountDetails : public QWidget {
Q_OBJECT
friend class FormEditRedditAccount;
public:
explicit RedditAccountDetails(QWidget* parent = nullptr);
public slots:
void testSetup(const QNetworkProxy& custom_proxy);
private slots:
void registerApi();
void checkOAuthValue(const QString& value);
void checkUsername(const QString& username);
void onAuthFailed();
void onAuthError(const QString& error, const QString& detailed_description);
void onAuthGranted();
private:
void hookNetwork();
private:
Ui::RedditAccountDetails m_ui;
// Pointer to live OAuth.
OAuth2Service* m_oauth;
QNetworkProxy m_lastProxy;
};
#endif // REDDITACCOUNTDETAILS_H

View File

@ -1,202 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RedditAccountDetails</class>
<widget class="QWidget" name="RedditAccountDetails">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>431</width>
<height>259</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="m_lblUsername">
<property name="text">
<string>Username</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="LineEditWithStatus" name="m_txtUsername" native="true"/>
</item>
<item row="1" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>OAuth 2.0 settings</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="m_lblUsername_2">
<property name="text">
<string>Client ID</string>
</property>
<property name="buddy">
<cstring>m_txtAppId</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="LineEditWithStatus" name="m_txtAppId" native="true"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="m_lblUsername_3">
<property name="text">
<string>Client secret</string>
</property>
<property name="buddy">
<cstring>m_txtAppKey</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="LineEditWithStatus" name="m_txtAppKey" native="true"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="m_lblUsername_4">
<property name="text">
<string>Redirect URL</string>
</property>
<property name="buddy">
<cstring>m_txtRedirectUrl</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="LineEditWithStatus" name="m_txtRedirectUrl" native="true"/>
</item>
<item row="4" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="m_btnRegisterApi">
<property name="text">
<string>Get my credentials</string>
</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="3" column="0" colspan="2">
<widget class="HelpSpoiler" name="m_lblInfo" native="true"/>
</item>
</layout>
</widget>
</item>
<item row="3" column="0" colspan="2">
<layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0">
<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 row="0" column="1">
<widget class="MessageCountSpinBox" name="m_spinLimitMessages">
<property name="maximumSize">
<size>
<width>140</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0" colspan="2">
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QPushButton" name="m_btnTestSetup">
<property name="text">
<string>&amp;Login</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="LabelWithStatus" name="m_lblTestResult" native="true">
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="0" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>410</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="m_cbDownloadOnlyUnreadMessages">
<property name="text">
<string>Download unread articles only</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_btnRegisterApi</tabstop>
<tabstop>m_cbDownloadOnlyUnreadMessages</tabstop>
<tabstop>m_spinLimitMessages</tabstop>
<tabstop>m_btnTestSetup</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -1,19 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/reddit/redditcategory.h"
RedditCategory::RedditCategory(Type type, RootItem* parent_item) : Category(parent_item), m_type(type) {
updateTitle();
}
RedditCategory::Type RedditCategory::type() const {
return m_type;
}
void RedditCategory::updateTitle() {
switch (m_type) {
case Type::Subscriptions:
setTitle(tr("Subscriptions"));
break;
}
}

View File

@ -1,27 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef REDDITCATEGORY_H
#define REDDITCATEGORY_H
#include "services/abstract/category.h"
class RedditCategory : public Category {
Q_OBJECT
public:
enum class Type {
Subscriptions = 1
};
explicit RedditCategory(Type type = Type::Subscriptions, RootItem* parent_item = nullptr);
Type type() const;
private:
void updateTitle();
private:
Type m_type;
};
#endif // REDDITCATEGORY_H

View File

@ -1,44 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/reddit/redditentrypoint.h"
#include "database/databasequeries.h"
#include "definitions/definitions.h"
#include "miscellaneous/application.h"
#include "miscellaneous/iconfactory.h"
#include "services/reddit/gui/formeditredditaccount.h"
#include "services/reddit/redditserviceroot.h"
#include <QMessageBox>
ServiceRoot* RedditEntryPoint::createNewRoot() const {
FormEditRedditAccount form_acc(qApp->mainFormWidget());
return form_acc.addEditAccount<RedditServiceRoot>();
}
QList<ServiceRoot*> RedditEntryPoint::initializeSubtree() const {
QSqlDatabase database = qApp->database()->driver()->connection(QSL("RedditEntryPoint"));
return DatabaseQueries::getAccounts<RedditServiceRoot>(database, code());
}
QString RedditEntryPoint::name() const {
return QSL("Reddit");
}
QString RedditEntryPoint::code() const {
return QSL(SERVICE_CODE_REDDIT);
}
QString RedditEntryPoint::description() const {
return QObject::tr("Simplistic Reddit client.");
}
QString RedditEntryPoint::author() const {
return QSL(APP_AUTHOR);
}
QIcon RedditEntryPoint::icon() const {
return qApp->icons()->miscIcon(QSL("reddit"));
}

View File

@ -1,19 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef REDDITENTRYPOINT_H
#define REDDITENTRYPOINT_H
#include "services/abstract/serviceentrypoint.h"
class RedditEntryPoint : public ServiceEntryPoint {
public:
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 // REDDITENTRYPOINT_H

View File

@ -1,322 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/reddit/redditnetworkfactory.h"
#include "database/databasequeries.h"
#include "definitions/definitions.h"
#include "exceptions/applicationexception.h"
#include "exceptions/networkexception.h"
#include "miscellaneous/application.h"
#include "miscellaneous/settings.h"
#include "network-web/networkfactory.h"
#include "network-web/oauth2service.h"
#include "services/reddit/definitions.h"
#include "services/reddit/redditserviceroot.h"
#include "services/reddit/redditsubscription.h"
#include <QHttpMultiPart>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression>
#include <QUrl>
RedditNetworkFactory::RedditNetworkFactory(QObject* parent)
: QObject(parent), m_service(nullptr), m_username(QString()), m_batchSize(REDDIT_DEFAULT_BATCH_SIZE),
m_downloadOnlyUnreadMessages(false), m_oauth2(new OAuth2Service(QSL(REDDIT_OAUTH_AUTH_URL),
QSL(REDDIT_OAUTH_TOKEN_URL),
{},
{},
QSL(REDDIT_OAUTH_SCOPE),
this)) {
initializeOauth();
}
void RedditNetworkFactory::setService(RedditServiceRoot* service) {
m_service = service;
}
OAuth2Service* RedditNetworkFactory::oauth() const {
return m_oauth2;
}
QString RedditNetworkFactory::username() const {
return m_username;
}
int RedditNetworkFactory::batchSize() const {
return m_batchSize;
}
void RedditNetworkFactory::setBatchSize(int batch_size) {
m_batchSize = batch_size;
}
void RedditNetworkFactory::initializeOauth() {
m_oauth2->setUseHttpBasicAuthWithClientData(true);
m_oauth2->setRedirectUrl(QSL(OAUTH_REDIRECT_URI) + QL1C(':') + QString::number(REDDIT_OAUTH_REDIRECT_URI_PORT), true);
connect(m_oauth2, &OAuth2Service::tokensRetrieveError, this, &RedditNetworkFactory::onTokensError);
connect(m_oauth2, &OAuth2Service::authFailed, this, &RedditNetworkFactory::onAuthFailed);
connect(m_oauth2,
&OAuth2Service::tokensRetrieved,
this,
[this](QString access_token, QString refresh_token, int expires_in) {
Q_UNUSED(expires_in)
Q_UNUSED(access_token)
if (m_service != nullptr && !refresh_token.isEmpty()) {
QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
DatabaseQueries::storeNewOauthTokens(database, refresh_token, m_service->accountId());
}
});
}
bool RedditNetworkFactory::downloadOnlyUnreadMessages() const {
return m_downloadOnlyUnreadMessages;
}
void RedditNetworkFactory::setDownloadOnlyUnreadMessages(bool download_only_unread_messages) {
m_downloadOnlyUnreadMessages = download_only_unread_messages;
}
void RedditNetworkFactory::setOauth(OAuth2Service* oauth) {
m_oauth2 = oauth;
}
void RedditNetworkFactory::setUsername(const QString& username) {
m_username = username;
}
QVariantHash RedditNetworkFactory::me(const QNetworkProxy& custom_proxy) {
QString bearer = m_oauth2->bearer().toLocal8Bit();
if (bearer.isEmpty()) {
throw ApplicationException(tr("you are not logged in"));
}
QList<QPair<QByteArray, QByteArray>> headers;
headers.append(QPair<QByteArray, QByteArray>(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(),
m_oauth2->bearer().toLocal8Bit()));
int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt();
QByteArray output;
auto result = NetworkFactory::performNetworkOperation(QSL(REDDIT_API_GET_PROFILE),
timeout,
{},
output,
QNetworkAccessManager::Operation::GetOperation,
headers,
false,
{},
{},
custom_proxy)
.m_networkError;
if (result != QNetworkReply::NetworkError::NoError) {
throw NetworkException(result, output);
}
else {
QJsonDocument doc = QJsonDocument::fromJson(output);
return doc.object().toVariantHash();
}
}
QList<Feed*> RedditNetworkFactory::subreddits(const QNetworkProxy& custom_proxy) {
QString bearer = m_oauth2->bearer().toLocal8Bit();
if (bearer.isEmpty()) {
throw ApplicationException(tr("you are not logged in"));
}
QList<QPair<QByteArray, QByteArray>> headers;
headers.append(QPair<QByteArray, QByteArray>(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(),
m_oauth2->bearer().toLocal8Bit()));
int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt();
QString after;
QList<Feed*> subs;
do {
QByteArray output;
QString final_url = QSL(REDDIT_API_SUBREDDITS).arg(QString::number(100));
if (!after.isEmpty()) {
final_url += QSL("&after=%1").arg(after);
}
auto result = NetworkFactory::performNetworkOperation(final_url,
timeout,
{},
output,
QNetworkAccessManager::Operation::GetOperation,
headers,
false,
{},
{},
custom_proxy)
.m_networkError;
if (result != QNetworkReply::NetworkError::NoError) {
throw NetworkException(result, output);
}
else {
QJsonDocument doc = QJsonDocument::fromJson(output);
QJsonObject root_doc = doc.object();
after = root_doc["data"].toObject()["after"].toString();
for (const QJsonValue& sub_val : root_doc["data"].toObject()["children"].toArray()) {
const auto sub_obj = sub_val.toObject()["data"].toObject();
RedditSubscription* new_sub = new RedditSubscription();
new_sub->setCustomId(sub_obj["id"].toString());
new_sub->setTitle(sub_obj["title"].toString());
new_sub->setDescription(sub_obj["public_description"].toString());
new_sub->setPrefixedName(sub_obj["url"].toString());
QPixmap icon;
QString icon_url = sub_obj["community_icon"].toString();
if (icon_url.isEmpty()) {
icon_url = sub_obj["icon_img"].toString();
}
if (icon_url.contains(QL1S("?"))) {
icon_url = icon_url.mid(0, icon_url.indexOf(QL1S("?")));
}
if (!icon_url.isEmpty() &&
NetworkFactory::downloadIcon({{icon_url, true}}, timeout, icon, headers, custom_proxy) ==
QNetworkReply::NetworkError::NoError) {
new_sub->setIcon(icon);
}
subs.append(new_sub);
}
}
}
while (!after.isEmpty());
// posty dle jmena redditu
// https://oauth.reddit.com/<SUBREDDIT>/new
//
// komenty pro post dle id postu
// https://oauth.reddit.com/<SUBREDDIT>/comments/<ID-POSTU>
return subs;
}
QList<Message> RedditNetworkFactory::hot(const QString& sub_name, const QNetworkProxy& custom_proxy) {
QString bearer = m_oauth2->bearer().toLocal8Bit();
if (bearer.isEmpty()) {
throw ApplicationException(tr("you are not logged in"));
}
QList<QPair<QByteArray, QByteArray>> headers;
headers.append(QPair<QByteArray, QByteArray>(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(),
m_oauth2->bearer().toLocal8Bit()));
int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt();
QString after;
QList<Message> msgs;
int desired_count = batchSize();
do {
int next_batch = desired_count <= 0 ? 100 : std::min(100, int(desired_count - msgs.size()));
QByteArray output;
QString final_url =
QSL(REDDIT_API_HOT).arg(sub_name, QString::number(next_batch), QString::number(msgs.size()), QSL("GLOBAL"));
if (!after.isEmpty()) {
final_url += QSL("&after=%1").arg(after);
}
auto result = NetworkFactory::performNetworkOperation(final_url,
timeout,
{},
output,
QNetworkAccessManager::Operation::GetOperation,
headers,
false,
{},
{},
custom_proxy)
.m_networkError;
if (result != QNetworkReply::NetworkError::NoError) {
throw NetworkException(result, output);
}
else {
QJsonDocument doc = QJsonDocument::fromJson(output);
QJsonObject root_doc = doc.object();
after = root_doc["data"].toObject()["after"].toString();
for (const QJsonValue& sub_val : root_doc["data"].toObject()["children"].toArray()) {
const auto msg_obj = sub_val.toObject()["data"].toObject();
Message new_msg;
new_msg.m_customId = msg_obj["id"].toString();
new_msg.m_title = msg_obj["title"].toString();
new_msg.m_author = msg_obj["author"].toString();
new_msg.m_createdFromFeed = true;
new_msg.m_created =
QDateTime::fromSecsSinceEpoch(msg_obj["created_utc"].toVariant().toLongLong(), Qt::TimeSpec::UTC);
new_msg.m_url = QSL("https://reddit.com") + msg_obj["permalink"].toString();
new_msg.m_contents =
msg_obj["description_html"]
.toString(); // když prazdny, je poustnutej třeba obrazek či odkaz?, viz property "post_hint"?
new_msg.m_rawContents = QJsonDocument(msg_obj).toJson(QJsonDocument::JsonFormat::Compact);
msgs.append(new_msg);
}
}
}
while (!after.isEmpty() && (desired_count <= 0 || desired_count > msgs.size()));
// posty dle jmena redditu
// https://oauth.reddit.com/<SUBREDDIT>/new
//
// komenty pro post dle id postu
// https://oauth.reddit.com/<SUBREDDIT>/comments/<ID-POSTU>
return msgs;
}
void RedditNetworkFactory::onTokensError(const QString& error, const QString& error_description) {
Q_UNUSED(error)
qApp->showGuiMessage(Notification::Event::LoginFailure,
{tr("Reddit: authentication error"),
tr("Click this to login again. Error is: '%1'").arg(error_description),
QSystemTrayIcon::MessageIcon::Critical},
{},
{tr("Login"), [this]() {
m_oauth2->setAccessToken(QString());
m_oauth2->setRefreshToken(QString());
m_oauth2->login();
}});
}
void RedditNetworkFactory::onAuthFailed() {
qApp->showGuiMessage(Notification::Event::LoginFailure,
{tr("Reddit: authorization denied"),
tr("Click this to login again."),
QSystemTrayIcon::MessageIcon::Critical},
{},
{tr("Login"), [this]() {
m_oauth2->login();
}});
}

View File

@ -1,60 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef REDDITNETWORKFACTORY_H
#define REDDITNETWORKFACTORY_H
#include "core/message.h"
#include "services/abstract/feed.h"
#include "services/abstract/rootitem.h"
#include <QNetworkReply>
#include <QObject>
class RootItem;
class RedditServiceRoot;
class OAuth2Service;
class Downloader;
struct Subreddit {};
class RedditNetworkFactory : public QObject {
Q_OBJECT
public:
explicit RedditNetworkFactory(QObject* parent = nullptr);
void setService(RedditServiceRoot* service);
OAuth2Service* oauth() const;
void setOauth(OAuth2Service* oauth);
QString username() const;
void setUsername(const QString& username);
int batchSize() const;
void setBatchSize(int batch_size);
bool downloadOnlyUnreadMessages() const;
void setDownloadOnlyUnreadMessages(bool download_only_unread_messages);
// API methods.
QVariantHash me(const QNetworkProxy& custom_proxy);
QList<Feed*> subreddits(const QNetworkProxy& custom_proxy);
QList<Message> hot(const QString& sub_name, const QNetworkProxy& custom_proxy);
private slots:
void onTokensError(const QString& error, const QString& error_description);
void onAuthFailed();
private:
void initializeOauth();
private:
RedditServiceRoot* m_service;
QString m_username;
int m_batchSize;
bool m_downloadOnlyUnreadMessages;
OAuth2Service* m_oauth2;
};
#endif // REDDITNETWORKFACTORY_H

View File

@ -1,142 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/reddit/redditserviceroot.h"
#include "database/databasequeries.h"
#include "miscellaneous/application.h"
#include "network-web/oauth2service.h"
#include "services/reddit/gui/formeditredditaccount.h"
#include "services/reddit/redditcategory.h"
#include "services/reddit/redditentrypoint.h"
#include "services/reddit/redditnetworkfactory.h"
#include "services/reddit/redditsubscription.h"
#include <QFileDialog>
RedditServiceRoot::RedditServiceRoot(RootItem* parent)
: ServiceRoot(parent), m_network(new RedditNetworkFactory(this)) {
m_network->setService(this);
setIcon(RedditEntryPoint().icon());
}
void RedditServiceRoot::updateTitle() {
setTitle(TextFactory::extractUsernameFromEmail(m_network->username()) + QSL(" (Reddit)"));
}
RootItem* RedditServiceRoot::obtainNewTreeForSyncIn() const {
auto* root = new RootItem();
auto feeds = m_network->subreddits(networkProxy());
for (auto* feed : feeds) {
root->appendChild(feed);
}
return root;
}
QVariantHash RedditServiceRoot::customDatabaseData() const {
QVariantHash data = ServiceRoot::customDatabaseData();
data[QSL("username")] = m_network->username();
data[QSL("batch_size")] = m_network->batchSize();
data[QSL("download_only_unread")] = m_network->downloadOnlyUnreadMessages();
data[QSL("client_id")] = m_network->oauth()->clientId();
data[QSL("client_secret")] = m_network->oauth()->clientSecret();
data[QSL("refresh_token")] = m_network->oauth()->refreshToken();
data[QSL("redirect_uri")] = m_network->oauth()->redirectUrl();
return data;
}
void RedditServiceRoot::setCustomDatabaseData(const QVariantHash& data) {
ServiceRoot::setCustomDatabaseData(data);
m_network->setUsername(data[QSL("username")].toString());
m_network->setBatchSize(data[QSL("batch_size")].toInt());
m_network->setDownloadOnlyUnreadMessages(data[QSL("download_only_unread")].toBool());
m_network->oauth()->setClientId(data[QSL("client_id")].toString());
m_network->oauth()->setClientSecret(data[QSL("client_secret")].toString());
m_network->oauth()->setRefreshToken(data[QSL("refresh_token")].toString());
m_network->oauth()->setRedirectUrl(data[QSL("redirect_uri")].toString(), true);
}
QList<Message> RedditServiceRoot::obtainNewMessages(Feed* feed,
const QHash<ServiceRoot::BagOfMessages, QStringList>&
stated_messages,
const QHash<QString, QStringList>& tagged_messages) {
Q_UNUSED(stated_messages)
Q_UNUSED(tagged_messages)
Q_UNUSED(feed)
QList<Message> messages = m_network->hot(qobject_cast<RedditSubscription*>(feed)->prefixedName(), networkProxy());
return messages;
}
bool RedditServiceRoot::isSyncable() const {
return true;
}
bool RedditServiceRoot::canBeEdited() const {
return true;
}
void RedditServiceRoot::editItems(const QList<RootItem*>& items) {
if (items.first()->kind() == RootItem::Kind::ServiceRoot) {
QScopedPointer<FormEditRedditAccount> p(qobject_cast<FormEditRedditAccount*>(accountSetupDialog()));
p->addEditAccount(this);
return;
}
ServiceRoot::editItems(items);
}
FormAccountDetails* RedditServiceRoot::accountSetupDialog() const {
return new FormEditRedditAccount(qApp->mainFormWidget());
}
bool RedditServiceRoot::supportsFeedAdding() const {
return false;
}
bool RedditServiceRoot::supportsCategoryAdding() const {
return false;
}
void RedditServiceRoot::start(bool freshly_activated) {
if (!freshly_activated) {
DatabaseQueries::loadRootFromDatabase<RedditCategory, RedditSubscription>(this);
loadCacheFromFile();
}
updateTitle();
if (getSubTreeFeeds().isEmpty()) {
m_network->oauth()->login([this]() {
syncIn();
});
}
else {
m_network->oauth()->login();
}
}
QString RedditServiceRoot::code() const {
return RedditEntryPoint().code();
}
QString RedditServiceRoot::additionalTooltip() const {
return ServiceRoot::additionalTooltip() + QSL("\n") +
tr("Authentication status: %1\n"
"Login tokens expiration: %2")
.arg(network()->oauth()->isFullyLoggedIn() ? tr("logged-in") : tr("NOT logged-in"),
network()->oauth()->tokensExpireIn().isValid() ? network()->oauth()->tokensExpireIn().toString()
: QSL("-"));
}
void RedditServiceRoot::saveAllCachedData(bool ignore_errors) {
Q_UNUSED(ignore_errors)
auto msg_cache = takeMessageCache();
}

View File

@ -1,54 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef REDDITSERVICEROOT_H
#define REDDITSERVICEROOT_H
#include "services/abstract/cacheforserviceroot.h"
#include "services/abstract/serviceroot.h"
class RedditNetworkFactory;
class RedditServiceRoot : public ServiceRoot, public CacheForServiceRoot {
Q_OBJECT
public:
explicit RedditServiceRoot(RootItem* parent = nullptr);
void setNetwork(RedditNetworkFactory* network);
RedditNetworkFactory* network() 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 start(bool freshly_activated);
virtual QString code() const;
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);
protected:
virtual RootItem* obtainNewTreeForSyncIn() const;
private:
void updateTitle();
private:
RedditNetworkFactory* m_network;
};
inline void RedditServiceRoot::setNetwork(RedditNetworkFactory* network) {
m_network = network;
}
inline RedditNetworkFactory* RedditServiceRoot::network() const {
return m_network;
}
#endif // REDDITSERVICEROOT_H

View File

@ -1,32 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/reddit/redditsubscription.h"
#include "definitions/definitions.h"
#include "services/reddit/redditserviceroot.h"
RedditSubscription::RedditSubscription(RootItem* parent) : Feed(parent), m_prefixedName(QString()) {}
RedditServiceRoot* RedditSubscription::serviceRoot() const {
return qobject_cast<RedditServiceRoot*>(getParentServiceRoot());
}
QString RedditSubscription::prefixedName() const {
return m_prefixedName;
}
void RedditSubscription::setPrefixedName(const QString& prefixed_name) {
m_prefixedName = prefixed_name;
}
QVariantHash RedditSubscription::customDatabaseData() const {
QVariantHash data;
data.insert(QSL("prefixed_name"), prefixedName());
return data;
}
void RedditSubscription::setCustomDatabaseData(const QVariantHash& data) {
setPrefixedName(data.value(QSL("prefixed_name")).toString());
}

View File

@ -1,28 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef REDDITSUBSCRIPTION_H
#define REDDITSUBSCRIPTION_H
#include "services/abstract/feed.h"
class RedditServiceRoot;
class RedditSubscription : public Feed {
Q_OBJECT
public:
explicit RedditSubscription(RootItem* parent = nullptr);
RedditServiceRoot* serviceRoot() const;
QString prefixedName() const;
void setPrefixedName(const QString& prefixed_name);
virtual QVariantHash customDatabaseData() const;
virtual void setCustomDatabaseData(const QVariantHash& data);
private:
QString m_prefixedName;
};
#endif // REDDITSUBSCRIPTION_H

View File

@ -1,49 +0,0 @@
// 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

@ -1,69 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/tt-rss/gui/formeditttrssaccount.h"
#include "miscellaneous/iconfactory.h"
#include "services/tt-rss/gui/ttrssaccountdetails.h"
#include "services/tt-rss/ttrssnetworkfactory.h"
#include "services/tt-rss/ttrssserviceroot.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

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

@ -1,73 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/tt-rss/gui/formttrssfeeddetails.h"
#include "exceptions/applicationexception.h"
#include "miscellaneous/application.h"
#include "services/abstract/gui/authenticationdetails.h"
#include "services/tt-rss/definitions.h"
#include "services/tt-rss/gui/ttrssfeeddetails.h"
#include "services/tt-rss/ttrssnetworkfactory.h"
#include "services/tt-rss/ttrssserviceroot.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

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

@ -1,78 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/tt-rss/gui/formttrssnote.h"
#include "gui/guiutilities.h"
#include "gui/messagebox.h"
#include "miscellaneous/application.h"
#include "miscellaneous/iconfactory.h"
#include "services/tt-rss/definitions.h"
#include "services/tt-rss/ttrssnetworkfactory.h"
#include "services/tt-rss/ttrssnotetopublish.h"
#include "services/tt-rss/ttrssserviceroot.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

@ -1,33 +0,0 @@
// 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

@ -1,105 +0,0 @@
<?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

@ -1,188 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/tt-rss/gui/ttrssaccountdetails.h"
#include "definitions/definitions.h"
#include "network-web/networkfactory.h"
#include "services/tt-rss/definitions.h"
#include "services/tt-rss/ttrssnetworkfactory.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

@ -1,34 +0,0 @@
// 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

@ -1,252 +0,0 @@
<?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

@ -1,56 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/tt-rss/gui/ttrssfeeddetails.h"
#include "definitions/definitions.h"
#include "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

@ -1,31 +0,0 @@
// 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

@ -1,68 +0,0 @@
<?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

@ -1,61 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/tt-rss/ttrssfeed.h"
#include "database/databasequeries.h"
#include "definitions/definitions.h"
#include "miscellaneous/application.h"
#include "miscellaneous/iconfactory.h"
#include "services/tt-rss/definitions.h"
#include "services/tt-rss/ttrssnetworkfactory.h"
#include "services/tt-rss/ttrssserviceroot.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

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

@ -1,246 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef TTRSSNETWORKFACTORY_H
#define TTRSSNETWORKFACTORY_H
#include "core/message.h"
#include "services/tt-rss/ttrssnotetopublish.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

@ -1,15 +0,0 @@
// 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

@ -1,47 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/tt-rss/ttrssserviceentrypoint.h"
#include "database/databasequeries.h"
#include "definitions/definitions.h"
#include "miscellaneous/iconfactory.h"
#include "services/tt-rss/definitions.h"
#include "services/tt-rss/gui/formeditttrssaccount.h"
#include "services/tt-rss/ttrssserviceroot.h"
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

@ -1,19 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef TTRSSSERVICEENTRYPOINT_H
#define TTRSSSERVICEENTRYPOINT_H
#include "services/abstract/serviceentrypoint.h"
class TtRssServiceEntryPoint : public ServiceEntryPoint {
public:
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

@ -1,422 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/tt-rss/ttrssserviceroot.h"
#include "3rd-party/boolinq/boolinq.h"
#include "database/databasequeries.h"
#include "exceptions/feedfetchexception.h"
#include "exceptions/networkexception.h"
#include "miscellaneous/application.h"
#include "miscellaneous/mutex.h"
#include "miscellaneous/textfactory.h"
#include "network-web/networkfactory.h"
#include "services/abstract/labelsnode.h"
#include "services/tt-rss/definitions.h"
#include "services/tt-rss/gui/formeditttrssaccount.h"
#include "services/tt-rss/gui/formttrssfeeddetails.h"
#include "services/tt-rss/gui/formttrssnote.h"
#include "services/tt-rss/ttrssfeed.h"
#include "services/tt-rss/ttrssnetworkfactory.h"
#include "services/tt-rss/ttrssserviceentrypoint.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

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