aboutsummaryrefslogtreecommitdiff
path: root/lib/jobs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/jobs')
-rw-r--r--lib/jobs/basejob.cpp115
-rw-r--r--lib/jobs/basejob.h645
-rw-r--r--lib/jobs/downloadfilejob.cpp25
-rw-r--r--lib/jobs/downloadfilejob.h40
-rw-r--r--lib/jobs/mediathumbnailjob.cpp12
-rw-r--r--lib/jobs/mediathumbnailjob.h43
-rw-r--r--lib/jobs/postreadmarkersjob.h12
-rw-r--r--lib/jobs/requestdata.cpp15
-rw-r--r--lib/jobs/requestdata.h54
-rw-r--r--lib/jobs/syncjob.cpp3
-rw-r--r--lib/jobs/syncjob.h32
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