libQuotient
A Qt library for building matrix clients
basejob.h
Go to the documentation of this file.
1 // SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de>
2 // SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
3 // SPDX-License-Identifier: LGPL-2.1-or-later
4 
5 #pragma once
6 
7 #include "requestdata.h"
8 
9 #include <QtCore/QObject>
10 #include <QtCore/QStringBuilder>
11 #include <QtCore/QLoggingCategory>
12 #include <QtCore/QFuture>
13 
14 #include <Quotient/converters.h> // Common for csapi/ headers even though not used here
15 #include <Quotient/quotient_common.h> // For DECL_DEPRECATED_ENUMERATOR
16 
17 class QNetworkRequest;
18 class QNetworkReply;
19 class QSslError;
20 
21 namespace Quotient {
22 class ConnectionData;
23 
24 enum class HttpVerb { Get, Put, Post, Delete };
25 
26 class QUOTIENT_API BaseJob : public QObject {
27  Q_OBJECT
28  Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT)
29  Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries)
30  Q_PROPERTY(int statusCode READ error NOTIFY statusChanged)
31 
32  static QByteArray encodeIfParam(const QString& paramPart);
33  template <int N>
34  static auto encodeIfParam(const char (&constPart)[N])
35  {
36  return constPart;
37  }
38 
39 public:
40  /*! The status code of a job
41  *
42  * Every job is created in Unprepared status; upon calling prepare()
43  * from Connection (if things are fine) it go to Pending status. After
44  * that, the next transition comes after the reply arrives and its contents
45  * are analysed. At any point in time the job can be abandon()ed, causing
46  * it to switch to status Abandoned for a brief period before deletion.
47  */
48  enum StatusCode {
49  Success = 0,
50  NoError = Success,
51  Pending = 1,
52  WarningLevel = 20, //!< Warnings have codes starting from this
53  UnexpectedResponseType = 21,
54  UnexpectedResponseTypeWarning = UnexpectedResponseType,
55  Unprepared = 25, //!< Initial job state is incomplete, hence warning level
56  Abandoned = 50, //!< A tiny period between abandoning and object deletion
57  ErrorLevel = 100, //!< Errors have codes starting from this
58  NetworkError = 101,
59  Timeout,
60  Unauthorised,
61  ContentAccessError,
62  NotFound,
63  IncorrectRequest,
64  IncorrectResponse,
65  TooManyRequests,
66  RateLimited = TooManyRequests,
67  RequestNotImplemented,
68  UnsupportedRoomVersion,
69  NetworkAuthRequired,
70  UserConsentRequired,
71  CannotLeaveRoom,
72  UserDeactivated,
73  FileError,
74  UserDefinedError = 256
75  };
76  Q_ENUM(StatusCode)
77 
78  template <typename... StrTs>
79  static QByteArray makePath(StrTs&&... parts)
80  {
81  return (QByteArray() % ... % encodeIfParam(parts));
82  }
83 
84  /*!
85  * This structure stores the status of a server call job. The status
86  * consists of a code, that is described (but not delimited) by the
87  * respective enum, and a freeform message.
88  *
89  * To extend the list of error codes, define an (anonymous) enum
90  * along the lines of StatusCode, with additional values
91  * starting at UserDefinedError
92  */
93  struct Status {
94  Status(StatusCode c) : code(c) {}
95  Status(int c, QString m) : code(c), message(std::move(m)) {}
96 
97  static StatusCode fromHttpCode(int httpCode);
98  static Status fromHttpCode(int httpCode, QString msg)
99  {
100  return { fromHttpCode(httpCode), std::move(msg) };
101  }
102 
103  bool good() const { return code < ErrorLevel; }
104  QDebug dumpToLog(QDebug dbg) const;
105  friend QDebug operator<<(const QDebug& dbg, const Status& s)
106  {
107  return s.dumpToLog(dbg);
108  }
109 
110  bool operator==(const Status& other) const
111  {
112  return code == other.code && message == other.message;
113  }
114  bool operator!=(const Status& other) const
115  {
116  return !operator==(other);
117  }
118  bool operator==(int otherCode) const
119  {
120  return code == otherCode;
121  }
122  bool operator!=(int otherCode) const
123  {
124  return !operator==(otherCode);
125  }
126 
127  int code;
128  QString message;
129  };
130 
131 public:
132  BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint,
133  bool needsToken = true);
134  BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint,
135  const QUrlQuery& query, RequestData&& data = {},
136  bool needsToken = true);
137 
138  QUrl requestUrl() const;
139  bool isBackground() const;
140 
141  /** Current status of the job */
142  Status status() const;
143 
144  /** Short human-friendly message on the job status */
145  QString statusCaption() const;
146 
147  /*! Get first bytes of the raw response body as received from the server
148  *
149  * \param bytesAtMost the number of leftmost bytes to return
150  *
151  * \sa rawDataSample
152  */
153  QByteArray rawData(int bytesAtMost) const;
154 
155  /*! Access the whole response body as received from the server */
156  const QByteArray& rawData() const;
157 
158  /** Get UI-friendly sample of raw data
159  *
160  * This is almost the same as rawData but appends the "truncated"
161  * suffix if not all data fit in bytesAtMost. This call is
162  * recommended to present a sample of raw data as "details" next to
163  * error messages. Note that the default \p bytesAtMost value is
164  * also tailored to UI cases.
165  *
166  * \sa rawData
167  */
168  QString rawDataSample(int bytesAtMost = 65535) const;
169 
170  /** Get the response body as a JSON object
171  *
172  * If the job's returned content type is not `application/json`
173  * or if the top-level JSON entity is not an object, an empty object
174  * is returned.
175  */
176  QJsonObject jsonData() const;
177 
178  /** Get the response body as a JSON array
179  *
180  * If the job's returned content type is not `application/json`
181  * or if the top-level JSON entity is not an array, an empty array
182  * is returned.
183  */
184  QJsonArray jsonItems() const;
185 
186  /** Load the property from the JSON response assuming a given C++ type
187  *
188  * If there's no top-level JSON object in the response or if there's
189  * no node with the key \p keyName, \p defaultValue is returned.
190  */
191  template <typename T, typename StrT>
192  T loadFromJson(const StrT& keyName, T&& defaultValue = {}) const
193  {
194  const auto& jv = jsonData().value(keyName);
195  return jv.isUndefined() ? std::forward<T>(defaultValue)
196  : fromJson<T>(jv);
197  }
198 
199  /** Load the property from the JSON response and delete it from JSON
200  *
201  * If there's no top-level JSON object in the response or if there's
202  * no node with the key \p keyName, \p defaultValue is returned.
203  */
204  template <typename T>
205  T takeFromJson(const QString& key, T&& defaultValue = {})
206  {
207  if (const auto& jv = takeValueFromJson(key); !jv.isUndefined())
208  return fromJson<T>(jv);
209 
210  return std::forward<T>(defaultValue);
211  }
212 
213  /** Error (more generally, status) code
214  * Equivalent to status().code
215  * \sa status
216  */
217  int error() const;
218 
219  /** Error-specific message, as returned by the server */
220  virtual QString errorString() const;
221 
222  /** A URL to help/clarify the error, if provided by the server */
223  QUrl errorUrl() const;
224 
225  int maxRetries() const;
226  void setMaxRetries(int newMaxRetries);
227 
228  using duration_ms_t = std::chrono::milliseconds::rep; // normally int64_t
229 
230  std::chrono::seconds getCurrentTimeout() const;
231  Q_INVOKABLE Quotient::BaseJob::duration_ms_t getCurrentTimeoutMs() const;
232  std::chrono::seconds getNextRetryInterval() const;
233  Q_INVOKABLE Quotient::BaseJob::duration_ms_t getNextRetryMs() const;
234  std::chrono::milliseconds timeToRetry() const;
235  Q_INVOKABLE Quotient::BaseJob::duration_ms_t millisToRetry() const;
236 
237  friend QDebug operator<<(QDebug dbg, const BaseJob* j)
238  {
239  return dbg << j->objectName();
240  }
241 
242 public Q_SLOTS:
243  void initiate(Quotient::ConnectionData* connData, bool inBackground);
244 
245  //! \brief Abandon the result of this job, arrived or unarrived.
246  //!
247  //! This aborts waiting for a reply from the server (if there was
248  //! any pending) and deletes the job object. No result signals
249  //! (result, success, failure) are emitted, only finished() is.
250  void abandon();
251 
252 Q_SIGNALS:
253  //! \brief The job is about to send a network request
254  //!
255  //! This signal is emitted every time a network request is made (which can
256  //! occur several times due to job retries). You can use it to change
257  //! the request parameters (such as redirect policy) if necessary. If you
258  //! need to set additional request headers or query items, do that using
259  //! setRequestHeaders() and setRequestQuery() instead.
260  //! \note \p req is not guaranteed to exist (i.e. it may point to garbage)
261  //! unless this signal is handled via a DirectConnection (or
262  //! BlockingQueuedConnection if in another thread), i.e.,
263  //! synchronously.
264  //! \sa setRequestHeaders, setRequestQuery
265  void aboutToSendRequest(QNetworkRequest* req);
266 
267  /** The job has sent a network request */
268  void sentRequest();
269 
270  /** The job has changed its status */
271  void statusChanged(Quotient::BaseJob::Status newStatus);
272 
273  /**
274  * The previous network request has failed; the next attempt will
275  * be done in the specified time
276  * @param nextAttempt the 1-based number of attempt (will always be more
277  * than 1)
278  * @param inMilliseconds the interval after which the next attempt will be
279  * taken
280  */
281  void retryScheduled(int nextAttempt,
282  Quotient::BaseJob::duration_ms_t inMilliseconds);
283 
284  /**
285  * The previous network request has been rate-limited; the next attempt
286  * will be queued and run sometime later. Since other jobs may already
287  * wait in the queue, it's not possible to predict the wait time.
288  */
289  void rateLimited();
290 
291  /**
292  * Emitted when the job is finished, in any case. It is used to notify
293  * observers that the job is terminated and that progress can be hidden.
294  *
295  * This should not be emitted directly by subclasses;
296  * use finishJob() instead.
297  *
298  * In general, to be notified of a job's completion, client code
299  * should connect to result(), success(), or failure()
300  * rather than finished(). However if you need to track the job's
301  * lifecycle you should connect to this instead of result();
302  * in particular, only this signal will be emitted on abandoning.
303  *
304  * @param job the job that emitted this signal
305  *
306  * @see result, success, failure
307  */
308  void finished(Quotient::BaseJob* job);
309 
310  /**
311  * Emitted when the job is finished (except when abandoned).
312  *
313  * Use error() to know if the job was finished with error.
314  *
315  * @param job the job that emitted this signal
316  *
317  * @see success, failure
318  */
319  void result(Quotient::BaseJob* job);
320 
321  /**
322  * Emitted together with result() in case there's no error.
323  *
324  * @see result, failure
325  */
326  void success(Quotient::BaseJob*);
327 
328  /**
329  * Emitted together with result() if there's an error.
330  * Similar to result(), this won't be emitted in case of abandon().
331  *
332  * @see result, success
333  */
334  void failure(Quotient::BaseJob*);
335 
336  void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
337  void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
338 
339 protected:
340  using headers_t = QHash<QByteArray, QByteArray>;
341 
342  const headers_t& requestHeaders() const;
343  void setRequestHeader(const headers_t::key_type& headerName,
344  const headers_t::mapped_type& headerValue);
345  void setRequestHeaders(const headers_t& headers);
346  QUrlQuery query() const;
347  void setRequestQuery(const QUrlQuery& query);
348  const RequestData& requestData() const;
349  void setRequestData(RequestData&& data);
350  const QByteArrayList& expectedContentTypes() const;
351  void addExpectedContentType(const QByteArray& contentType);
352  void setExpectedContentTypes(const QByteArrayList& contentTypes);
353  QByteArrayList expectedKeys() const;
354  void addExpectedKey(const QByteArray &key);
355  void setExpectedKeys(const QByteArrayList &keys);
356 
357  const QNetworkReply* reply() const;
358  QNetworkReply* reply();
359 
360  /** Construct a URL out of baseUrl, path and query
361  *
362  * The function ensures exactly one '/' between the path component of
363  * \p baseUrl and \p path. The query component of \p baseUrl is ignored.
364  */
365  static QUrl makeRequestUrl(QUrl baseUrl, const QByteArray &encodedPath,
366  const QUrlQuery& query = {});
367 
368  /*! Prepares the job for execution
369  *
370  * This method is called no more than once per job lifecycle,
371  * when it's first scheduled for execution; in particular, it is not called
372  * on retries.
373  */
374  virtual void doPrepare();
375 
376  /*! Postprocessing after the network request has been sent
377  *
378  * This method is called every time the job receives a running
379  * QNetworkReply object from NetworkAccessManager - basically, after
380  * successfully sending a network request (including retries).
381  */
382  virtual void onSentRequest(QNetworkReply*);
383  virtual void beforeAbandon();
384 
385  /*! \brief An extension point for additional reply processing.
386  *
387  * The base implementation does nothing and returns Success.
388  *
389  * \sa gotReply
390  */
391  virtual Status prepareResult();
392 
393  /*! \brief Process details of the error
394  *
395  * The function processes the reply in case when status from checkReply()
396  * was not good (usually because of an unsuccessful HTTP code).
397  * The base implementation assumes Matrix JSON error object in the body;
398  * overrides are strongly recommended to call it for all stock Matrix
399  * responses as early as possible and only then process custom errors,
400  * with JSON or non-JSON payload.
401  *
402  * \return updated (if necessary) job status
403  */
404  virtual Status prepareError(Status currentStatus);
405 
406  /*! \brief Get direct access to the JSON response object in the job
407  *
408  * This allows to implement deserialisation with "move" semantics for parts
409  * of the response. Assuming that the response body is a valid JSON object,
410  * the function calls QJsonObject::take(key) on it and returns the result.
411  *
412  * \return QJsonValue::Null, if the response content type is not
413  * advertised as `application/json`;
414  * QJsonValue::Undefined, if the response is a JSON object but
415  * doesn't have \p key;
416  * the value for \p key otherwise.
417  *
418  * \sa takeFromJson
419  */
420  QJsonValue takeValueFromJson(const QString& key);
421 
422  void setStatus(Status s);
423  void setStatus(int code, QString message);
424 
425  //! \brief Set the logging category for the given job instance
426  //!
427  //! \param lcf The logging category function to provide the category -
428  //! the one you define with Q_LOGGING_CATEGORY (without
429  //! parentheses, BaseJob will call it for you)
430  void setLoggingCategory(QMessageLogger::CategoryFunction lcf);
431 
432  // Job objects should only be deleted via QObject::deleteLater
433  ~BaseJob() override;
434 
435 protected Q_SLOTS:
436  void timeout();
437 
438  /*! \brief Check the pending or received reply for upfront issues
439  *
440  * This is invoked when headers are first received and also once
441  * the complete reply is obtained; the base implementation checks the HTTP
442  * headers to detect general issues such as network errors or access denial
443  * and it's strongly recommended to call it from overrides,
444  * as early as possible.
445  * This slot is const and cannot read the response body. If you need to read
446  * the body on the fly, override onSentRequest() and connect in it
447  * to reply->readyRead(); and if you only need to validate the body after
448  * it fully arrived, use prepareResult() for that). Returning anything
449  * except NoError/Success switches further processing from prepareResult()
450  * to prepareError().
451  *
452  * @return the result of checking the reply
453  *
454  * @see gotReply
455  */
456  virtual Status checkReply(const QNetworkReply *reply) const;
457 
458 private Q_SLOTS:
459  void sendRequest();
460  void gotReply();
461 
462 private:
463  friend class ConnectionData; // to provide access to sendRequest()
464  template <class JobT>
465  friend class JobHandle;
466 
467  void stop();
468  void finishJob();
469  QFuture<void> future();
470 
471  class Private;
472  ImplPtr<Private> d;
473 };
474 
475 inline bool QUOTIENT_API isJobPending(BaseJob* job)
476 {
477  return job && job->error() == BaseJob::Pending;
478 }
479 } // namespace Quotient