build reddit plugin, fix encoding in oauth handler
This commit is contained in:
parent
7c6854996e
commit
c7f381b098
23 changed files with 2824 additions and 0 deletions
|
@ -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.
|
||||
|
|
31
src/librssguard-reddit/CMakeLists.txt
Normal file
31
src/librssguard-reddit/CMakeLists.txt
Normal 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})
|
5
src/librssguard-reddit/plugin.json
Normal file
5
src/librssguard-reddit/plugin.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "Reddit",
|
||||
"author": "Martin Rotter",
|
||||
"website": "https://github.com/martinrotter/rssguard"
|
||||
}
|
1271
src/librssguard-reddit/src/3rd-party/mimesis/mimesis.cpp
vendored
Normal file
1271
src/librssguard-reddit/src/3rd-party/mimesis/mimesis.cpp
vendored
Normal file
File diff suppressed because it is too large
Load diff
181
src/librssguard-reddit/src/3rd-party/mimesis/mimesis.hpp
vendored
Normal file
181
src/librssguard-reddit/src/3rd-party/mimesis/mimesis.hpp
vendored
Normal 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;
|
||||
}
|
63
src/librssguard-reddit/src/3rd-party/mimesis/quoted-printable.cpp
vendored
Normal file
63
src/librssguard-reddit/src/3rd-party/mimesis/quoted-printable.cpp
vendored
Normal 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;
|
||||
}
|
25
src/librssguard-reddit/src/3rd-party/mimesis/quoted-printable.hpp
vendored
Normal file
25
src/librssguard-reddit/src/3rd-party/mimesis/quoted-printable.hpp
vendored
Normal 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);
|
23
src/librssguard-reddit/src/definitions.h
Normal file
23
src/librssguard-reddit/src/definitions.h
Normal 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
|
67
src/librssguard-reddit/src/gui/formeditredditaccount.cpp
Normal file
67
src/librssguard-reddit/src/gui/formeditredditaccount.cpp
Normal 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());
|
||||
}
|
29
src/librssguard-reddit/src/gui/formeditredditaccount.h
Normal file
29
src/librssguard-reddit/src/gui/formeditredditaccount.h
Normal 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
|
120
src/librssguard-reddit/src/gui/redditaccountdetails.cpp
Normal file
120
src/librssguard-reddit/src/gui/redditaccountdetails.cpp
Normal 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."));
|
||||
}
|
||||
}
|
||||
}
|
44
src/librssguard-reddit/src/gui/redditaccountdetails.h
Normal file
44
src/librssguard-reddit/src/gui/redditaccountdetails.h
Normal 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
|
202
src/librssguard-reddit/src/gui/redditaccountdetails.ui
Normal file
202
src/librssguard-reddit/src/gui/redditaccountdetails.ui
Normal 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>&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>
|
20
src/librssguard-reddit/src/redditcategory.cpp
Normal file
20
src/librssguard-reddit/src/redditcategory.cpp
Normal 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;
|
||||
}
|
||||
}
|
27
src/librssguard-reddit/src/redditcategory.h
Normal file
27
src/librssguard-reddit/src/redditcategory.h
Normal 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
|
50
src/librssguard-reddit/src/redditentrypoint.cpp
Normal file
50
src/librssguard-reddit/src/redditentrypoint.cpp
Normal 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"));
|
||||
}
|
26
src/librssguard-reddit/src/redditentrypoint.h
Normal file
26
src/librssguard-reddit/src/redditentrypoint.h
Normal 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
|
322
src/librssguard-reddit/src/redditnetworkfactory.cpp
Normal file
322
src/librssguard-reddit/src/redditnetworkfactory.cpp
Normal 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();
|
||||
}});
|
||||
}
|
61
src/librssguard-reddit/src/redditnetworkfactory.h
Normal file
61
src/librssguard-reddit/src/redditnetworkfactory.h
Normal 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
|
142
src/librssguard-reddit/src/redditserviceroot.cpp
Normal file
142
src/librssguard-reddit/src/redditserviceroot.cpp
Normal 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();
|
||||
}
|
54
src/librssguard-reddit/src/redditserviceroot.h
Normal file
54
src/librssguard-reddit/src/redditserviceroot.h
Normal 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
|
32
src/librssguard-reddit/src/redditsubscription.cpp
Normal file
32
src/librssguard-reddit/src/redditsubscription.cpp
Normal 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());
|
||||
}
|
28
src/librssguard-reddit/src/redditsubscription.h
Normal file
28
src/librssguard-reddit/src/redditsubscription.h
Normal 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
|
Loading…
Add table
Reference in a new issue