aboutsummaryrefslogtreecommitdiff
path: root/lib/jobs
diff options
context:
space:
mode:
authorn-peugnet <n.peugnet@free.fr>2022-10-06 19:27:24 +0200
committern-peugnet <n.peugnet@free.fr>2022-10-06 19:27:24 +0200
commitd911b207f49e936b3e992200796110f0749ed150 (patch)
tree96d20ebb4d074a4c1755e21cb316a52d584daee3 /lib/jobs
parent8ad8a74152c5701b6ca1f9a00487ba9257a439b4 (diff)
parent56c2f2e2b809b9077393eb617828f33d144f5634 (diff)
downloadlibquotient-d911b207f49e936b3e992200796110f0749ed150.tar.gz
libquotient-d911b207f49e936b3e992200796110f0749ed150.zip
New upstream version 0.7.0
Diffstat (limited to 'lib/jobs')
-rw-r--r--lib/jobs/basejob.cpp943
-rw-r--r--lib/jobs/basejob.h776
-rw-r--r--lib/jobs/downloadfilejob.cpp158
-rw-r--r--lib/jobs/downloadfilejob.h45
-rw-r--r--lib/jobs/mediathumbnailjob.cpp63
-rw-r--r--lib/jobs/mediathumbnailjob.h63
-rw-r--r--lib/jobs/postreadmarkersjob.h39
-rw-r--r--lib/jobs/requestdata.cpp33
-rw-r--r--lib/jobs/requestdata.h74
-rw-r--r--lib/jobs/syncjob.cpp160
-rw-r--r--lib/jobs/syncjob.h98
11 files changed, 1285 insertions, 1167 deletions
diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp
index b21173ae..da645a2d 100644
--- a/lib/jobs/basejob.cpp
+++ b/lib/jobs/basejob.cpp
@@ -1,134 +1,217 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library 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 2.1 of the License, or (at your option) any later version.
- *
- * This library 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de>
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "basejob.h"
#include "connectiondata.h"
-#include "util.h"
+#include <QtCore/QRegularExpression>
+#include <QtCore/QTimer>
+#include <QtCore/QMetaEnum>
+#include <QtCore/QPointer>
#include <QtNetwork/QNetworkAccessManager>
-#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
-#include <QtCore/QTimer>
-#include <QtCore/QRegularExpression>
-#include <QtCore/QJsonObject>
+#include <QtNetwork/QNetworkRequest>
-#include <array>
+using namespace Quotient;
+using std::chrono::seconds, std::chrono::milliseconds;
+using namespace std::chrono_literals;
+
+BaseJob::StatusCode BaseJob::Status::fromHttpCode(int httpCode)
+{
+ // Based on https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
+ if (httpCode / 10 == 41) // 41x errors
+ return httpCode == 410 ? IncorrectRequest : NotFound;
+ switch (httpCode) {
+ case 401:
+ return Unauthorised;
+ // clang-format off
+ case 403: case 407: // clang-format on
+ return ContentAccessError;
+ case 404:
+ return NotFound;
+ // clang-format off
+ case 400: case 405: case 406: case 426: case 428: case 505: // clang-format on
+ case 494: // Unofficial nginx "Request header too large"
+ case 497: // Unofficial nginx "HTTP request sent to HTTPS port"
+ return IncorrectRequest;
+ case 429:
+ return TooManyRequests;
+ case 501:
+ case 510:
+ return RequestNotImplemented;
+ case 511:
+ return NetworkAuthRequired;
+ default:
+ return NetworkError;
+ }
+}
-using namespace QMatrixClient;
+QDebug BaseJob::Status::dumpToLog(QDebug dbg) const
+{
+ QDebugStateSaver _s(dbg);
+ dbg.noquote().nospace();
+ if (auto* const k = QMetaEnum::fromType<StatusCode>().valueToKey(code)) {
+ const QByteArray b = k;
+ dbg << b.mid(b.lastIndexOf(':'));
+ } else
+ dbg << code;
+ return dbg << ": " << message;
+}
+
+class BaseJob::Private {
+public:
+ struct JobTimeoutConfig {
+ seconds jobTimeout;
+ seconds nextRetryInterval;
+ };
+
+ // Using an idiom from clang-tidy:
+ // http://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html
+ Private(HttpVerb v, QByteArray endpoint, const QUrlQuery& q,
+ RequestData&& data, bool nt)
+ : verb(v)
+ , apiEndpoint(std::move(endpoint))
+ , requestQuery(q)
+ , requestData(std::move(data))
+ , needsToken(nt)
+ {
+ timer.setSingleShot(true);
+ retryTimer.setSingleShot(true);
+ }
-struct NetworkReplyDeleter : public QScopedPointerDeleteLater
-{
- static inline void cleanup(QNetworkReply* reply)
+ ~Private()
+ {
+ if (reply) {
+ if (reply->isRunning()) {
+ reply->abort();
+ }
+ delete reply;
+ }
+ }
+
+ void sendRequest();
+ /*! \brief Parse the response byte array into JSON
+ *
+ * This calls QJsonDocument::fromJson() on rawResponse, converts
+ * the QJsonParseError result to BaseJob::Status and stores the resulting
+ * JSON in jsonResponse.
+ */
+ Status parseJson();
+
+ ConnectionData* connection = nullptr;
+
+ // Contents for the network request
+ HttpVerb verb;
+ QByteArray apiEndpoint;
+ QHash<QByteArray, QByteArray> requestHeaders;
+ QUrlQuery requestQuery;
+ RequestData requestData;
+ bool needsToken;
+
+ bool inBackground = false;
+
+ // There's no use of QMimeType here because we don't want to match
+ // content types against the known MIME type hierarchy; and at the same
+ // type QMimeType is of little help with MIME type globs (`text/*` etc.)
+ QByteArrayList expectedContentTypes { "application/json" };
+
+ QByteArrayList expectedKeys;
+
+ // When the QNetworkAccessManager is destroyed it destroys all pending replies.
+ // Using QPointer allows us to know when that happend.
+ QPointer<QNetworkReply> reply;
+
+ Status status = Unprepared;
+ QByteArray rawResponse;
+ /// Contains a null document in case of non-JSON body (for a successful
+ /// or unsuccessful response); a document with QJsonObject or QJsonArray
+ /// in case of a successful response with JSON payload, as per the API
+ /// definition (including an empty JSON object - QJsonObject{});
+ /// and QJsonObject in case of an API error.
+ QJsonDocument jsonResponse;
+ QUrl errorUrl; //< May contain a URL to help with some errors
+
+ LoggingCategory logCat = JOBS;
+
+ QTimer timer;
+ QTimer retryTimer;
+
+ static constexpr auto errorStrategy = std::to_array<const JobTimeoutConfig>(
+ { { 90s, 5s }, { 90s, 10s }, { 120s, 30s } });
+ int maxRetries = int(errorStrategy.size());
+ int retriesTaken = 0;
+
+ [[nodiscard]] const JobTimeoutConfig& getCurrentTimeoutConfig() const
{
- if (reply && reply->isRunning())
- reply->abort();
- QScopedPointerDeleteLater::cleanup(reply);
+ return errorStrategy[std::min(size_t(retriesTaken),
+ errorStrategy.size() - 1)];
}
-};
-class BaseJob::Private
-{
- public:
- // Using an idiom from clang-tidy:
- // http://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html
- Private(HttpVerb v, QString endpoint, const QUrlQuery& q,
- Data&& data, bool nt)
- : verb(v), apiEndpoint(std::move(endpoint)), requestQuery(q)
- , requestData(std::move(data)), needsToken(nt)
- { }
-
- void sendRequest(bool inBackground);
- const JobTimeoutConfig& getCurrentTimeoutConfig() const;
-
- const ConnectionData* connection = nullptr;
-
- // Contents for the network request
- HttpVerb verb;
- QString apiEndpoint;
- QHash<QByteArray, QByteArray> requestHeaders;
- QUrlQuery requestQuery;
- Data requestData;
- bool needsToken;
-
- // There's no use of QMimeType here because we don't want to match
- // content types against the known MIME type hierarchy; and at the same
- // type QMimeType is of little help with MIME type globs (`text/*` etc.)
- QByteArrayList expectedContentTypes;
-
- QScopedPointer<QNetworkReply, NetworkReplyDeleter> reply;
- Status status = Pending;
- QByteArray rawResponse;
- QUrl errorUrl; //< May contain a URL to help with some errors
-
- QTimer timer;
- QTimer retryTimer;
-
- QVector<JobTimeoutConfig> errorStrategy =
- { { 90, 5 }, { 90, 10 }, { 120, 30 } };
- int maxRetries = errorStrategy.size();
- int retriesTaken = 0;
-
- LoggingCategory logCat = JOBS;
+ [[nodiscard]] QString dumpRequest() const
+ {
+ static const std::array verbs { "GET"_ls, "PUT"_ls, "POST"_ls,
+ "DELETE"_ls };
+ const auto verbWord = verbs.at(size_t(verb));
+ return verbWord % ' '
+ % (reply ? reply->url().toString(QUrl::RemoveQuery)
+ : makeRequestUrl(connection->baseUrl(), apiEndpoint)
+ .toString());
+ }
};
-BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, bool needsToken)
- : BaseJob(verb, name, endpoint, Query { }, Data { }, needsToken)
-{ }
-
-BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
- const Query& query, Data&& data, bool needsToken)
- : d(new Private(verb, endpoint, query, std::move(data), needsToken))
+inline bool isHex(QChar c)
{
- setObjectName(name);
- setExpectedContentTypes({ "application/json" });
- d->timer.setSingleShot(true);
- connect (&d->timer, &QTimer::timeout, this, &BaseJob::timeout);
+ return c.isDigit() || (c >= u'A' && c <= u'F') || (c >= u'a' && c <= u'f');
}
-BaseJob::~BaseJob()
+QByteArray BaseJob::encodeIfParam(const QString& paramPart)
{
- stop();
- qCDebug(d->logCat) << this << "destroyed";
+ const auto percentIndex = paramPart.indexOf('%');
+ if (percentIndex != -1 && paramPart.size() > percentIndex + 2
+ && isHex(paramPart[percentIndex + 1])
+ && isHex(paramPart[percentIndex + 2])) {
+ qCWarning(JOBS)
+ << "Developers, upfront percent-encoding of job parameters is "
+ "deprecated since libQuotient 0.7; the string involved is"
+ << paramPart;
+ return QUrl(paramPart, QUrl::TolerantMode).toEncoded();
+ }
+ return QUrl::toPercentEncoding(paramPart);
}
-QUrl BaseJob::requestUrl() const
-{
- return d->reply ? d->reply->request().url() : QUrl();
-}
+BaseJob::BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint,
+ bool needsToken)
+ : BaseJob(verb, name, std::move(endpoint), QUrlQuery {}, RequestData {},
+ needsToken)
+{}
-bool BaseJob::isBackground() const
+BaseJob::BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint,
+ const QUrlQuery& query, RequestData&& data, bool needsToken)
+ : d(makeImpl<Private>(verb, std::move(endpoint), query, std::move(data),
+ needsToken))
{
- return d->reply && d->reply->request().attribute(
- QNetworkRequest::BackgroundRequestAttribute).toBool();
+ setObjectName(name);
+ connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout);
+ connect(&d->retryTimer, &QTimer::timeout, this, [this] {
+ qCDebug(d->logCat) << "Retrying" << this;
+ d->connection->submit(this);
+ });
}
-const QString& BaseJob::apiEndpoint() const
+BaseJob::~BaseJob()
{
- return d->apiEndpoint;
+ stop();
+ d->retryTimer.stop(); // See #398
+ qCDebug(d->logCat) << this << "destroyed";
}
-void BaseJob::setApiEndpoint(const QString& apiEndpoint)
-{
- d->apiEndpoint = apiEndpoint;
-}
+QUrl BaseJob::requestUrl() const { return d->reply ? d->reply->url() : QUrl(); }
-const BaseJob::headers_t&BaseJob::requestHeaders() const
+bool BaseJob::isBackground() const { return d->inBackground; }
+
+const BaseJob::headers_t& BaseJob::requestHeaders() const
{
return d->requestHeaders;
}
@@ -144,22 +227,16 @@ void BaseJob::setRequestHeaders(const BaseJob::headers_t& headers)
d->requestHeaders = headers;
}
-const QUrlQuery& BaseJob::query() const
-{
- return d->requestQuery;
-}
+QUrlQuery BaseJob::query() const { return d->requestQuery; }
void BaseJob::setRequestQuery(const QUrlQuery& query)
{
d->requestQuery = query;
}
-const BaseJob::Data& BaseJob::requestData() const
-{
- return d->requestData;
-}
+const RequestData& BaseJob::requestData() const { return d->requestData; }
-void BaseJob::setRequestData(Data&& data)
+void BaseJob::setRequestData(RequestData&& data)
{
std::swap(d->requestData, data);
}
@@ -179,159 +256,190 @@ void BaseJob::setExpectedContentTypes(const QByteArrayList& contentTypes)
d->expectedContentTypes = contentTypes;
}
-QUrl BaseJob::makeRequestUrl(QUrl baseUrl,
- const QString& path, const QUrlQuery& query)
+QByteArrayList BaseJob::expectedKeys() const { return d->expectedKeys; }
+
+void BaseJob::addExpectedKey(const QByteArray& key) { d->expectedKeys << key; }
+
+void BaseJob::setExpectedKeys(const QByteArrayList& keys)
{
- auto pathBase = baseUrl.path();
- if (!pathBase.endsWith('/') && !path.startsWith('/'))
- pathBase.push_back('/');
+ d->expectedKeys = keys;
+}
+
+const QNetworkReply* BaseJob::reply() const { return d->reply.data(); }
- baseUrl.setPath( pathBase + path );
+QNetworkReply* BaseJob::reply() { return d->reply.data(); }
+
+QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QByteArray& encodedPath,
+ const QUrlQuery& query)
+{
+ // Make sure the added path is relative even if it's not (the official
+ // API definitions have the leading slash though it's not really correct).
+ const auto pathUrl =
+ QUrl::fromEncoded(encodedPath.mid(encodedPath.startsWith('/')),
+ QUrl::StrictMode);
+ Q_ASSERT_X(pathUrl.isValid(), __FUNCTION__,
+ qPrintable(pathUrl.errorString()));
+ baseUrl = baseUrl.resolved(pathUrl);
baseUrl.setQuery(query);
return baseUrl;
}
-void BaseJob::Private::sendRequest(bool inBackground)
+void BaseJob::Private::sendRequest()
{
- QNetworkRequest req
- { makeRequestUrl(connection->baseUrl(), apiEndpoint, requestQuery) };
+ QNetworkRequest req { makeRequestUrl(connection->baseUrl(), apiEndpoint,
+ requestQuery) };
if (!requestHeaders.contains("Content-Type"))
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- req.setRawHeader("Authorization",
- QByteArray("Bearer ") + connection->accessToken());
+ if (needsToken)
+ req.setRawHeader("Authorization",
+ QByteArray("Bearer ") + connection->accessToken());
req.setAttribute(QNetworkRequest::BackgroundRequestAttribute, inBackground);
-#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
- req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+ req.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
+ QNetworkRequest::NoLessSafeRedirectPolicy);
req.setMaximumRedirectsAllowed(10);
-#endif
req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true);
-#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
- // some sources claim that there are issues with QT 5.8
- req.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
-#endif
+ // Qt doesn't combine HTTP2 with SSL quite right, occasionally crashing at
+ // what seems like an attempt to write to a closed channel. If/when that
+ // changes, false should be turned to true below.
+ req.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
+ Q_ASSERT(req.url().isValid());
for (auto it = requestHeaders.cbegin(); it != requestHeaders.cend(); ++it)
req.setRawHeader(it.key(), it.value());
- switch( verb )
- {
- case HttpVerb::Get:
- reply.reset( connection->nam()->get(req) );
- break;
- case HttpVerb::Post:
- reply.reset( connection->nam()->post(req, requestData.source()) );
- break;
- case HttpVerb::Put:
- reply.reset( connection->nam()->put(req, requestData.source()) );
- break;
- case HttpVerb::Delete:
- reply.reset( connection->nam()->deleteResource(req) );
- break;
+
+ switch (verb) {
+ case HttpVerb::Get:
+ reply = connection->nam()->get(req);
+ break;
+ case HttpVerb::Post:
+ reply = connection->nam()->post(req, requestData.source());
+ break;
+ case HttpVerb::Put:
+ reply = connection->nam()->put(req, requestData.source());
+ break;
+ case HttpVerb::Delete:
+ reply = connection->nam()->sendCustomRequest(req, "DELETE", requestData.source());
+ break;
}
}
-void BaseJob::beforeStart(const ConnectionData*)
-{ }
+void BaseJob::doPrepare() { }
-void BaseJob::afterStart(const ConnectionData*, QNetworkReply*)
-{ }
+void BaseJob::onSentRequest(QNetworkReply*) { }
-void BaseJob::beforeAbandon(QNetworkReply*)
-{ }
+void BaseJob::beforeAbandon() { }
-void BaseJob::start(const ConnectionData* connData, bool inBackground)
+void BaseJob::initiate(ConnectionData* connData, bool inBackground)
{
- d->connection = connData;
- d->retryTimer.setSingleShot(true);
- connect (&d->retryTimer, &QTimer::timeout,
- this, [this,inBackground] { sendRequest(inBackground); });
+ if (Q_LIKELY(connData && connData->baseUrl().isValid())) {
+ d->inBackground = inBackground;
+ d->connection = connData;
+ doPrepare();
- beforeStart(connData);
- if (status().good())
- sendRequest(inBackground);
- if (status().good())
- afterStart(connData, d->reply.data());
- if (!status().good())
- QTimer::singleShot(0, this, &BaseJob::finishJob);
+ if (d->needsToken && d->connection->accessToken().isEmpty())
+ setStatus(Unauthorised);
+ else if ((d->verb == HttpVerb::Post || d->verb == HttpVerb::Put)
+ && d->requestData.source()
+ && !d->requestData.source()->isReadable()) {
+ setStatus(FileError, "Request data not ready");
+ }
+ Q_ASSERT(status().code != Pending); // doPrepare() must NOT set this
+ if (Q_LIKELY(status().code == Unprepared)) {
+ d->connection->submit(this);
+ return;
+ }
+ qCWarning(d->logCat).noquote()
+ << "Request failed preparation and won't be sent:"
+ << d->dumpRequest();
+ } else {
+ qCCritical(d->logCat)
+ << "Developers, ensure the Connection is valid before using it";
+ Q_ASSERT(false);
+ setStatus(IncorrectRequest, tr("Invalid server connection"));
+ }
+ // The status is no good, finalise
+ QTimer::singleShot(0, this, &BaseJob::finishJob);
}
-void BaseJob::sendRequest(bool inBackground)
+void BaseJob::sendRequest()
{
- emit aboutToStart();
- d->retryTimer.stop(); // In case we were counting down at the moment
- qCDebug(d->logCat) << this << "sending request to" << d->apiEndpoint;
- if (!d->requestQuery.isEmpty())
- qCDebug(d->logCat) << " query:" << d->requestQuery.toString();
- d->sendRequest(inBackground);
- connect( d->reply.data(), &QNetworkReply::finished, this, &BaseJob::gotReply );
- if (d->reply->isRunning())
- {
- connect( d->reply.data(), &QNetworkReply::metaDataChanged,
- this, &BaseJob::checkReply);
- connect( d->reply.data(), &QNetworkReply::uploadProgress,
- this, &BaseJob::uploadProgress);
- connect( d->reply.data(), &QNetworkReply::downloadProgress,
- this, &BaseJob::downloadProgress);
- d->timer.start(getCurrentTimeout());
- qCDebug(d->logCat) << this << "request has been sent";
- emit started();
+ if (status().code == Abandoned) {
+ qCDebug(d->logCat) << "Won't proceed with the abandoned request:"
+ << d->dumpRequest();
+ return;
}
- else
- qCWarning(d->logCat) << this << "request could not start";
+ Q_ASSERT(d->connection && status().code == Pending);
+ qCDebug(d->logCat).noquote() << "Making" << d->dumpRequest();
+ d->needsToken |= d->connection->needsToken(objectName());
+ emit aboutToSendRequest();
+ d->sendRequest();
+ Q_ASSERT(d->reply);
+ connect(reply(), &QNetworkReply::finished, this, [this] {
+ gotReply();
+ finishJob();
+ });
+ if (d->reply->isRunning()) {
+ connect(reply(), &QNetworkReply::metaDataChanged, this,
+ [this] { checkReply(reply()); });
+ connect(reply(), &QNetworkReply::uploadProgress, this,
+ &BaseJob::uploadProgress);
+ connect(reply(), &QNetworkReply::downloadProgress, this,
+ &BaseJob::downloadProgress);
+ d->timer.start(getCurrentTimeout());
+ qCInfo(d->logCat).noquote() << "Sent" << d->dumpRequest();
+ onSentRequest(reply());
+ emit sentRequest();
+ } else
+ qCCritical(d->logCat).noquote()
+ << "Request could not start:" << d->dumpRequest();
}
-void BaseJob::checkReply()
+BaseJob::Status BaseJob::Private::parseJson()
{
- setStatus(doCheckReply(d->reply.data()));
+ QJsonParseError error { 0, QJsonParseError::MissingObject };
+ jsonResponse = QJsonDocument::fromJson(rawResponse, &error);
+ return { error.error == QJsonParseError::NoError ? NoError
+ : IncorrectResponse,
+ error.errorString() };
}
void BaseJob::gotReply()
{
- checkReply();
- if (status().good())
- setStatus(parseReply(d->reply.data()));
- else {
- // FIXME: Factor out to smth like BaseJob::handleError()
- d->rawResponse = d->reply->readAll();
- const auto jsonBody =
- d->reply->rawHeader("Content-Type") == "application/json";
- qCDebug(d->logCat).noquote()
- << "Error body (truncated if long):" << d->rawResponse.left(500);
- if (jsonBody)
- {
- auto json = QJsonDocument::fromJson(d->rawResponse).object();
- const auto errCode = json.value("errcode"_ls).toString();
- if (error() == TooManyRequestsError ||
- errCode == "M_LIMIT_EXCEEDED")
- {
- QString msg = tr("Too many requests");
- auto retryInterval = json.value("retry_after_ms"_ls).toInt(-1);
- if (retryInterval != -1)
- msg += tr(", next retry advised after %1 ms")
- .arg(retryInterval);
- else // We still have to figure some reasonable interval
- retryInterval = getNextRetryInterval();
-
- setStatus(TooManyRequestsError, msg);
-
- // Shortcut to retry instead of executing finishJob()
- stop();
- qCWarning(d->logCat)
- << this << "will retry in" << retryInterval << "ms";
- d->retryTimer.start(retryInterval);
- emit retryScheduled(d->retriesTaken, retryInterval);
- return;
- }
- if (errCode == "M_CONSENT_NOT_GIVEN")
- {
- d->status.code = UserConsentRequiredError;
- d->errorUrl = json.value("consent_uri"_ls).toString();
- }
- else if (!json.isEmpty()) // Not localisable on the client side
- setStatus(IncorrectRequestError,
- json.value("error"_ls).toString());
+ // Defer actually updating the status until it's finalised
+ auto statusSoFar = checkReply(reply());
+ if (statusSoFar.good()
+ && d->expectedContentTypes == QByteArrayList { "application/json" }) //
+ {
+ d->rawResponse = reply()->readAll();
+ statusSoFar = d->parseJson();
+ if (statusSoFar.good() && !expectedKeys().empty()) {
+ const auto& responseObject = jsonData();
+ QByteArrayList missingKeys;
+ for (const auto& k: expectedKeys())
+ if (!responseObject.contains(k))
+ missingKeys.push_back(k);
+ if (!missingKeys.empty())
+ statusSoFar = { IncorrectResponse,
+ tr("Required JSON keys missing: ")
+ + missingKeys.join() };
}
+ setStatus(statusSoFar);
+ if (!status().good()) // Bad JSON in a "good" reply: bail out
+ return;
+ }
+ // If the endpoint expects anything else than just (API-related) JSON
+ // reply()->readAll() is not performed and the whole reply processing
+ // is left to derived job classes: they may read it piecemeal or customise
+ // per content type in prepareResult(), or even have read it already
+ // (see, e.g., DownloadFileJob).
+ if (statusSoFar.good()) {
+ setStatus(prepareResult());
+ return;
}
- finishJob();
+ d->rawResponse = reply()->readAll();
+ qCDebug(d->logCat).noquote()
+ << "Error body (truncated if long):" << rawDataSample(500);
+ setStatus(prepareError(statusSoFar));
}
bool checkContentType(const QByteArray& type, const QByteArrayList& patterns)
@@ -342,138 +450,183 @@ bool checkContentType(const QByteArray& type, const QByteArrayList& patterns)
// ignore possible appendixes of the content type
const auto ctype = type.split(';').front();
- for (const auto& pattern: patterns)
- {
+ for (const auto& pattern: patterns) {
if (pattern.startsWith('*') || ctype == pattern) // Fast lane
return true;
auto patternParts = pattern.split('/');
Q_ASSERT_X(patternParts.size() <= 2, __FUNCTION__,
- "BaseJob: Expected content type should have up to two"
- " /-separated parts; violating pattern: " + pattern);
+ "BaseJob: Expected content type should have up to two"
+ " /-separated parts; violating pattern: "
+ + pattern);
- if (ctype.split('/').front() == patternParts.front() &&
- patternParts.back() == "*")
+ if (ctype.split('/').front() == patternParts.front()
+ && patternParts.back() == "*")
return true; // Exact match already went on fast lane
}
return false;
}
-BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const
+BaseJob::Status BaseJob::checkReply(const QNetworkReply* reply) const
{
- // QNetworkReply error codes seem to be flawed when it comes to HTTP;
- // see, e.g., https://github.com/QMatrixClient/libqmatrixclient/issues/200
- // so check genuine HTTP codes. The below processing is based on
- // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
+ // QNetworkReply error codes are insufficient for our purposes (e.g. they
+ // don't allow to discern HTTP code 429) so check the original code instead
const auto httpCodeHeader =
- reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
- if (!httpCodeHeader.isValid())
- {
- qCWarning(d->logCat) << this << "didn't get valid HTTP headers";
+ reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
+ if (!httpCodeHeader.isValid()) {
+ qCWarning(d->logCat).noquote()
+ << "No valid HTTP headers from" << d->dumpRequest();
return { NetworkError, reply->errorString() };
}
- const QString replyState = reply->isRunning() ?
- QStringLiteral("(tentative)") : QStringLiteral("(final)");
- const auto urlString = '|' + d->reply->url().toDisplayString();
const auto httpCode = httpCodeHeader.toInt();
- const auto reason =
- reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
if (httpCode / 100 == 2) // 2xx
{
- qCDebug(d->logCat).noquote().nospace() << this << urlString;
- qCDebug(d->logCat).noquote() << " " << httpCode << reason << replyState;
+ if (reply->isFinished())
+ qCInfo(d->logCat).noquote() << httpCode << "<-" << d->dumpRequest();
if (!checkContentType(reply->rawHeader("Content-Type"),
- d->expectedContentTypes))
+ d->expectedContentTypes))
return { UnexpectedResponseTypeWarning,
"Unexpected content type of the response" };
return NoError;
}
+ if (reply->isFinished())
+ qCWarning(d->logCat).noquote() << httpCode << "<-" << d->dumpRequest();
- qCWarning(d->logCat).noquote().nospace() << this << urlString;
- qCWarning(d->logCat).noquote() << " " << httpCode << reason << replyState;
- return { [httpCode]() -> StatusCode {
- if (httpCode / 10 == 41)
- return httpCode == 410 ? IncorrectRequestError : NotFoundError;
- switch (httpCode)
- {
- case 401: case 403: case 407:
- return ContentAccessError;
- case 404:
- return NotFoundError;
- case 400: case 405: case 406: case 426: case 428:
- case 505:
- case 494: // Unofficial nginx "Request header too large"
- case 497: // Unofficial nginx "HTTP request sent to HTTPS port"
- return IncorrectRequestError;
- case 429:
- return TooManyRequestsError;
- case 501: case 510:
- return RequestNotImplementedError;
- case 511:
- return NetworkAuthRequiredError;
- default:
- return NetworkError;
- }
- }(), reply->errorString() };
+ auto message = reply->errorString();
+ if (message.isEmpty())
+ message = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute)
+ .toString();
+
+ return Status::fromHttpCode(httpCode, message);
}
-BaseJob::Status BaseJob::parseReply(QNetworkReply* reply)
+BaseJob::Status BaseJob::prepareResult() { return Success; }
+
+BaseJob::Status BaseJob::prepareError(Status currentStatus)
{
- d->rawResponse = reply->readAll();
- QJsonParseError error;
- const auto& json = QJsonDocument::fromJson(d->rawResponse, &error);
- if( error.error == QJsonParseError::NoError )
- return parseJson(json);
- else
- return { IncorrectResponseError, error.errorString() };
+ // Try to make sense of the error payload but be prepared for all kinds
+ // of unexpected stuff (raw HTML, plain text, foreign JSON among those)
+ if (!d->rawResponse.isEmpty()
+ && reply()->rawHeader("Content-Type") == "application/json")
+ d->parseJson();
+
+ // By now, if d->parseJson() above succeeded then jsonData() will return
+ // a valid JSON object - or an empty object otherwise (in which case most
+ // of if's below will fall through retaining the current status)
+ const auto& errorJson = jsonData();
+ const auto errCode = errorJson.value("errcode"_ls).toString();
+ if (error() == TooManyRequests || errCode == "M_LIMIT_EXCEEDED") {
+ QString msg = tr("Too many requests");
+ int64_t retryAfterMs = errorJson.value("retry_after_ms"_ls).toInt(-1);
+ if (retryAfterMs >= 0)
+ msg += tr(", next retry advised after %1 ms").arg(retryAfterMs);
+ else // We still have to figure some reasonable interval
+ retryAfterMs = getNextRetryMs();
+
+ d->connection->limitRate(milliseconds(retryAfterMs));
+
+ return { TooManyRequests, msg };
+ }
+
+ if (errCode == "M_CONSENT_NOT_GIVEN") {
+ d->errorUrl = QUrl(errorJson.value("consent_uri"_ls).toString());
+ return { UserConsentRequired };
+ }
+ if (errCode == "M_UNSUPPORTED_ROOM_VERSION"
+ || errCode == "M_INCOMPATIBLE_ROOM_VERSION")
+ return { UnsupportedRoomVersion,
+ errorJson.contains("room_version"_ls)
+ ? tr("Requested room version: %1")
+ .arg(errorJson.value("room_version"_ls).toString())
+ : errorJson.value("error"_ls).toString() };
+ if (errCode == "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM")
+ return { CannotLeaveRoom,
+ tr("It's not allowed to leave a server notices room") };
+ if (errCode == "M_USER_DEACTIVATED")
+ return { UserDeactivated };
+
+ // Not localisable on the client side
+ if (errorJson.contains("error"_ls)) // Keep the code, update the message
+ return { currentStatus.code, errorJson.value("error"_ls).toString() };
+
+ return currentStatus; // The error payload is not recognised
}
-BaseJob::Status BaseJob::parseJson(const QJsonDocument&)
+QJsonValue BaseJob::takeValueFromJson(const QString& key)
{
- return Success;
+ if (!d->jsonResponse.isObject())
+ return QJsonValue::Undefined;
+ auto o = d->jsonResponse.object();
+ auto v = o.take(key);
+ d->jsonResponse.setObject(o);
+ return v;
}
void BaseJob::stop()
{
+ // This method is (also) used to semi-finalise the job before retrying; so
+ // stop the timeout timer but keep the retry timer running.
d->timer.stop();
- if (d->reply)
- {
+ if (d->reply) {
d->reply->disconnect(this); // Ignore whatever comes from the reply
- if (d->reply->isRunning())
- {
- qCWarning(d->logCat) << this << "stopped without ready network reply";
- d->reply->abort();
+ if (d->reply->isRunning()) {
+ qCWarning(d->logCat)
+ << this << "stopped without ready network reply";
+ d->reply->abort(); // Keep the reply object in case clients need it
}
- }
- else
+ } else
qCWarning(d->logCat) << this << "stopped with empty network reply";
}
void BaseJob::finishJob()
{
stop();
- if ((error() == NetworkError || error() == TimeoutError)
- && d->retriesTaken < d->maxRetries)
- {
- // TODO: The whole retrying thing should be put to ConnectionManager
- // otherwise independently retrying jobs make a bit of notification
- // storm towards the UI.
- const auto retryInterval =
- error() == TimeoutError ? 0 : getNextRetryInterval();
- ++d->retriesTaken;
- qCWarning(d->logCat).nospace() << this << ": retry #" << d->retriesTaken
- << " in " << retryInterval/1000 << " s";
- d->retryTimer.start(retryInterval);
- emit retryScheduled(d->retriesTaken, retryInterval);
+ switch(error()) {
+ case TooManyRequests:
+ emit rateLimited();
+ d->connection->submit(this);
return;
+ case Unauthorised:
+ if (!d->needsToken && !d->connection->accessToken().isEmpty()) {
+ // Rerun with access token (extension of the spec while
+ // https://github.com/matrix-org/matrix-doc/issues/701 is pending)
+ d->connection->setNeedsToken(objectName());
+ qCWarning(d->logCat) << this << "re-running with authentication";
+ emit retryScheduled(d->retriesTaken, 0);
+ d->connection->submit(this);
+ return;
+ }
+ break;
+ case NetworkError:
+ case IncorrectResponse:
+ case Timeout:
+ if (d->retriesTaken < d->maxRetries) {
+ // TODO: The whole retrying thing should be put to
+ // Connection(Manager) otherwise independently retrying jobs make a
+ // bit of notification storm towards the UI.
+ const seconds retryIn = error() == Timeout ? 0s
+ : getNextRetryInterval();
+ ++d->retriesTaken;
+ qCWarning(d->logCat).nospace()
+ << this << ": retry #" << d->retriesTaken << " in "
+ << retryIn.count() << " s";
+ setStatus(Pending, "Pending retry");
+ d->retryTimer.start(retryIn);
+ emit retryScheduled(d->retriesTaken, milliseconds(retryIn).count());
+ return;
+ }
+ [[fallthrough]];
+ default:;
}
- // Notify those interested in any completion of the job (including killing)
+ Q_ASSERT(status().code != Pending);
+
+ // Notify those interested in any completion of the job including abandon()
emit finished(this);
- emit result(this);
+ emit result(this); // abandon() doesn't emit this
if (error())
emit failure(this);
else
@@ -482,107 +635,147 @@ void BaseJob::finishJob()
deleteLater();
}
-const JobTimeoutConfig& BaseJob::Private::getCurrentTimeoutConfig() const
+seconds BaseJob::getCurrentTimeout() const
{
- return errorStrategy[std::min(retriesTaken, errorStrategy.size() - 1)];
+ return d->getCurrentTimeoutConfig().jobTimeout;
}
-BaseJob::duration_t BaseJob::getCurrentTimeout() const
+BaseJob::duration_ms_t BaseJob::getCurrentTimeoutMs() const
{
- return d->getCurrentTimeoutConfig().jobTimeout * 1000;
+ return milliseconds(getCurrentTimeout()).count();
}
-BaseJob::duration_t BaseJob::getNextRetryInterval() const
+seconds BaseJob::getNextRetryInterval() const
{
- return d->getCurrentTimeoutConfig().nextRetryInterval * 1000;
+ return d->getCurrentTimeoutConfig().nextRetryInterval;
}
-BaseJob::duration_t BaseJob::millisToRetry() const
+BaseJob::duration_ms_t BaseJob::getNextRetryMs() const
{
- return d->retryTimer.isActive() ? d->retryTimer.remainingTime() : 0;
+ return milliseconds(getNextRetryInterval()).count();
}
-int BaseJob::maxRetries() const
+milliseconds BaseJob::timeToRetry() const
{
- return d->maxRetries;
+ return d->retryTimer.isActive() ? d->retryTimer.remainingTimeAsDuration()
+ : 0s;
}
-void BaseJob::setMaxRetries(int newMaxRetries)
+BaseJob::duration_ms_t BaseJob::millisToRetry() const
{
- d->maxRetries = newMaxRetries;
+ return timeToRetry().count();
}
-BaseJob::Status BaseJob::status() const
+int BaseJob::maxRetries() const { return d->maxRetries; }
+
+void BaseJob::setMaxRetries(int newMaxRetries)
{
- return d->status;
+ d->maxRetries = newMaxRetries;
}
+BaseJob::Status BaseJob::status() const { return d->status; }
+
QByteArray BaseJob::rawData(int bytesAtMost) const
{
- return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost ?
- d->rawResponse.left(bytesAtMost) + "...(truncated)" : d->rawResponse;
+ return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost
+ ? d->rawResponse.left(bytesAtMost)
+ : d->rawResponse;
}
-QString BaseJob::statusCaption() const
+const QByteArray& BaseJob::rawData() const { return d->rawResponse; }
+
+QString BaseJob::rawDataSample(int bytesAtMost) const
{
- switch (d->status.code)
- {
- case Success:
- return tr("Success");
- case Pending:
- return tr("Request still pending response");
- case UnexpectedResponseTypeWarning:
- return tr("Warning: Unexpected response type");
- case Abandoned:
- return tr("Request was abandoned");
- case NetworkError:
- return tr("Network problems");
- case JsonParseError:
- return tr("Response could not be parsed");
- case TimeoutError:
- return tr("Request timed out");
- case ContentAccessError:
- return tr("Access error");
- case NotFoundError:
- return tr("Not found");
- case IncorrectRequestError:
- return tr("Invalid request");
- case IncorrectResponseError:
- return tr("Response could not be parsed");
- case TooManyRequestsError:
- return tr("Too many requests");
- case RequestNotImplementedError:
- return tr("Function not implemented by the server");
- case NetworkAuthRequiredError:
- return tr("Network authentication required");
- case UserConsentRequiredError:
- return tr("User consent required");
- default:
- return tr("Request failed");
- }
+ auto data = rawData(bytesAtMost);
+ Q_ASSERT(data.size() <= d->rawResponse.size());
+ return data.size() == d->rawResponse.size()
+ ? data
+ : data + tr("...(truncated, %Ln bytes in total)",
+ "Comes after trimmed raw network response",
+ d->rawResponse.size());
}
-int BaseJob::error() const
+QJsonObject BaseJob::jsonData() const
{
- return d->status.code;
+ return d->jsonResponse.object();
}
-QString BaseJob::errorString() const
+QJsonArray BaseJob::jsonItems() const
{
- return d->status.message;
+ return d->jsonResponse.array();
}
-QUrl BaseJob::errorUrl() const
+QString BaseJob::statusCaption() const
{
- return d->errorUrl;
+ switch (d->status.code) {
+ case Success:
+ return tr("Success");
+ case Pending:
+ return tr("Request still pending response");
+ case UnexpectedResponseTypeWarning:
+ return tr("Warning: Unexpected response type");
+ case Abandoned:
+ return tr("Request was abandoned");
+ case NetworkError:
+ return tr("Network problems");
+ case Timeout:
+ return tr("Request timed out");
+ case Unauthorised:
+ return tr("Unauthorised request");
+ case ContentAccessError:
+ return tr("Access error");
+ case NotFound:
+ return tr("Not found");
+ case IncorrectRequest:
+ return tr("Invalid request");
+ case IncorrectResponse:
+ return tr("Response could not be parsed");
+ case TooManyRequests:
+ return tr("Too many requests");
+ case RequestNotImplemented:
+ return tr("Function not implemented by the server");
+ case NetworkAuthRequired:
+ return tr("Network authentication required");
+ case UserConsentRequired:
+ return tr("User consent required");
+ case UnsupportedRoomVersion:
+ return tr("The server does not support the needed room version");
+ default:
+ return tr("Request failed");
+ }
}
+int BaseJob::error() const {
+ return d->status.code; }
+
+QString BaseJob::errorString() const {
+ return d->status.message; }
+
+QUrl BaseJob::errorUrl() const {
+ return d->errorUrl; }
+
void BaseJob::setStatus(Status s)
{
+ // The crash that led to this code has been reported in
+ // https://github.com/quotient-im/Quaternion/issues/566 - basically,
+ // when cleaning up children of a deleted Connection, there's a chance
+ // of pending jobs being abandoned, calling setStatus(Abandoned).
+ // There's nothing wrong with this; however, the safety check for
+ // cleartext access tokens below uses d->connection - which is a dangling
+ // pointer.
+ // To alleviate that, a stricter condition is applied, that for Abandoned
+ // and to-be-Abandoned jobs the status message will be disregarded entirely.
+ // We could rectify the situation by making d->connection a QPointer<>
+ // (and deriving ConnectionData from QObject, respectively) but it's
+ // a too edge case for the hassle.
if (d->status == s)
return;
- if (!d->connection->accessToken().isEmpty())
+ if (d->status.code == Abandoned || s.code == Abandoned)
+ s.message.clear();
+
+ if (!s.message.isEmpty() && d->connection
+ && !d->connection->accessToken().isEmpty())
s.message.replace(d->connection->accessToken(), "(REDACTED)");
if (!s.good())
qCWarning(d->logCat) << this << "status" << s;
@@ -597,9 +790,10 @@ void BaseJob::setStatus(int code, QString message)
void BaseJob::abandon()
{
- beforeAbandon(d->reply.data());
+ beforeAbandon();
+ d->timer.stop();
+ d->retryTimer.stop(); // In case abandon() was called between retries
setStatus(Abandoned);
- this->disconnect();
if (d->reply)
d->reply->disconnect(this);
emit finished(this);
@@ -609,11 +803,8 @@ void BaseJob::abandon()
void BaseJob::timeout()
{
- setStatus( TimeoutError, "The job has timed out" );
+ setStatus(Timeout, "The job has timed out");
finishJob();
}
-void BaseJob::setLoggingCategory(LoggingCategory lcf)
-{
- d->logCat = lcf;
-}
+void BaseJob::setLoggingCategory(LoggingCategory lcf) { d->logCat = lcf; }
diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h
index 4ef25ab8..555c602b 100644
--- a/lib/jobs/basejob.h
+++ b/lib/jobs/basejob.h
@@ -1,339 +1,477 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library 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 2.1 of the License, or (at your option) any later version.
- *
- * This library 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de>
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
-#include "../logging.h"
#include "requestdata.h"
+#include "logging.h"
+#include "converters.h" // Common for csapi/ headers even though not used here
+#include "quotient_common.h" // For DECL_DEPRECATED_ENUMERATOR
#include <QtCore/QObject>
-#include <QtCore/QUrlQuery>
-#include <QtCore/QJsonDocument>
+#include <QtCore/QStringBuilder>
class QNetworkReply;
class QSslError;
-namespace QMatrixClient
-{
- class ConnectionData;
+namespace Quotient {
+class ConnectionData;
+
+enum class HttpVerb { Get, Put, Post, Delete };
- enum class HttpVerb { Get, Put, Post, Delete };
+class QUOTIENT_API BaseJob : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT)
+ Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries)
+ Q_PROPERTY(int statusCode READ error NOTIFY statusChanged)
- struct JobTimeoutConfig
+ static QByteArray encodeIfParam(const QString& paramPart);
+ template <int N>
+ static auto encodeIfParam(const char (&constPart)[N])
{
- int jobTimeout;
- int nextRetryInterval;
+ return constPart;
+ }
+
+public:
+#define WITH_DEPRECATED_ERROR_VERSION(Recommended) \
+ Recommended, DECL_DEPRECATED_ENUMERATOR(Recommended##Error, Recommended)
+
+ /*! The status code of a job
+ *
+ * Every job is created in Unprepared status; upon calling prepare()
+ * from Connection (if things are fine) it go to Pending status. After
+ * that, the next transition comes after the reply arrives and its contents
+ * are analysed. At any point in time the job can be abandon()ed, causing
+ * it to switch to status Abandoned for a brief period before deletion.
+ */
+ enum StatusCode {
+ Success = 0,
+ NoError = Success,
+ Pending = 1,
+ WarningLevel = 20, //< Warnings have codes starting from this
+ UnexpectedResponseType = 21,
+ UnexpectedResponseTypeWarning = UnexpectedResponseType,
+ Unprepared = 25, //< Initial job state is incomplete, hence warning level
+ Abandoned = 50, //< A tiny period between abandoning and object deletion
+ ErrorLevel = 100, //< Errors have codes starting from this
+ NetworkError = 101,
+ WITH_DEPRECATED_ERROR_VERSION(Timeout),
+ Unauthorised,
+ ContentAccessError,
+ WITH_DEPRECATED_ERROR_VERSION(NotFound),
+ WITH_DEPRECATED_ERROR_VERSION(IncorrectRequest),
+ WITH_DEPRECATED_ERROR_VERSION(IncorrectResponse),
+ WITH_DEPRECATED_ERROR_VERSION(TooManyRequests),
+ RateLimited = TooManyRequests,
+ WITH_DEPRECATED_ERROR_VERSION(RequestNotImplemented),
+ WITH_DEPRECATED_ERROR_VERSION(UnsupportedRoomVersion),
+ WITH_DEPRECATED_ERROR_VERSION(NetworkAuthRequired),
+ WITH_DEPRECATED_ERROR_VERSION(UserConsentRequired),
+ CannotLeaveRoom,
+ UserDeactivated,
+ FileError,
+ UserDefinedError = 256
};
+ Q_ENUM(StatusCode)
+
+#undef WITH_DEPRECATED_ERROR_VERSION
- class BaseJob: public QObject
+ template <typename... StrTs>
+ static QByteArray makePath(StrTs&&... parts)
{
- Q_OBJECT
- Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT)
- Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries)
- public:
- /* Just in case, the values are compatible with KJob
- * (which BaseJob used to inherit from). */
- enum StatusCode { NoError = 0 // To be compatible with Qt conventions
- , Success = 0
- , Pending = 1
- , WarningLevel = 20
- , UnexpectedResponseTypeWarning = 21
- , Abandoned = 50 //< A very brief period between abandoning and object deletion
- , ErrorLevel = 100 //< Errors have codes starting from this
- , NetworkError = 100
- , JsonParseError // TODO: Merge into IncorrectResponseError
- , TimeoutError
- , ContentAccessError
- , NotFoundError
- , IncorrectRequestError
- , IncorrectResponseError
- , TooManyRequestsError
- , RequestNotImplementedError
- , NetworkAuthRequiredError
- , UserConsentRequiredError
- , UserDefinedError = 200
- };
-
- /**
- * A simple wrapper around QUrlQuery that allows its creation from
- * a list of string pairs
- */
- class Query : public QUrlQuery
- {
- public:
- using QUrlQuery::QUrlQuery;
- Query() = default;
- Query(const std::initializer_list< QPair<QString, QString> >& l)
- {
- setQueryItems(l);
- }
- };
-
- using Data = RequestData;
-
- /**
- * This structure stores the status of a server call job. The status consists
- * of a code, that is described (but not delimited) by the respective enum,
- * and a freeform message.
- *
- * To extend the list of error codes, define an (anonymous) enum
- * along the lines of StatusCode, with additional values
- * starting at UserDefinedError
- */
- class Status
- {
- public:
- Status(StatusCode c) : code(c) { }
- Status(int c, QString m) : code(c), message(std::move(m)) { }
-
- bool good() const { return code < ErrorLevel; }
- friend QDebug operator<<(QDebug dbg, const Status& s)
- {
- QDebugStateSaver _s(dbg);
- return dbg.noquote().nospace()
- << s.code << ": " << s.message;
- }
-
- bool operator==(const Status& other) const
- {
- return code == other.code && message == other.message;
- }
- bool operator!=(const Status& other) const
- {
- return !operator==(other);
- }
-
- int code;
- QString message;
- };
-
- using duration_t = int; // milliseconds
-
- public:
- BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
- bool needsToken = true);
- BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
- const Query& query, Data&& data = {},
- bool needsToken = true);
-
- QUrl requestUrl() const;
- bool isBackground() const;
-
- /** Current status of the job */
- Status status() const;
- /** Short human-friendly message on the job status */
- QString statusCaption() const;
- /** Raw response body as received from the server */
- QByteArray rawData(int bytesAtMost = -1) const;
-
- /** Error (more generally, status) code
- * Equivalent to status().code
- * \sa status
- */
- int error() const;
- /** Error-specific message, as returned by the server */
- virtual QString errorString() const;
- /** A URL to help/clarify the error, if provided by the server */
- QUrl errorUrl() const;
-
- int maxRetries() const;
- void setMaxRetries(int newMaxRetries);
-
- Q_INVOKABLE duration_t getCurrentTimeout() const;
- Q_INVOKABLE duration_t getNextRetryInterval() const;
- Q_INVOKABLE duration_t millisToRetry() const;
-
- friend QDebug operator<<(QDebug dbg, const BaseJob* j)
- {
- return dbg << j->objectName();
- }
-
- public slots:
- void start(const ConnectionData* connData,
- bool inBackground = false);
-
- /**
- * Abandons the result of this job, arrived or unarrived.
- *
- * This aborts waiting for a reply from the server (if there was
- * any pending) and deletes the job object. No result signals
- * (result, success, failure) are emitted.
- */
- void abandon();
-
- signals:
- /** The job is about to send a network request */
- void aboutToStart();
-
- /** The job has sent a network request */
- void started();
-
- /** The job has changed its status */
- void statusChanged(Status newStatus);
-
- /**
- * The previous network request has failed; the next attempt will
- * be done in the specified time
- * @param nextAttempt the 1-based number of attempt (will always be more than 1)
- * @param inMilliseconds the interval after which the next attempt will be taken
- */
- void retryScheduled(int nextAttempt, int inMilliseconds);
-
- /**
- * Emitted when the job is finished, in any case. It is used to notify
- * observers that the job is terminated and that progress can be hidden.
- *
- * This should not be emitted directly by subclasses;
- * use finishJob() instead.
- *
- * In general, to be notified of a job's completion, client code
- * should connect to result(), success(), or failure()
- * rather than finished(). However if you store a list of jobs
- * and need to track their lifecycle, then you should connect to this
- * instead of result(), to avoid dangling pointers in your list.
- *
- * @param job the job that emitted this signal
- *
- * @see result, success, failure
- */
- void finished(BaseJob* job);
-
- /**
- * Emitted when the job is finished (except when abandoned).
- *
- * Use error() to know if the job was finished with error.
- *
- * @param job the job that emitted this signal
- *
- * @see success, failure
- */
- void result(BaseJob* job);
-
- /**
- * Emitted together with result() in case there's no error.
- *
- * @see result, failure
- */
- void success(BaseJob*);
-
- /**
- * Emitted together with result() if there's an error.
- * Similar to result(), this won't be emitted in case of abandon().
- *
- * @see result, success
- */
- void failure(BaseJob*);
-
- void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
- void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
-
- protected:
- using headers_t = QHash<QByteArray, QByteArray>;
-
- const QString& apiEndpoint() const;
- void setApiEndpoint(const QString& apiEndpoint);
- const headers_t& requestHeaders() const;
- void setRequestHeader(const headers_t::key_type& headerName,
- const headers_t::mapped_type& headerValue);
- void setRequestHeaders(const headers_t& headers);
- const QUrlQuery& query() const;
- void setRequestQuery(const QUrlQuery& query);
- const Data& requestData() const;
- void setRequestData(Data&& data);
- const QByteArrayList& expectedContentTypes() const;
- void addExpectedContentType(const QByteArray& contentType);
- void setExpectedContentTypes(const QByteArrayList& contentTypes);
-
- /** Construct a URL out of baseUrl, path and query
- * The function automatically adds '/' between baseUrl's path and
- * \p path if necessary. The query component of \p baseUrl
- * is ignored.
- */
- static QUrl makeRequestUrl(QUrl baseUrl, const QString& path,
- const QUrlQuery& query = {});
-
- virtual void beforeStart(const ConnectionData* connData);
- virtual void afterStart(const ConnectionData* connData,
- QNetworkReply* reply);
- virtual void beforeAbandon(QNetworkReply*);
-
- /**
- * Used by gotReply() to check the received reply for general
- * issues such as network errors or access denial.
- * Returning anything except NoError/Success prevents
- * further parseReply()/parseJson() invocation.
- *
- * @param reply the reply received from the server
- * @return the result of checking the reply
- *
- * @see gotReply
- */
- virtual Status doCheckReply(QNetworkReply* reply) const;
-
- /**
- * Processes the reply. By default, parses the reply into
- * a QJsonDocument and calls parseJson() if it's a valid JSON.
- *
- * @param reply raw contents of a HTTP reply from the server (without headers)
- *
- * @see gotReply, parseJson
- */
- virtual Status parseReply(QNetworkReply* reply);
-
- /**
- * Processes the JSON document received from the Matrix server.
- * By default returns succesful status without analysing the JSON.
- *
- * @param json valid JSON document received from the server
- *
- * @see parseReply
- */
- virtual Status parseJson(const QJsonDocument&);
-
- void setStatus(Status s);
- void setStatus(int code, QString message);
-
- // Q_DECLARE_LOGGING_CATEGORY return different function types
- // in different versions
- using LoggingCategory = decltype(JOBS)*;
- void setLoggingCategory(LoggingCategory lcf);
-
- // Job objects should only be deleted via QObject::deleteLater
- ~BaseJob() override;
-
- protected slots:
- void timeout();
-
- private slots:
- void sendRequest(bool inBackground);
- void checkReply();
- void gotReply();
-
- private:
- void stop();
- void finishJob();
-
- class Private;
- QScopedPointer<Private> d;
+ return (QByteArray() % ... % encodeIfParam(parts));
+ }
+
+ using Data
+#ifndef Q_CC_MSVC
+ Q_DECL_DEPRECATED_X("Use Quotient::RequestData instead")
+#endif
+ = RequestData;
+
+ /*!
+ * This structure stores the status of a server call job. The status
+ * consists of a code, that is described (but not delimited) by the
+ * respective enum, and a freeform message.
+ *
+ * To extend the list of error codes, define an (anonymous) enum
+ * along the lines of StatusCode, with additional values
+ * starting at UserDefinedError
+ */
+ struct Status {
+ Status(StatusCode c) : code(c) {}
+ Status(int c, QString m) : code(c), message(std::move(m)) {}
+
+ static StatusCode fromHttpCode(int httpCode);
+ static Status fromHttpCode(int httpCode, QString msg)
+ {
+ return { fromHttpCode(httpCode), std::move(msg) };
+ }
+
+ bool good() const { return code < ErrorLevel; }
+ QDebug dumpToLog(QDebug dbg) const;
+ friend QDebug operator<<(const QDebug& dbg, const Status& s)
+ {
+ return s.dumpToLog(dbg);
+ }
+
+ bool operator==(const Status& other) const
+ {
+ return code == other.code && message == other.message;
+ }
+ bool operator!=(const Status& other) const
+ {
+ return !operator==(other);
+ }
+ bool operator==(int otherCode) const
+ {
+ return code == otherCode;
+ }
+ bool operator!=(int otherCode) const
+ {
+ return !operator==(otherCode);
+ }
+
+ int code;
+ QString message;
};
- inline bool isJobRunning(BaseJob* job)
+public:
+ BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint,
+ bool needsToken = true);
+ BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint,
+ const QUrlQuery& query, RequestData&& data = {},
+ bool needsToken = true);
+
+ QUrl requestUrl() const;
+ bool isBackground() const;
+
+ /** Current status of the job */
+ Status status() const;
+
+ /** Short human-friendly message on the job status */
+ QString statusCaption() const;
+
+ /*! Get first bytes of the raw response body as received from the server
+ *
+ * \param bytesAtMost the number of leftmost bytes to return
+ *
+ * \sa rawDataSample
+ */
+ QByteArray rawData(int bytesAtMost) const;
+
+ /*! Access the whole response body as received from the server */
+ const QByteArray& rawData() const;
+
+ /** Get UI-friendly sample of raw data
+ *
+ * This is almost the same as rawData but appends the "truncated"
+ * suffix if not all data fit in bytesAtMost. This call is
+ * recommended to present a sample of raw data as "details" next to
+ * error messages. Note that the default \p bytesAtMost value is
+ * also tailored to UI cases.
+ *
+ * \sa rawData
+ */
+ QString rawDataSample(int bytesAtMost = 65535) const;
+
+ /** Get the response body as a JSON object
+ *
+ * If the job's returned content type is not `application/json`
+ * or if the top-level JSON entity is not an object, an empty object
+ * is returned.
+ */
+ QJsonObject jsonData() const;
+
+ /** Get the response body as a JSON array
+ *
+ * If the job's returned content type is not `application/json`
+ * or if the top-level JSON entity is not an array, an empty array
+ * is returned.
+ */
+ QJsonArray jsonItems() const;
+
+ /** Load the property from the JSON response assuming a given C++ type
+ *
+ * If there's no top-level JSON object in the response or if there's
+ * no node with the key \p keyName, \p defaultValue is returned.
+ */
+ template <typename T, typename StrT>
+ T loadFromJson(const StrT& keyName, T&& defaultValue = {}) const
+ {
+ const auto& jv = jsonData().value(keyName);
+ return jv.isUndefined() ? std::forward<T>(defaultValue)
+ : fromJson<T>(jv);
+ }
+
+ /** Load the property from the JSON response and delete it from JSON
+ *
+ * If there's no top-level JSON object in the response or if there's
+ * no node with the key \p keyName, \p defaultValue is returned.
+ */
+ template <typename T>
+ T takeFromJson(const QString& key, T&& defaultValue = {})
+ {
+ if (const auto& jv = takeValueFromJson(key); !jv.isUndefined())
+ return fromJson<T>(jv);
+
+ return std::forward<T>(defaultValue);
+ }
+
+ /** Error (more generally, status) code
+ * Equivalent to status().code
+ * \sa status
+ */
+ int error() const;
+
+ /** Error-specific message, as returned by the server */
+ virtual QString errorString() const;
+
+ /** A URL to help/clarify the error, if provided by the server */
+ QUrl errorUrl() const;
+
+ int maxRetries() const;
+ void setMaxRetries(int newMaxRetries);
+
+ using duration_ms_t = std::chrono::milliseconds::rep; // normally int64_t
+
+ std::chrono::seconds getCurrentTimeout() const;
+ Q_INVOKABLE Quotient::BaseJob::duration_ms_t getCurrentTimeoutMs() const;
+ std::chrono::seconds getNextRetryInterval() const;
+ Q_INVOKABLE Quotient::BaseJob::duration_ms_t getNextRetryMs() const;
+ std::chrono::milliseconds timeToRetry() const;
+ Q_INVOKABLE Quotient::BaseJob::duration_ms_t millisToRetry() const;
+
+ friend QDebug operator<<(QDebug dbg, const BaseJob* j)
{
- return job && job->error() == BaseJob::Pending;
+ return dbg << j->objectName();
}
-} // namespace QMatrixClient
+
+public Q_SLOTS:
+ void initiate(Quotient::ConnectionData* connData, bool inBackground);
+
+ /**
+ * Abandons the result of this job, arrived or unarrived.
+ *
+ * This aborts waiting for a reply from the server (if there was
+ * any pending) and deletes the job object. No result signals
+ * (result, success, failure) are emitted.
+ */
+ void abandon();
+
+Q_SIGNALS:
+ /** The job is about to send a network request */
+ void aboutToSendRequest();
+
+ /** The job has sent a network request */
+ void sentRequest();
+
+ /** The job has changed its status */
+ void statusChanged(Quotient::BaseJob::Status newStatus);
+
+ /**
+ * The previous network request has failed; the next attempt will
+ * be done in the specified time
+ * @param nextAttempt the 1-based number of attempt (will always be more
+ * than 1)
+ * @param inMilliseconds the interval after which the next attempt will be
+ * taken
+ */
+ void retryScheduled(int nextAttempt,
+ Quotient::BaseJob::duration_ms_t inMilliseconds);
+
+ /**
+ * The previous network request has been rate-limited; the next attempt
+ * will be queued and run sometime later. Since other jobs may already
+ * wait in the queue, it's not possible to predict the wait time.
+ */
+ void rateLimited();
+
+ /**
+ * Emitted when the job is finished, in any case. It is used to notify
+ * observers that the job is terminated and that progress can be hidden.
+ *
+ * This should not be emitted directly by subclasses;
+ * use finishJob() instead.
+ *
+ * In general, to be notified of a job's completion, client code
+ * should connect to result(), success(), or failure()
+ * rather than finished(). However if you need to track the job's
+ * lifecycle you should connect to this instead of result();
+ * in particular, only this signal will be emitted on abandoning.
+ *
+ * @param job the job that emitted this signal
+ *
+ * @see result, success, failure
+ */
+ void finished(Quotient::BaseJob* job);
+
+ /**
+ * Emitted when the job is finished (except when abandoned).
+ *
+ * Use error() to know if the job was finished with error.
+ *
+ * @param job the job that emitted this signal
+ *
+ * @see success, failure
+ */
+ void result(Quotient::BaseJob* job);
+
+ /**
+ * Emitted together with result() in case there's no error.
+ *
+ * @see result, failure
+ */
+ void success(Quotient::BaseJob*);
+
+ /**
+ * Emitted together with result() if there's an error.
+ * Similar to result(), this won't be emitted in case of abandon().
+ *
+ * @see result, success
+ */
+ void failure(Quotient::BaseJob*);
+
+ void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
+
+protected:
+ using headers_t = QHash<QByteArray, QByteArray>;
+
+ Q_DECL_DEPRECATED_X("Deprecated due to being unused")
+ const QString& apiEndpoint() const;
+ Q_DECL_DEPRECATED_X("Deprecated due to being unused")
+ void setApiEndpoint(const QString& apiEndpoint);
+ const headers_t& requestHeaders() const;
+ void setRequestHeader(const headers_t::key_type& headerName,
+ const headers_t::mapped_type& headerValue);
+ void setRequestHeaders(const headers_t& headers);
+ QUrlQuery query() const;
+ void setRequestQuery(const QUrlQuery& query);
+ const RequestData& requestData() const;
+ void setRequestData(RequestData&& data);
+ const QByteArrayList& expectedContentTypes() const;
+ void addExpectedContentType(const QByteArray& contentType);
+ void setExpectedContentTypes(const QByteArrayList& contentTypes);
+ QByteArrayList expectedKeys() const;
+ void addExpectedKey(const QByteArray &key);
+ void setExpectedKeys(const QByteArrayList &keys);
+
+ const QNetworkReply* reply() const;
+ QNetworkReply* reply();
+
+ /** Construct a URL out of baseUrl, path and query
+ *
+ * The function ensures exactly one '/' between the path component of
+ * \p baseUrl and \p path. The query component of \p baseUrl is ignored.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QByteArray &encodedPath,
+ const QUrlQuery& query = {});
+
+ /*! Prepares the job for execution
+ *
+ * This method is called no more than once per job lifecycle,
+ * when it's first scheduled for execution; in particular, it is not called
+ * on retries.
+ */
+ virtual void doPrepare();
+
+ /*! Postprocessing after the network request has been sent
+ *
+ * This method is called every time the job receives a running
+ * QNetworkReply object from NetworkAccessManager - basically, after
+ * successfully sending a network request (including retries).
+ */
+ virtual void onSentRequest(QNetworkReply*);
+ virtual void beforeAbandon();
+
+ /*! \brief An extension point for additional reply processing.
+ *
+ * The base implementation does nothing and returns Success.
+ *
+ * \sa gotReply
+ */
+ virtual Status prepareResult();
+
+ /*! \brief Process details of the error
+ *
+ * The function processes the reply in case when status from checkReply()
+ * was not good (usually because of an unsuccessful HTTP code).
+ * The base implementation assumes Matrix JSON error object in the body;
+ * overrides are strongly recommended to call it for all stock Matrix
+ * responses as early as possible and only then process custom errors,
+ * with JSON or non-JSON payload.
+ *
+ * \return updated (if necessary) job status
+ */
+ virtual Status prepareError(Status currentStatus);
+
+ /*! \brief Get direct access to the JSON response object in the job
+ *
+ * This allows to implement deserialisation with "move" semantics for parts
+ * of the response. Assuming that the response body is a valid JSON object,
+ * the function calls QJsonObject::take(key) on it and returns the result.
+ *
+ * \return QJsonValue::Null, if the response content type is not
+ * advertised as `application/json`;
+ * QJsonValue::Undefined, if the response is a JSON object but
+ * doesn't have \p key;
+ * the value for \p key otherwise.
+ *
+ * \sa takeFromJson
+ */
+ QJsonValue takeValueFromJson(const QString& key);
+
+ void setStatus(Status s);
+ void setStatus(int code, QString message);
+
+ // Q_DECLARE_LOGGING_CATEGORY return different function types
+ // in different versions
+ using LoggingCategory = decltype(JOBS)*;
+ void setLoggingCategory(LoggingCategory lcf);
+
+ // Job objects should only be deleted via QObject::deleteLater
+ ~BaseJob() override;
+
+protected Q_SLOTS:
+ void timeout();
+
+ /*! \brief Check the pending or received reply for upfront issues
+ *
+ * This is invoked when headers are first received and also once
+ * the complete reply is obtained; the base implementation checks the HTTP
+ * headers to detect general issues such as network errors or access denial
+ * and it's strongly recommended to call it from overrides,
+ * as early as possible.
+ * This slot is const and cannot read the response body. If you need to read
+ * the body on the fly, override onSentRequest() and connect in it
+ * to reply->readyRead(); and if you only need to validate the body after
+ * it fully arrived, use prepareResult() for that). Returning anything
+ * except NoError/Success switches further processing from prepareResult()
+ * to prepareError().
+ *
+ * @return the result of checking the reply
+ *
+ * @see gotReply
+ */
+ virtual Status checkReply(const QNetworkReply *reply) const;
+
+private Q_SLOTS:
+ void sendRequest();
+ void gotReply();
+
+ friend class ConnectionData; // to provide access to sendRequest()
+
+private:
+ void stop();
+ void finishJob();
+
+ class Private;
+ ImplPtr<Private> d;
+};
+
+inline bool QUOTIENT_API isJobPending(BaseJob* job)
+{
+ return job && job->error() == BaseJob::Pending;
+}
+} // namespace Quotient
diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp
index 2bf9dd8f..759d52c9 100644
--- a/lib/jobs/downloadfilejob.cpp
+++ b/lib/jobs/downloadfilejob.cpp
@@ -1,56 +1,80 @@
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#include "downloadfilejob.h"
-#include <QtNetwork/QNetworkReply>
#include <QtCore/QFile>
#include <QtCore/QTemporaryFile>
+#include <QtNetwork/QNetworkReply>
-using namespace QMatrixClient;
+#ifdef Quotient_E2EE_ENABLED
+# include "events/filesourceinfo.h"
-class DownloadFileJob::Private
-{
- public:
- Private() : tempFile(new QTemporaryFile()) { }
+# include <QtCore/QCryptographicHash>
+#endif
- explicit Private(const QString& localFilename)
- : targetFile(new QFile(localFilename))
- , tempFile(new QFile(targetFile->fileName() + ".qmcdownload"))
- { }
+using namespace Quotient;
+class DownloadFileJob::Private {
+public:
+ Private() : tempFile(new QTemporaryFile()) {}
- QScopedPointer<QFile> targetFile;
- QScopedPointer<QFile> tempFile;
+ explicit Private(const QString& localFilename)
+ : targetFile(new QFile(localFilename))
+ , tempFile(new QFile(targetFile->fileName() + ".qtntdownload"))
+ {}
+
+ QScopedPointer<QFile> targetFile;
+ QScopedPointer<QFile> tempFile;
+
+#ifdef Quotient_E2EE_ENABLED
+ Omittable<EncryptedFileMetadata> encryptedFileMetadata;
+#endif
};
QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri)
{
- return makeRequestUrl(baseUrl, mxcUri.authority(), mxcUri.path().mid(1));
+ return makeRequestUrl(std::move(baseUrl), mxcUri.authority(),
+ mxcUri.path().mid(1));
}
DownloadFileJob::DownloadFileJob(const QString& serverName,
const QString& mediaId,
const QString& localFilename)
: GetContentJob(serverName, mediaId)
- , d(localFilename.isEmpty() ? new Private : new Private(localFilename))
+ , d(localFilename.isEmpty() ? makeImpl<Private>()
+ : makeImpl<Private>(localFilename))
{
- setObjectName("DownloadFileJob");
+ setObjectName(QStringLiteral("DownloadFileJob"));
}
+#ifdef Quotient_E2EE_ENABLED
+DownloadFileJob::DownloadFileJob(const QString& serverName,
+ const QString& mediaId,
+ const EncryptedFileMetadata& file,
+ const QString& localFilename)
+ : GetContentJob(serverName, mediaId)
+ , d(localFilename.isEmpty() ? makeImpl<Private>()
+ : makeImpl<Private>(localFilename))
+{
+ setObjectName(QStringLiteral("DownloadFileJob"));
+ d->encryptedFileMetadata = file;
+}
+#endif
QString DownloadFileJob::targetFileName() const
{
return (d->targetFile ? d->targetFile : d->tempFile)->fileName();
}
-void DownloadFileJob::beforeStart(const ConnectionData*)
+void DownloadFileJob::doPrepare()
{
- if (d->targetFile && !d->targetFile->isReadable() &&
- !d->targetFile->open(QIODevice::WriteOnly))
- {
- qCWarning(JOBS) << "Couldn't open the file"
- << d->targetFile->fileName() << "for writing";
+ if (d->targetFile && !d->targetFile->isReadable()
+ && !d->targetFile->open(QIODevice::WriteOnly)) {
+ qCWarning(JOBS) << "Couldn't open the file" << d->targetFile->fileName()
+ << "for writing";
setStatus(FileError, "Could not open the target file for writing");
return;
}
- if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::WriteOnly))
- {
+ if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::ReadWrite)) {
qCWarning(JOBS) << "Couldn't open the temporary file"
<< d->tempFile->fileName() << "for writing";
setStatus(FileError, "Could not open the temporary download file");
@@ -59,18 +83,16 @@ void DownloadFileJob::beforeStart(const ConnectionData*)
qCDebug(JOBS) << "Downloading to" << d->tempFile->fileName();
}
-void DownloadFileJob::afterStart(const ConnectionData*, QNetworkReply* reply)
+void DownloadFileJob::onSentRequest(QNetworkReply* reply)
{
- connect(reply, &QNetworkReply::metaDataChanged, this, [this,reply] {
+ connect(reply, &QNetworkReply::metaDataChanged, this, [this, reply] {
if (!status().good())
return;
auto sizeHeader = reply->header(QNetworkRequest::ContentLengthHeader);
- if (sizeHeader.isValid())
- {
- auto targetSize = sizeHeader.value<qint64>();
+ if (sizeHeader.isValid()) {
+ auto targetSize = sizeHeader.toLongLong();
if (targetSize != -1)
- if (!d->tempFile->resize(targetSize))
- {
+ if (!d->tempFile->resize(targetSize)) {
qCWarning(JOBS) << "Failed to allocate" << targetSize
<< "bytes for" << d->tempFile->fileName();
setStatus(FileError,
@@ -78,45 +100,79 @@ void DownloadFileJob::afterStart(const ConnectionData*, QNetworkReply* reply)
}
}
});
- connect(reply, &QIODevice::readyRead, this, [this,reply] {
+ connect(reply, &QIODevice::readyRead, this, [this, reply] {
if (!status().good())
return;
auto bytes = reply->read(reply->bytesAvailable());
if (!bytes.isEmpty())
d->tempFile->write(bytes);
else
- qCWarning(JOBS)
- << "Unexpected empty chunk when downloading from"
- << reply->url() << "to" << d->tempFile->fileName();
+ qCWarning(JOBS) << "Unexpected empty chunk when downloading from"
+ << reply->url() << "to" << d->tempFile->fileName();
});
}
-void DownloadFileJob::beforeAbandon(QNetworkReply*)
+void DownloadFileJob::beforeAbandon()
{
if (d->targetFile)
d->targetFile->remove();
d->tempFile->remove();
}
-BaseJob::Status DownloadFileJob::parseReply(QNetworkReply*)
+void decryptFile(QFile& sourceFile, const EncryptedFileMetadata& metadata,
+ QFile& targetFile)
{
- if (d->targetFile)
- {
- d->targetFile->close();
- if (!d->targetFile->remove())
- {
- qCWarning(JOBS) << "Failed to remove the target file placeholder";
- return { FileError, "Couldn't finalise the download" };
+ sourceFile.seek(0);
+ const auto encrypted = sourceFile.readAll(); // TODO: stream decryption
+ const auto decrypted = decryptFile(encrypted, metadata);
+ targetFile.write(decrypted);
+}
+
+BaseJob::Status DownloadFileJob::prepareResult()
+{
+ if (d->targetFile) {
+#ifdef Quotient_E2EE_ENABLED
+ if (d->encryptedFileMetadata.has_value()) {
+ decryptFile(*d->tempFile, *d->encryptedFileMetadata, *d->targetFile);
+ d->tempFile->remove();
+ } else {
+#endif
+ d->targetFile->close();
+ if (!d->targetFile->remove()) {
+ qWarning(JOBS) << "Failed to remove the target file placeholder";
+ return { FileError, "Couldn't finalise the download" };
+ }
+ if (!d->tempFile->rename(d->targetFile->fileName())) {
+ qWarning(JOBS) << "Failed to rename" << d->tempFile->fileName()
+ << "to" << d->targetFile->fileName();
+ return { FileError, "Couldn't finalise the download" };
+ }
+#ifdef Quotient_E2EE_ENABLED
}
- if (!d->tempFile->rename(d->targetFile->fileName()))
- {
- qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName()
- << "to" << d->targetFile->fileName();
- return { FileError, "Couldn't finalise the download" };
+#endif
+ } else {
+#ifdef Quotient_E2EE_ENABLED
+ if (d->encryptedFileMetadata.has_value()) {
+ QTemporaryFile tempTempFile; // Assuming it to be next to tempFile
+ decryptFile(*d->tempFile, *d->encryptedFileMetadata, tempTempFile);
+ d->tempFile->close();
+ if (!d->tempFile->remove()) {
+ qWarning(JOBS)
+ << "Failed to remove the decrypted file placeholder";
+ return { FileError, "Couldn't finalise the download" };
+ }
+ if (!tempTempFile.rename(d->tempFile->fileName())) {
+ qWarning(JOBS) << "Failed to rename" << tempTempFile.fileName()
+ << "to" << d->tempFile->fileName();
+ return { FileError, "Couldn't finalise the download" };
+ }
+ } else {
+#endif
+ d->tempFile->close();
+#ifdef Quotient_E2EE_ENABLED
}
+#endif
}
- else
- d->tempFile->close();
- qCDebug(JOBS) << "Saved a file as" << targetFileName();
+ qDebug(JOBS) << "Saved a file as" << targetFileName();
return Success;
}
diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h
index ce47ab1c..cbbfd244 100644
--- a/lib/jobs/downloadfilejob.h
+++ b/lib/jobs/downloadfilejob.h
@@ -1,30 +1,33 @@
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#pragma once
#include "csapi/content-repo.h"
-namespace QMatrixClient
-{
- class DownloadFileJob : public GetContentJob
- {
- public:
- enum { FileError = BaseJob::UserDefinedError + 1 };
+#include "events/filesourceinfo.h"
- using GetContentJob::makeRequestUrl;
- static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri);
+namespace Quotient {
+class QUOTIENT_API DownloadFileJob : public GetContentJob {
+public:
+ using GetContentJob::makeRequestUrl;
+ static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri);
- DownloadFileJob(const QString& serverName, const QString& mediaId,
- const QString& localFilename = {});
+ DownloadFileJob(const QString& serverName, const QString& mediaId,
+ const QString& localFilename = {});
- QString targetFileName() const;
+#ifdef Quotient_E2EE_ENABLED
+ DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFileMetadata& file, const QString& localFilename = {});
+#endif
+ QString targetFileName() const;
- private:
- class Private;
- QScopedPointer<Private> d;
+private:
+ class Private;
+ ImplPtr<Private> d;
- void beforeStart(const ConnectionData*) override;
- void afterStart(const ConnectionData*,
- QNetworkReply* reply) override;
- void beforeAbandon(QNetworkReply*) override;
- Status parseReply(QNetworkReply*) override;
- };
-}
+ void doPrepare() override;
+ void onSentRequest(QNetworkReply* reply) override;
+ void beforeAbandon() override;
+ Status prepareResult() override;
+};
+} // namespace Quotient
diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp
index aeb49839..6fe8ef26 100644
--- a/lib/jobs/mediathumbnailjob.cpp
+++ b/lib/jobs/mediathumbnailjob.cpp
@@ -1,63 +1,46 @@
-/******************************************************************************
- * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de>
- *
- * This library 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 2.1 of the License, or (at your option) any later version.
- *
- * This library 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "mediathumbnailjob.h"
-using namespace QMatrixClient;
+using namespace Quotient;
-QUrl MediaThumbnailJob::makeRequestUrl(QUrl baseUrl,
- const QUrl& mxcUri, QSize requestedSize)
+QUrl MediaThumbnailJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri,
+ QSize requestedSize)
{
- return makeRequestUrl(std::move(baseUrl),
- mxcUri.authority(), mxcUri.path().mid(1),
- requestedSize.width(), requestedSize.height());
+ return makeRequestUrl(std::move(baseUrl), mxcUri.authority(),
+ mxcUri.path().mid(1), requestedSize.width(),
+ requestedSize.height());
}
MediaThumbnailJob::MediaThumbnailJob(const QString& serverName,
const QString& mediaId, QSize requestedSize)
- : GetContentThumbnailJob(serverName, mediaId,
- requestedSize.width(), requestedSize.height())
-{ }
+ : GetContentThumbnailJob(serverName, mediaId, requestedSize.width(),
+ requestedSize.height(), "scale")
+{
+ setLoggingCategory(THUMBNAILJOB);
+}
MediaThumbnailJob::MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize)
- : MediaThumbnailJob(mxcUri.authority(), mxcUri.path().mid(1), // sans leading '/'
+ : MediaThumbnailJob(mxcUri.authority(),
+ mxcUri.path().mid(1), // sans leading '/'
requestedSize)
-{ }
-
-QImage MediaThumbnailJob::thumbnail() const
{
- return _thumbnail;
+ setLoggingCategory(THUMBNAILJOB);
}
+QImage MediaThumbnailJob::thumbnail() const { return _thumbnail; }
+
QImage MediaThumbnailJob::scaledThumbnail(QSize toSize) const
{
- return _thumbnail.scaled(toSize,
- Qt::KeepAspectRatio, Qt::SmoothTransformation);
+ return _thumbnail.scaled(toSize, Qt::KeepAspectRatio,
+ Qt::SmoothTransformation);
}
-BaseJob::Status MediaThumbnailJob::parseReply(QNetworkReply* reply)
+BaseJob::Status MediaThumbnailJob::prepareResult()
{
- auto result = GetContentThumbnailJob::parseReply(reply);
- if (!result.good())
- return result;
-
- if( _thumbnail.loadFromData(data()->readAll()) )
+ if (_thumbnail.loadFromData(data()->readAll()))
return Success;
- return { IncorrectResponseError, "Could not read image data" };
+ return { IncorrectResponse, QStringLiteral("Could not read image data") };
}
diff --git a/lib/jobs/mediathumbnailjob.h b/lib/jobs/mediathumbnailjob.h
index 7963796e..c9f6da35 100644
--- a/lib/jobs/mediathumbnailjob.h
+++ b/lib/jobs/mediathumbnailjob.h
@@ -1,20 +1,5 @@
-/******************************************************************************
- * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de>
- *
- * This library 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 2.1 of the License, or (at your option) any later version.
- *
- * This library 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
@@ -22,26 +7,24 @@
#include <QtGui/QPixmap>
-namespace QMatrixClient
-{
- class MediaThumbnailJob: public GetContentThumbnailJob
- {
- public:
- using GetContentThumbnailJob::makeRequestUrl;
- static QUrl makeRequestUrl(QUrl baseUrl,
- const QUrl& mxcUri, QSize requestedSize);
-
- MediaThumbnailJob(const QString& serverName, const QString& mediaId,
- QSize requestedSize);
- MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize);
-
- QImage thumbnail() const;
- QImage scaledThumbnail(QSize toSize) const;
-
- protected:
- Status parseReply(QNetworkReply* reply) override;
-
- private:
- QImage _thumbnail;
- };
-} // namespace QMatrixClient
+namespace Quotient {
+class QUOTIENT_API MediaThumbnailJob : public GetContentThumbnailJob {
+public:
+ using GetContentThumbnailJob::makeRequestUrl;
+ static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri,
+ QSize requestedSize);
+
+ MediaThumbnailJob(const QString& serverName, const QString& mediaId,
+ QSize requestedSize);
+ MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize);
+
+ QImage thumbnail() const;
+ QImage scaledThumbnail(QSize toSize) const;
+
+protected:
+ Status prepareResult() override;
+
+private:
+ QImage _thumbnail;
+};
+} // namespace Quotient
diff --git a/lib/jobs/postreadmarkersjob.h b/lib/jobs/postreadmarkersjob.h
deleted file mode 100644
index 63a8e1d0..00000000
--- a/lib/jobs/postreadmarkersjob.h
+++ /dev/null
@@ -1,39 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library 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 2.1 of the License, or (at your option) any later version.
- *
- * This library 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#pragma once
-
-#include "basejob.h"
-
-#include <QtCore/QJsonObject>
-
-using namespace QMatrixClient;
-
-class PostReadMarkersJob : public BaseJob
-{
- public:
- explicit PostReadMarkersJob(const QString& roomId,
- const QString& readUpToEventId)
- : BaseJob(HttpVerb::Post, "PostReadMarkersJob",
- QStringLiteral("_matrix/client/r0/rooms/%1/read_markers")
- .arg(roomId))
- {
- setRequestData(QJsonObject {{
- QStringLiteral("m.fully_read"), readUpToEventId }});
- }
-};
diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp
index 5cb62221..ab249f6d 100644
--- a/lib/jobs/requestdata.cpp
+++ b/lib/jobs/requestdata.cpp
@@ -1,19 +1,22 @@
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#include "requestdata.h"
+#include <QtCore/QIODevice>
+#include <QtCore/QBuffer>
#include <QtCore/QByteArray>
-#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
-#include <QtCore/QBuffer>
+#include <QtCore/QJsonObject>
-using namespace QMatrixClient;
+using namespace Quotient;
auto fromData(const QByteArray& data)
{
- auto source = std::make_unique<QBuffer>();
- source->open(QIODevice::WriteOnly);
- source->write(data);
- source->close();
+ auto source = makeImpl<QBuffer, QIODevice>();
+ source->setData(data);
+ source->open(QIODevice::ReadOnly);
return source;
}
@@ -23,16 +26,12 @@ inline auto fromJson(const JsonDataT& jdata)
return fromData(QJsonDocument(jdata).toJson(QJsonDocument::Compact));
}
-RequestData::RequestData(const QByteArray& a)
- : _source(fromData(a))
-{ }
+RequestData::RequestData(const QByteArray& a) : _source(fromData(a)) {}
-RequestData::RequestData(const QJsonObject& jo)
- : _source(fromJson(jo))
-{ }
+RequestData::RequestData(const QJsonObject& jo) : _source(fromJson(jo)) {}
-RequestData::RequestData(const QJsonArray& ja)
- : _source(fromJson(ja))
-{ }
+RequestData::RequestData(const QJsonArray& ja) : _source(fromJson(ja)) {}
-RequestData::~RequestData() = default;
+RequestData::RequestData(QIODevice* source)
+ : _source(acquireImpl(source))
+{}
diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h
index db011b61..accc8f71 100644
--- a/lib/jobs/requestdata.h
+++ b/lib/jobs/requestdata.h
@@ -1,61 +1,35 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library 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 2.1 of the License, or (at your option) any later version.
- *
- * This library 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
-#include <memory>
+#include "util.h"
-class QByteArray;
class QJsonObject;
class QJsonArray;
class QJsonDocument;
class QIODevice;
-namespace QMatrixClient
-{
- /**
- * A simple wrapper that represents the request body.
- * Provides a unified interface to dump an unstructured byte stream
- * as well as JSON (and possibly other structures in the future) to
- * a QByteArray consumed by QNetworkAccessManager request methods.
- */
- class RequestData
- {
- public:
- RequestData() = default;
- RequestData(const QByteArray& a);
- RequestData(const QJsonObject& jo);
- RequestData(const QJsonArray& ja);
- RequestData(QIODevice* source)
- : _source(std::unique_ptr<QIODevice>(source))
- { }
- RequestData(const RequestData&) = delete;
- RequestData& operator=(const RequestData&) = delete;
- RequestData(RequestData&&) = default;
- RequestData& operator=(RequestData&&) = default;
- ~RequestData();
+namespace Quotient {
+/**
+ * A simple wrapper that represents the request body.
+ * Provides a unified interface to dump an unstructured byte stream
+ * as well as JSON (and possibly other structures in the future) to
+ * a QByteArray consumed by QNetworkAccessManager request methods.
+ */
+class QUOTIENT_API RequestData {
+public:
+ // NOLINTBEGIN(google-explicit-constructor): that check should learn about
+ // explicit(false)
+ QUO_IMPLICIT RequestData(const QByteArray& a = {});
+ QUO_IMPLICIT RequestData(const QJsonObject& jo);
+ QUO_IMPLICIT RequestData(const QJsonArray& ja);
+ QUO_IMPLICIT RequestData(QIODevice* source);
+ // NOLINTEND(google-explicit-constructor)
- QIODevice* source() const
- {
- return _source.get();
- }
+ QIODevice* source() const { return _source.get(); }
- private:
- std::unique_ptr<QIODevice> _source;
- };
-} // namespace QMatrixClient
+private:
+ ImplPtr<QIODevice> _source;
+};
+} // namespace Quotient
diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp
index 6baf388e..f5c632bf 100644
--- a/lib/jobs/syncjob.cpp
+++ b/lib/jobs/syncjob.cpp
@@ -1,156 +1,48 @@
-/******************************************************************************
- * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de>
- *
- * This library 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 2.1 of the License, or (at your option) any later version.
- *
- * This library 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "syncjob.h"
-#include "events/eventloader.h"
-
-#include <QtCore/QElapsedTimer>
-
-using namespace QMatrixClient;
+using namespace Quotient;
static size_t jobId = 0;
SyncJob::SyncJob(const QString& since, const QString& filter, int timeout,
const QString& presence)
: BaseJob(HttpVerb::Get, QStringLiteral("SyncJob-%1").arg(++jobId),
- QStringLiteral("_matrix/client/r0/sync"))
+ "_matrix/client/r0/sync")
{
setLoggingCategory(SYNCJOB);
QUrlQuery query;
- if( !filter.isEmpty() )
+ if (!filter.isEmpty())
query.addQueryItem(QStringLiteral("filter"), filter);
- if( !presence.isEmpty() )
+ if (!presence.isEmpty())
query.addQueryItem(QStringLiteral("set_presence"), presence);
- if( timeout >= 0 )
+ if (timeout >= 0)
query.addQueryItem(QStringLiteral("timeout"), QString::number(timeout));
- if( !since.isEmpty() )
+ if (!since.isEmpty())
query.addQueryItem(QStringLiteral("since"), since);
setRequestQuery(query);
setMaxRetries(std::numeric_limits<int>::max());
}
-QString SyncData::nextBatch() const
-{
- return nextBatch_;
-}
-
-SyncDataList&& SyncData::takeRoomData()
-{
- return std::move(roomData);
-}
-
-Events&& SyncData::takePresenceData()
-{
- return std::move(presenceData);
-}
-
-Events&& SyncData::takeAccountData()
-{
- return std::move(accountData);
-}
-
-Events&&SyncData::takeToDeviceEvents()
-{
- return std::move(toDeviceEvents);
-}
-
-template <typename EventsArrayT, typename StrT>
-inline EventsArrayT load(const QJsonObject& batches, StrT keyName)
-{
- return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls));
-}
-
-BaseJob::Status SyncJob::parseJson(const QJsonDocument& data)
-{
- return d.parseJson(data);
-}
-
-BaseJob::Status SyncData::parseJson(const QJsonDocument &data)
-{
- QElapsedTimer et; et.start();
-
- auto json = data.object();
- nextBatch_ = json.value("next_batch"_ls).toString();
- presenceData = load<Events>(json, "presence"_ls);
- accountData = load<Events>(json, "account_data"_ls);
- toDeviceEvents = load<Events>(json, "to_device"_ls);
-
- auto rooms = json.value("rooms"_ls).toObject();
- JoinStates::Int ii = 1; // ii is used to make a JoinState value
- auto totalRooms = 0;
- auto totalEvents = 0;
- for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1)
- {
- const auto rs = rooms.value(JoinStateStrings[i]).toObject();
- // We have a Qt container on the right and an STL one on the left
- roomData.reserve(static_cast<size_t>(rs.size()));
- for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt)
- {
- roomData.emplace_back(roomIt.key(), JoinState(ii),
- roomIt.value().toObject());
- const auto& r = roomData.back();
- totalEvents += r.state.size() + r.ephemeral.size() +
- r.accountData.size() + r.timeline.size();
- }
- totalRooms += rs.size();
- }
- if (totalRooms > 9 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with"
- << totalRooms << "room(s),"
- << totalEvents << "event(s) in" << et;
- return BaseJob::Success;
-}
-
-const QString SyncRoomData::UnreadCountKey =
- QStringLiteral("x-qmatrixclient.unread_count");
-
-SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_,
- const QJsonObject& room_)
- : roomId(roomId_)
- , joinState(joinState_)
- , state(load<StateEvents>(room_,
- joinState == JoinState::Invite ? "invite_state"_ls : "state"_ls))
-{
- switch (joinState) {
- case JoinState::Join:
- ephemeral = load<Events>(room_, "ephemeral"_ls);
- FALLTHROUGH;
- case JoinState::Leave:
- {
- accountData = load<Events>(room_, "account_data"_ls);
- timeline = load<RoomEvents>(room_, "timeline"_ls);
- const auto timelineJson = room_.value("timeline"_ls).toObject();
- timelineLimited = timelineJson.value("limited"_ls).toBool();
- timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString();
-
- break;
- }
- default: /* nothing on top of state */;
- }
-
- const auto unreadJson = room_.value("unread_notifications"_ls).toObject();
- unreadCount = unreadJson.value(UnreadCountKey).toInt(-2);
- highlightCount = unreadJson.value("highlight_count"_ls).toInt();
- notificationCount = unreadJson.value("notification_count"_ls).toInt();
- if (highlightCount > 0 || notificationCount > 0)
- qCDebug(SYNCJOB) << "Room" << roomId_
- << "has highlights:" << highlightCount
- << "and notifications:" << notificationCount;
+SyncJob::SyncJob(const QString& since, const Filter& filter, int timeout,
+ const QString& presence)
+ : SyncJob(since,
+ QJsonDocument(toJson(filter)).toJson(QJsonDocument::Compact),
+ timeout, presence)
+{}
+
+BaseJob::Status SyncJob::prepareResult()
+{
+ d.parseJson(jsonData());
+ if (Q_LIKELY(d.unresolvedRooms().isEmpty()))
+ return Success;
+
+ Q_ASSERT(d.unresolvedRooms().isEmpty());
+ qCCritical(MAIN).noquote() << "Rooms missing after processing sync "
+ "response, possibly a bug in SyncData: "
+ << d.unresolvedRooms().join(',');
+ return IncorrectResponse;
}
diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h
index 6b9bedfa..b7bfbbb3 100644
--- a/lib/jobs/syncjob.h
+++ b/lib/jobs/syncjob.h
@@ -1,88 +1,26 @@
-/******************************************************************************
- * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de>
- *
- * This library 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 2.1 of the License, or (at your option) any later version.
- *
- * This library 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
+#include "../csapi/definitions/sync_filter.h"
+#include "../syncdata.h"
#include "basejob.h"
-#include "joinstate.h"
-#include "events/stateevent.h"
-#include "util.h"
+namespace Quotient {
+class SyncJob : public BaseJob {
+public:
+ explicit SyncJob(const QString& since = {}, const QString& filter = {},
+ int timeout = -1, const QString& presence = {});
+ explicit SyncJob(const QString& since, const Filter& filter,
+ int timeout = -1, const QString& presence = {});
-namespace QMatrixClient
-{
- class SyncRoomData
- {
- public:
- QString roomId;
- JoinState joinState;
- StateEvents state;
- RoomEvents timeline;
- Events ephemeral;
- Events accountData;
+ SyncData takeData() { return std::move(d); }
- bool timelineLimited;
- QString timelinePrevBatch;
- int unreadCount;
- int highlightCount;
- int notificationCount;
+protected:
+ Status prepareResult() override;
- SyncRoomData(const QString& roomId, JoinState joinState_,
- const QJsonObject& room_);
- SyncRoomData(SyncRoomData&&) = default;
- SyncRoomData& operator=(SyncRoomData&&) = default;
-
- static const QString UnreadCountKey;
- };
- // QVector cannot work with non-copiable objects, std::vector can.
- using SyncDataList = std::vector<SyncRoomData>;
-
- class SyncData
- {
- public:
- BaseJob::Status parseJson(const QJsonDocument &data);
- Events&& takePresenceData();
- Events&& takeAccountData();
- Events&& takeToDeviceEvents();
- SyncDataList&& takeRoomData();
- QString nextBatch() const;
-
- private:
- QString nextBatch_;
- Events presenceData;
- Events accountData;
- Events toDeviceEvents;
- SyncDataList roomData;
- };
-
- class SyncJob: public BaseJob
- {
- public:
- explicit SyncJob(const QString& since = {},
- const QString& filter = {},
- int timeout = -1, const QString& presence = {});
-
- SyncData &&takeData() { return std::move(d); }
-
- protected:
- Status parseJson(const QJsonDocument& data) override;
-
- private:
- SyncData d;
- };
-} // namespace QMatrixClient
+private:
+ SyncData d;
+};
+} // namespace Quotient