diff options
author | Kitsune Ral <Kitsune-Ral@users.sf.net> | 2019-06-24 07:21:13 +0900 |
---|---|---|
committer | Kitsune Ral <Kitsune-Ral@users.sf.net> | 2019-06-24 07:21:13 +0900 |
commit | 63ae79c3e2820efc2ba60d33e2caf2d7b9b3c408 (patch) | |
tree | 2552f5049a6ef7ba0034483b25ca4ab33d1fcb13 /lib/jobs | |
parent | e083d327e6f6581210f8d077d8bbe1151e81e82c (diff) | |
parent | 93f0c8fe89f448d1d58caa757573f17102369471 (diff) | |
download | libquotient-63ae79c3e2820efc2ba60d33e2caf2d7b9b3c408.tar.gz libquotient-63ae79c3e2820efc2ba60d33e2caf2d7b9b3c408.zip |
Merge branch 'master' into clang-format
# Conflicts:
# CMakeLists.txt
# lib/avatar.cpp
# lib/connection.cpp
# lib/connection.h
# lib/connectiondata.cpp
# lib/csapi/account-data.cpp
# lib/csapi/account-data.h
# lib/csapi/capabilities.cpp
# lib/csapi/capabilities.h
# lib/csapi/content-repo.cpp
# lib/csapi/create_room.cpp
# lib/csapi/filter.cpp
# lib/csapi/joining.cpp
# lib/csapi/keys.cpp
# lib/csapi/list_joined_rooms.cpp
# lib/csapi/notifications.cpp
# lib/csapi/openid.cpp
# lib/csapi/presence.cpp
# lib/csapi/pushrules.cpp
# lib/csapi/registration.cpp
# lib/csapi/room_upgrades.cpp
# lib/csapi/room_upgrades.h
# lib/csapi/search.cpp
# lib/csapi/users.cpp
# lib/csapi/versions.cpp
# lib/csapi/whoami.cpp
# lib/csapi/{{base}}.cpp.mustache
# lib/events/accountdataevents.h
# lib/events/eventcontent.h
# lib/events/roommemberevent.cpp
# lib/events/stateevent.cpp
# lib/jobs/basejob.cpp
# lib/jobs/basejob.h
# lib/networkaccessmanager.cpp
# lib/networksettings.cpp
# lib/room.cpp
# lib/room.h
# lib/settings.cpp
# lib/settings.h
# lib/syncdata.cpp
# lib/user.cpp
# lib/user.h
# lib/util.cpp
Diffstat (limited to 'lib/jobs')
-rw-r--r-- | lib/jobs/basejob.cpp | 115 | ||||
-rw-r--r-- | lib/jobs/basejob.h | 645 | ||||
-rw-r--r-- | lib/jobs/downloadfilejob.cpp | 25 | ||||
-rw-r--r-- | lib/jobs/downloadfilejob.h | 40 | ||||
-rw-r--r-- | lib/jobs/mediathumbnailjob.cpp | 12 | ||||
-rw-r--r-- | lib/jobs/mediathumbnailjob.h | 43 | ||||
-rw-r--r-- | lib/jobs/postreadmarkersjob.h | 12 | ||||
-rw-r--r-- | lib/jobs/requestdata.cpp | 15 | ||||
-rw-r--r-- | lib/jobs/requestdata.h | 54 | ||||
-rw-r--r-- | lib/jobs/syncjob.cpp | 3 | ||||
-rw-r--r-- | lib/jobs/syncjob.h | 32 |
11 files changed, 518 insertions, 478 deletions
diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index a023d4f7..a0a3dc29 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -32,7 +32,8 @@ using namespace QMatrixClient; -struct NetworkReplyDeleter : public QScopedPointerDeleteLater { +struct NetworkReplyDeleter : public QScopedPointerDeleteLater +{ static inline void cleanup(QNetworkReply* reply) { if (reply && reply->isRunning()) @@ -43,18 +44,17 @@ struct NetworkReplyDeleter : public QScopedPointerDeleteLater { class BaseJob::Private { - public: +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) - { - } + : verb(v) + , apiEndpoint(std::move(endpoint)) + , requestQuery(q) + , requestData(std::move(data)) + , needsToken(nt) + {} void sendRequest(bool inBackground); const JobTimeoutConfig& getCurrentTimeoutConfig() const; @@ -94,8 +94,7 @@ class BaseJob::Private 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) @@ -121,9 +120,9 @@ QUrl BaseJob::requestUrl() const bool BaseJob::isBackground() const { return d->reply - && d->reply->request() - .attribute(QNetworkRequest::BackgroundRequestAttribute) - .toBool(); + && d->reply->request() + .attribute(QNetworkRequest::BackgroundRequestAttribute) + .toBool(); } const QString& BaseJob::apiEndpoint() const { return d->apiEndpoint; } @@ -182,7 +181,7 @@ QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QString& path, if (!pathBase.endsWith('/') && !path.startsWith('/')) pathBase.push_back('/'); - baseUrl.setPath(pathBase + path); + baseUrl.setPath(pathBase + path, QUrl::TolerantMode); baseUrl.setQuery(query); return baseUrl; } @@ -253,8 +252,7 @@ void BaseJob::sendRequest(bool inBackground) if (!d->requestQuery.isEmpty()) qCDebug(d->logCat) << " query:" << d->requestQuery.toString(); d->sendRequest(inBackground); - connect(d->reply.data(), &QNetworkReply::finished, this, - &BaseJob::gotReply); + connect(d->reply.data(), &QNetworkReply::finished, this, &BaseJob::gotReply); if (d->reply->isRunning()) { connect(d->reply.data(), &QNetworkReply::metaDataChanged, this, &BaseJob::checkReply); @@ -279,10 +277,10 @@ void BaseJob::gotReply() 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); + 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(); @@ -291,8 +289,8 @@ void BaseJob::gotReply() 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); + msg += + tr(", next retry advised after %1 ms").arg(retryInterval); else // We still have to figure some reasonable interval retryInterval = getNextRetryInterval(); @@ -301,7 +299,7 @@ void BaseJob::gotReply() // Shortcut to retry instead of executing finishJob() stop(); qCWarning(d->logCat) - << this << "will retry in" << retryInterval << "ms"; + << this << "will retry in" << retryInterval << "ms"; d->retryTimer.start(retryInterval); emit retryScheduled(d->retriesTaken, retryInterval); return; @@ -314,11 +312,10 @@ void BaseJob::gotReply() d->status.code = UnsupportedRoomVersionError; if (json.contains("room_version")) d->status.message = - tr("Requested room version: %1") - .arg(json.value("room_version").toString()); + tr("Requested room version: %1") + .arg(json.value("room_version").toString()); } else if (!json.isEmpty()) // Not localisable on the client side - setStatus(IncorrectRequestError, - json.value("error"_ls).toString()); + setStatus(d->status.code, json.value("error"_ls).toString()); } } @@ -341,7 +338,7 @@ bool checkContentType(const QByteArray& type, const QByteArrayList& patterns) Q_ASSERT_X(patternParts.size() <= 2, __FUNCTION__, "BaseJob: Expected content type should have up to two" " /-separated parts; violating pattern: " - + pattern); + + pattern); if (ctype.split('/').front() == patternParts.front() && patternParts.back() == "*") @@ -358,25 +355,23 @@ BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const // so check genuine HTTP codes. The below processing is based on // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes const auto httpCodeHeader = - reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); if (!httpCodeHeader.isValid()) { qCWarning(d->logCat) << this << "didn't get valid HTTP headers"; return { NetworkError, reply->errorString() }; } const QString replyState = reply->isRunning() - ? QStringLiteral("(tentative)") - : QStringLiteral("(final)"); + ? QStringLiteral("(tentative)") + : QStringLiteral("(final)"); const auto urlString = '|' + d->reply->url().toDisplayString(); const auto httpCode = httpCodeHeader.toInt(); const auto reason = - reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute) - .toString(); + reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); if (httpCode / 100 == 2) // 2xx { qCDebug(d->logCat).noquote().nospace() << this << urlString; - qCDebug(d->logCat).noquote() - << " " << httpCode << reason << replyState; + qCDebug(d->logCat).noquote() << " " << httpCode << reason << replyState; if (!checkContentType(reply->rawHeader("Content-Type"), d->expectedContentTypes)) return { UnexpectedResponseTypeWarning, @@ -423,7 +418,7 @@ BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const BaseJob::Status BaseJob::parseReply(QNetworkReply* reply) { d->rawResponse = reply->readAll(); - QJsonParseError error; + QJsonParseError error { 0, QJsonParseError::MissingObject }; const auto& json = QJsonDocument::fromJson(d->rawResponse, &error); if (error.error == QJsonParseError::NoError) return parseJson(json); @@ -440,7 +435,7 @@ void BaseJob::stop() d->reply->disconnect(this); // Ignore whatever comes from the reply if (d->reply->isRunning()) { qCWarning(d->logCat) - << this << "stopped without ready network reply"; + << this << "stopped without ready network reply"; d->reply->abort(); } } else @@ -455,12 +450,12 @@ void BaseJob::finishJob() // 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(); + const auto retryInterval = error() == TimeoutError + ? 0 + : getNextRetryInterval(); ++d->retriesTaken; - qCWarning(d->logCat).nospace() - << this << ": retry #" << d->retriesTaken << " in " - << retryInterval / 1000 << " s"; + qCWarning(d->logCat).nospace() << this << ": retry #" << d->retriesTaken + << " in " << retryInterval / 1000 << " s"; d->retryTimer.start(retryInterval); emit retryScheduled(d->retriesTaken, retryInterval); return; @@ -510,19 +505,20 @@ 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) - : d->rawResponse; + ? d->rawResponse.left(bytesAtMost) + : d->rawResponse; } QString BaseJob::rawDataSample(int bytesAtMost) const { 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()); + return data.size() == d->rawResponse.size() + ? data + : data + + tr("...(truncated, %Ln bytes in total)", + "Comes after trimmed raw network response", + d->rawResponse.size()); } QString BaseJob::statusCaption() const @@ -538,8 +534,6 @@ QString BaseJob::statusCaption() const 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: @@ -573,10 +567,25 @@ 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/QMatrixClient/Quaternion/issues/566 - basically, + // when cleaning up childrent 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. + // For 0.6 we might rectify the situation by making d->connection + // a QPointer<> (and derive ConnectionData from QObject, respectively). + if (d->status.code == Abandoned || s.code == Abandoned) + s.message.clear(); + if (d->status == s) return; - if (d->connection && !d->connection->accessToken().isEmpty()) + 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; diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 8ff25d42..04d79c66 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -28,329 +28,350 @@ class QNetworkReply; class QSslError; -namespace QMatrixClient { - class ConnectionData; - - enum class HttpVerb { Get, Put, Post, Delete }; +namespace QMatrixClient +{ +class ConnectionData; + +enum class HttpVerb +{ + Get, + Put, + Post, + Delete +}; + +struct JobTimeoutConfig +{ + int jobTimeout; + int nextRetryInterval; +}; + +class BaseJob : public QObject +{ + Q_OBJECT + Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT) + Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries) +public: + enum StatusCode + { + NoError = 0 // To be compatible with Qt conventions + , + Success = 0, + Pending = 1, + WarningLevel = 20, + UnexpectedResponseType = 21, + UnexpectedResponseTypeWarning = UnexpectedResponseType, + Abandoned = 50 //< A very brief period between abandoning and object + //deletion + , + ErrorLevel = 100 //< Errors have codes starting from this + , + NetworkError = 100, + Timeout, + TimeoutError = Timeout, + ContentAccessError, + NotFoundError, + IncorrectRequest, + IncorrectRequestError = IncorrectRequest, + IncorrectResponse, + IncorrectResponseError = IncorrectResponse, + JsonParseError //< deprecated; Use IncorrectResponse instead + = IncorrectResponse, + TooManyRequests, + TooManyRequestsError = TooManyRequests, + RequestNotImplemented, + RequestNotImplementedError = RequestNotImplemented, + UnsupportedRoomVersion, + UnsupportedRoomVersionError = UnsupportedRoomVersion, + NetworkAuthRequired, + NetworkAuthRequiredError = NetworkAuthRequired, + UserConsentRequired, + UserConsentRequiredError = UserConsentRequired, + UserDefinedError = 256 + }; - struct JobTimeoutConfig { - int jobTimeout; - int nextRetryInterval; + /** + * 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); + } }; - class BaseJob : public QObject + 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 { - 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, - UnsupportedRoomVersionError, - NetworkAuthRequiredError, - UserConsentRequiredError, - UserDefinedError = 200 - }; - - /** - * A simple wrapper around QUrlQuery that allows its creation from - * a list of string pairs - */ - class Query : public QUrlQuery + 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) { - 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 + QDebugStateSaver _s(dbg); + return dbg.noquote().nospace() << s.code << ": " << s.message; + } + + bool operator==(const Status& other) const { - 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; - /** Get raw response body as received from the server - * \param bytesAtMost return this number of leftmost bytes, or -1 - * to return the entire response - */ - QByteArray rawData(int bytesAtMost = -1) 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. - */ - QString rawDataSample(int bytesAtMost = 65535) 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 code == other.code && message == other.message; + } + bool operator!=(const Status& other) const { - return dbg << j->objectName(); + return !operator==(other); } - 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 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(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; + int code; + QString message; }; - inline bool isJobRunning(BaseJob* job) + 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; + /** Get raw response body as received from the server + * \param bytesAtMost return this number of leftmost bytes, or -1 + * to return the entire response + */ + QByteArray rawData(int bytesAtMost = -1) 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. + */ + QString rawDataSample(int bytesAtMost = 65535) 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 job && job->error() == BaseJob::Pending; + 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 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(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; +}; + +inline bool isJobRunning(BaseJob* job) +{ + return job && job->error() == BaseJob::Pending; +} } // namespace QMatrixClient diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 12aacb8b..3dff5a68 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -8,14 +8,15 @@ using namespace QMatrixClient; class DownloadFileJob::Private { - public: - Private() : tempFile(new QTemporaryFile()) {} +public: + Private() + : tempFile(new QTemporaryFile()) + {} explicit Private(const QString& localFilename) - : targetFile(new QFile(localFilename)), - tempFile(new QFile(targetFile->fileName() + ".qmcdownload")) - { - } + : targetFile(new QFile(localFilename)) + , tempFile(new QFile(targetFile->fileName() + ".qmcdownload")) + {} QScopedPointer<QFile> targetFile; QScopedPointer<QFile> tempFile; @@ -23,16 +24,17 @@ class DownloadFileJob::Private 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)) + : GetContentJob(serverName, mediaId) + , d(localFilename.isEmpty() ? new Private : new Private(localFilename)) { - setObjectName("DownloadFileJob"); + setObjectName(QStringLiteral("DownloadFileJob")); } QString DownloadFileJob::targetFileName() const @@ -49,8 +51,7 @@ void DownloadFileJob::beforeStart(const ConnectionData*) 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::WriteOnly)) { qCWarning(JOBS) << "Couldn't open the temporary file" << d->tempFile->fileName() << "for writing"; setStatus(FileError, "Could not open the temporary download file"); diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h index fd34ba5a..58858448 100644 --- a/lib/jobs/downloadfilejob.h +++ b/lib/jobs/downloadfilejob.h @@ -2,27 +2,31 @@ #include "csapi/content-repo.h" -namespace QMatrixClient { - class DownloadFileJob : public GetContentJob +namespace QMatrixClient +{ +class DownloadFileJob : public GetContentJob +{ +public: + enum { - public: - enum { FileError = BaseJob::UserDefinedError + 1 }; + FileError = BaseJob::UserDefinedError + 1 + }; - using GetContentJob::makeRequestUrl; - static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri); + 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; + QString targetFileName() const; - private: - class Private; - QScopedPointer<Private> d; +private: + class Private; + QScopedPointer<Private> d; - void beforeStart(const ConnectionData*) override; - void afterStart(const ConnectionData*, QNetworkReply* reply) override; - void beforeAbandon(QNetworkReply*) override; - Status parseReply(QNetworkReply*) override; - }; -} + void beforeStart(const ConnectionData*) override; + void afterStart(const ConnectionData*, QNetworkReply* reply) override; + void beforeAbandon(QNetworkReply*) override; + Status parseReply(QNetworkReply*) override; +}; +} // namespace QMatrixClient diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp index d3370f1f..db2bbc13 100644 --- a/lib/jobs/mediathumbnailjob.cpp +++ b/lib/jobs/mediathumbnailjob.cpp @@ -29,19 +29,16 @@ QUrl MediaThumbnailJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri, } MediaThumbnailJob::MediaThumbnailJob(const QString& serverName, - const QString& mediaId, - QSize requestedSize) + const QString& mediaId, QSize requestedSize) : GetContentThumbnailJob(serverName, mediaId, requestedSize.width(), requestedSize.height()) -{ -} +{} MediaThumbnailJob::MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize) : MediaThumbnailJob(mxcUri.authority(), mxcUri.path().mid(1), // sans leading '/' requestedSize) -{ -} +{} QImage MediaThumbnailJob::thumbnail() const { return _thumbnail; } @@ -60,5 +57,6 @@ BaseJob::Status MediaThumbnailJob::parseReply(QNetworkReply* reply) if (_thumbnail.loadFromData(data()->readAll())) return Success; - return { IncorrectResponseError, "Could not read image data" }; + return { IncorrectResponseError, + QStringLiteral("Could not read image data") }; } diff --git a/lib/jobs/mediathumbnailjob.h b/lib/jobs/mediathumbnailjob.h index 1dcf8ccb..eeabe7a9 100644 --- a/lib/jobs/mediathumbnailjob.h +++ b/lib/jobs/mediathumbnailjob.h @@ -22,25 +22,26 @@ #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 +{ +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 diff --git a/lib/jobs/postreadmarkersjob.h b/lib/jobs/postreadmarkersjob.h index 3c5cac89..d53ae66c 100644 --- a/lib/jobs/postreadmarkersjob.h +++ b/lib/jobs/postreadmarkersjob.h @@ -26,14 +26,14 @@ using namespace QMatrixClient; class PostReadMarkersJob : public BaseJob { - public: +public: explicit PostReadMarkersJob(const QString& roomId, const QString& readUpToEventId) - : BaseJob(HttpVerb::Post, "PostReadMarkersJob", - QStringLiteral("_matrix/client/r0/rooms/%1/read_markers") - .arg(roomId)) + : BaseJob( + HttpVerb::Post, "PostReadMarkersJob", + QStringLiteral("_matrix/client/r0/rooms/%1/read_markers").arg(roomId)) { - setRequestData(QJsonObject { - { QStringLiteral("m.fully_read"), readUpToEventId } }); + setRequestData( + QJsonObject { { QStringLiteral("m.fully_read"), readUpToEventId } }); } }; diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp index 477f49e7..8248d6b1 100644 --- a/lib/jobs/requestdata.cpp +++ b/lib/jobs/requestdata.cpp @@ -17,15 +17,22 @@ auto fromData(const QByteArray& data) return source; } -template <typename JsonDataT> inline auto fromJson(const JsonDataT& jdata) +template <typename JsonDataT> +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; diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h index 207ff731..974a9ddf 100644 --- a/lib/jobs/requestdata.h +++ b/lib/jobs/requestdata.h @@ -26,33 +26,33 @@ 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 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(); - QIODevice* source() const { return _source.get(); } + QIODevice* source() const { return _source.get(); } - private: - std::unique_ptr<QIODevice> _source; - }; +private: + std::unique_ptr<QIODevice> _source; +}; } // namespace QMatrixClient diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index db11005a..f660e1b6 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -47,8 +47,7 @@ SyncJob::SyncJob(const QString& since, const Filter& filter, int timeout, : SyncJob(since, QJsonDocument(toJson(filter)).toJson(QJsonDocument::Compact), timeout, presence) -{ -} +{} BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) { diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h index 2afaf0f7..e2cec8f7 100644 --- a/lib/jobs/syncjob.h +++ b/lib/jobs/syncjob.h @@ -18,26 +18,26 @@ #pragma once -#include "basejob.h" - #include "../csapi/definitions/sync_filter.h" #include "../syncdata.h" +#include "basejob.h" -namespace QMatrixClient { - 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 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 = {}); - SyncData&& takeData() { return std::move(d); } + SyncData&& takeData() { return std::move(d); } - protected: - Status parseJson(const QJsonDocument& data) override; +protected: + Status parseJson(const QJsonDocument& data) override; - private: - SyncData d; - }; +private: + SyncData d; +}; } // namespace QMatrixClient |