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> <url type="donation">https://github.com/sponsors/martinrotter</url>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
<releases> <releases>
<release version="4.2.3" date="2022-08-19"/> <release version="4.2.3" date="2022-08-22"/>
</releases> </releases>
<content_rating type="oars-1.0"> <content_rating type="oars-1.0">
<content_attribute id="violence-cartoon">none</content_attribute> <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.png</file>
<file>graphics/misc/image-placeholder-error.png</file> <file>graphics/misc/image-placeholder-error.png</file>
<file>graphics/misc/inoreader.png</file> <file>graphics/misc/inoreader.png</file>
<file>graphics/misc/miniflux.png</file>
<file>graphics/misc/newsblur.png</file> <file>graphics/misc/newsblur.png</file>
<file>graphics/misc/nextcloud.png</file> <file>graphics/misc/nextcloud.png</file>
<file>graphics/misc/reddit.png</file> <file>graphics/misc/reddit.png</file>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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