build reddit plugin, fix encoding in oauth handler

This commit is contained in:
Martin Rotter 2024-06-24 07:09:37 +02:00
parent 7c6854996e
commit c7f381b098
23 changed files with 2824 additions and 0 deletions

View file

@ -326,6 +326,7 @@ add_subdirectory(src/librssguard-feedly)
add_subdirectory(src/librssguard-gmail)
add_subdirectory(src/librssguard-greader)
add_subdirectory(src/librssguard-ttrss)
add_subdirectory(src/librssguard-reddit)
add_subdirectory(src/librssguard-nextcloud)
# GUI executable.

View file

@ -0,0 +1,31 @@
include(../cmake_plugins.cmake)
set(PLUGIN_TARGET "rssguard-reddit")
set(SOURCES
src/definitions.h
src/gui/formeditredditaccount.cpp
src/gui/formeditredditaccount.h
src/gui/redditaccountdetails.cpp
src/gui/redditaccountdetails.h
src/redditcategory.cpp
src/redditcategory.h
src/redditentrypoint.cpp
src/redditentrypoint.h
src/redditnetworkfactory.cpp
src/redditnetworkfactory.h
src/redditserviceroot.cpp
src/redditserviceroot.h
src/redditsubscription.cpp
src/redditsubscription.h
src/3rd-party/mimesis/mimesis.cpp
src/3rd-party/mimesis/mimesis.hpp
src/3rd-party/mimesis/quoted-printable.cpp
src/3rd-party/mimesis/quoted-printable.hpp
)
set(UI_FILES
src/gui/redditaccountdetails.ui
)
prepare_rssguard_plugin(${PLUGIN_TARGET})

View file

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,181 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#pragma once
/* Mimesis -- a library for parsing and creating RFC2822 messages
Copyright © 2017 Guus Sliepen <guus@lightbts.info>
Mimesis is free software; you can redistribute it and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <chrono>
#include <functional>
#include <iosfwd>
#include <string>
#include <utility>
#include <vector>
namespace Mimesis {
std::string base64_encode(std::string_view in);
std::string base64_decode(std::string_view in);
class Part {
std::vector<std::pair<std::string, std::string>> headers;
std::string preamble;
std::string body;
std::string epilogue;
std::vector<Part> parts;
std::string boundary;
bool multipart;
bool crlf;
protected:
bool message;
public:
Part();
friend bool operator==(const Part& lhs, const Part& rhs);
friend bool operator!=(const Part& lhs, const Part& rhs);
// Loading and saving a whole MIME message
std::string load(std::istream& in, const std::string& parent_boundary = {});
void load(const std::string& filename);
void save(std::ostream& out) const;
void save(const std::string& filename) const;
void from_string(const std::string& data);
std::string to_string() const;
// Low-level access
std::string get_body() const;
std::string get_preamble() const;
std::string get_epilogue() const;
std::string get_boundary() const;
std::vector<Part>& get_parts();
const std::vector<Part>& get_parts() const;
std::vector<std::pair<std::string, std::string>>& get_headers();
const std::vector<std::pair<std::string, std::string>>& get_headers() const;
bool is_multipart() const;
bool is_multipart(const std::string& subtype) const;
bool is_singlepart() const;
bool is_singlepart(const std::string& type) const;
void set_body(const std::string& body);
void set_preamble(const std::string& preamble);
void set_epilogue(const std::string& epilogue);
void set_boundary(const std::string& boundary);
void set_parts(const std::vector<Part>& parts);
void set_headers(const std::vector<std::pair<std::string, std::string>>& headers);
void clear();
void clear_body();
// Header manipulation
std::string get_header(const std::string& field) const;
void set_header(const std::string& field, const std::string& value);
std::string& operator[](const std::string& field);
const std::string& operator[](const std::string& field) const;
void append_header(const std::string& field, const std::string& value);
void prepend_header(const std::string& field, const std::string& value);
void erase_header(const std::string& field);
void clear_headers();
// Specialized header functions
std::string get_multipart_type() const;
std::string get_header_value(const std::string& field) const;
std::string get_header_parameter(const std::string& field, const std::string& parameter) const;
void set_header_value(const std::string& field, const std::string& value);
void set_header_parameter(const std::string& field, const std::string& paramter, const std::string& value);
void add_received(const std::string& domain, const std::chrono::system_clock::time_point& date = std::chrono::system_clock::now());
void generate_msgid(const std::string& domain);
void set_date(const std::chrono::system_clock::time_point& date = std::chrono::system_clock::now());
// Part manipulation
Part& append_part(const Part& part = {});
Part& prepend_part(const Part& part = {});
void clear_parts();
void make_multipart(const std::string& type, const std::string& boundary = {});
bool flatten();
std::string get_mime_type() const;
void set_mime_type(const std::string& type);
bool is_mime_type(const std::string& type) const;
bool has_mime_type() const;
// Body and attachments
Part& set_alternative(const std::string& subtype, const std::string& text);
void set_plain(const std::string& text);
void set_html(const std::string& text);
const Part* get_first_matching_part(std::function<bool(const Part&)> predicate) const;
Part* get_first_matching_part(std::function<bool(const Part&)> predicate);
const Part* get_first_matching_part(const std::string& type) const;
Part* get_first_matching_part(const std::string& type);
std::string get_first_matching_body(const std::string& type) const;
std::string get_text() const;
std::string get_plain() const;
std::string get_html() const;
Part& attach(const Part& attachment);
Part& attach(const std::string& data, const std::string& mime_type, const std::string& filename = {});
Part& attach(std::istream& in, const std::string& mime_type, const std::string& filename = {});
std::vector<const Part*> get_attachments() const;
void clear_alternative(const std::string& subtype);
void clear_text();
void clear_plain();
void clear_html();
void clear_attachments();
void simplify();
bool has_text() const;
bool has_plain() const;
bool has_html() const;
bool has_attachments() const;
bool is_attachment() const;
bool is_inline() const;
// Format manipulation
void set_crlf(bool value = true);
};
class Message : public Part {
public:
Message();
};
bool operator==(const Part& lhs, const Part& rhs);
bool operator!=(const Part& lhs, const Part& rhs);
}
inline std::ostream& operator<<(std::ostream& out, const Mimesis::Part& part) {
part.save(out);
return out;
}
inline std::istream& operator>>(std::istream& in, Mimesis::Part& part) {
part.load(in);
return in;
}

View file

@ -0,0 +1,63 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
/* Mimesis -- a library for parsing and creating RFC2822 messages
Copyright © 2017 Guus Sliepen <guus@lightbts.info>
Mimesis is free software; you can redistribute it and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "quoted-printable.hpp"
#include <cstdint>
using namespace std;
string quoted_printable_decode(string_view in) {
string out;
out.reserve(in.size());
int decode = 0;
uint8_t val = 0;
for (auto&& c: in) {
if (decode) {
if (c >= '0' && c <= '9') {
val <<= 4;
val |= c - '0';
decode--;
}
else if (c >= 'A' && c <= 'F') {
val <<= 4;
val |= 10 + (c - 'A');
decode--;
}
else {
decode = 0;
continue;
}
if (decode == 0)
out.push_back(static_cast<char>(val));
}
else {
if (c == '=')
decode = 2;
else
out.push_back(c);
}
}
return out;
}

View file

@ -0,0 +1,25 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#pragma once
/* Mimesis -- a library for parsing and creating RFC2822 messages
Copyright © 2017 Guus Sliepen <guus@lightbts.info>
Mimesis is free software; you can redistribute it and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <string>
#include <string_view>
std::string quoted_printable_decode(std::string_view in);

View file

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

@ -0,0 +1,67 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/gui/formeditredditaccount.h"
#include "miscellaneous/application.h"
#include "miscellaneous/iconfactory.h"
#include "network-web/oauth2service.h"
#include "src/gui/redditaccountdetails.h"
#include "src/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

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

@ -0,0 +1,120 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/gui/redditaccountdetails.h"
#include "exceptions/applicationexception.h"
#include "miscellaneous/application.h"
#include "network-web/oauth2service.h"
#include "network-web/webfactory.h"
#include "src/definitions.h"
#include "src/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

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

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

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

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

@ -0,0 +1,50 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/redditentrypoint.h"
#include "database/databasequeries.h"
#include "definitions/definitions.h"
#include "miscellaneous/application.h"
#include "miscellaneous/iconfactory.h"
#include "src/gui/formeditredditaccount.h"
#include "src/redditserviceroot.h"
#include <QMessageBox>
RedditEntryPoint::RedditEntryPoint(QObject* parent) : QObject(parent) {}
RedditEntryPoint::~RedditEntryPoint() {
qDebugNN << LOGSEC_GMAIL << "Destructing" << QUOTE_W_SPACE(QSL(SERVICE_CODE_NEXTCLOUD)) << "plugin.";
}
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 QObject::tr("Reddit (WIP, no real functionality yet)");
}
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

@ -0,0 +1,26 @@
// 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 QObject, public ServiceEntryPoint {
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.github.martinrotter.rssguard.reddit" FILE "plugin.json")
Q_INTERFACES(ServiceEntryPoint)
public:
explicit RedditEntryPoint(QObject* parent = nullptr);
virtual ~RedditEntryPoint();
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

@ -0,0 +1,322 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/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 "src/definitions.h"
#include "src/redditserviceroot.h"
#include "src/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

@ -0,0 +1,61 @@
// 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 "src/3rd-party/mimesis/mimesis.hpp"
#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

@ -0,0 +1,142 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "src/redditserviceroot.h"
#include "database/databasequeries.h"
#include "miscellaneous/application.h"
#include "network-web/oauth2service.h"
#include "src/gui/formeditredditaccount.h"
#include "src/redditcategory.h"
#include "src/redditentrypoint.h"
#include "src/redditnetworkfactory.h"
#include "src/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

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

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

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