preliminary miniflux support

This commit is contained in:
Martin Rotter 2022-08-22 09:43:10 +02:00
parent a902beaf80
commit 35e9f299a6
12 changed files with 231 additions and 178 deletions

View File

@ -26,7 +26,7 @@
<url type="donation">https://github.com/sponsors/martinrotter</url>
<content_rating type="oars-1.1" />
<releases>
<release version="4.2.3" date="2022-08-19"/>
<release version="4.2.3" date="2022-08-22"/>
</releases>
<content_rating type="oars-1.0">
<content_attribute id="violence-cartoon">none</content_attribute>

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

View File

@ -38,6 +38,7 @@
<file>graphics/misc/image-placeholder.png</file>
<file>graphics/misc/image-placeholder-error.png</file>
<file>graphics/misc/inoreader.png</file>
<file>graphics/misc/miniflux.png</file>
<file>graphics/misc/newsblur.png</file>
<file>graphics/misc/nextcloud.png</file>
<file>graphics/misc/reddit.png</file>

View File

@ -26,11 +26,14 @@
#include <QThread>
#include <QUrl>
GmailNetworkFactory::GmailNetworkFactory(QObject* parent) : QObject(parent),
m_service(nullptr), m_username(QString()), m_batchSize(GMAIL_DEFAULT_BATCH_SIZE),
m_downloadOnlyUnreadMessages(false),
m_oauth2(new OAuth2Service(QSL(GMAIL_OAUTH_AUTH_URL), QSL(GMAIL_OAUTH_TOKEN_URL),
{}, {}, QSL(GMAIL_OAUTH_SCOPE), this)) {
GmailNetworkFactory::GmailNetworkFactory(QObject* parent)
: QObject(parent), m_service(nullptr), m_username(QString()), m_batchSize(GMAIL_DEFAULT_BATCH_SIZE),
m_downloadOnlyUnreadMessages(false), m_oauth2(new OAuth2Service(QSL(GMAIL_OAUTH_AUTH_URL),
QSL(GMAIL_OAUTH_TOKEN_URL),
{},
{},
QSL(GMAIL_OAUTH_SCOPE),
this)) {
initializeOauth();
}
@ -54,19 +57,19 @@ void GmailNetworkFactory::setBatchSize(int batch_size) {
m_batchSize = batch_size;
}
QString GmailNetworkFactory::sendEmail(Mimesis::Message msg, const QNetworkProxy& custom_proxy, Message* reply_to_message) {
QString GmailNetworkFactory::sendEmail(Mimesis::Message msg,
const QNetworkProxy& custom_proxy,
Message* reply_to_message) {
QString bearer = m_oauth2->bearer().toLocal8Bit();
if (bearer.isEmpty()) {
//throw ApplicationException(tr("you aren't logged in"));
// throw ApplicationException(tr("you aren't logged in"));
}
if (reply_to_message != nullptr) {
// We need to obtain some extra information.
auto metadata = getMessageMetadata(reply_to_message->m_customId, {
QSL("References"),
QSL("Message-ID")
}, custom_proxy);
auto metadata =
getMessageMetadata(reply_to_message->m_customId, {QSL("References"), QSL("Message-ID")}, custom_proxy);
if (metadata.contains(QSL("Message-ID"))) {
msg["References"] = metadata.value(QSL("Message-ID")).toStdString();
@ -120,23 +123,23 @@ void GmailNetworkFactory::initializeOauth() {
m_oauth2->setClientSecretSecret(TextFactory::decrypt(QSL(GMAIL_CLIENT_SECRET), OAUTH_DECRYPTION_KEY));
#endif
m_oauth2->setRedirectUrl(QSL(OAUTH_REDIRECT_URI) +
QL1C(':') +
QString::number(GMAIL_OAUTH_REDIRECT_URI_PORT),
true);
m_oauth2->setRedirectUrl(QSL(OAUTH_REDIRECT_URI) + QL1C(':') + QString::number(GMAIL_OAUTH_REDIRECT_URI_PORT), true);
connect(m_oauth2, &OAuth2Service::tokensRetrieveError, this, &GmailNetworkFactory::onTokensError);
connect(m_oauth2, &OAuth2Service::authFailed, this, &GmailNetworkFactory::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)
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());
if (m_service != nullptr && !refresh_token.isEmpty()) {
QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
DatabaseQueries::storeNewOauthTokens(database, refresh_token, m_service->accountId());
}
});
DatabaseQueries::storeNewOauthTokens(database, refresh_token, m_service->accountId());
}
});
}
bool GmailNetworkFactory::downloadOnlyUnreadMessages() const {
@ -197,8 +200,7 @@ QList<Message> GmailNetworkFactory::messages(const QString& stream_id,
}
}
catch (const NetworkException& net_ex) {
qCriticalNN << LOGSEC_GMAIL
<< "Failed to get list of e-mail IDs:" << QUOTE_W_SPACE_DOT(net_ex.message());
qCriticalNN << LOGSEC_GMAIL << "Failed to get list of e-mail IDs:" << QUOTE_W_SPACE_DOT(net_ex.message());
return {};
}
@ -300,7 +302,8 @@ QNetworkReply::NetworkError GmailNetworkFactory::markMessagesRead(RootItem::Read
false,
{},
{},
custom_proxy).m_networkError;
custom_proxy)
.m_networkError;
if (result != QNetworkReply::NetworkError::NoError) {
return result;
@ -359,7 +362,8 @@ QNetworkReply::NetworkError GmailNetworkFactory::markMessagesStarred(RootItem::I
false,
{},
{},
custom_proxy).m_networkError;
custom_proxy)
.m_networkError;
if (result != QNetworkReply::NetworkError::NoError) {
return result;
@ -413,29 +417,29 @@ QStringList GmailNetworkFactory::list(const QString& stream_id,
}
QByteArray messages_raw_data;
auto netw = NetworkFactory::performNetworkOperation(target_url,
timeout,
{},
messages_raw_data,
QNetworkAccessManager::Operation::GetOperation,
{ { QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(),
bearer.toLocal8Bit() } },
false,
{},
{},
custom_proxy);
auto netw =
NetworkFactory::performNetworkOperation(target_url,
timeout,
{},
messages_raw_data,
QNetworkAccessManager::Operation::GetOperation,
{{QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), bearer.toLocal8Bit()}},
false,
{},
{},
custom_proxy);
if (netw.m_networkError == QNetworkReply::NetworkError::NoError) {
// We parse this chunk.
QString messages_data = QString::fromUtf8(messages_raw_data);
message_ids << decodeLiteMessages(messages_data, next_page_token);
}
else {
throw NetworkException(netw.m_networkError, tr("failed to download IDs of e-mail messages"));
}
} while (!next_page_token.isEmpty() && (max_results <= 0 || message_ids.size() < max_results));
}
while (!next_page_token.isEmpty() && (max_results <= 0 || message_ids.size() < max_results));
return message_ids;
}
@ -463,7 +467,8 @@ QVariantHash GmailNetworkFactory::getProfile(const QNetworkProxy& custom_proxy)
false,
{},
{},
custom_proxy).m_networkError;
custom_proxy)
.m_networkError;
if (result != QNetworkReply::NetworkError::NoError) {
throw NetworkException(result, output);
@ -478,32 +483,30 @@ QVariantHash GmailNetworkFactory::getProfile(const QNetworkProxy& custom_proxy)
void GmailNetworkFactory::onTokensError(const QString& error, const QString& error_description) {
Q_UNUSED(error)
qApp->showGuiMessage(Notification::Event::LoginFailure, {
tr("Gmail: 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();
} });
qApp->showGuiMessage(Notification::Event::LoginFailure,
{tr("Gmail: 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 GmailNetworkFactory::onAuthFailed() {
qApp->showGuiMessage(Notification::Event::LoginFailure, {
tr("Gmail: authorization denied"),
tr("Click this to login again."),
QSystemTrayIcon::MessageIcon::Critical },
{}, {
tr("Login"),
[this]() {
m_oauth2->login();
} });
qApp->showGuiMessage(Notification::Event::LoginFailure,
{tr("Gmail: authorization denied"),
tr("Click this to login again."),
QSystemTrayIcon::MessageIcon::Critical},
{},
{tr("Login"), [this]() {
m_oauth2->login();
}});
}
bool GmailNetworkFactory::fillFullMessage(Message& msg, const QJsonObject& json, const QString& feed_id) {
bool GmailNetworkFactory::fillFullMessage(Message& msg, const QJsonObject& json, const QString& feed_id) const {
QHash<QString, QString> headers;
auto json_headers = json[QSL("payload")].toObject()[QSL("headers")].toArray();
@ -543,7 +546,7 @@ bool GmailNetworkFactory::fillFullMessage(Message& msg, const QJsonObject& json,
}
}
msg.m_author = headers[QSL("From")];
msg.m_author = sanitizeEmailAuthor(headers[QSL("From")]);
msg.m_title = headers[QSL("Subject")];
msg.m_createdFromFeed = true;
msg.m_created = TextFactory::parseDateTime(headers[QSL("Date")]);
@ -587,7 +590,8 @@ bool GmailNetworkFactory::fillFullMessage(Message& msg, const QJsonObject& json,
// We check if it is HTML.
if (msg.m_contents.isEmpty()) {
if (mime.contains(QL1S("text/html"))) {
msg.m_contents = QByteArray::fromBase64(body[QSL("data")].toString().toUtf8(), QByteArray::Base64Option::Base64UrlEncoding);
msg.m_contents =
QByteArray::fromBase64(body[QSL("data")].toString().toUtf8(), QByteArray::Base64Option::Base64UrlEncoding);
if (msg.m_contents.contains(QSL("<body>"))) {
int strt = msg.m_contents.indexOf(QSL("<body>"));
@ -599,20 +603,20 @@ bool GmailNetworkFactory::fillFullMessage(Message& msg, const QJsonObject& json,
}
}
else if (backup_contents.isEmpty()) {
backup_contents = QByteArray::fromBase64(body[QSL("data")].toString().toUtf8(), QByteArray::Base64Option::Base64UrlEncoding);
backup_contents =
QByteArray::fromBase64(body[QSL("data")].toString().toUtf8(), QByteArray::Base64Option::Base64UrlEncoding);
backup_contents = backup_contents
.replace(QSL("\r\n"), QSL("\n"))
.replace(QSL("\n"), QSL("\n"))
.replace(QSL("\n"), QSL("<br/>"));
backup_contents = backup_contents.replace(QSL("\r\n"), QSL("\n"))
.replace(QSL("\n"), QSL("\n"))
.replace(QSL("\n"), QSL("<br/>"));
}
}
}
else if (!filename.isEmpty()) {
// We have attachment.
msg.m_enclosures.append(Enclosure(filename +
QSL(GMAIL_ATTACHMENT_SEP) + body[QSL("attachmentId")].toString(),
filename + QSL(" (%1 KB)").arg(QString::number(body["size"].toInt() / 1000.0))));
msg.m_enclosures.append(Enclosure(filename + QSL(GMAIL_ATTACHMENT_SEP) + body[QSL("attachmentId")].toString(),
filename +
QSL(" (%1 KB)").arg(QString::number(body["size"].toInt() / 1000.0))));
}
}
@ -636,12 +640,10 @@ QMap<QString, QString> GmailNetworkFactory::getMessageMetadata(const QString& ms
QByteArray output;
int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt();
headers.append(QPair<QByteArray, QByteArray>(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(),
bearer.toLocal8Bit()));
headers.append(QPair<QByteArray, QByteArray>(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), bearer.toLocal8Bit()));
QString query = QString("%1/%2?format=metadata&metadataHeaders=%3").arg(QSL(GMAIL_API_MSGS_LIST),
msg_id,
metadata.join(QSL("&metadataHeaders=")));
QString query = QString("%1/%2?format=metadata&metadataHeaders=%3")
.arg(QSL(GMAIL_API_MSGS_LIST), msg_id, metadata.join(QSL("&metadataHeaders=")));
NetworkResult res = NetworkFactory::performNetworkOperation(query,
timeout,
QByteArray(),
@ -673,7 +675,7 @@ QMap<QString, QString> GmailNetworkFactory::getMessageMetadata(const QString& ms
QList<Message> GmailNetworkFactory::obtainAndDecodeFullMessages(const QStringList& message_ids,
const QString& feed_id,
const QNetworkProxy& custom_proxy) {
const QNetworkProxy& custom_proxy) const {
QHash<QString, Message> msgs;
int next_message = 0;
QString bearer = m_oauth2->bearer();
@ -687,7 +689,7 @@ QList<Message> GmailNetworkFactory::obtainAndDecodeFullMessages(const QStringLis
multi->setContentType(QHttpMultiPart::ContentType::MixedType);
for (int window = next_message + 100; next_message < window && next_message < message_ids.size(); next_message++ ) {
for (int window = next_message + 100; next_message < window && next_message < message_ids.size(); next_message++) {
QString msg_id = message_ids[next_message];
Message msg;
QHttpPart part;
@ -706,8 +708,7 @@ QList<Message> GmailNetworkFactory::obtainAndDecodeFullMessages(const QStringLis
QList<HttpResponse> output;
int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt();
headers.append(QPair<QByteArray, QByteArray>(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(),
bearer.toLocal8Bit()));
headers.append(QPair<QByteArray, QByteArray>(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), bearer.toLocal8Bit()));
NetworkResult res = NetworkFactory::performNetworkOperation(GMAIL_API_BATCH,
timeout,
@ -750,7 +751,7 @@ QList<Message> GmailNetworkFactory::obtainAndDecodeFullMessages(const QStringLis
return msgs.values();
}
QStringList GmailNetworkFactory::decodeLiteMessages(const QString& messages_json_data, QString& next_page_token) {
QStringList GmailNetworkFactory::decodeLiteMessages(const QString& messages_json_data, QString& next_page_token) const {
QList<QString> message_ids;
QJsonObject top_object = QJsonDocument::fromJson(messages_json_data.toUtf8()).object();
QJsonArray json_msgs = top_object[QSL("messages")].toArray();
@ -766,3 +767,7 @@ QStringList GmailNetworkFactory::decodeLiteMessages(const QString& messages_json
return message_ids;
}
QString GmailNetworkFactory::sanitizeEmailAuthor(const QString& author) const {
return author.mid(0, author.indexOf(QL1S(" <"))).replace(QL1S("\""), QString());
}

View File

@ -68,11 +68,12 @@ class GmailNetworkFactory : public QObject {
void onAuthFailed();
private:
bool fillFullMessage(Message& msg, const QJsonObject& json, const QString& feed_id);
bool fillFullMessage(Message& msg, const QJsonObject& json, const QString& feed_id) const;
QList<Message> obtainAndDecodeFullMessages(const QStringList& message_ids,
const QString& feed_id,
const QNetworkProxy& custom_proxy);
QStringList decodeLiteMessages(const QString& messages_json_data, QString& next_page_token);
const QNetworkProxy& custom_proxy) const;
QStringList decodeLiteMessages(const QString& messages_json_data, QString& next_page_token) const;
QString sanitizeEmailAuthor(const QString& author) const;
void initializeOauth();

View File

@ -41,7 +41,7 @@ FormAddEditEmail::FormAddEditEmail(GmailServiceRoot* root, QWidget* parent)
m_possibleRecipients = DatabaseQueries::getAllGmailRecipients(db, m_root->accountId());
auto ctrls = recipientControls();
for (auto* rec: qAsConst(ctrls)) {
for (auto* rec : qAsConst(ctrls)) {
rec->setPossibleRecipients(m_possibleRecipients);
}
}
@ -58,7 +58,10 @@ void FormAddEditEmail::execForReply(Message* original_message) {
m_ui.m_txtSubject->setEnabled(false);
m_ui.m_txtMessage->setFocus();
addRecipientRow(m_originalMessage->m_author);
auto from_header =
m_root->network()->getMessageMetadata(original_message->m_customId, {QSL("FROM")}, m_root->networkProxy());
addRecipientRow(from_header["From"]);
exec();
}
@ -70,15 +73,15 @@ void FormAddEditEmail::execForForward(Message* original_message) {
m_ui.m_txtMessage->setFocus();
// TODO: Obtain "To" header from Gmail API and fill it in too.
const QString forward_header = QSL("<pre>"
"---------- Forwarded message ---------<br/>"
"From: %1<br/>"
"Date: %2<br/>"
"Subject: %3<br/>"
"To: -"
"</pre><br/>").arg(m_originalMessage->m_author,
m_originalMessage->m_created.toString(),
m_originalMessage->m_title);
const QString forward_header =
QSL("<pre>"
"---------- Forwarded message ---------<br/>"
"From: %1<br/>"
"Date: %2<br/>"
"Subject: %3<br/>"
"To: -"
"</pre><br/>")
.arg(m_originalMessage->m_author, m_originalMessage->m_created.toString(), m_originalMessage->m_title);
m_ui.m_txtMessage->setHtml(forward_header + m_originalMessage->m_contents);
m_ui.m_txtMessage->moveCursor(QTextCursor::MoveOperation::Start);
@ -145,9 +148,10 @@ void FormAddEditEmail::onOkClicked() {
msg["Reply-To"] = rec_repl.join(',').toStdString();
}
msg["Subject"] = QSL("=?utf-8?B?%1?=")
.arg(QString(m_ui.m_txtSubject->text().toUtf8().toBase64(QByteArray::Base64Option::Base64UrlEncoding)))
.toStdString();
msg["Subject"] =
QSL("=?utf-8?B?%1?=")
.arg(QString(m_ui.m_txtSubject->text().toUtf8().toBase64(QByteArray::Base64Option::Base64UrlEncoding)))
.toStdString();
// TODO: Maybe use some more advanced subclass of QTextEdit
// to allow to change formatting etc.
@ -161,8 +165,10 @@ void FormAddEditEmail::onOkClicked() {
accept();
}
catch (const ApplicationException& ex) {
MsgBox::show(this, QMessageBox::Icon::Critical,
tr("E-mail NOT sent"), tr("Your e-mail message wasn't sent."),
MsgBox::show(this,
QMessageBox::Icon::Critical,
tr("E-mail NOT sent"),
tr("Your e-mail message wasn't sent."),
QString(),
ex.message());
}

View File

@ -1,60 +1,60 @@
#ifndef GREADER_DEFINITIONS_H
#define GREADER_DEFINITIONS_H
#define GREADER_DEFAULT_BATCH_SIZE 100
#define GREADER_DEFAULT_BATCH_SIZE 100
// URLs.
#define GREADER_URL_REEDAH "https://www.reedah.com"
#define GREADER_URL_TOR "https://theoldreader.com"
#define GREADER_URL_BAZQUX "https://bazqux.com"
#define GREADER_URL_REEDAH "https://www.reedah.com"
#define GREADER_URL_TOR "https://theoldreader.com"
#define GREADER_URL_BAZQUX "https://bazqux.com"
#define GREADER_URL_INOREADER "https://www.inoreader.com"
// States.
#define GREADER_API_STATE_READING_LIST "state/com.google/reading-list"
#define GREADER_API_STATE_READING_LIST "state/com.google/reading-list"
// Means "read" message. If both "reading-list" and "read" are specified, message is READ. If this state
// is not present, message is UNREAD.
#define GREADER_API_STATE_READ "state/com.google/read"
#define GREADER_API_STATE_IMPORTANT "state/com.google/starred"
#define GREADER_API_STATE_READ "state/com.google/read"
#define GREADER_API_STATE_IMPORTANT "state/com.google/starred"
#define GREADER_API_FULL_STATE_READING_LIST "user/-/state/com.google/reading-list"
#define GREADER_API_FULL_STATE_READ "user/-/state/com.google/read"
#define GREADER_API_FULL_STATE_IMPORTANT "user/-/state/com.google/starred"
#define GREADER_API_FULL_STATE_READING_LIST "user/-/state/com.google/reading-list"
#define GREADER_API_FULL_STATE_READ "user/-/state/com.google/read"
#define GREADER_API_FULL_STATE_IMPORTANT "user/-/state/com.google/starred"
// API.
#define GREADER_API_CLIENT_LOGIN "accounts/ClientLogin"
#define GREADER_API_TAG_LIST "reader/api/0/tag/list?output=json"
#define GREADER_API_SUBSCRIPTION_LIST "reader/api/0/subscription/list?output=json"
#define GREADER_API_STREAM_CONTENTS "reader/api/0/stream/contents/%1?output=json&n=%2"
#define GREADER_API_EDIT_TAG "reader/api/0/edit-tag"
#define GREADER_API_ITEM_IDS "reader/api/0/stream/items/ids?output=json&n=%2&s=%1"
#define GREADER_API_ITEM_CONTENTS "reader/api/0/stream/items/contents?output=json&n=200000"
#define GREADER_API_TOKEN "reader/api/0/token"
#define GREADER_API_USER_INFO "reader/api/0/user-info?output=json"
#define GREADER_API_CLIENT_LOGIN "accounts/ClientLogin"
#define GREADER_API_TAG_LIST "reader/api/0/tag/list?output=json"
#define GREADER_API_SUBSCRIPTION_LIST "reader/api/0/subscription/list?output=json"
#define GREADER_API_STREAM_CONTENTS "reader/api/0/stream/contents/%1?output=json&n=%2"
#define GREADER_API_EDIT_TAG "reader/api/0/edit-tag"
#define GREADER_API_ITEM_IDS "reader/api/0/stream/items/ids?output=json&n=%2&s=%1"
#define GREADER_API_ITEM_CONTENTS "reader/api/0/stream/items/contents?output=json&n=200000"
#define GREADER_API_TOKEN "reader/api/0/token"
#define GREADER_API_USER_INFO "reader/api/0/user-info?output=json"
// Misc.
#define GREADET_API_ITEM_IDS_MAX 200000
#define GREADER_API_EDIT_TAG_BATCH 200
#define GREADET_API_ITEM_IDS_MAX 200000
#define GREADER_API_EDIT_TAG_BATCH 200
#define GREADER_API_ITEM_CONTENTS_BATCH 999
#define GREADER_GLOBAL_UPDATE_THRES 0.3
#define GREADER_GLOBAL_UPDATE_THRES 0.3
// The Old Reader.
#define TOR_SPONSORED_STREAM_ID "tor/sponsored"
#define TOR_ITEM_CONTENTS_BATCH 9999
#define TOR_SPONSORED_STREAM_ID "tor/sponsored"
#define TOR_ITEM_CONTENTS_BATCH 9999
// Inoreader.
#define INO_ITEM_CONTENTS_BATCH 250
#define INO_ITEM_CONTENTS_BATCH 250
#define INO_HEADER_APPID "AppId"
#define INO_HEADER_APPKEY "AppKey"
#define INO_HEADER_APPID "AppId"
#define INO_HEADER_APPKEY "AppKey"
#define INO_OAUTH_REDIRECT_URI_PORT 14488
#define INO_OAUTH_SCOPE "read write"
#define INO_OAUTH_TOKEN_URL "https://www.inoreader.com/oauth2/token"
#define INO_OAUTH_AUTH_URL "https://www.inoreader.com/oauth2/auth"
#define INO_REG_API_URL "https://www.inoreader.com/developers/register-app"
#define INO_OAUTH_REDIRECT_URI_PORT 14488
#define INO_OAUTH_SCOPE "read write"
#define INO_OAUTH_TOKEN_URL "https://www.inoreader.com/oauth2/token"
#define INO_OAUTH_AUTH_URL "https://www.inoreader.com/oauth2/auth"
#define INO_REG_API_URL "https://www.inoreader.com/developers/register-app"
// FreshRSS.
#define FRESHRSS_BASE_URL_PATH "api/greader.php/"
#define FRESHRSS_BASE_URL_PATH "api/greader.php/"
#endif // GREADER_DEFINITIONS_H

View File

@ -73,7 +73,7 @@ QNetworkReply::NetworkError GreaderNetwork::editLabels(const QString& state,
args += working_subset.join(QL1C('&'));
if (m_service == GreaderServiceRoot::Service::Reedah) {
args += QSL("&T=%1").arg(m_authToken);
args += QSL("&%1").arg(tokenParameter());
}
// We send this batch.
@ -493,7 +493,13 @@ QList<Message> GreaderNetwork::itemContents(ServiceRoot* root,
: QUrl::toPercentEncoding(id));
})
.toStdList();
QByteArray input = FROM_STD_LIST(QStringList, inp).join(QSL("&")).toUtf8();
QStringList inp_s = FROM_STD_LIST(QStringList, inp);
if (m_service == GreaderServiceRoot::Service::Reedah || m_service == GreaderServiceRoot::Service::Miniflux) {
inp_s.append(tokenParameter());
}
QByteArray input = inp_s.join(QSL("&")).toUtf8();
QByteArray output_stream;
auto result_stream =
NetworkFactory::performNetworkOperation(full_url,
@ -861,7 +867,7 @@ QNetworkReply::NetworkError GreaderNetwork::clientLogin(const QNetworkProxy& pro
return QNetworkReply::NetworkError::InternalServerError;
}
if (m_service == GreaderServiceRoot::Service::Reedah) {
if (m_service == GreaderServiceRoot::Service::Reedah || m_service == GreaderServiceRoot::Service::Miniflux) {
// We need "T=" token for editing.
full_url = generateFullUrl(Operations::Token);
@ -929,6 +935,10 @@ QPair<QByteArray, QByteArray> GreaderNetwork::authHeader() const {
}
}
QString GreaderNetwork::tokenParameter() const {
return QSL("T=%1").arg(m_authToken);
}
bool GreaderNetwork::ensureLogin(const QNetworkProxy& proxy, QNetworkReply::NetworkError* output) {
if (m_service == GreaderServiceRoot::Service::Inoreader) {
return !m_oauth->bearer().isEmpty();

View File

@ -12,7 +12,7 @@
class OAuth2Service;
class GreaderNetwork : public QObject {
Q_OBJECT
Q_OBJECT
public:
enum class Operations {
@ -83,15 +83,24 @@ class GreaderNetwork : public QObject {
void setOauth(OAuth2Service* oauth);
// API methods.
QNetworkReply::NetworkError editLabels(const QString& state, bool assign,
const QStringList& msg_custom_ids, const QNetworkProxy& proxy);
QNetworkReply::NetworkError editLabels(const QString& state,
bool assign,
const QStringList& msg_custom_ids,
const QNetworkProxy& proxy);
QVariantHash userInfo(const QNetworkProxy& proxy);
QStringList itemIds(const QString& stream_id, bool unread_only, const QNetworkProxy& proxy, int max_count = -1,
QStringList itemIds(const QString& stream_id,
bool unread_only,
const QNetworkProxy& proxy,
int max_count = -1,
QDate newer_than = {});
QList<Message> itemContents(ServiceRoot* root, const QList<QString>& stream_ids,
Feed::Status& error, const QNetworkProxy& proxy);
QList<Message> streamContents(ServiceRoot* root, const QString& stream_id,
Feed::Status& error, const QNetworkProxy& proxy);
QList<Message> itemContents(ServiceRoot* root,
const QList<QString>& stream_ids,
Feed::Status& error,
const QNetworkProxy& proxy);
QList<Message> streamContents(ServiceRoot* root,
const QString& stream_id,
Feed::Status& error,
const QNetworkProxy& proxy);
QNetworkReply::NetworkError clientLogin(const QNetworkProxy& proxy);
QDate newerThanFilter() const;
@ -103,6 +112,7 @@ class GreaderNetwork : public QObject {
private:
QPair<QByteArray, QByteArray> authHeader() const;
QString tokenParameter() const;
// Make sure we are logged in and if we are not, return error.
bool ensureLogin(const QNetworkProxy& proxy, QNetworkReply::NetworkError* output = nullptr);
@ -112,8 +122,14 @@ class GreaderNetwork : public QObject {
QString simplifyStreamId(const QString& stream_id) const;
QStringList decodeItemIds(const QString& stream_json_data, QString& continuation);
QList<Message> decodeStreamContents(ServiceRoot* root, const QString& stream_json_data, const QString& stream_id, QString& continuation);
RootItem* decodeTagsSubscriptions(const QString& categories, const QString& feeds, bool obtain_icons, const QNetworkProxy& proxy);
QList<Message> decodeStreamContents(ServiceRoot* root,
const QString& stream_json_data,
const QString& stream_id,
QString& continuation);
RootItem* decodeTagsSubscriptions(const QString& categories,
const QString& feeds,
bool obtain_icons,
const QNetworkProxy& proxy);
QString sanitizedBaseUrl() const;
QString generateFullUrl(Operations operation) const;

View File

@ -17,8 +17,7 @@
#include "services/greader/greadernetwork.h"
#include "services/greader/gui/formeditgreaderaccount.h"
GreaderServiceRoot::GreaderServiceRoot(RootItem* parent)
: ServiceRoot(parent), m_network(new GreaderNetwork(this)) {
GreaderServiceRoot::GreaderServiceRoot(RootItem* parent) : ServiceRoot(parent), m_network(new GreaderNetwork(this)) {
setIcon(GreaderEntryPoint().icon());
m_network->setRoot(this);
}
@ -91,7 +90,8 @@ void GreaderServiceRoot::setCustomDatabaseData(const QVariantHash& data) {
}
void GreaderServiceRoot::aboutToBeginFeedFetching(const QList<Feed*>& feeds,
const QHash<QString, QHash<BagOfMessages, QStringList>>& stated_messages,
const QHash<QString, QHash<BagOfMessages, QStringList>>&
stated_messages,
const QHash<QString, QStringList>& tagged_messages) {
if (m_network->intelligentSynchronization()) {
m_network->prepareFeedFetching(this, feeds, stated_messages, tagged_messages, networkProxy());
@ -118,13 +118,17 @@ QString GreaderServiceRoot::serviceToString(Service service) {
case Service::Inoreader:
return QSL("Inoreader");
case Service::Miniflux:
return QSL("Miniflux");
default:
return tr("Other services");
}
}
QList<Message> GreaderServiceRoot::obtainNewMessages(Feed* feed,
const QHash<ServiceRoot::BagOfMessages, QStringList>& stated_messages,
const QHash<ServiceRoot::BagOfMessages, QStringList>&
stated_messages,
const QHash<QString, QStringList>& tagged_messages) {
Feed::Status error = Feed::Status::Normal;
QList<Message> msgs;
@ -207,7 +211,8 @@ void GreaderServiceRoot::saveAllCachedData(bool ignore_errors) {
QList<Message> messages = j.value();
if (!messages.isEmpty()) {
QStringList custom_ids; custom_ids.reserve(messages.size());
QStringList custom_ids;
custom_ids.reserve(messages.size());
for (const Message& msg : messages) {
custom_ids.append(msg.m_customId);
@ -231,7 +236,8 @@ void GreaderServiceRoot::saveAllCachedData(bool ignore_errors) {
QStringList messages = k.value();
if (!messages.isEmpty()) {
if (network()->editLabels(label_custom_id, true, messages, networkProxy()) != QNetworkReply::NetworkError::NoError &&
if (network()->editLabels(label_custom_id, true, messages, networkProxy()) !=
QNetworkReply::NetworkError::NoError &&
!ignore_errors) {
addLabelsAssignmentsToCache(messages, label_custom_id, true);
}
@ -247,7 +253,8 @@ void GreaderServiceRoot::saveAllCachedData(bool ignore_errors) {
QStringList messages = l.value();
if (!messages.isEmpty()) {
if (network()->editLabels(label_custom_id, false, messages, networkProxy()) != QNetworkReply::NetworkError::NoError &&
if (network()->editLabels(label_custom_id, false, messages, networkProxy()) !=
QNetworkReply::NetworkError::NoError &&
!ignore_errors) {
addLabelsAssignmentsToCache(messages, label_custom_id, false);
}
@ -285,6 +292,10 @@ void GreaderServiceRoot::updateTitleIcon() {
setIcon(qApp->icons()->miscIcon(QSL("inoreader")));
break;
case Service::Miniflux:
setIcon(qApp->icons()->miscIcon(QSL("miniflux")));
break;
default:
setIcon(GreaderEntryPoint().icon());
break;

View File

@ -9,7 +9,7 @@
class GreaderNetwork;
class GreaderServiceRoot : public ServiceRoot, public CacheForServiceRoot {
Q_OBJECT
Q_OBJECT
public:
enum class Service {
@ -18,9 +18,12 @@ class GreaderServiceRoot : public ServiceRoot, public CacheForServiceRoot {
Bazqux = 4,
Reedah = 8,
Inoreader = 16,
Miniflux = 32,
Other = 1024
};
Q_ENUM(Service)
explicit GreaderServiceRoot(RootItem* parent = nullptr);
virtual bool isSyncable() const;
@ -33,7 +36,8 @@ class GreaderServiceRoot : public ServiceRoot, public CacheForServiceRoot {
virtual QVariantHash customDatabaseData() const;
virtual void setCustomDatabaseData(const QVariantHash& data);
virtual void aboutToBeginFeedFetching(const QList<Feed*>& feeds,
const QHash<QString, QHash<ServiceRoot::BagOfMessages, QStringList>>& stated_messages,
const QHash<QString, QHash<ServiceRoot::BagOfMessages, QStringList>>&
stated_messages,
const QHash<QString, QStringList>& tagged_messages);
virtual QList<Message> obtainNewMessages(Feed* feed,
const QHash<ServiceRoot::BagOfMessages, QStringList>& stated_messages,

View File

@ -12,18 +12,17 @@
#include "services/greader/definitions.h"
#include "services/greader/greadernetwork.h"
#include <QMetaEnum>
#include <QVariantHash>
GreaderAccountDetails::GreaderAccountDetails(QWidget* parent) : QWidget(parent),
m_oauth(nullptr), m_lastProxy({}) {
GreaderAccountDetails::GreaderAccountDetails(QWidget* parent) : QWidget(parent), m_oauth(nullptr), m_lastProxy({}) {
m_ui.setupUi(this);
for (auto serv : { GreaderServiceRoot::Service::Bazqux,
GreaderServiceRoot::Service::FreshRss,
GreaderServiceRoot::Service::Inoreader,
GreaderServiceRoot::Service::Reedah,
GreaderServiceRoot::Service::TheOldReader,
GreaderServiceRoot::Service::Other }) {
QMetaEnum me = QMetaEnum::fromType<GreaderServiceRoot::Service>();
for (int i = 0; i < me.keyCount(); i++) {
GreaderServiceRoot::Service serv = static_cast<GreaderServiceRoot::Service>(me.value(i));
m_ui.m_cmbService->addItem(GreaderServiceRoot::serviceToString(serv), QVariant::fromValue(serv));
}
@ -54,12 +53,13 @@ GreaderAccountDetails::GreaderAccountDetails(QWidget* parent) : QWidget(parent),
false);
#if defined(INOREADER_OFFICIAL_SUPPORT)
m_ui.m_lblInfo->setHelpText(tr("There are some preconfigured OAuth tokens so you do not have to fill in your "
"client ID/secret, but it is strongly recommended to obtain your "
"own as preconfigured tokens have limited global usage quota. If you wish "
"to use preconfigured tokens, simply leave all above fields to their default values even "
"if they are empty."),
true);
m_ui.m_lblInfo
->setHelpText(tr("There are some preconfigured OAuth tokens so you do not have to fill in your "
"client ID/secret, but it is strongly recommended to obtain your "
"own as preconfigured tokens have limited global usage quota. If you wish "
"to use preconfigured tokens, simply leave all above fields to their default values even "
"if they are empty."),
true);
#else
m_ui.m_lblInfo->setHelpText(tr("You have to fill in your client ID/secret and also fill in correct redirect URL."),
true);
@ -68,7 +68,10 @@ GreaderAccountDetails::GreaderAccountDetails(QWidget* parent) : QWidget(parent),
connect(m_ui.m_txtPassword->lineEdit(), &BaseLineEdit::textChanged, this, &GreaderAccountDetails::onPasswordChanged);
connect(m_ui.m_txtUsername->lineEdit(), &BaseLineEdit::textChanged, this, &GreaderAccountDetails::onUsernameChanged);
connect(m_ui.m_txtUrl->lineEdit(), &BaseLineEdit::textChanged, this, &GreaderAccountDetails::onUrlChanged);
connect(m_ui.m_cmbService, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &GreaderAccountDetails::fillPredefinedUrl);
connect(m_ui.m_cmbService,
QOverload<int>::of(&QComboBox::currentIndexChanged),
this,
&GreaderAccountDetails::fillPredefinedUrl);
connect(m_ui.m_cbNewAlgorithm, &QCheckBox::toggled, m_ui.m_spinLimitMessages, &MessageCountSpinBox::setDisabled);
connect(m_ui.m_txtAppId->lineEdit(), &BaseLineEdit::textChanged, this, &GreaderAccountDetails::checkOAuthValue);
connect(m_ui.m_txtAppKey->lineEdit(), &BaseLineEdit::textChanged, this, &GreaderAccountDetails::checkOAuthValue);
@ -126,9 +129,7 @@ void GreaderAccountDetails::onAuthGranted() {
m_ui.m_txtUsername->lineEdit()->setText(resp[QSL("userEmail")].toString());
}
catch (const ApplicationException& ex) {
qCriticalNN << LOGSEC_GREADER
<< "Failed to obtain profile with error:"
<< QUOTE_W_SPACE_DOT(ex.message());
qCriticalNN << LOGSEC_GREADER << "Failed to obtain profile with error:" << QUOTE_W_SPACE_DOT(ex.message());
}
}
@ -198,9 +199,7 @@ void GreaderAccountDetails::performTest(const QNetworkProxy& custom_proxy) {
tr("Network error, have you entered correct Nextcloud endpoint and password?"));
}
else {
m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Ok,
tr("You are good to go!"),
tr("Yeah."));
m_ui.m_lblTestResult->setStatus(WidgetWithStatus::StatusType::Ok, tr("You are good to go!"), tr("Yeah."));
}
}
}