diff options
Diffstat (limited to 'lib')
244 files changed, 17130 insertions, 17658 deletions
diff --git a/lib/application-service/definitions/location.cpp b/lib/application-service/definitions/location.cpp deleted file mode 100644 index a53db8d7..00000000 --- a/lib/application-service/definitions/location.cpp +++ /dev/null @@ -1,24 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "location.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<ThirdPartyLocation>::dumpTo( - QJsonObject& jo, const ThirdPartyLocation& pod) -{ - addParam<>(jo, QStringLiteral("alias"), pod.alias); - addParam<>(jo, QStringLiteral("protocol"), pod.protocol); - addParam<>(jo, QStringLiteral("fields"), pod.fields); -} - -void JsonObjectConverter<ThirdPartyLocation>::fillFrom( - const QJsonObject& jo, ThirdPartyLocation& result) -{ - fromJson(jo.value("alias"_ls), result.alias); - fromJson(jo.value("protocol"_ls), result.protocol); - fromJson(jo.value("fields"_ls), result.fields); -} - diff --git a/lib/application-service/definitions/location.h b/lib/application-service/definitions/location.h index 5586cfc6..6801c99f 100644 --- a/lib/application-service/definitions/location.h +++ b/lib/application-service/definitions/location.h @@ -6,25 +6,33 @@ #include "converters.h" -#include <QtCore/QJsonObject> +namespace Quotient { -namespace QMatrixClient -{ - // Data structures +struct ThirdPartyLocation { + /// An alias for a matrix room. + QString alias; - struct ThirdPartyLocation + /// The protocol ID that the third party location is a part of. + QString protocol; + + /// Information used to identify this third party location. + QJsonObject fields; +}; + +template <> +struct JsonObjectConverter<ThirdPartyLocation> { + static void dumpTo(QJsonObject& jo, const ThirdPartyLocation& pod) { - /// An alias for a matrix room. - QString alias; - /// The protocol ID that the third party location is a part of. - QString protocol; - /// Information used to identify this third party location. - QJsonObject fields; - }; - template <> struct JsonObjectConverter<ThirdPartyLocation> + addParam<>(jo, QStringLiteral("alias"), pod.alias); + addParam<>(jo, QStringLiteral("protocol"), pod.protocol); + addParam<>(jo, QStringLiteral("fields"), pod.fields); + } + static void fillFrom(const QJsonObject& jo, ThirdPartyLocation& pod) { - static void dumpTo(QJsonObject& jo, const ThirdPartyLocation& pod); - static void fillFrom(const QJsonObject& jo, ThirdPartyLocation& pod); - }; + fromJson(jo.value("alias"_ls), pod.alias); + fromJson(jo.value("protocol"_ls), pod.protocol); + fromJson(jo.value("fields"_ls), pod.fields); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/application-service/definitions/protocol.cpp b/lib/application-service/definitions/protocol.cpp deleted file mode 100644 index 2a62b15d..00000000 --- a/lib/application-service/definitions/protocol.cpp +++ /dev/null @@ -1,60 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "protocol.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<FieldType>::dumpTo( - QJsonObject& jo, const FieldType& pod) -{ - addParam<>(jo, QStringLiteral("regexp"), pod.regexp); - addParam<>(jo, QStringLiteral("placeholder"), pod.placeholder); -} - -void JsonObjectConverter<FieldType>::fillFrom( - const QJsonObject& jo, FieldType& result) -{ - fromJson(jo.value("regexp"_ls), result.regexp); - fromJson(jo.value("placeholder"_ls), result.placeholder); -} - -void JsonObjectConverter<ProtocolInstance>::dumpTo( - QJsonObject& jo, const ProtocolInstance& pod) -{ - addParam<>(jo, QStringLiteral("desc"), pod.desc); - addParam<IfNotEmpty>(jo, QStringLiteral("icon"), pod.icon); - addParam<>(jo, QStringLiteral("fields"), pod.fields); - addParam<>(jo, QStringLiteral("network_id"), pod.networkId); -} - -void JsonObjectConverter<ProtocolInstance>::fillFrom( - const QJsonObject& jo, ProtocolInstance& result) -{ - fromJson(jo.value("desc"_ls), result.desc); - fromJson(jo.value("icon"_ls), result.icon); - fromJson(jo.value("fields"_ls), result.fields); - fromJson(jo.value("network_id"_ls), result.networkId); -} - -void JsonObjectConverter<ThirdPartyProtocol>::dumpTo( - QJsonObject& jo, const ThirdPartyProtocol& pod) -{ - addParam<>(jo, QStringLiteral("user_fields"), pod.userFields); - addParam<>(jo, QStringLiteral("location_fields"), pod.locationFields); - addParam<>(jo, QStringLiteral("icon"), pod.icon); - addParam<>(jo, QStringLiteral("field_types"), pod.fieldTypes); - addParam<>(jo, QStringLiteral("instances"), pod.instances); -} - -void JsonObjectConverter<ThirdPartyProtocol>::fillFrom( - const QJsonObject& jo, ThirdPartyProtocol& result) -{ - fromJson(jo.value("user_fields"_ls), result.userFields); - fromJson(jo.value("location_fields"_ls), result.locationFields); - fromJson(jo.value("icon"_ls), result.icon); - fromJson(jo.value("field_types"_ls), result.fieldTypes); - fromJson(jo.value("instances"_ls), result.instances); -} - diff --git a/lib/application-service/definitions/protocol.h b/lib/application-service/definitions/protocol.h index 0a1f9a21..6aee9c57 100644 --- a/lib/application-service/definitions/protocol.h +++ b/lib/application-service/definitions/protocol.h @@ -6,78 +6,113 @@ #include "converters.h" -#include <QtCore/QHash> -#include <QtCore/QJsonObject> -#include "converters.h" -#include <QtCore/QVector> +namespace Quotient { +/// Definition of valid values for a field. +struct FieldType { + /// A regular expression for validation of a field's value. This may be + /// relatively coarse to verify the value as the application service + /// providing this protocol may apply additional validation or filtering. + QString regexp; -namespace QMatrixClient -{ - // Data structures + /// An placeholder serving as a valid example of the field value. + QString placeholder; +}; - /// Definition of valid values for a field. - struct FieldType +template <> +struct JsonObjectConverter<FieldType> { + static void dumpTo(QJsonObject& jo, const FieldType& pod) { - /// A regular expression for validation of a field's value. This may be relatively - /// coarse to verify the value as the application service providing this protocol - /// may apply additional validation or filtering. - QString regexp; - /// An placeholder serving as a valid example of the field value. - QString placeholder; - }; - template <> struct JsonObjectConverter<FieldType> + addParam<>(jo, QStringLiteral("regexp"), pod.regexp); + addParam<>(jo, QStringLiteral("placeholder"), pod.placeholder); + } + static void fillFrom(const QJsonObject& jo, FieldType& pod) { - static void dumpTo(QJsonObject& jo, const FieldType& pod); - static void fillFrom(const QJsonObject& jo, FieldType& pod); - }; + fromJson(jo.value("regexp"_ls), pod.regexp); + fromJson(jo.value("placeholder"_ls), pod.placeholder); + } +}; + +struct ProtocolInstance { + /// A human-readable description for the protocol, such as the name. + QString desc; + + /// An optional content URI representing the protocol. Overrides the one + /// provided at the higher level Protocol object. + QString icon; + + /// Preset values for ``fields`` the client may use to search by. + QJsonObject fields; + + /// A unique identifier across all instances. + QString networkId; +}; - struct ProtocolInstance +template <> +struct JsonObjectConverter<ProtocolInstance> { + static void dumpTo(QJsonObject& jo, const ProtocolInstance& pod) { - /// A human-readable description for the protocol, such as the name. - QString desc; - /// An optional content URI representing the protocol. Overrides the one provided - /// at the higher level Protocol object. - QString icon; - /// Preset values for ``fields`` the client may use to search by. - QJsonObject fields; - /// A unique identifier across all instances. - QString networkId; - }; - template <> struct JsonObjectConverter<ProtocolInstance> + addParam<>(jo, QStringLiteral("desc"), pod.desc); + addParam<IfNotEmpty>(jo, QStringLiteral("icon"), pod.icon); + addParam<>(jo, QStringLiteral("fields"), pod.fields); + addParam<>(jo, QStringLiteral("network_id"), pod.networkId); + } + static void fillFrom(const QJsonObject& jo, ProtocolInstance& pod) { - static void dumpTo(QJsonObject& jo, const ProtocolInstance& pod); - static void fillFrom(const QJsonObject& jo, ProtocolInstance& pod); - }; + fromJson(jo.value("desc"_ls), pod.desc); + fromJson(jo.value("icon"_ls), pod.icon); + fromJson(jo.value("fields"_ls), pod.fields); + fromJson(jo.value("network_id"_ls), pod.networkId); + } +}; + +struct ThirdPartyProtocol { + /// Fields which may be used to identify a third party user. These should be + /// ordered to suggest the way that entities may be grouped, where higher + /// groupings are ordered first. For example, the name of a network should + /// be searched before the nickname of a user. + QStringList userFields; + + /// Fields which may be used to identify a third party location. These + /// should be ordered to suggest the way that entities may be grouped, where + /// higher groupings are ordered first. For example, the name of a network + /// should be searched before the name of a channel. + QStringList locationFields; + + /// A content URI representing an icon for the third party protocol. + QString icon; + + /// The type definitions for the fields defined in the ``user_fields`` and + /// ``location_fields``. Each entry in those arrays MUST have an entry here. + /// The + /// ``string`` key for this object is field name itself. + /// + /// May be an empty object if no fields are defined. + QHash<QString, FieldType> fieldTypes; + + /// A list of objects representing independent instances of configuration. + /// For example, multiple networks on IRC if multiple are provided by the + /// same application service. + QVector<ProtocolInstance> instances; +}; - struct ThirdPartyProtocol +template <> +struct JsonObjectConverter<ThirdPartyProtocol> { + static void dumpTo(QJsonObject& jo, const ThirdPartyProtocol& pod) { - /// Fields which may be used to identify a third party user. These should be - /// ordered to suggest the way that entities may be grouped, where higher - /// groupings are ordered first. For example, the name of a network should be - /// searched before the nickname of a user. - QStringList userFields; - /// Fields which may be used to identify a third party location. These should be - /// ordered to suggest the way that entities may be grouped, where higher - /// groupings are ordered first. For example, the name of a network should be - /// searched before the name of a channel. - QStringList locationFields; - /// A content URI representing an icon for the third party protocol. - QString icon; - /// The type definitions for the fields defined in the ``user_fields`` and - /// ``location_fields``. Each entry in those arrays MUST have an entry here. The - /// ``string`` key for this object is field name itself. - /// - /// May be an empty object if no fields are defined. - QHash<QString, FieldType> fieldTypes; - /// A list of objects representing independent instances of configuration. - /// For example, multiple networks on IRC if multiple are provided by the - /// same application service. - QVector<ProtocolInstance> instances; - }; - template <> struct JsonObjectConverter<ThirdPartyProtocol> + addParam<>(jo, QStringLiteral("user_fields"), pod.userFields); + addParam<>(jo, QStringLiteral("location_fields"), pod.locationFields); + addParam<>(jo, QStringLiteral("icon"), pod.icon); + addParam<>(jo, QStringLiteral("field_types"), pod.fieldTypes); + addParam<>(jo, QStringLiteral("instances"), pod.instances); + } + static void fillFrom(const QJsonObject& jo, ThirdPartyProtocol& pod) { - static void dumpTo(QJsonObject& jo, const ThirdPartyProtocol& pod); - static void fillFrom(const QJsonObject& jo, ThirdPartyProtocol& pod); - }; + fromJson(jo.value("user_fields"_ls), pod.userFields); + fromJson(jo.value("location_fields"_ls), pod.locationFields); + fromJson(jo.value("icon"_ls), pod.icon); + fromJson(jo.value("field_types"_ls), pod.fieldTypes); + fromJson(jo.value("instances"_ls), pod.instances); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/application-service/definitions/user.cpp b/lib/application-service/definitions/user.cpp deleted file mode 100644 index 8ba92321..00000000 --- a/lib/application-service/definitions/user.cpp +++ /dev/null @@ -1,24 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "user.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<ThirdPartyUser>::dumpTo( - QJsonObject& jo, const ThirdPartyUser& pod) -{ - addParam<>(jo, QStringLiteral("userid"), pod.userid); - addParam<>(jo, QStringLiteral("protocol"), pod.protocol); - addParam<>(jo, QStringLiteral("fields"), pod.fields); -} - -void JsonObjectConverter<ThirdPartyUser>::fillFrom( - const QJsonObject& jo, ThirdPartyUser& result) -{ - fromJson(jo.value("userid"_ls), result.userid); - fromJson(jo.value("protocol"_ls), result.protocol); - fromJson(jo.value("fields"_ls), result.fields); -} - diff --git a/lib/application-service/definitions/user.h b/lib/application-service/definitions/user.h index 062d2cac..3342ef80 100644 --- a/lib/application-service/definitions/user.h +++ b/lib/application-service/definitions/user.h @@ -6,25 +6,33 @@ #include "converters.h" -#include <QtCore/QJsonObject> +namespace Quotient { -namespace QMatrixClient -{ - // Data structures +struct ThirdPartyUser { + /// A Matrix User ID represting a third party user. + QString userid; - struct ThirdPartyUser + /// The protocol ID that the third party location is a part of. + QString protocol; + + /// Information used to identify this third party location. + QJsonObject fields; +}; + +template <> +struct JsonObjectConverter<ThirdPartyUser> { + static void dumpTo(QJsonObject& jo, const ThirdPartyUser& pod) { - /// A Matrix User ID represting a third party user. - QString userid; - /// The protocol ID that the third party location is a part of. - QString protocol; - /// Information used to identify this third party location. - QJsonObject fields; - }; - template <> struct JsonObjectConverter<ThirdPartyUser> + addParam<>(jo, QStringLiteral("userid"), pod.userid); + addParam<>(jo, QStringLiteral("protocol"), pod.protocol); + addParam<>(jo, QStringLiteral("fields"), pod.fields); + } + static void fillFrom(const QJsonObject& jo, ThirdPartyUser& pod) { - static void dumpTo(QJsonObject& jo, const ThirdPartyUser& pod); - static void fillFrom(const QJsonObject& jo, ThirdPartyUser& pod); - }; + fromJson(jo.value("userid"_ls), pod.userid); + fromJson(jo.value("protocol"_ls), pod.protocol); + fromJson(jo.value("fields"_ls), pod.fields); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/avatar.cpp b/lib/avatar.cpp index 9279ef9d..c65aa25c 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -1,5 +1,3 @@ -#include <utility> - /****************************************************************************** * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> * @@ -15,64 +13,58 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "avatar.h" -#include "jobs/mediathumbnailjob.h" -#include "events/eventcontent.h" #include "connection.h" -#include <QtGui/QPainter> -#include <QtCore/QPointer> +#include "events/eventcontent.h" +#include "jobs/mediathumbnailjob.h" + #include <QtCore/QDir> +#include <QtCore/QPointer> #include <QtCore/QStandardPaths> #include <QtCore/QStringBuilder> +#include <QtGui/QPainter> -using namespace QMatrixClient; +using namespace Quotient; using std::move; -class Avatar::Private -{ - public: - explicit Private(QUrl url = {}) - : _url(move(url)) - { } - ~Private() - { - if (isJobRunning(_thumbnailRequest)) - _thumbnailRequest->abandon(); - if (isJobRunning(_uploadRequest)) - _uploadRequest->abandon(); - } - - QImage get(Connection* connection, QSize size, - get_callback_t callback) const; - bool upload(UploadContentJob* job, upload_callback_t callback); - - bool checkUrl(const QUrl& url) const; - QString localFile() const; - - QUrl _url; - - // The below are related to image caching, hence mutable - mutable QImage _originalImage; - mutable std::vector<QPair<QSize, QImage>> _scaledImages; - mutable QSize _requestedSize; - mutable enum { Unknown, Cache, Network, Banned } _imageSource = Unknown; - mutable QPointer<MediaThumbnailJob> _thumbnailRequest = nullptr; - mutable QPointer<BaseJob> _uploadRequest = nullptr; - mutable std::vector<get_callback_t> callbacks; +class Avatar::Private { +public: + explicit Private(QUrl url = {}) : _url(move(url)) {} + ~Private() + { + if (isJobRunning(_thumbnailRequest)) + _thumbnailRequest->abandon(); + if (isJobRunning(_uploadRequest)) + _uploadRequest->abandon(); + } + + QImage get(Connection* connection, QSize size, + get_callback_t callback) const; + bool upload(UploadContentJob* job, upload_callback_t&& callback); + + bool checkUrl(const QUrl& url) const; + QString localFile() const; + + QUrl _url; + + // The below are related to image caching, hence mutable + mutable QImage _originalImage; + mutable std::vector<QPair<QSize, QImage>> _scaledImages; + mutable QSize _requestedSize; + mutable enum { Unknown, Cache, Network, Banned } _imageSource = Unknown; + mutable QPointer<MediaThumbnailJob> _thumbnailRequest = nullptr; + mutable QPointer<BaseJob> _uploadRequest = nullptr; + mutable std::vector<get_callback_t> callbacks; }; -Avatar::Avatar() - : d(std::make_unique<Private>()) -{ } +Avatar::Avatar() : d(std::make_unique<Private>()) {} -Avatar::Avatar(QUrl url) - : d(std::make_unique<Private>(std::move(url))) -{ } +Avatar::Avatar(QUrl url) : d(std::make_unique<Private>(std::move(url))) {} Avatar::Avatar(Avatar&&) = default; @@ -83,13 +75,13 @@ Avatar& Avatar::operator=(Avatar&&) = default; QImage Avatar::get(Connection* connection, int dimension, get_callback_t callback) const { - return d->get(connection, {dimension, dimension}, move(callback)); + return d->get(connection, { dimension, dimension }, move(callback)); } QImage Avatar::get(Connection* connection, int width, int height, get_callback_t callback) const { - return d->get(connection, {width, height}, move(callback)); + return d->get(connection, { width, height }, move(callback)); } bool Avatar::upload(Connection* connection, const QString& fileName, @@ -108,22 +100,17 @@ bool Avatar::upload(Connection* connection, QIODevice* source, return d->upload(connection->uploadContent(source), move(callback)); } -QString Avatar::mediaId() const -{ - return d->_url.authority() + d->_url.path(); -} +QString Avatar::mediaId() const { return d->_url.authority() + d->_url.path(); } QImage Avatar::Private::get(Connection* connection, QSize size, get_callback_t callback) const { - if (!callback) - { + if (!callback) { qCCritical(MAIN) << "Null callbacks are not allowed in Avatar::get"; Q_ASSERT(false); } - if (_imageSource == Unknown && _originalImage.load(localFile())) - { + if (_imageSource == Unknown && _originalImage.load(localFile())) { _imageSource = Cache; _requestedSize = _originalImage.size(); } @@ -132,9 +119,10 @@ QImage Avatar::Private::get(Connection* connection, QSize size, // to trick the below code into constantly getting another image from // the server because the existing one is alleged unsatisfactory. // Client authors can only blame themselves if they do so. - if (((_imageSource == Unknown && !_thumbnailRequest) || - size.width() > _requestedSize.width() || - size.height() > _requestedSize.height()) && checkUrl(_url)) { + if (((_imageSource == Unknown && !_thumbnailRequest) + || size.width() > _requestedSize.width() + || size.height() > _requestedSize.height()) + && checkUrl(_url)) { qCDebug(MAIN) << "Getting avatar from" << _url.toString(); _requestedSize = size; if (isJobRunning(_thumbnailRequest)) @@ -142,35 +130,37 @@ QImage Avatar::Private::get(Connection* connection, QSize size, if (callback) callbacks.emplace_back(move(callback)); _thumbnailRequest = connection->getThumbnail(_url, size); - QObject::connect( _thumbnailRequest, &MediaThumbnailJob::success, - _thumbnailRequest, [this] { - _imageSource = Network; - _originalImage = - _thumbnailRequest->scaledThumbnail(_requestedSize); - _originalImage.save(localFile()); - _scaledImages.clear(); - for (const auto& n: callbacks) - n(); - callbacks.clear(); - }); + QObject::connect(_thumbnailRequest, &MediaThumbnailJob::success, + _thumbnailRequest, [this] { + _imageSource = Network; + _originalImage = _thumbnailRequest->scaledThumbnail( + _requestedSize); + _originalImage.save(localFile()); + _scaledImages.clear(); + for (const auto& n : callbacks) + n(); + callbacks.clear(); + }); } - for (const auto& p: _scaledImages) + for (const auto& p : _scaledImages) if (p.first == size) return p.second; - auto result = _originalImage.isNull() ? QImage() : _originalImage.scaled(size, - Qt::KeepAspectRatio, Qt::SmoothTransformation); + auto result = _originalImage.isNull() + ? QImage() + : _originalImage.scaled(size, Qt::KeepAspectRatio, + Qt::SmoothTransformation); _scaledImages.emplace_back(size, result); return result; } -bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t callback) +bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t &&callback) { _uploadRequest = job; if (!isJobRunning(_uploadRequest)) return false; _uploadRequest->connect(_uploadRequest, &BaseJob::success, _uploadRequest, - [job,callback] { callback(job->contentUri()); }); + [job, callback] { callback(job->contentUri()); }); return true; } @@ -181,8 +171,7 @@ bool Avatar::Private::checkUrl(const QUrl& url) const // FIXME: Make "mxc" a library-wide constant and maybe even make // the URL checker a Connection(?) method. - if (!url.isValid() || url.scheme() != "mxc" || url.path().count('/') != 1) - { + if (!url.isValid() || url.scheme() != "mxc" || url.path().count('/') != 1) { qCWarning(MAIN) << "Avatar URL is invalid or not mxc-based:" << url.toDisplayString(); _imageSource = Banned; @@ -190,7 +179,8 @@ bool Avatar::Private::checkUrl(const QUrl& url) const return _imageSource != Banned; } -QString Avatar::Private::localFile() const { +QString Avatar::Private::localFile() const +{ static const auto cachePath = cacheLocation(QStringLiteral("avatars")); return cachePath % _url.authority() % '_' % _url.fileName() % ".png"; } diff --git a/lib/avatar.h b/lib/avatar.h index c86345e3..7a566bfa 100644 --- a/lib/avatar.h +++ b/lib/avatar.h @@ -13,49 +13,49 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once -#include <QtGui/QIcon> #include <QtCore/QUrl> +#include <QtGui/QIcon> #include <functional> #include <memory> -namespace QMatrixClient -{ - class Connection; - - class Avatar - { - public: - explicit Avatar(); - explicit Avatar(QUrl url); - Avatar(Avatar&&); - ~Avatar(); - Avatar& operator=(Avatar&&); - - using get_callback_t = std::function<void()>; - using upload_callback_t = std::function<void(QString)>; - - QImage get(Connection* connection, int dimension, - get_callback_t callback) const; - QImage get(Connection* connection, int w, int h, - get_callback_t callback) const; - - bool upload(Connection* connection, const QString& fileName, - upload_callback_t callback) const; - bool upload(Connection* connection, QIODevice* source, - upload_callback_t callback) const; - - QString mediaId() const; - QUrl url() const; - bool updateUrl(const QUrl& newUrl); - - private: - class Private; - std::unique_ptr<Private> d; - }; -} // namespace QMatrixClient +namespace Quotient { +class Connection; + +class Avatar { +public: + explicit Avatar(); + explicit Avatar(QUrl url); + Avatar(Avatar&&); + ~Avatar(); + Avatar& operator=(Avatar&&); + + using get_callback_t = std::function<void()>; + using upload_callback_t = std::function<void(QString)>; + + QImage get(Connection* connection, int dimension, + get_callback_t callback) const; + QImage get(Connection* connection, int w, int h, + get_callback_t callback) const; + + bool upload(Connection* connection, const QString& fileName, + upload_callback_t callback) const; + bool upload(Connection* connection, QIODevice* source, + upload_callback_t callback) const; + + QString mediaId() const; + QUrl url() const; + bool updateUrl(const QUrl& newUrl); + +private: + class Private; + std::unique_ptr<Private> d; +}; +} // namespace Quotient +/// \deprecated Use namespace Quotient instead +namespace QMatrixClient = Quotient;
\ No newline at end of file diff --git a/lib/connection.cpp b/lib/connection.cpp index 0c98c383..3ed71bb4 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -13,52 +13,64 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "connection.h" + #include "connectiondata.h" -#include "user.h" -#include "events/directchatevent.h" -#include "events/eventloader.h" +#ifdef Quotient_E2EE_ENABLED +# include "encryptionmanager.h" +#endif // Quotient_E2EE_ENABLED #include "room.h" #include "settings.h" -#include "csapi/login.h" +#include "user.h" + +#include "csapi/account-data.h" #include "csapi/capabilities.h" +#include "csapi/joining.h" +#include "csapi/leaving.h" #include "csapi/logout.h" #include "csapi/receipts.h" -#include "csapi/leaving.h" -#include "csapi/account-data.h" -#include "csapi/joining.h" -#include "csapi/to_device.h" #include "csapi/room_send.h" -#include "csapi/wellknown.h" +#include "csapi/to_device.h" #include "csapi/versions.h" -#include "jobs/syncjob.h" -#include "jobs/mediathumbnailjob.h" -#include "jobs/downloadfilejob.h" #include "csapi/voip.h" +#include "csapi/wellknown.h" -#include <QtCore/QFile> +#include "events/directchatevent.h" +#include "events/eventloader.h" +#include "jobs/downloadfilejob.h" +#include "jobs/mediathumbnailjob.h" +#include "jobs/syncjob.h" + +#ifdef Quotient_E2EE_ENABLED +# include "account.h" // QtOlm +#endif // Quotient_E2EE_ENABLED + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) +# include <QtCore/QCborValue> +#endif + +#include <QtCore/QCoreApplication> #include <QtCore/QDir> -#include <QtCore/QStandardPaths> -#include <QtCore/QStringBuilder> #include <QtCore/QElapsedTimer> -#include <QtCore/QRegularExpression> +#include <QtCore/QFile> #include <QtCore/QMimeDatabase> -#include <QtCore/QCoreApplication> +#include <QtCore/QRegularExpression> +#include <QtCore/QStandardPaths> +#include <QtCore/QStringBuilder> +#include <QtNetwork/QDnsLookup> -using namespace QMatrixClient; +using namespace Quotient; // This is very much Qt-specific; STL iterators don't have key() and value() template <typename HashT, typename Pred> HashT erase_if(HashT& hashMap, Pred pred) { HashT removals; - for (auto it = hashMap.begin(); it != hashMap.end();) - { - if (pred(it)) - { + for (auto it = hashMap.begin(); it != hashMap.end();) { + if (pred(it)) { removals.insert(it.key(), it.value()); it = hashMap.erase(it); } else @@ -67,168 +79,256 @@ HashT erase_if(HashT& hashMap, Pred pred) return removals; } -class Connection::Private -{ - public: - explicit Private(std::unique_ptr<ConnectionData>&& connection) - : data(move(connection)) - { } - Q_DISABLE_COPY(Private) - DISABLE_MOVE(Private) - - Connection* q = nullptr; - std::unique_ptr<ConnectionData> data; - // A complex key below is a pair of room name and whether its - // state is Invited. The spec mandates to keep Invited room state - // separately; specifically, we should keep objects for Invite and - // Leave state of the same room if the two happen to co-exist. - QHash<QPair<QString, bool>, Room*> roomMap; - /// Mapping from aliases to room ids, as of the last sync - QHash<QString, QString> roomAliasMap; - QVector<QString> roomIdsToForget; - QVector<Room*> firstTimeRooms; - QVector<QString> pendingStateRoomIds; - QMap<QString, User*> userMap; - DirectChatsMap directChats; - DirectChatUsersMap directChatUsers; - // The below two variables track local changes between sync completions. - // See also: https://github.com/QMatrixClient/libqmatrixclient/wiki/Handling-direct-chat-events - DirectChatsMap dcLocalAdditions; - DirectChatsMap dcLocalRemovals; - std::unordered_map<QString, EventPtr> accountData; - QString userId; - int syncLoopTimeout = -1; - - GetCapabilitiesJob* capabilitiesJob = nullptr; - GetCapabilitiesJob::Capabilities capabilities; - - QVector<GetLoginFlowsJob::LoginFlow> loginFlows; - - SyncJob* syncJob = nullptr; - - bool cacheState = true; - bool cacheToBinary = SettingsGroup("libqmatrixclient") - .value("cache_type").toString() != "json"; - bool lazyLoading = false; - - template <typename... LoginArgTs> - void loginToServer(LoginArgTs&&... loginArgs); - void assumeIdentity(const QString& newUserId, const QString& accessToken, - const QString& deviceId); - - template <typename EventT> - EventT* unpackAccountData() const - { - const auto& eventIt = accountData.find(EventT::matrixTypeId()); - return eventIt == accountData.end() - ? nullptr : weakPtrCast<EventT>(eventIt->second); +class Connection::Private { +public: + explicit Private(std::unique_ptr<ConnectionData>&& connection) + : data(move(connection)) + {} + Q_DISABLE_COPY(Private) + DISABLE_MOVE(Private) + + Connection* q = nullptr; + std::unique_ptr<ConnectionData> data; + // A complex key below is a pair of room name and whether its + // state is Invited. The spec mandates to keep Invited room state + // separately; specifically, we should keep objects for Invite and + // Leave state of the same room if the two happen to co-exist. + QHash<QPair<QString, bool>, Room*> roomMap; + /// Mapping from serverparts to alias/room id mappings, + /// as of the last sync + QHash<QString, QString> roomAliasMap; + QVector<QString> roomIdsToForget; + QVector<Room*> firstTimeRooms; + QVector<QString> pendingStateRoomIds; + QMap<QString, User*> userMap; + DirectChatsMap directChats; + DirectChatUsersMap directChatUsers; + // The below two variables track local changes between sync completions. + // See https://github.com/quotient-im/libQuotient/wiki/Handling-direct-chat-events + DirectChatsMap dcLocalAdditions; + DirectChatsMap dcLocalRemovals; + UnorderedMap<QString, EventPtr> accountData; + QMetaObject::Connection syncLoopConnection {}; + int syncTimeout = -1; + + GetCapabilitiesJob* capabilitiesJob = nullptr; + GetCapabilitiesJob::Capabilities capabilities; + + QVector<GetLoginFlowsJob::LoginFlow> loginFlows; + +#ifdef Quotient_E2EE_ENABLED + QScopedPointer<EncryptionManager> encryptionManager; +#endif // Quotient_E2EE_ENABLED + + QPointer<GetWellknownJob> resolverJob = nullptr; + QPointer<GetLoginFlowsJob> loginFlowsJob = nullptr; + + SyncJob* syncJob = nullptr; + QPointer<LogoutJob> logoutJob = nullptr; + + bool cacheState = true; + bool cacheToBinary = + SettingsGroup("libQuotient").get("cache_type", + SettingsGroup("libQMatrixClient").get<QString>("cache_type")) + != "json"; + bool lazyLoading = false; + + /** \brief Check the homeserver and resolve it if needed, before connecting + * + * A single entry for functions that need to check whether the homeserver + * is valid before running. May execute connectFn either synchronously + * or asynchronously. In case of errors, emits resolveError() if + * the homeserver URL is not valid and cannot be resolved from userId, or + * the homeserver doesn't support the requested login flow. + * + * \param userId fully-qualified MXID to resolve HS from + * \param connectFn a function to execute once the HS URL is good + * \param flow optionally, a login flow that should be supported for + * connectFn to work; `none`, if there's no login flow + * requirements + * \sa resolveServer, resolveError + */ + void checkAndConnect(const QString &userId, + const std::function<void ()> &connectFn, + const std::optional<LoginFlows::LoginFlow> &flow = none); + template <typename... LoginArgTs> + void loginToServer(LoginArgTs&&... loginArgs); + void completeSetup(const QString& mxId); + void removeRoom(const QString& roomId); + + void consumeRoomData(SyncDataList&& roomDataList, bool fromCache); + void consumeAccountData(Events&& accountDataEvents); + void consumePresenceData(Events&& presenceData); + void consumeToDeviceEvents(Events&& toDeviceEvents); + + template <typename EventT> + EventT* unpackAccountData() const + { + const auto& eventIt = accountData.find(EventT::matrixTypeId()); + return eventIt == accountData.end() + ? nullptr + : weakPtrCast<EventT>(eventIt->second); + } + + void packAndSendAccountData(EventPtr&& event) + { + const auto eventType = event->matrixType(); + q->callApi<SetAccountDataJob>(data->userId(), eventType, + event->contentJson()); + accountData[eventType] = std::move(event); + emit q->accountDataChanged(eventType); + } + + template <typename EventT, typename ContentT> + void packAndSendAccountData(ContentT&& content) + { + packAndSendAccountData( + makeEvent<EventT>(std::forward<ContentT>(content))); + } + QString topLevelStatePath() const + { + return q->stateCacheDir().filePath("state.json"); + } + + EventPtr sessionDecryptMessage(const EncryptedEvent& encryptedEvent) + { +#ifndef Quotient_E2EE_ENABLED + qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; + return {}; +#else // Quotient_E2EE_ENABLED + if (encryptedEvent.algorithm() != OlmV1Curve25519AesSha2AlgoKey) + return {}; + + const auto identityKey = + encryptionManager->account()->curve25519IdentityKey(); + const auto personalCipherObject = + encryptedEvent.ciphertext(identityKey); + if (personalCipherObject.isEmpty()) { + qCDebug(E2EE) << "Encrypted event is not for the current device"; + return {}; } + const auto decrypted = encryptionManager->sessionDecryptMessage( + personalCipherObject, encryptedEvent.senderKey().toLatin1()); + if (decrypted.isEmpty()) { + qCDebug(E2EE) << "Problem with new session from senderKey:" + << encryptedEvent.senderKey() + << encryptionManager->account()->oneTimeKeys(); + return {}; + } + + auto&& decryptedEvent = + fromJson<EventPtr>(QJsonDocument::fromJson(decrypted.toUtf8())); - void packAndSendAccountData(EventPtr&& event) - { - const auto eventType = event->matrixType(); - q->callApi<SetAccountDataJob>(userId, eventType, - event->contentJson()); - accountData[eventType] = std::move(event); - emit q->accountDataChanged(eventType); + if (auto sender = decryptedEvent->fullJson()["sender"_ls].toString(); + sender != encryptedEvent.senderId()) { + qCWarning(E2EE) << "Found user" << sender + << "instead of sender" << encryptedEvent.senderId() + << "in Olm plaintext"; + return {}; } - template <typename EventT, typename ContentT> - void packAndSendAccountData(ContentT&& content) - { - packAndSendAccountData( - makeEvent<EventT>(std::forward<ContentT>(content))); + // TODO: keys to constants + const auto decryptedEventObject = decryptedEvent->fullJson(); + const auto recipient = + decryptedEventObject.value("recipient"_ls).toString(); + if (recipient != data->userId()) { + qCDebug(E2EE) << "Found user" << recipient << "instead of us" + << data->userId() << "in Olm plaintext"; + return {}; } + const auto ourKey = + decryptedEventObject.value("recipient_keys"_ls).toObject() + .value(Ed25519Key).toString(); + if (ourKey + != QString::fromUtf8( + encryptionManager->account()->ed25519IdentityKey())) { + qCDebug(E2EE) << "Found key" << ourKey + << "instead of ours own ed25519 key" + << encryptionManager->account()->ed25519IdentityKey() + << "in Olm plaintext"; + return {}; + } + + return std::move(decryptedEvent); +#endif // Quotient_E2EE_ENABLED + } }; Connection::Connection(const QUrl& server, QObject* parent) - : QObject(parent) - , d(std::make_unique<Private>(std::make_unique<ConnectionData>(server))) + : QObject(parent), d(new Private(std::make_unique<ConnectionData>(server))) { d->q = this; // All d initialization should occur before this line } -Connection::Connection(QObject* parent) - : Connection({}, parent) -{ } +Connection::Connection(QObject* parent) : Connection({}, parent) {} Connection::~Connection() { - qCDebug(MAIN) << "deconstructing connection object for" << d->userId; + qCDebug(MAIN) << "deconstructing connection object for" << userId(); stopSync(); } -void Connection::resolveServer(const QString& mxidOrDomain) +void Connection::resolveServer(const QString& mxid) { - // At this point we may have something as complex as - // @username:[IPv6:address]:port, or as simple as a plain domain name. - - // Try to parse as an FQID; if there's no @ part, assume it's a domain name. - QRegularExpression parser( - "^(@.+?:)?" // Optional username (allow everything for compatibility) - "(\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address - "(:\\d{1,5})?$", // Optional port - QRegularExpression::UseUnicodePropertiesOption); // Because asian digits - auto match = parser.match(mxidOrDomain); + if (isJobRunning(d->resolverJob)) + d->resolverJob->abandon(); - QUrl maybeBaseUrl = QUrl::fromUserInput(match.captured(2)); + auto maybeBaseUrl = QUrl::fromUserInput(serverPart(mxid)); maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http" - if (!match.hasMatch() || !maybeBaseUrl.isValid()) - { + if (maybeBaseUrl.isEmpty() || !maybeBaseUrl.isValid()) { emit resolveError(tr("%1 is not a valid homeserver address") .arg(maybeBaseUrl.toString())); return; } - auto domain = maybeBaseUrl.host(); - qCDebug(MAIN) << "Finding the server" << domain; - - d->data->setBaseUrl(maybeBaseUrl); // Just enough to check .well-known file - auto getWellKnownJob = callApi<GetWellknownJob>(); - // This is a workaround for 0.5.x; due to the way Quaternion's login dialog - // operates, Connection can disappear any moment during server resolution. - // Quotient 0.6 will reparent all jobs to enforce lifetimes. See also #398. - getWellKnownJob->setParent(this); - connect(getWellKnownJob, &BaseJob::finished, this, - [this, getWellKnownJob, maybeBaseUrl] { - if (getWellKnownJob->status() != BaseJob::NotFoundError) { - if (getWellKnownJob->status() != BaseJob::Success) { - qCWarning(MAIN) - << "Fetching .well-known file failed, FAIL_PROMPT"; - emit resolveError(tr("Failed resolving the homeserver")); - return; - } - QUrl baseUrl { getWellKnownJob->data().homeserver.baseUrl }; - if (baseUrl.isEmpty()) { - qCWarning(MAIN) << "base_url not provided, FAIL_PROMPT"; - emit resolveError( - tr("The homeserver base URL is not provided")); - return; - } - if (!baseUrl.isValid()) { - qCWarning(MAIN) << "base_url invalid, FAIL_ERROR"; - emit resolveError(tr("The homeserver base URL is invalid")); - return; - } - qCInfo(MAIN) << ".well-known URL for" << maybeBaseUrl.host() - << "is" << baseUrl.authority(); - setHomeserver(baseUrl); - } else { - qCInfo(MAIN) << "No .well-known file, using" << maybeBaseUrl - << "for base URL"; - setHomeserver(maybeBaseUrl); - } + qCDebug(MAIN) << "Finding the server" << maybeBaseUrl.host(); + + const auto& oldBaseUrl = d->data->baseUrl(); + d->data->setBaseUrl(maybeBaseUrl); // Temporarily set it for this one call + d->resolverJob = callApi<GetWellknownJob>(); + // Connect to finished() to make sure baseUrl is restored in any case + connect(d->resolverJob, &BaseJob::finished, this, [this, maybeBaseUrl, oldBaseUrl] { + // Revert baseUrl so that setHomeserver() below triggers signals + // in case the base URL actually changed + d->data->setBaseUrl(oldBaseUrl); + if (d->resolverJob->error() == BaseJob::Abandoned) + return; - auto getVersionsJob = callApi<GetVersionsJob>(); - getVersionsJob->setParent(this); // Same workaround as above - connect(getVersionsJob, &BaseJob::success, this, - &Connection::resolved); - connect(getVersionsJob, &BaseJob::failure, this, [this] { - qCWarning(MAIN) << "Homeserver base URL invalid"; - emit resolveError(tr("The homeserver base URL " - "doesn't seem to work")); - }); + if (d->resolverJob->error() != BaseJob::NotFoundError) { + if (!d->resolverJob->status().good()) { + qCWarning(MAIN) + << "Fetching .well-known file failed, FAIL_PROMPT"; + emit resolveError(tr("Failed resolving the homeserver")); + return; + } + QUrl baseUrl { d->resolverJob->data().homeserver.baseUrl }; + if (baseUrl.isEmpty()) { + qCWarning(MAIN) << "base_url not provided, FAIL_PROMPT"; + emit resolveError( + tr("The homeserver base URL is not provided")); + return; + } + if (!baseUrl.isValid()) { + qCWarning(MAIN) << "base_url invalid, FAIL_ERROR"; + emit resolveError(tr("The homeserver base URL is invalid")); + return; + } + qCInfo(MAIN) << ".well-known URL for" << maybeBaseUrl.host() << "is" + << baseUrl.toString(); + setHomeserver(baseUrl); + } else { + qCInfo(MAIN) << "No .well-known file, using" << maybeBaseUrl + << "for base URL"; + setHomeserver(maybeBaseUrl); + } + Q_ASSERT(d->loginFlowsJob != nullptr); // Ensured by setHomeserver() + connect(d->loginFlowsJob, &BaseJob::success, this, + &Connection::resolved); + connect(d->loginFlowsJob, &BaseJob::failure, this, [this] { + qCWarning(MAIN) << "Homeserver base URL sanity check failed"; + emit resolveError(tr("The homeserver doesn't seem to be working")); }); + }); } inline UserIdentifier makeUserIdentifier(const QString& id) @@ -244,22 +344,15 @@ inline UserIdentifier make3rdPartyIdentifier(const QString& medium, { QStringLiteral("address"), address } } }; } -void Connection::connectToServer(const QString& user, const QString& password, - const QString& initialDeviceName, - const QString& deviceId) -{ - checkAndConnect(user, - [=] { - doConnectToServer(user, password, initialDeviceName, deviceId); - }); -} - -void Connection::doConnectToServer(const QString& user, const QString& password, +void Connection::loginWithPassword(const QString& userId, + const QString& password, const QString& initialDeviceName, const QString& deviceId) { - d->loginToServer(LoginFlows::Password.type, makeUserIdentifier(user), - password, /*token*/ "", deviceId, initialDeviceName); + d->checkAndConnect(userId, [=] { + d->loginToServer(LoginFlows::Password.type, makeUserIdentifier(userId), + password, /*token*/ "", deviceId, initialDeviceName); + }, LoginFlows::Password); } SsoSession* Connection::prepareForSso(const QString& initialDeviceName, @@ -272,53 +365,43 @@ void Connection::loginWithToken(const QByteArray& loginToken, const QString& initialDeviceName, const QString& deviceId) { + Q_ASSERT(d->data->baseUrl().isValid() && d->loginFlows.contains(LoginFlows::Token)); d->loginToServer(LoginFlows::Token.type, - makeUserIdentifier(/*user is encoded in loginToken*/ {}), - /*password*/ "", loginToken, deviceId, initialDeviceName); -} - -void Connection::syncLoopIteration() -{ - sync(d->syncLoopTimeout); + none /*user is encoded in loginToken*/, "" /*password*/, + loginToken, deviceId, initialDeviceName); } -void Connection::connectWithToken(const QString& userId, - const QString& accessToken, - const QString& deviceId) -{ - assumeIdentity(userId, accessToken, deviceId); -} - -void Connection::assumeIdentity(const QString& userId, - const QString& accessToken, +void Connection::assumeIdentity(const QString& mxId, const QString& accessToken, const QString& deviceId) { - checkAndConnect(userId, - [=] { d->assumeIdentity(userId, accessToken, deviceId); }); + d->checkAndConnect(mxId, [this, mxId, accessToken, deviceId] { + d->data->setToken(accessToken.toLatin1()); + d->data->setDeviceId(deviceId); + d->completeSetup(mxId); + }); } void Connection::reloadCapabilities() { d->capabilitiesJob = callApi<GetCapabilitiesJob>(BackgroundRequest); - connect(d->capabilitiesJob, &BaseJob::finished, this, [this] { - if (d->capabilitiesJob->error() == BaseJob::Success) - d->capabilities = d->capabilitiesJob->capabilities(); - else if (d->capabilitiesJob->error() == BaseJob::IncorrectRequestError) - qCDebug(MAIN) << "Server doesn't support /capabilities"; - - if (d->capabilities.roomVersions.omitted()) - { - qCWarning(MAIN) << "Pinning supported room version to 1"; - d->capabilities.roomVersions = { "1", {{ "1", "stable" }} }; - } else { - qCDebug(MAIN) << "Room versions:" - << defaultRoomVersion() << "is default, full list:" - << availableRoomVersions(); - } - Q_ASSERT(!d->capabilities.roomVersions.omitted()); - emit capabilitiesLoaded(); - for (auto* r: d->roomMap) - r->checkVersion(); + connect(d->capabilitiesJob, &BaseJob::success, this, [this] { + d->capabilities = d->capabilitiesJob->capabilities(); + + if (d->capabilities.roomVersions) { + qCDebug(MAIN) << "Room versions:" << defaultRoomVersion() + << "is default, full list:" << availableRoomVersions(); + emit capabilitiesLoaded(); + for (auto* r: std::as_const(d->roomMap)) + r->checkVersion(); + } else + qCWarning(MAIN) + << "The server returned an empty set of supported versions;" + " disabling version upgrade recommendations to reduce noise"; + }); + connect(d->capabilitiesJob, &BaseJob::failure, this, [this] { + if (d->capabilitiesJob->error() == BaseJob::IncorrectRequestError) + qCDebug(MAIN) << "Server doesn't support /capabilities;" + " version upgrade recommendations won't be issued"; }); } @@ -326,7 +409,7 @@ bool Connection::loadingCapabilities() const { // (Ab)use the fact that room versions cannot be omitted after // the capabilities have been loaded (see reloadCapabilities() above). - return d->capabilities.roomVersions.omitted(); + return !d->capabilities.roomVersions; } template <typename... LoginArgTs> @@ -335,110 +418,179 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) auto loginJob = q->callApi<LoginJob>(std::forward<LoginArgTs>(loginArgs)...); connect(loginJob, &BaseJob::success, q, [this, loginJob] { - assumeIdentity(loginJob->userId(), loginJob->accessToken(), - loginJob->deviceId()); + data->setToken(loginJob->accessToken().toLatin1()); + data->setDeviceId(loginJob->deviceId()); + completeSetup(loginJob->userId()); +#ifndef Quotient_E2EE_ENABLED + qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; +#else // Quotient_E2EE_ENABLED + encryptionManager->uploadIdentityKeys(q); + encryptionManager->uploadOneTimeKeys(q); +#endif // Quotient_E2EE_ENABLED }); connect(loginJob, &BaseJob::failure, q, [this, loginJob] { emit q->loginError(loginJob->errorString(), loginJob->rawDataSample()); }); } -void Connection::Private::assumeIdentity(const QString& newUserId, - const QString& accessToken, - const QString& deviceId) +void Connection::Private::completeSetup(const QString& mxId) { - userId = newUserId; + data->setUserId(mxId); q->user(); // Creates a User object for the local user - data->setToken(accessToken.toLatin1()); - data->setDeviceId(deviceId); + q->setObjectName(data->userId() % '/' % data->deviceId()); qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString() - << "by user" << userId << "from device" << deviceId; + << "by user" << data->userId() + << "from device" << data->deviceId(); +#ifndef Quotient_E2EE_ENABLED + qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; +#else // Quotient_E2EE_ENABLED + AccountSettings accountSettings(data->userId()); + encryptionManager.reset( + new EncryptionManager(accountSettings.encryptionAccountPickle())); + if (accountSettings.encryptionAccountPickle().isEmpty()) { + accountSettings.setEncryptionAccountPickle( + encryptionManager->olmAccountPickle()); + } +#endif // Quotient_E2EE_ENABLED emit q->stateChanged(); emit q->connected(); q->reloadCapabilities(); } -void Connection::checkAndConnect(const QString& userId, - std::function<void()> connectFn) +void Connection::Private::checkAndConnect(const QString& userId, + const std::function<void()>& connectFn, + const std::optional<LoginFlows::LoginFlow>& flow) { - if (d->data->baseUrl().isValid()) - { + if (data->baseUrl().isValid() && (!flow || loginFlows.contains(*flow))) { connectFn(); return; } - // Not good to go, try to fix the homeserver URL. - if (userId.startsWith('@') && userId.indexOf(':') != -1) - { - connectSingleShot(this, &Connection::homeserverChanged, this, connectFn); - // NB: doResolveServer can emit resolveError, so this is a part of - // checkAndConnect function contract. - resolveServer(userId); + // Not good to go, try to ascertain the homeserver URL and flows + if (userId.startsWith('@') && userId.indexOf(':') != -1) { + q->resolveServer(userId); + if (flow) + connectSingleShot(q, &Connection::loginFlowsChanged, q, + [this, flow, connectFn] { + if (loginFlows.contains(*flow)) + connectFn(); + else + emit q->loginError( + tr("The homeserver at %1 does not support" + " the login flow '%2'") + .arg(data->baseUrl().toDisplayString()), + flow->type); + }); + else + connectSingleShot(q, &Connection::homeserverChanged, q, connectFn); } else - emit resolveError( - tr("%1 is an invalid homeserver URL") - .arg(d->data->baseUrl().toString())); + emit q->resolveError(tr("Please provide the fully-qualified user id" + " (such as @user:example.org) so that the" + " homeserver could be resolved; the current" + " homeserver URL(%1) is not good") + .arg(data->baseUrl().toDisplayString())); } void Connection::logout() { - auto job = callApi<LogoutJob>(); - connect( job, &LogoutJob::finished, this, [job,this] { - if (job->status().good() || job->error() == BaseJob::ContentAccessError) - { - stopSync(); + // If there's an ongoing sync job, stop it (this also suspends sync loop) + const auto wasSyncing = bool(d->syncJob); + if (wasSyncing) + { + d->syncJob->abandon(); + d->syncJob = nullptr; + } + + d->logoutJob = callApi<LogoutJob>(); + emit stateChanged(); // isLoggedIn() == false from now + + connect(d->logoutJob, &LogoutJob::finished, this, [this, wasSyncing] { + if (d->logoutJob->status().good() + || d->logoutJob->error() == BaseJob::Unauthorised + || d->logoutJob->error() == BaseJob::ContentAccessError) { + if (d->syncLoopConnection) + disconnect(d->syncLoopConnection); d->data->setToken({}); - emit stateChanged(); emit loggedOut(); + } else { // logout() somehow didn't proceed - restore the session state + emit stateChanged(); + if (wasSyncing) + syncLoopIteration(); // Resume sync loop (or a single sync) } }); } void Connection::sync(int timeout) { - if (d->syncJob) + if (d->syncJob) { + qCInfo(MAIN) << d->syncJob << "is already running"; + return; + } + if (!isLoggedIn()) { + qCWarning(MAIN) << "Not logged in, not going to sync"; return; + } + d->syncTimeout = timeout; Filter filter; - filter.room->timeline->limit = 100; - filter.room->state->lazyLoadMembers = d->lazyLoading; - auto job = d->syncJob = callApi<SyncJob>(BackgroundRequest, - d->data->lastEvent(), filter, timeout); - connect( job, &SyncJob::success, this, [this, job] { + filter.room.timeline.limit.emplace(100); + filter.room.state.lazyLoadMembers.emplace(d->lazyLoading); + auto job = d->syncJob = + callApi<SyncJob>(BackgroundRequest, d->data->lastEvent(), filter, + timeout); + connect(job, &SyncJob::success, this, [this, job] { onSyncSuccess(job->takeData()); d->syncJob = nullptr; emit syncDone(); }); - connect( job, &SyncJob::retryScheduled, this, - [this,job] (int retriesTaken, int nextInMilliseconds) - { - emit networkError(job->errorString(), job->rawDataSample(), - retriesTaken, nextInMilliseconds); - }); - connect( job, &SyncJob::failure, this, [this, job] { - d->syncJob = nullptr; - if (job->error() == BaseJob::ContentAccessError) - { + connect(job, &SyncJob::retryScheduled, this, + [this, job](int retriesTaken, int nextInMilliseconds) { + emit networkError(job->errorString(), job->rawDataSample(), + retriesTaken, nextInMilliseconds); + }); + connect(job, &SyncJob::failure, this, [this, job] { + // SyncJob persists with retries on transient errors; if it fails, + // there's likely something serious enough to stop the loop. + stopSync(); + if (job->error() == BaseJob::Unauthorised) { qCWarning(SYNCJOB) - << "Sync job failed with ContentAccessError - login expired?"; + << "Sync job failed with Unauthorised - login expired?"; emit loginError(job->errorString(), job->rawDataSample()); - } - else + } else emit syncError(job->errorString(), job->rawDataSample()); }); } void Connection::syncLoop(int timeout) { - d->syncLoopTimeout = timeout; - connect(this, &Connection::syncDone, this, &Connection::syncLoopIteration); - syncLoopIteration(); // initial sync to start the loop + if (d->syncLoopConnection && d->syncTimeout == timeout) { + qCInfo(MAIN) << "Attempt to run sync loop but there's one already " + "running; nothing will be done"; + return; + } + std::swap(d->syncTimeout, timeout); + if (d->syncLoopConnection) { + qCInfo(MAIN) << "Timeout for next syncs changed from" + << timeout << "to" << d->syncTimeout; + } else { + d->syncLoopConnection = connect(this, &Connection::syncDone, + this, &Connection::syncLoopIteration, + Qt::QueuedConnection); + syncLoopIteration(); // initial sync to start the loop + } } -QJsonObject toJson(const Connection::DirectChatsMap& directChats) +void Connection::syncLoopIteration() +{ + if (isLoggedIn()) + sync(d->syncTimeout); + else + qCInfo(MAIN) << "Logged out, sync loop will stop now"; +} + +QJsonObject toJson(const DirectChatsMap& directChats) { QJsonObject json; - for (auto it = directChats.begin(); it != directChats.end();) - { + for (auto it = directChats.begin(); it != directChats.end();) { QJsonArray roomIds; const auto* user = it.key(); for (; it != directChats.end() && it.key() == user; ++it) @@ -448,33 +600,52 @@ QJsonObject toJson(const Connection::DirectChatsMap& directChats) return json; } -void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { +void Connection::onSyncSuccess(SyncData&& data, bool fromCache) +{ d->data->setLastEvent(data.nextBatch()); - for (auto&& roomData: data.takeRoomData()) + d->consumeRoomData(data.takeRoomData(), fromCache); + d->consumeAccountData(data.takeAccountData()); + d->consumePresenceData(data.takePresenceData()); + d->consumeToDeviceEvents(data.takeToDeviceEvents()); +#ifdef Quotient_E2EE_ENABLED + // handling device_one_time_keys_count + if (!d->encryptionManager) { - const auto forgetIdx = d->roomIdsToForget.indexOf(roomData.roomId); - if (forgetIdx != -1) - { - d->roomIdsToForget.removeAt(forgetIdx); - if (roomData.joinState == JoinState::Leave) - { - qDebug(MAIN) << "Room" << roomData.roomId + qCDebug(E2EE) << "Encryption manager is not there yet, updating " + "one-time key counts will be skipped"; + return; + } + if (const auto deviceOneTimeKeysCount = data.deviceOneTimeKeysCount(); + !deviceOneTimeKeysCount.isEmpty()) + d->encryptionManager->updateOneTimeKeyCounts(this, + deviceOneTimeKeysCount); +#endif // Quotient_E2EE_ENABLED +} + +void Connection::Private::consumeRoomData(SyncDataList&& roomDataList, + bool fromCache) +{ + for (auto&& roomData: roomDataList) { + const auto forgetIdx = roomIdsToForget.indexOf(roomData.roomId); + if (forgetIdx != -1) { + roomIdsToForget.removeAt(forgetIdx); + if (roomData.joinState == JoinState::Leave) { + qDebug(MAIN) + << "Room" << roomData.roomId << "has been forgotten, ignoring /sync response for it"; continue; } qWarning(MAIN) << "Room" << roomData.roomId - << "has just been forgotten but /sync returned it in" - << toCString(roomData.joinState) - << "state - suspiciously fast turnaround"; + << "has just been forgotten but /sync returned it in" + << toCString(roomData.joinState) + << "state - suspiciously fast turnaround"; } - if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) ) - { - d->pendingStateRoomIds.removeOne(roomData.roomId); + if (auto* r = q->provideRoom(roomData.roomId, roomData.joinState)) { + pendingStateRoomIds.removeOne(roomData.roomId); r->updateData(std::move(roomData), fromCache); - if (d->firstTimeRooms.removeOne(r)) - { - emit loadedRoomState(r); - if (!d->capabilities.roomVersions.omitted()) + if (firstTimeRooms.removeOne(r)) { + emit q->loadedRoomState(r); + if (capabilities.roomVersions) r->checkVersion(); // Otherwise, the version will be checked in reloadCapabilities() } @@ -482,111 +653,146 @@ void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { // Let UI update itself after updating each room QCoreApplication::processEvents(); } +} + +void Connection::Private::consumeAccountData(Events&& accountDataEvents) +{ // After running this loop, the account data events not saved in - // d->accountData (see the end of the loop body) are auto-cleaned away - for (auto& eventPtr : data.takeAccountData()) - { + // accountData (see the end of the loop body) are auto-cleaned away + for (auto&& eventPtr: accountDataEvents) { visit(*eventPtr, - [this](const DirectChatEvent& dce) { - // See https://github.com/QMatrixClient/libqmatrixclient/wiki/Handling-direct-chat-events - const auto& usersToDCs = dce.usersToDirectChats(); - DirectChatsMap remoteRemovals = - erase_if(d->directChats, [&usersToDCs, this](auto it) { - return !(usersToDCs.contains(it.key()->id(), it.value()) - || d->dcLocalAdditions.contains(it.key(), - it.value())); - }); - erase_if(d->directChatUsers, [&remoteRemovals](auto it) { - return remoteRemovals.contains(it.value(), it.key()); - }); - // Remove from dcLocalRemovals what the server already has. - erase_if(d->dcLocalRemovals, [&remoteRemovals](auto it) { - return remoteRemovals.contains(it.key(), it.value()); - }); - if (MAIN().isDebugEnabled()) - for (auto it = remoteRemovals.begin(); - it != remoteRemovals.end(); ++it) { - qCDebug(MAIN) - << it.value() << "is no more a direct chat with" - << it.key()->id(); - } - - DirectChatsMap remoteAdditions; - for (auto it = usersToDCs.begin(); it != usersToDCs.end(); - ++it) { - if (auto* u = user(it.key())) { - if (!d->directChats.contains(u, it.value()) - && !d->dcLocalRemovals.contains(u, it.value())) - { - Q_ASSERT( - !d->directChatUsers.contains(it.value(), u)); - remoteAdditions.insert(u, it.value()); - d->directChats.insert(u, it.value()); - d->directChatUsers.insert(it.value(), u); - qCDebug(MAIN) - << "Marked room" << it.value() - << "as a direct chat with" << u->id(); - } - } else - qCWarning(MAIN) << "Couldn't get a user object for" - << it.key(); - } - // Remove from dcLocalAdditions what the server already has. - erase_if(d->dcLocalAdditions, [&remoteAdditions](auto it) { - return remoteAdditions.contains(it.key(), it.value()); - }); - if (!remoteAdditions.isEmpty() || !remoteRemovals.isEmpty()) - emit directChatsListChanged(remoteAdditions, remoteRemovals); - }, - // catch-all, passing eventPtr for a possible take-over - [this, &eventPtr](const Event& accountEvent) { - if (is<IgnoredUsersEvent>(accountEvent)) - qCDebug(MAIN) - << "Users ignored by" << d->userId << "updated:" - << QStringList::fromSet(ignoredUsers()).join(','); - - auto& currentData = d->accountData[accountEvent.matrixType()]; - // A polymorphic event-specific comparison might be a bit more - // efficient; maaybe do it another day - if (!currentData - || currentData->contentJson() - != accountEvent.contentJson()) { - currentData = std::move(eventPtr); - qCDebug(MAIN) << "Updated account data of type" - << currentData->matrixType(); - emit accountDataChanged(currentData->matrixType()); - } - }); + [this](const DirectChatEvent& dce) { + // https://github.com/quotient-im/libQuotient/wiki/Handling-direct-chat-events + const auto& usersToDCs = dce.usersToDirectChats(); + DirectChatsMap remoteRemovals = + erase_if(directChats, [&usersToDCs, this](auto it) { + return !( + usersToDCs.contains(it.key()->id(), it.value()) + || dcLocalAdditions.contains(it.key(), it.value())); + }); + erase_if(directChatUsers, [&remoteRemovals](auto it) { + return remoteRemovals.contains(it.value(), it.key()); + }); + // Remove from dcLocalRemovals what the server already has. + erase_if(dcLocalRemovals, [&remoteRemovals](auto it) { + return remoteRemovals.contains(it.key(), it.value()); + }); + if (MAIN().isDebugEnabled()) + for (auto it = remoteRemovals.begin(); + it != remoteRemovals.end(); ++it) { + qCDebug(MAIN) + << it.value() << "is no more a direct chat with" + << it.key()->id(); + } + + DirectChatsMap remoteAdditions; + for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) { + if (auto* u = q->user(it.key())) { + if (!directChats.contains(u, it.value()) + && !dcLocalRemovals.contains(u, it.value())) { + Q_ASSERT(!directChatUsers.contains(it.value(), u)); + remoteAdditions.insert(u, it.value()); + directChats.insert(u, it.value()); + directChatUsers.insert(it.value(), u); + qCDebug(MAIN) << "Marked room" << it.value() + << "as a direct chat with" << u->id(); + } + } else + qCWarning(MAIN) + << "Couldn't get a user object for" << it.key(); + } + // Remove from dcLocalAdditions what the server already has. + erase_if(dcLocalAdditions, [&remoteAdditions](auto it) { + return remoteAdditions.contains(it.key(), it.value()); + }); + if (!remoteAdditions.isEmpty() || !remoteRemovals.isEmpty()) + emit q->directChatsListChanged(remoteAdditions, + remoteRemovals); + }, + // catch-all, passing eventPtr for a possible take-over + [this, &eventPtr](const Event& accountEvent) { + if (is<IgnoredUsersEvent>(accountEvent)) + qCDebug(MAIN) + << "Users ignored by" << data->userId() << "updated:" + << QStringList(q->ignoredUsers().values()).join(','); + + auto& currentData = accountData[accountEvent.matrixType()]; + // A polymorphic event-specific comparison might be a bit + // more efficient; maaybe do it another day + if (!currentData + || currentData->contentJson() != accountEvent.contentJson()) { + currentData = std::move(eventPtr); + qCDebug(MAIN) << "Updated account data of type" + << currentData->matrixType(); + emit q->accountDataChanged(currentData->matrixType()); + } + }); } - if (!d->dcLocalAdditions.isEmpty() || !d->dcLocalRemovals.isEmpty()) { + if (!dcLocalAdditions.isEmpty() || !dcLocalRemovals.isEmpty()) { qDebug(MAIN) << "Sending updated direct chats to the server:" - << d->dcLocalRemovals.size() << "removal(s)," - << d->dcLocalAdditions.size() << "addition(s)"; - callApi<SetAccountDataJob>(d->userId, QStringLiteral("m.direct"), - toJson(d->directChats)); - d->dcLocalAdditions.clear(); - d->dcLocalRemovals.clear(); + << dcLocalRemovals.size() << "removal(s)," + << dcLocalAdditions.size() << "addition(s)"; + q->callApi<SetAccountDataJob>(data->userId(), QStringLiteral("m.direct"), + toJson(directChats)); + dcLocalAdditions.clear(); + dcLocalRemovals.clear(); } } +void Connection::Private::consumePresenceData(Events&& presenceData) +{ + // To be implemented +} + +void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) +{ +#ifdef Quotient_E2EE_ENABLED + // handling m.room_key to-device encrypted event + visitEach(toDeviceEvents, [this](const EncryptedEvent& ee) { + if (ee.algorithm() != OlmV1Curve25519AesSha2AlgoKey) { + qCDebug(E2EE) << "Encrypted event" << ee.id() << "algorithm" + << ee.algorithm() << "is not supported"; + return; + } + + // TODO: full maintaining of the device keys + // with device_lists sync extention and /keys/query + qCDebug(E2EE) << "Getting device keys for the m.room_key sender:" + << ee.senderId(); + // encryptionManager->updateDeviceKeys(); + + visit(*sessionDecryptMessage(ee), + [this, senderKey = ee.senderKey()](const RoomKeyEvent& roomKeyEvent) { + if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) + detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); + else + qCDebug(E2EE) + << "Encrypted event room id" << roomKeyEvent.roomId() + << "is not found at the connection" << q->objectName(); + }, + [](const Event& evt) { + qCDebug(E2EE) << "Skipping encrypted to_device event, type" + << evt.matrixType(); + }); + }); +#endif +} + void Connection::stopSync() { // If there's a sync loop, break it - disconnect(this, &Connection::syncDone, - this, &Connection::syncLoopIteration); + disconnect(d->syncLoopConnection); if (d->syncJob) // If there's an ongoing sync job, stop it too { - d->syncJob->abandon(); + if (d->syncJob->status().code == BaseJob::Pending) + d->syncJob->abandon(); d->syncJob = nullptr; } } -QString Connection::nextBatchToken() const -{ - return d->data->lastEvent(); -} +QString Connection::nextBatchToken() const { return d->data->lastEvent(); } -PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const +PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) { return callApi<PostReceiptJob>(room->id(), "m.read", event->id()); } @@ -595,10 +801,14 @@ JoinRoomJob* Connection::joinRoom(const QString& roomAlias, const QStringList& serverNames) { auto job = callApi<JoinRoomJob>(roomAlias, serverNames); - // Upon completion, ensure a room object in Join state is created but only - // if it's not already there due to a sync completing earlier. - connect(job, &JoinRoomJob::success, - this, [this, job] { provideRoom(job->roomId()); }); + // Upon completion, ensure a room object in Join state is created + // (or it might already be there due to a sync completing earlier). + // finished() is used here instead of success() to overtake clients + // that may add their own slots to finished(). + connect(job, &BaseJob::finished, this, [this, job] { + if (job->status().good()) + provideRoom(job->roomId()); + }); return job; } @@ -606,15 +816,13 @@ LeaveRoomJob* Connection::leaveRoom(Room* room) { const auto& roomId = room->id(); const auto job = callApi<LeaveRoomJob>(roomId); - if (room->joinState() == JoinState::Invite) - { + if (room->joinState() == JoinState::Invite) { // Workaround matrix-org/synapse#2181 - if the room is in invite state // the invite may have been cancelled but Synapse didn't send it in // `/sync`. See also #273 for the discussion in the library context. d->pendingStateRoomIds.push_back(roomId); - connect(job, &LeaveRoomJob::success, this, [this,roomId] { - if (d->pendingStateRoomIds.removeOne(roomId)) - { + connect(job, &LeaveRoomJob::success, this, [this, roomId] { + if (d->pendingStateRoomIds.removeOne(roomId)) { qCDebug(MAIN) << "Forcing the room to Leave status"; provideRoom(roomId, JoinState::Leave); } @@ -627,41 +835,48 @@ inline auto splitMediaId(const QString& mediaId) { auto idParts = mediaId.split('/'); Q_ASSERT_X(idParts.size() == 2, __FUNCTION__, - ("'" + mediaId + - "' doesn't look like 'serverName/localMediaId'").toLatin1()); + ("'" + mediaId + "' doesn't look like 'serverName/localMediaId'") + .toLatin1()); return idParts; } MediaThumbnailJob* Connection::getThumbnail(const QString& mediaId, - QSize requestedSize, RunningPolicy policy) const + QSize requestedSize, + RunningPolicy policy) { auto idParts = splitMediaId(mediaId); - return callApi<MediaThumbnailJob>(policy, - idParts.front(), idParts.back(), requestedSize); + return callApi<MediaThumbnailJob>(policy, idParts.front(), idParts.back(), + requestedSize); } -MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, - QSize requestedSize, RunningPolicy policy) const +MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, QSize requestedSize, + RunningPolicy policy) { return getThumbnail(url.authority() + url.path(), requestedSize, policy); } -MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, - int requestedWidth, int requestedHeight, RunningPolicy policy) const +MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, int requestedWidth, + int requestedHeight, + RunningPolicy policy) { return getThumbnail(url, QSize(requestedWidth, requestedHeight), policy); } -UploadContentJob* Connection::uploadContent(QIODevice* contentSource, - const QString& filename, const QString& overrideContentType) const +UploadContentJob* +Connection::uploadContent(QIODevice* contentSource, const QString& filename, + const QString& overrideContentType) { + Q_ASSERT(contentSource != nullptr); auto contentType = overrideContentType; - if (contentType.isEmpty()) - { - contentType = - QMimeDatabase().mimeTypeForFileNameAndData(filename, contentSource) - .name(); - contentSource->open(QIODevice::ReadOnly); + if (contentType.isEmpty()) { + contentType = QMimeDatabase() + .mimeTypeForFileNameAndData(filename, contentSource) + .name(); + if (!contentSource->open(QIODevice::ReadOnly)) { + qCWarning(MAIN) << "Couldn't open content source" << filename + << "for reading:" << contentSource->errorString(); + return nullptr; + } } return callApi<UploadContentJob>(contentSource, filename, contentType); } @@ -670,61 +885,57 @@ UploadContentJob* Connection::uploadFile(const QString& fileName, const QString& overrideContentType) { auto sourceFile = new QFile(fileName); - if (!sourceFile->open(QIODevice::ReadOnly)) - { - qCWarning(MAIN) << "Couldn't open" << sourceFile->fileName() - << "for reading"; - return nullptr; - } return uploadContent(sourceFile, QFileInfo(*sourceFile).fileName(), overrideContentType); } -GetContentJob* Connection::getContent(const QString& mediaId) const +GetContentJob* Connection::getContent(const QString& mediaId) { auto idParts = splitMediaId(mediaId); return callApi<GetContentJob>(idParts.front(), idParts.back()); } -GetContentJob* Connection::getContent(const QUrl& url) const +GetContentJob* Connection::getContent(const QUrl& url) { return getContent(url.authority() + url.path()); } DownloadFileJob* Connection::downloadFile(const QUrl& url, - const QString& localFilename) const + const QString& localFilename) { auto mediaId = url.authority() + url.path(); auto idParts = splitMediaId(mediaId); - auto* job = callApi<DownloadFileJob>(idParts.front(), idParts.back(), - localFilename); + auto* job = + callApi<DownloadFileJob>(idParts.front(), idParts.back(), localFilename); return job; } -CreateRoomJob* Connection::createRoom(RoomVisibility visibility, - const QString& alias, const QString& name, const QString& topic, - QStringList invites, const QString& presetName, - const QString& roomVersion, bool isDirect, - const QVector<CreateRoomJob::StateEvent>& initialState, - const QVector<CreateRoomJob::Invite3pid>& invite3pids, - const QJsonObject& creationContent) -{ - invites.removeOne(d->userId); // The creator is by definition in the room - auto job = callApi<CreateRoomJob>( - visibility == PublishRoom ? QStringLiteral("public") - : QStringLiteral("private"), - alias, name, topic, invites, invite3pids, roomVersion, - creationContent, initialState, presetName, isDirect); - connect(job, &BaseJob::success, this, [this,job,invites,isDirect] { +CreateRoomJob* +Connection::createRoom(RoomVisibility visibility, const QString& alias, + const QString& name, const QString& topic, + QStringList invites, const QString& presetName, + const QString& roomVersion, bool isDirect, + const QVector<CreateRoomJob::StateEvent>& initialState, + const QVector<CreateRoomJob::Invite3pid>& invite3pids, + const QJsonObject& creationContent) +{ + invites.removeOne(userId()); // The creator is by definition in the room + auto job = callApi<CreateRoomJob>(visibility == PublishRoom + ? QStringLiteral("public") + : QStringLiteral("private"), + alias, name, topic, invites, invite3pids, + roomVersion, creationContent, + initialState, presetName, isDirect); + connect(job, &BaseJob::success, this, [this, job, invites, isDirect] { auto* room = provideRoom(job->roomId(), JoinState::Join); - if (!room) - { - Q_ASSERT_X(room, "Connection::createRoom", "Failed to create a room"); + if (!room) { + Q_ASSERT_X(room, "Connection::createRoom", + "Failed to create a room"); return; } emit createdRoom(room); if (isDirect) - for (const auto& i: invites) + for (const auto& i : invites) addToDirectChats(room, user(i)); }); return job; @@ -732,17 +943,12 @@ CreateRoomJob* Connection::createRoom(RoomVisibility visibility, void Connection::requestDirectChat(const QString& userId) { - if (auto* u = user(userId)) - requestDirectChat(u); - else - qCCritical(MAIN) - << "Connection::requestDirectChat: Couldn't get a user object for" - << userId; + doInDirectChat(userId, [this](Room* r) { emit directChatAvailable(r); }); } void Connection::requestDirectChat(User* u) { - doInDirectChat(u, [this] (Room* r) { emit directChatAvailable(r); }); + doInDirectChat(u, [this](Room* r) { emit directChatAvailable(r); }); } void Connection::doInDirectChat(const QString& userId, @@ -760,34 +966,33 @@ void Connection::doInDirectChat(User* u, const std::function<void(Room*)>& operation) { Q_ASSERT(u); - const auto& userId = u->id(); + const auto& otherUserId = u->id(); // There can be more than one DC; find the first valid (existing and // not left), and delete inexistent (forgotten?) ones along the way. DirectChatsMap removals; - for (auto it = d->directChats.find(u); - it != d->directChats.end() && it.key() == u; ++it) - { + for (auto it = d->directChats.constFind(u); + it != d->directChats.cend() && it.key() == u; ++it) { const auto& roomId = *it; - if (auto r = room(roomId, JoinState::Join)) - { + if (auto r = room(roomId, JoinState::Join)) { Q_ASSERT(r->id() == roomId); // A direct chat with yourself should only involve yourself :) - if (userId == d->userId && r->totalMemberCount() > 1) + if (otherUserId == userId() && r->totalMemberCount() > 1) continue; - qCDebug(MAIN) << "Requested direct chat with" << userId + qCDebug(MAIN) << "Requested direct chat with" << otherUserId << "is already available as" << r->id(); operation(r); return; } - if (auto ir = invitation(roomId)) - { + if (auto ir = invitation(roomId)) { Q_ASSERT(ir->id() == roomId); auto j = joinRoom(ir->id()); - connect(j, &BaseJob::success, this, [this,roomId,userId,operation] { - qCDebug(MAIN) << "Joined the already invited direct chat with" - << userId << "as" << roomId; - operation(room(roomId, JoinState::Join)); - }); + connect(j, &BaseJob::success, this, + [this, roomId, otherUserId, operation] { + qCDebug(MAIN) + << "Joined the already invited direct chat with" + << otherUserId << "as" << roomId; + operation(room(roomId, JoinState::Join)); + }); return; } // Avoid reusing previously left chats but don't remove them @@ -795,17 +1000,15 @@ void Connection::doInDirectChat(User* u, if (room(roomId, JoinState::Leave)) continue; - qCWarning(MAIN) << "Direct chat with" << userId << "known as room" + qCWarning(MAIN) << "Direct chat with" << otherUserId << "known as room" << roomId << "is not valid and will be discarded"; // Postpone actual deletion until we finish iterating d->directChats. removals.insert(it.key(), it.value()); // Add to the list of updates to send to the server upon the next sync. d->dcLocalRemovals.insert(it.key(), it.value()); } - if (!removals.isEmpty()) - { - for (auto it = removals.cbegin(); it != removals.cend(); ++it) - { + if (!removals.isEmpty()) { + for (auto it = removals.cbegin(); it != removals.cend(); ++it) { d->directChats.remove(it.key(), it.value()); d->directChatUsers.remove(it.value(), const_cast<User*>(it.key())); // FIXME @@ -813,19 +1016,19 @@ void Connection::doInDirectChat(User* u, emit directChatsListChanged({}, removals); } - auto j = createDirectChat(userId); - connect(j, &BaseJob::success, this, [this,j,userId,operation] { - qCDebug(MAIN) << "Direct chat with" << userId - << "has been created as" << j->roomId(); + auto j = createDirectChat(otherUserId); + connect(j, &BaseJob::success, this, [this, j, otherUserId, operation] { + qCDebug(MAIN) << "Direct chat with" << otherUserId << "has been created as" + << j->roomId(); operation(room(j->roomId(), JoinState::Join)); }); - } CreateRoomJob* Connection::createDirectChat(const QString& userId, - const QString& topic, const QString& name) + const QString& topic, + const QString& name) { - return createRoom(UnpublishRoom, {}, name, topic, {userId}, + return createRoom(UnpublishRoom, {}, name, topic, { userId }, QStringLiteral("trusted_private_chat"), {}, true); } @@ -839,76 +1042,78 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id) // a ForgetRoomJob is created in advance and can be returned in a probably // not-yet-started state (it will start once /leave completes). auto forgetJob = new ForgetRoomJob(id); - auto room = d->roomMap.value({id, false}); + auto room = d->roomMap.value({ id, false }); if (!room) - room = d->roomMap.value({id, true}); - if (room && room->joinState() != JoinState::Leave) - { - auto leaveJob = room->leaveRoom(); - connect(leaveJob, &BaseJob::success, this, [this, forgetJob, room] { - forgetJob->start(connectionData()); - // If the matching /sync response hasn't arrived yet, mark the room - // for explicit deletion - if (room->joinState() != JoinState::Leave) - d->roomIdsToForget.push_back(room->id()); - }); + room = d->roomMap.value({ id, true }); + if (room && room->joinState() != JoinState::Leave) { + auto leaveJob = leaveRoom(room); + connect(leaveJob, &BaseJob::result, this, + [this, leaveJob, forgetJob, room] { + if (leaveJob->error() == BaseJob::Success + || leaveJob->error() == BaseJob::NotFoundError) { + run(forgetJob); + // If the matching /sync response hasn't arrived yet, + // mark the room for explicit deletion + if (room->joinState() != JoinState::Leave) + d->roomIdsToForget.push_back(room->id()); + } else { + qCWarning(MAIN).nospace() + << "Error leaving room " << room->objectName() + << ": " << leaveJob->errorString(); + forgetJob->abandon(); + } + }); connect(leaveJob, &BaseJob::failure, forgetJob, &BaseJob::abandon); - } - else - forgetJob->start(connectionData()); - connect(forgetJob, &BaseJob::success, this, [this, id] - { - // Delete whatever instances of the room are still in the map. - for (auto f: {false, true}) - if (auto r = d->roomMap.take({ id, f })) - { - qCDebug(MAIN) << "Room" << r->objectName() - << "in state" << toCString(r->joinState()) - << "will be deleted"; - emit r->beforeDestruction(r); - r->deleteLater(); - } + } else + run(forgetJob); + connect(forgetJob, &BaseJob::result, this, [this, id, forgetJob] { + // Leave room in case of success, or room not known by server + if (forgetJob->error() == BaseJob::Success + || forgetJob->error() == BaseJob::NotFoundError) + d->removeRoom(id); // Delete the room from roomMap + else + qCWarning(MAIN).nospace() << "Error forgetting room " << id << ": " + << forgetJob->errorString(); }); return forgetJob; } -SendToDeviceJob* Connection::sendToDevices(const QString& eventType, - const UsersToDevicesToEvents& eventsMap) const +SendToDeviceJob* +Connection::sendToDevices(const QString& eventType, + const UsersToDevicesToEvents& eventsMap) { QHash<QString, QHash<QString, QJsonObject>> json; json.reserve(int(eventsMap.size())); std::for_each(eventsMap.begin(), eventsMap.end(), - [&json] (const auto& userTodevicesToEvents) { - auto& jsonUser = json[userTodevicesToEvents.first]; - const auto& devicesToEvents = userTodevicesToEvents.second; - std::for_each(devicesToEvents.begin(), devicesToEvents.end(), - [&jsonUser] (const auto& deviceToEvents) { - jsonUser.insert(deviceToEvents.first, - deviceToEvents.second.contentJson()); - }); - }); - return callApi<SendToDeviceJob>(BackgroundRequest, - eventType, generateTxnId(), json); + [&json](const auto& userTodevicesToEvents) { + auto& jsonUser = json[userTodevicesToEvents.first]; + const auto& devicesToEvents = userTodevicesToEvents.second; + std::for_each(devicesToEvents.begin(), + devicesToEvents.end(), + [&jsonUser](const auto& deviceToEvents) { + jsonUser.insert( + deviceToEvents.first, + deviceToEvents.second.contentJson()); + }); + }); + return callApi<SendToDeviceJob>(BackgroundRequest, eventType, + generateTxnId(), json); } SendMessageJob* Connection::sendMessage(const QString& roomId, - const RoomEvent& event) const + const RoomEvent& event) { - const auto txnId = event.transactionId().isEmpty() - ? generateTxnId() : event.transactionId(); - return callApi<SendMessageJob>(roomId, event.matrixType(), - txnId, event.contentJson()); + const auto txnId = event.transactionId().isEmpty() ? generateTxnId() + : event.transactionId(); + return callApi<SendMessageJob>(roomId, event.matrixType(), txnId, + event.contentJson()); } -QUrl Connection::homeserver() const -{ - return d->data->baseUrl(); -} +QUrl Connection::homeserver() const { return d->data->baseUrl(); } -QString Connection::domain() const -{ - return d->userId.section(':', 1); -} +QString Connection::domain() const { return userId().section(':', 1); } + +bool Connection::isUsable() const { return !loginFlows().isEmpty(); } QVector<GetLoginFlowsJob::LoginFlow> Connection::loginFlows() const { @@ -927,17 +1132,17 @@ bool Connection::supportsSso() const Room* Connection::room(const QString& roomId, JoinStates states) const { - Room* room = d->roomMap.value({roomId, false}, nullptr); - if (states.testFlag(JoinState::Join) && - room && room->joinState() == JoinState::Join) + Room* room = d->roomMap.value({ roomId, false }, nullptr); + if (states.testFlag(JoinState::Join) && room + && room->joinState() == JoinState::Join) return room; if (states.testFlag(JoinState::Invite)) if (Room* invRoom = invitation(roomId)) return invRoom; - if (states.testFlag(JoinState::Leave) && - room && room->joinState() == JoinState::Leave) + if (states.testFlag(JoinState::Leave) && room + && room->joinState() == JoinState::Leave) return room; return nullptr; @@ -948,6 +1153,7 @@ Room* Connection::roomByAlias(const QString& roomAlias, JoinStates states) const const auto id = d->roomAliasMap.value(roomAlias); if (!id.isEmpty()) return room(id, states); + qCWarning(MAIN) << "Room for alias" << roomAlias << "is not found under account" << userId(); return nullptr; @@ -957,21 +1163,18 @@ void Connection::updateRoomAliases(const QString& roomId, const QStringList& previousRoomAliases, const QStringList& roomAliases) { - for (const auto& a: previousRoomAliases) + for (const auto& a : previousRoomAliases) if (d->roomAliasMap.remove(a) == 0) qCWarning(MAIN) << "Alias" << a << "is not found (already deleted?)"; - for (const auto& a: roomAliases) - { + for (const auto& a : roomAliases) { auto& mappedId = d->roomAliasMap[a]; - if (!mappedId.isEmpty()) - { + if (!mappedId.isEmpty()) { if (mappedId == roomId) - qCDebug(MAIN) << "Alias" << a << "is already mapped to room" - << roomId; + qCDebug(MAIN) + << "Alias" << a << "is already mapped to" << roomId; else - qCWarning(MAIN) << "Alias" << a - << "will be force-remapped from room" + qCWarning(MAIN) << "Alias" << a << "will be force-remapped from" << mappedId << "to" << roomId; } mappedId = roomId; @@ -980,72 +1183,65 @@ void Connection::updateRoomAliases(const QString& roomId, Room* Connection::invitation(const QString& roomId) const { - return d->roomMap.value({roomId, true}, nullptr); + return d->roomMap.value({ roomId, true }, nullptr); } -User* Connection::user(const QString& userId) +User* Connection::user(const QString& uId) { - if (userId.isEmpty()) + if (uId.isEmpty()) return nullptr; - if (!userId.startsWith('@') || !userId.contains(':')) - { - qCCritical(MAIN) << "Malformed userId:" << userId; + if (!uId.startsWith('@') || serverPart(uId).isEmpty()) { + qCCritical(MAIN) << "Malformed userId:" << uId; return nullptr; } - if( d->userMap.contains(userId) ) - return d->userMap.value(userId); - auto* user = userFactory()(this, userId); - d->userMap.insert(userId, user); + if (d->userMap.contains(uId)) + return d->userMap.value(uId); + auto* user = userFactory()(this, uId); + d->userMap.insert(uId, user); emit newUser(user); return user; } const User* Connection::user() const { - return d->userMap.value(d->userId, nullptr); + return d->userMap.value(userId(), nullptr); } -User* Connection::user() -{ - return user(d->userId); -} +User* Connection::user() { return user(userId()); } -QString Connection::userId() const -{ - return d->userId; -} +QString Connection::userId() const { return d->data->userId(); } -QString Connection::deviceId() const -{ - return d->data->deviceId(); -} - -QString Connection::token() const -{ - return accessToken(); -} +QString Connection::deviceId() const { return d->data->deviceId(); } QByteArray Connection::accessToken() const { - return d->data->accessToken(); + // The logout job needs access token to do its job; so the token is + // kept inside d->data but no more exposed to the outside world. + return isJobRunning(d->logoutJob) ? QByteArray() : d->data->accessToken(); } -SyncJob* Connection::syncJob() const +bool Connection::isLoggedIn() const { return !accessToken().isEmpty(); } + +#ifdef Quotient_E2EE_ENABLED +QtOlm::Account* Connection::olmAccount() const { - return d->syncJob; + return d->encryptionManager->account(); } +#endif // Quotient_E2EE_ENABLED + +SyncJob* Connection::syncJob() const { return d->syncJob; } int Connection::millisToReconnect() const { return d->syncJob ? d->syncJob->millisToRetry() : 0; } -QHash< QPair<QString, bool>, Room* > Connection::roomMap() const +QHash<QPair<QString, bool>, Room*> Connection::roomMap() const { - // Copy-on-write-and-remove-elements is faster than copying elements one by one. - QHash< QPair<QString, bool>, Room* > roomMap = d->roomMap; - for (auto it = roomMap.begin(); it != roomMap.end(); ) - { + // Copy-on-write-and-remove-elements is faster than copying elements one by + // one. + QHash<QPair<QString, bool>, Room*> roomMap = d->roomMap; + for (auto it = roomMap.begin(); it != roomMap.end();) { if (it.value()->joinState() == JoinState::Leave) it = roomMap.erase(it); else @@ -1075,7 +1271,7 @@ int Connection::roomsCount(JoinStates joinStates) const { // Using int to maintain compatibility with QML // (consider also that QHash<>::size() returns int anyway). - return int(std::count_if(d->roomMap.begin(), d->roomMap.end(), + return int(std::count_if(d->roomMap.cbegin(), d->roomMap.cend(), [joinStates](Room* r) { return joinStates.testFlag(r->joinState()); })); @@ -1112,27 +1308,24 @@ void Connection::setAccountData(const QString& type, const QJsonObject& content) QHash<QString, QVector<Room*>> Connection::tagsToRooms() const { QHash<QString, QVector<Room*>> result; - for (auto* r: qAsConst(d->roomMap)) - { + for (auto* r : qAsConst(d->roomMap)) { const auto& tagNames = r->tagNames(); - for (const auto& tagName: tagNames) + for (const auto& tagName : tagNames) result[tagName].push_back(r); } for (auto it = result.begin(); it != result.end(); ++it) - std::sort(it->begin(), it->end(), - [t=it.key()] (Room* r1, Room* r2) { - return r1->tags().value(t) < r2->tags().value(t); - }); + std::sort(it->begin(), it->end(), [t = it.key()](Room* r1, Room* r2) { + return r1->tags().value(t) < r2->tags().value(t); + }); return result; } QStringList Connection::tagNames() const { - QStringList tags ({FavouriteTag}); - for (auto* r: qAsConst(d->roomMap)) - { + QStringList tags({ FavouriteTag }); + for (auto* r : qAsConst(d->roomMap)) { const auto& tagNames = r->tagNames(); - for (const auto& tag: tagNames) + for (const auto& tag : tagNames) if (tag != LowPriorityTag && !tags.contains(tag)) tags.push_back(tag); } @@ -1143,16 +1336,29 @@ QStringList Connection::tagNames() const QVector<Room*> Connection::roomsWithTag(const QString& tagName) const { QVector<Room*> rooms; - std::copy_if(d->roomMap.begin(), d->roomMap.end(), std::back_inserter(rooms), - [&tagName] (Room* r) { return r->tags().contains(tagName); }); + std::copy_if(d->roomMap.cbegin(), d->roomMap.cend(), + std::back_inserter(rooms), + [&tagName](Room* r) { return r->tags().contains(tagName); }); return rooms; } -Connection::DirectChatsMap Connection::directChats() const +DirectChatsMap Connection::directChats() const { return d->directChats; } +// Removes room with given id from roomMap +void Connection::Private::removeRoom(const QString& roomId) +{ + for (auto f : { false, true }) + if (auto r = roomMap.take({ roomId, f })) { + qCDebug(MAIN) << "Room" << r->objectName() << "in state" + << toCString(r->joinState()) << "will be deleted"; + emit r->beforeDestruction(r); + r->deleteLater(); + } +} + void Connection::addToDirectChats(const Room* room, User* user) { Q_ASSERT(room != nullptr && user != nullptr); @@ -1168,20 +1374,19 @@ void Connection::addToDirectChats(const Room* room, User* user) void Connection::removeFromDirectChats(const QString& roomId, User* user) { Q_ASSERT(!roomId.isEmpty()); - if ((user != nullptr && !d->directChats.contains(user, roomId)) || - d->directChats.key(roomId) == nullptr) + if ((user != nullptr && !d->directChats.contains(user, roomId)) + || d->directChats.key(roomId) == nullptr) return; DirectChatsMap removals; - if (user != nullptr) - { + if (user != nullptr) { d->directChats.remove(user, roomId); d->directChatUsers.remove(roomId, user); removals.insert(user, roomId); d->dcLocalRemovals.insert(user, roomId); } else { removals = erase_if(d->directChats, - [&roomId] (auto it) { return it.value() == roomId; }); + [&roomId](auto it) { return it.value() == roomId; }); d->directChatUsers.remove(roomId); d->dcLocalRemovals += removals; } @@ -1204,7 +1409,7 @@ bool Connection::isIgnored(const User* user) const return ignoredUsers().contains(user->id()); } -Connection::IgnoredUsersList Connection::ignoredUsers() const +IgnoredUsersList Connection::ignoredUsers() const { const auto* event = d->unpackAccountData<IgnoredUsersEvent>(); return event ? event->ignored_users() : IgnoredUsersList(); @@ -1215,11 +1420,10 @@ void Connection::addToIgnoredUsers(const User* user) Q_ASSERT(user != nullptr); auto ignoreList = ignoredUsers(); - if (!ignoreList.contains(user->id())) - { + if (!ignoreList.contains(user->id())) { ignoreList.insert(user->id()); d->packAndSendAccountData<IgnoredUsersEvent>(ignoreList); - emit ignoredUsersListChanged({{ user->id() }}, {}); + emit ignoredUsersListChanged({ { user->id() } }, {}); } } @@ -1228,17 +1432,13 @@ void Connection::removeFromIgnoredUsers(const User* user) Q_ASSERT(user != nullptr); auto ignoreList = ignoredUsers(); - if (ignoreList.remove(user->id()) != 0) - { + if (ignoreList.remove(user->id()) != 0) { d->packAndSendAccountData<IgnoredUsersEvent>(ignoreList); - emit ignoredUsersListChanged({}, {{ user->id() }}); + emit ignoredUsersListChanged({}, { { user->id() } }); } } -QMap<QString, User*> Connection::users() const -{ - return d->userMap; -} +QMap<QString, User*> Connection::users() const { return d->userMap; } const ConnectionData* Connection::connectionData() const { @@ -1250,64 +1450,56 @@ Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState) // TODO: This whole function is a strong case for a RoomManager class. Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id"); - // If joinState.omitted(), all joinState == comparisons below are false. + // If joinState is empty, all joinState == comparisons below are false. const auto roomKey = qMakePair(id, joinState == JoinState::Invite); auto* room = d->roomMap.value(roomKey, nullptr); - if (room) - { + if (room) { // Leave is a special case because in transition (5a) (see the .h file) // joinState == room->joinState but we still have to preempt the Invite // and emit a signal. For Invite and Join, there's no such problem. if (room->joinState() == joinState && joinState != JoinState::Leave) return room; - } else if (joinState.omitted()) - { + } else if (!joinState) { // No Join and Leave, maybe Invite? - room = d->roomMap.value({id, true}, nullptr); + room = d->roomMap.value({ id, true }, nullptr); if (room) return room; // No Invite either, setup a new room object below } - if (!room) - { - room = roomFactory()(this, id, - joinState.omitted() ? JoinState::Join : joinState.value()); - if (!room) - { + if (!room) { + room = roomFactory()(this, id, joinState.value_or(JoinState::Join)); + if (!room) { qCCritical(MAIN) << "Failed to create a room" << id; return nullptr; } d->roomMap.insert(roomKey, room); d->firstTimeRooms.push_back(room); - connect(room, &Room::beforeDestruction, - this, &Connection::aboutToDeleteRoom); + connect(room, &Room::beforeDestruction, this, + &Connection::aboutToDeleteRoom); emit newRoom(room); } - if (joinState.omitted()) + if (!joinState) return room; - if (joinState == JoinState::Invite) - { + if (*joinState == JoinState::Invite) { // prev is either Leave or nullptr - auto* prev = d->roomMap.value({id, false}, nullptr); + auto* prev = d->roomMap.value({ id, false }, nullptr); emit invitedRoom(room, prev); - } - else - { - room->setJoinState(joinState.value()); + } else { + room->setJoinState(*joinState); // Preempt the Invite room (if any) with a room in Join/Leave state. - auto* prevInvite = d->roomMap.take({id, true}); - if (joinState == JoinState::Join) + auto* prevInvite = d->roomMap.take({ id, true }); + if (*joinState == JoinState::Join) emit joinedRoom(room, prevInvite); - else if (joinState == JoinState::Leave) + else if (*joinState == JoinState::Leave) emit leftRoom(room, prevInvite); - if (prevInvite) - { + if (prevInvite) { const auto dcUsers = prevInvite->directChatUsers(); - for (auto* u: dcUsers) + for (auto* u : dcUsers) addToDirectChats(room, u); - qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id(); + qCDebug(MAIN) << "Deleting Invite state for room" + << prevInvite->id(); emit prevInvite->beforeDestruction(prevInvite); prevInvite->deleteLater(); } @@ -1326,15 +1518,9 @@ void Connection::setUserFactory(user_factory_t f) _userFactory = std::move(f); } -room_factory_t Connection::roomFactory() -{ - return _roomFactory; -} +room_factory_t Connection::roomFactory() { return _roomFactory; } -user_factory_t Connection::userFactory() -{ - return _userFactory; -} +user_factory_t Connection::userFactory() { return _userFactory; } room_factory_t Connection::_roomFactory = defaultRoomFactory<>(); user_factory_t Connection::_userFactory = defaultUserFactory<>(); @@ -1346,17 +1532,22 @@ QByteArray Connection::generateTxnId() const void Connection::setHomeserver(const QUrl& url) { + if (isJobRunning(d->resolverJob)) + d->resolverJob->abandon(); + if (isJobRunning(d->loginFlowsJob)) + d->loginFlowsJob->abandon(); + d->loginFlows.clear(); + if (homeserver() != url) { d->data->setBaseUrl(url); - d->loginFlows.clear(); emit homeserverChanged(homeserver()); } // Whenever a homeserver is updated, retrieve available login flows from it - auto* j = callApi<GetLoginFlowsJob>(BackgroundRequest); - connect(j, &BaseJob::finished, this, [this, j] { - if (j->status().good()) - d->loginFlows = j->flows(); + d->loginFlowsJob = callApi<GetLoginFlowsJob>(BackgroundRequest); + connect(d->loginFlowsJob, &BaseJob::result, this, [this] { + if (d->loginFlowsJob->status().good()) + d->loginFlows = d->loginFlowsJob->flows(); else d->loginFlows.clear(); emit loginFlowsChanged(); @@ -1369,17 +1560,24 @@ void Connection::saveRoomState(Room* r) const if (!d->cacheState) return; - QFile outRoomFile { stateCachePath() % SyncData::fileNameForRoom(r->id()) }; - if (outRoomFile.open(QFile::WriteOnly)) - { + QFile outRoomFile { stateCacheDir().filePath( + SyncData::fileNameForRoom(r->id())) }; + if (outRoomFile.open(QFile::WriteOnly)) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + const auto data = + d->cacheToBinary + ? QCborValue::fromJsonValue(r->toJson()).toCbor() + : QJsonDocument(r->toJson()).toJson(QJsonDocument::Compact); +#else QJsonDocument json { r->toJson() }; - auto data = d->cacheToBinary ? json.toBinaryData() - : json.toJson(QJsonDocument::Compact); + const auto data = d->cacheToBinary ? json.toBinaryData() + : json.toJson(QJsonDocument::Compact); +#endif outRoomFile.write(data.data(), data.size()); qCDebug(MAIN) << "Room state cache saved to" << outRoomFile.fileName(); } else { - qCWarning(MAIN) << "Error opening" << outRoomFile.fileName() - << ":" << outRoomFile.errorString(); + qCWarning(MAIN) << "Error opening" << outRoomFile.fileName() << ":" + << outRoomFile.errorString(); } } @@ -1388,23 +1586,24 @@ void Connection::saveState() const if (!d->cacheState) return; - QElapsedTimer et; et.start(); + QElapsedTimer et; + et.start(); - QFile outFile { stateCachePath() % "state.json" }; - if (!outFile.open(QFile::WriteOnly)) - { - qCWarning(MAIN) << "Error opening" << outFile.fileName() - << ":" << outFile.errorString(); + QFile outFile { d->topLevelStatePath() }; + if (!outFile.open(QFile::WriteOnly)) { + qCWarning(MAIN) << "Error opening" << outFile.fileName() << ":" + << outFile.errorString(); qCWarning(MAIN) << "Caching the rooms state disabled"; d->cacheState = false; return; } QJsonObject rootObj { - { QStringLiteral("cache_version"), QJsonObject { - { QStringLiteral("major"), SyncData::cacheVersion().first }, - { QStringLiteral("minor"), SyncData::cacheVersion().second } - }}}; + { QStringLiteral("cache_version"), + QJsonObject { + { QStringLiteral("major"), SyncData::cacheVersion().first }, + { QStringLiteral("minor"), SyncData::cacheVersion().second } } } + }; { QJsonObject roomsJson; QJsonObject inviteRoomsJson; @@ -1428,17 +1627,24 @@ void Connection::saveState() const QJsonArray accountDataEvents { basicEventJson(QStringLiteral("m.direct"), toJson(d->directChats)) }; - for (const auto &e : d->accountData) + for (const auto& e : d->accountData) accountDataEvents.append( basicEventJson(e.first, e.second->contentJson())); rootObj.insert(QStringLiteral("account_data"), - QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); + QJsonObject { + { QStringLiteral("events"), accountDataEvents } }); } +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + const auto data = + d->cacheToBinary ? QCborValue::fromJsonValue(rootObj).toCbor() + : QJsonDocument(rootObj).toJson(QJsonDocument::Compact); +#else QJsonDocument json { rootObj }; - auto data = d->cacheToBinary ? json.toBinaryData() : - json.toJson(QJsonDocument::Compact); + const auto data = d->cacheToBinary ? json.toBinaryData() + : json.toJson(QJsonDocument::Compact); +#endif qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et; outFile.write(data.data(), data.size()); @@ -1450,14 +1656,14 @@ void Connection::loadState() if (!d->cacheState) return; - QElapsedTimer et; et.start(); + QElapsedTimer et; + et.start(); - SyncData sync { stateCachePath() % "state.json" }; + SyncData sync { d->topLevelStatePath() }; if (sync.nextBatch().isEmpty()) // No token means no cache by definition return; - if (!sync.unresolvedRooms().isEmpty()) - { + if (!sync.unresolvedRooms().isEmpty()) { qCWarning(MAIN) << "State cache incomplete, discarding"; return; } @@ -1470,63 +1676,72 @@ void Connection::loadState() QString Connection::stateCachePath() const { + return stateCacheDir().path() % '/'; +} + +QDir Connection::stateCacheDir() const +{ auto safeUserId = userId(); safeUserId.replace(':', '_'); return cacheLocation(safeUserId); } -bool Connection::cacheState() const -{ - return d->cacheState; -} +bool Connection::cacheState() const { return d->cacheState; } void Connection::setCacheState(bool newValue) { - if (d->cacheState != newValue) - { + if (d->cacheState != newValue) { d->cacheState = newValue; emit cacheStateChanged(); } } -bool QMatrixClient::Connection::lazyLoading() const -{ - return d->lazyLoading; -} +bool Connection::lazyLoading() const { return d->lazyLoading; } -void QMatrixClient::Connection::setLazyLoading(bool newValue) +void Connection::setLazyLoading(bool newValue) { - if (d->lazyLoading != newValue) - { + if (d->lazyLoading != newValue) { d->lazyLoading = newValue; emit lazyLoadingChanged(); } } +BaseJob* Connection::run(BaseJob* job, RunningPolicy runningPolicy) +{ + // Reparent to protect from #397, #398 and to prevent BaseJob* from being + // garbage-collected if made by or returned to QML/JavaScript. + job->setParent(this); + connect(job, &BaseJob::failure, this, &Connection::requestFailed); + job->initiate(d->data.get(), runningPolicy & BackgroundRequest); + return job; +} + void Connection::getTurnServers() { auto job = callApi<GetTurnServerJob>(); - connect(job, &GetTurnServerJob::success, - this, [=] { emit turnServersChanged(job->data()); }); + connect(job, &GetTurnServerJob::success, this, + [=] { emit turnServersChanged(job->data()); }); } const QString Connection::SupportedRoomVersion::StableTag = - QStringLiteral("stable"); + QStringLiteral("stable"); QString Connection::defaultRoomVersion() const { - Q_ASSERT(!d->capabilities.roomVersions.omitted()); - return d->capabilities.roomVersions->defaultVersion; + return d->capabilities.roomVersions + ? d->capabilities.roomVersions->defaultVersion + : QString(); } QStringList Connection::stableRoomVersions() const { - Q_ASSERT(!d->capabilities.roomVersions.omitted()); QStringList l; - const auto& allVersions = d->capabilities.roomVersions->available; - for (auto it = allVersions.begin(); it != allVersions.end(); ++it) - if (it.value() == SupportedRoomVersion::StableTag) - l.push_back(it.key()); + if (d->capabilities.roomVersions) { + const auto& allVersions = d->capabilities.roomVersions->available; + for (auto it = allVersions.begin(); it != allVersions.end(); ++it) + if (it.value() == SupportedRoomVersion::StableTag) + l.push_back(it.key()); + } return l; } @@ -1541,18 +1756,19 @@ inline bool roomVersionLess(const Connection::SupportedRoomVersion& v1, QVector<Connection::SupportedRoomVersion> Connection::availableRoomVersions() const { - Q_ASSERT(!d->capabilities.roomVersions.omitted()); QVector<SupportedRoomVersion> result; - result.reserve(d->capabilities.roomVersions->available.size()); - for (auto it = d->capabilities.roomVersions->available.begin(); - it != d->capabilities.roomVersions->available.end(); ++it) - result.push_back({ it.key(), it.value() }); - // Put stable versions over unstable; within each group, - // sort numeric versions as numbers, the rest as strings. - const auto mid = std::partition(result.begin(), result.end(), - std::mem_fn(&SupportedRoomVersion::isStable)); - std::sort(result.begin(), mid, roomVersionLess); - std::sort(mid, result.end(), roomVersionLess); - + if (d->capabilities.roomVersions) { + const auto& allVersions = d->capabilities.roomVersions->available; + result.reserve(allVersions.size()); + for (auto it = allVersions.begin(); it != allVersions.end(); ++it) + result.push_back({ it.key(), it.value() }); + // Put stable versions over unstable; within each group, + // sort numeric versions as numbers, the rest as strings. + const auto mid = + std::partition(result.begin(), result.end(), + std::mem_fn(&SupportedRoomVersion::isStable)); + std::sort(result.begin(), mid, roomVersionLess); + std::sort(mid, result.end(), roomVersionLess); + } return result; } diff --git a/lib/connection.h b/lib/connection.h index b0dfeb5e..c90cb892 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -13,834 +13,878 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once -#include "csapi/login.h" #include "ssosession.h" -#include "csapi/create_room.h" #include "joinstate.h" -#include "events/accountdataevents.h" #include "qt_connection_util.h" +#include "quotient_common.h" + +#include "csapi/login.h" +#include "csapi/create_room.h" +#include "events/accountdataevents.h" + +#include <QtCore/QDir> #include <QtCore/QObject> -#include <QtCore/QUrl> #include <QtCore/QSize> +#include <QtCore/QUrl> #include <functional> -#include <memory> -Q_DECLARE_METATYPE(QMatrixClient::GetLoginFlowsJob::LoginFlow) +namespace QtOlm { +class Account; +} + +Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) + +namespace Quotient { + +class Room; +class User; +class ConnectionData; +class RoomEvent; + +class SyncJob; +class SyncData; +class RoomMessagesJob; +class PostReceiptJob; +class ForgetRoomJob; +class MediaThumbnailJob; +class JoinRoomJob; +class UploadContentJob; +class GetContentJob; +class DownloadFileJob; +class SendToDeviceJob; +class SendMessageJob; +class LeaveRoomJob; + +// To simplify comparisons of LoginFlows + +inline bool operator==(const GetLoginFlowsJob::LoginFlow& lhs, + const GetLoginFlowsJob::LoginFlow& rhs) +{ + return lhs.type == rhs.type; +} + +inline bool operator!=(const GetLoginFlowsJob::LoginFlow& lhs, + const GetLoginFlowsJob::LoginFlow& rhs) +{ + return !(lhs == rhs); +} + +/// Predefined login flows +struct LoginFlows { + using LoginFlow = GetLoginFlowsJob::LoginFlow; + static inline const LoginFlow Password { "m.login.password" }; + static inline const LoginFlow SSO { "m.login.sso" }; + static inline const LoginFlow Token { "m.login.token" }; +}; -namespace QMatrixClient +class Connection; + +using room_factory_t = + std::function<Room*(Connection*, const QString&, JoinState)>; +using user_factory_t = std::function<User*(Connection*, const QString&)>; + +/** The default factory to create room objects + * + * Just a wrapper around operator new. + * \sa Connection::setRoomFactory, Connection::setRoomType + */ +template <typename T = Room> +static inline room_factory_t defaultRoomFactory() { - class Room; - class User; - class ConnectionData; - class RoomEvent; - - class SyncJob; - class SyncData; - class RoomMessagesJob; - class PostReceiptJob; - class ForgetRoomJob; - class MediaThumbnailJob; - class JoinRoomJob; - class UploadContentJob; - class GetContentJob; - class DownloadFileJob; - class SendToDeviceJob; - class SendMessageJob; - class LeaveRoomJob; - - // To simplify comparisons of LoginFlows - - inline bool operator==(const GetLoginFlowsJob::LoginFlow& lhs, - const GetLoginFlowsJob::LoginFlow& rhs) + return [](Connection* c, const QString& id, JoinState js) { + return new T(c, id, js); + }; +} + +/** The default factory to create user objects + * + * Just a wrapper around operator new. + * \sa Connection::setUserFactory, Connection::setUserType + */ +template <typename T = User> +static inline user_factory_t defaultUserFactory() +{ + return [](Connection* c, const QString& id) { return new T(id, c); }; +} + +// Room ids, rather than room pointers, are used in the direct chat +// map types because the library keeps Invite rooms separate from +// rooms in Join and Leave state; and direct chats in account data +// are stored with no regard to their state. +using DirectChatsMap = QMultiHash<const User*, QString>; +using DirectChatUsersMap = QMultiHash<QString, User*>; +using IgnoredUsersList = IgnoredUsersEvent::content_type; + +class Connection : public QObject { + Q_OBJECT + + Q_PROPERTY(User* localUser READ user NOTIFY stateChanged) + Q_PROPERTY(QString localUserId READ userId NOTIFY stateChanged) + Q_PROPERTY(QString domain READ domain NOTIFY stateChanged STORED false) + Q_PROPERTY(QString deviceId READ deviceId NOTIFY stateChanged) + Q_PROPERTY(QByteArray accessToken READ accessToken NOTIFY stateChanged) + Q_PROPERTY(bool isLoggedIn READ isLoggedIn NOTIFY stateChanged STORED false) + Q_PROPERTY(QString defaultRoomVersion READ defaultRoomVersion NOTIFY capabilitiesLoaded) + Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) + Q_PROPERTY(QVector<GetLoginFlowsJob::LoginFlow> loginFlows READ loginFlows NOTIFY loginFlowsChanged) + Q_PROPERTY(bool isUsable READ isUsable NOTIFY loginFlowsChanged STORED false) + Q_PROPERTY(bool supportsSso READ supportsSso NOTIFY loginFlowsChanged STORED false) + Q_PROPERTY(bool supportsPasswordAuth READ supportsPasswordAuth NOTIFY loginFlowsChanged STORED false) + Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) + Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged) + +public: + using UsersToDevicesToEvents = + UnorderedMap<QString, UnorderedMap<QString, const Event&>>; + + enum RoomVisibility { + PublishRoom, + UnpublishRoom + }; // FIXME: Should go inside CreateRoomJob + + explicit Connection(QObject* parent = nullptr); + explicit Connection(const QUrl& server, QObject* parent = nullptr); + ~Connection() override; + + /// Get all Invited and Joined rooms + /*! + * \return a hashmap from a composite key - room name and whether + * it's an Invite rather than Join - to room pointers + * \sa allRooms, rooms, roomsWithTag + */ + [[deprecated("Use allRooms(), roomsWithTag() or rooms(joinStates) instead")]] + QHash<QPair<QString, bool>, Room*> roomMap() const; + + /// Get all rooms known within this Connection + /*! + * This includes Invite, Join and Leave rooms, in no particular order. + * \note Leave rooms will only show up in the list if they have been left + * in the same running session. The library doesn't cache left rooms + * between runs and it doesn't retrieve the full list of left rooms + * from the server. + * \sa rooms, room, roomsWithTag + */ + Q_INVOKABLE QVector<Quotient::Room*> allRooms() const; + + /// Get rooms that have either of the given join state(s) + /*! + * This method returns, in no particular order, rooms which join state + * matches the mask passed in \p joinStates. + * \note Similar to allRooms(), this won't retrieve the full list of + * Leave rooms from the server. + * \sa allRooms, room, roomsWithTag + */ + Q_INVOKABLE QVector<Quotient::Room*> + rooms(Quotient::JoinStates joinStates) const; + + /// Get the total number of rooms in the given join state(s) + Q_INVOKABLE int roomsCount(Quotient::JoinStates joinStates) const; + + /** Check whether the account has data of the given type + * Direct chats map is not supported by this method _yet_. + */ + bool hasAccountData(const QString& type) const; + + /** Get a generic account data event of the given type + * This returns an account data event of the given type + * stored on the server. Direct chats map cannot be retrieved + * using this method _yet_; use directChats() instead. + */ + const EventPtr& accountData(const QString& type) const; + + /** Get a generic account data event of the given type + * This returns an account data event of the given type + * stored on the server. Direct chats map cannot be retrieved + * using this method _yet_; use directChats() instead. + */ + template <typename EventT> + const typename EventT::content_type accountData() const { - return lhs.type == rhs.type; + if (const auto& eventPtr = accountData(EventT::matrixTypeId())) + return eventPtr->content(); + return {}; } - inline bool operator!=(const GetLoginFlowsJob::LoginFlow& lhs, - const GetLoginFlowsJob::LoginFlow& rhs) + /** Get account data as a JSON object + * This returns the content part of the account data event + * of the given type. Direct chats map cannot be retrieved using + * this method _yet_; use directChats() instead. + */ + Q_INVOKABLE QJsonObject accountDataJson(const QString& type) const; + + /** Set a generic account data event of the given type */ + void setAccountData(EventPtr&& event); + + Q_INVOKABLE void setAccountData(const QString& type, + const QJsonObject& content); + + /** Get all Invited and Joined rooms grouped by tag + * \return a hashmap from tag name to a vector of room pointers, + * sorted by their order in the tag - details are at + * https://matrix.org/speculator/spec/drafts%2Fe2e/client_server/unstable.html#id95 + */ + QHash<QString, QVector<Room*>> tagsToRooms() const; + + /** Get all room tags known on this connection */ + QStringList tagNames() const; + + /** Get the list of rooms with the specified tag */ + QVector<Room*> roomsWithTag(const QString& tagName) const; + + /*! \brief Mark the room as a direct chat with the user + * + * This function marks \p room as a direct chat with \p user. + * Emits the signal synchronously, without waiting to complete + * synchronisation with the server. + * + * \sa directChatsListChanged + */ + void addToDirectChats(const Room* room, User* user); + + /*! \brief Unmark the room from direct chats + * + * This function removes the room id from direct chats either for + * a specific \p user or for all users if \p user in nullptr. + * The room id is used to allow removal of, e.g., ids of forgotten + * rooms; a Room object need not exist. Emits the signal + * immediately, without waiting to complete synchronisation with + * the server. + * + * \sa directChatsListChanged + */ + void removeFromDirectChats(const QString& roomId, User* user = nullptr); + + /** Check whether the room id corresponds to a direct chat */ + bool isDirectChat(const QString& roomId) const; + + /** Get the whole map from users to direct chat rooms */ + DirectChatsMap directChats() const; + + /** Retrieve the list of users the room is a direct chat with + * @return The list of users for which this room is marked as + * a direct chat; an empty list if the room is not a direct chat + */ + QList<User*> directChatUsers(const Room* room) const; + + /** Check whether a particular user is in the ignore list */ + Q_INVOKABLE bool isIgnored(const Quotient::User* user) const; + + /** Get the whole list of ignored users */ + Q_INVOKABLE Quotient::IgnoredUsersList ignoredUsers() const; + + /** Add the user to the ignore list + * The change signal is emitted synchronously, without waiting + * to complete synchronisation with the server. + * + * \sa ignoredUsersListChanged + */ + Q_INVOKABLE void addToIgnoredUsers(const Quotient::User* user); + + /** Remove the user from the ignore list */ + /** Similar to adding, the change signal is emitted synchronously. + * + * \sa ignoredUsersListChanged + */ + Q_INVOKABLE void removeFromIgnoredUsers(const Quotient::User* user); + + /** Get the full list of users known to this account */ + QMap<QString, User*> users() const; + + /** Get the base URL of the homeserver to connect to */ + QUrl homeserver() const; + /** Get the domain name used for ids/aliases on the server */ + QString domain() const; + /** Check if the homeserver is known to be reachable and working */ + bool isUsable() const; + /** Get the list of supported login flows */ + QVector<GetLoginFlowsJob::LoginFlow> loginFlows() const; + /** Check whether the current homeserver supports password auth */ + bool supportsPasswordAuth() const; + /** Check whether the current homeserver supports SSO */ + bool supportsSso() const; + /** Find a room by its id and a mask of applicable states */ + Q_INVOKABLE Quotient::Room* + room(const QString& roomId, + Quotient::JoinStates states = JoinState::Invite | JoinState::Join) const; + /** Find a room by its alias and a mask of applicable states */ + Q_INVOKABLE Quotient::Room* + roomByAlias(const QString& roomAlias, + Quotient::JoinStates states = JoinState::Invite + | JoinState::Join) const; + /** Update the internal map of room aliases to IDs */ + /// This is used to maintain the internal index of room aliases. + /// It does NOT change aliases on the server, + /// \sa Room::setLocalAliases + void updateRoomAliases(const QString& roomId, + const QStringList& previousRoomAliases, + const QStringList& roomAliases); + Q_INVOKABLE Quotient::Room* invitation(const QString& roomId) const; + Q_INVOKABLE Quotient::User* user(const QString& uId); + const User* user() const; + User* user(); + QString userId() const; + QString deviceId() const; + QByteArray accessToken() const; + bool isLoggedIn() const; +#ifdef Quotient_E2EE_ENABLED + QtOlm::Account* olmAccount() const; +#endif // Quotient_E2EE_ENABLED + Q_INVOKABLE Quotient::SyncJob* syncJob() const; + Q_INVOKABLE int millisToReconnect() const; + + Q_INVOKABLE void getTurnServers(); + + struct SupportedRoomVersion { + QString id; + QString status; + + static const QString StableTag; // "stable", as of CS API 0.5 + bool isStable() const { return status == StableTag; } + + friend QDebug operator<<(QDebug dbg, const SupportedRoomVersion& v) + { + QDebugStateSaver _(dbg); + return dbg.nospace() << v.id << '/' << v.status; + } + }; + + /// Get the room version recommended by the server + /** Only works after server capabilities have been loaded. + * \sa loadingCapabilities */ + QString defaultRoomVersion() const; + /// Get the room version considered stable by the server + /** Only works after server capabilities have been loaded. + * \sa loadingCapabilities */ + QStringList stableRoomVersions() const; + /// Get all room versions supported by the server + /** Only works after server capabilities have been loaded. + * \sa loadingCapabilities */ + QVector<SupportedRoomVersion> availableRoomVersions() const; + + /** + * Call this before first sync to load from previously saved file. + * + * \param fromFile A local path to read the state from. Uses QUrl + * to be QML-friendly. Empty parameter means saving to the directory + * defined by stateCachePath() / stateCacheDir(). + */ + Q_INVOKABLE void loadState(); + /** + * This method saves the current state of rooms (but not messages + * in them) to a local cache file, so that it could be loaded by + * loadState() on a next run of the client. + * + * \param toFile A local path to save the state to. Uses QUrl to be + * QML-friendly. Empty parameter means saving to the directory + * defined by stateCachePath() / stateCacheDir(). + */ + Q_INVOKABLE void saveState() const; + + /// This method saves the current state of a single room. + void saveRoomState(Room* r) const; + + /// Get the default directory path to save the room state to + /** \sa stateCacheDir */ + Q_INVOKABLE QString stateCachePath() const; + + /// Get the default directory to save the room state to + /** + * This function returns the default directory to store the cached + * room state, defined as follows: + * \code + * QStandardPaths::writeableLocation(QStandardPaths::CacheLocation) + + * _safeUserId + "_state.json" \endcode where `_safeUserId` is userId() with + * `:` (colon) replaced by + * `_` (underscore), as colons are reserved characters on Windows. + * \sa loadState, saveState, stateCachePath + */ + QDir stateCacheDir() const; + + /** Whether or not the rooms state should be cached locally + * \sa loadState(), saveState() + */ + bool cacheState() const; + void setCacheState(bool newValue); + + bool lazyLoading() const; + void setLazyLoading(bool newValue); + + /*! Start a pre-created job object on this connection */ + Q_INVOKABLE BaseJob* run(BaseJob* job, + RunningPolicy runningPolicy = ForegroundRequest); + + /*! Start a job of a specified type with specified arguments and policy + * + * This is a universal method to create and start a job of a type passed + * as a template parameter. The policy allows to fine-tune the way + * the job is executed - as of this writing it means a choice + * between "foreground" and "background". + * + * \param runningPolicy controls how the job is executed + * \param jobArgs arguments to the job constructor + * + * \sa BaseJob::isBackground. QNetworkRequest::BackgroundRequestAttribute + */ + template <typename JobT, typename... JobArgTs> + JobT* callApi(RunningPolicy runningPolicy, JobArgTs&&... jobArgs) { - return !(lhs == rhs); + auto job = new JobT(std::forward<JobArgTs>(jobArgs)...); + run(job, runningPolicy); + return job; } - /// Predefined login flows - namespace LoginFlows { - using LoginFlow = GetLoginFlowsJob::LoginFlow; - static const LoginFlow Password { "m.login.password" }; - static const LoginFlow SSO { "m.login.sso" }; - static const LoginFlow Token { "m.login.token" }; + /*! Start a job of a specified type with specified arguments + * + * This is an overload that runs the job with "foreground" policy. + */ + template <typename JobT, typename... JobArgTs> + JobT* callApi(JobArgTs&&... jobArgs) + { + return callApi<JobT>(ForegroundRequest, + std::forward<JobArgTs>(jobArgs)...); } - class Connection; + /*! Get a request URL for a job with specified type and arguments + * + * This calls JobT::makeRequestUrl() prepending the connection's homeserver + * to the list of arguments. + */ + template <typename JobT, typename... JobArgTs> + QUrl getUrlForApi(JobArgTs&&... jobArgs) const + { + return JobT::makeRequestUrl(homeserver(), + std::forward<JobArgTs>(jobArgs)...); + } - using room_factory_t = std::function<Room*(Connection*, const QString&, - JoinState)>; - using user_factory_t = std::function<User*(Connection*, const QString&)>; + Q_INVOKABLE SsoSession* prepareForSso(const QString& initialDeviceName, + const QString& deviceId = {}); - /** The default factory to create room objects - * - * Just a wrapper around operator new. - * \sa Connection::setRoomFactory, Connection::setRoomType + /** Generate a new transaction id. Transaction id's are unique within + * a single Connection object */ - template <typename T = Room> - static inline room_factory_t defaultRoomFactory() + Q_INVOKABLE QByteArray generateTxnId() const; + + /// Set a room factory function + static void setRoomFactory(room_factory_t f); + + /// Set a user factory function + static void setUserFactory(user_factory_t f); + + /// Get a room factory function + static room_factory_t roomFactory(); + + /// Get a user factory function + static user_factory_t userFactory(); + + /// Set the room factory to default with the overriden room type + template <typename T> + static void setRoomType() + { + setRoomFactory(defaultRoomFactory<T>()); + } + + /// Set the user factory to default with the overriden user type + template <typename T> + static void setUserType() { - return [](Connection* c, const QString& id, JoinState js) - { - return new T(c, id, js); - }; + setUserFactory(defaultUserFactory<T>()); } - /** The default factory to create user objects +public slots: + /** Set the homeserver base URL */ + void setHomeserver(const QUrl& baseUrl); + + /** Determine and set the homeserver from MXID */ + void resolveServer(const QString& mxid); + + /** \brief Log in using a username and password pair + * + * Before logging in, this method checks if the homeserver is valid and + * supports the password login flow. If the homeserver is invalid but + * a full user MXID is provided, this method calls resolveServer() using + * this MXID. + * + * \sa resolveServer, resolveError, loginError + */ + void loginWithPassword(const QString& userId, const QString& password, + const QString& initialDeviceName, + const QString& deviceId = {}); + /** \brief Log in using a login token * - * Just a wrapper around operator new. - * \sa Connection::setUserFactory, Connection::setUserType + * One usual case for this method is the final stage of logging in via SSO. + * Unlike loginWithPassword() and assumeIdentity(), this method cannot + * resolve the server from the user name because the full user MXID is + * encoded in the login token. Callers should ensure the homeserver + * sanity in advance. */ - template <typename T = User> - static inline user_factory_t defaultUserFactory() + void loginWithToken(const QByteArray& loginToken, + const QString& initialDeviceName, + const QString& deviceId = {}); + /** \brief Use an existing access token to connect to the homeserver + * + * Similar to loginWithPassword(), this method checks that the homeserver + * URL is valid and tries to resolve it from the MXID in case it is not. + */ + void assumeIdentity(const QString& mxId, const QString& accessToken, + const QString& deviceId); + /*! \deprecated Use loginWithPassword instead */ + void connectToServer(const QString& userId, const QString& password, + const QString& initialDeviceName, + const QString& deviceId = {}) + { + loginWithPassword(userId, password, initialDeviceName, deviceId); + } + /*! \deprecated + * Use assumeIdentity() if you have an access token or + * loginWithToken() if you have a login token. + */ + void connectWithToken(const QString& userId, const QString& accessToken, + const QString& deviceId) { - return [](Connection* c, const QString& id) - { - return new T(id, c); - }; + assumeIdentity(userId, accessToken, deviceId); } + /// Explicitly request capabilities from the server + void reloadCapabilities(); + + /// Find out if capabilites are still loading from the server + bool loadingCapabilities() const; + + /** @deprecated Use stopSync() instead */ + void disconnectFromServer() { stopSync(); } + void logout(); + + void sync(int timeout = -1); + void syncLoop(int timeout = 30000); + + void stopSync(); + QString nextBatchToken() const; + + virtual MediaThumbnailJob* + getThumbnail(const QString& mediaId, QSize requestedSize, + RunningPolicy policy = BackgroundRequest); + MediaThumbnailJob* getThumbnail(const QUrl& url, QSize requestedSize, + RunningPolicy policy = BackgroundRequest); + MediaThumbnailJob* getThumbnail(const QUrl& url, int requestedWidth, + int requestedHeight, + RunningPolicy policy = BackgroundRequest); + + // QIODevice* should already be open + UploadContentJob* uploadContent(QIODevice* contentSource, + const QString& filename = {}, + const QString& overrideContentType = {}); + UploadContentJob* uploadFile(const QString& fileName, + const QString& overrideContentType = {}); + GetContentJob* getContent(const QString& mediaId); + GetContentJob* getContent(const QUrl& url); + // If localFilename is empty, a temporary file will be created + DownloadFileJob* downloadFile(const QUrl& url, + const QString& localFilename = {}); + + /** + * \brief Create a room (generic method) + * This method allows to customize room entirely to your liking, + * providing all the attributes the original CS API provides. + */ + CreateRoomJob* + createRoom(RoomVisibility visibility, const QString& alias, + const QString& name, const QString& topic, QStringList invites, + const QString& presetName = {}, const QString& roomVersion = {}, + bool isDirect = false, + const QVector<CreateRoomJob::StateEvent>& initialState = {}, + const QVector<CreateRoomJob::Invite3pid>& invite3pids = {}, + const QJsonObject& creationContent = {}); + + /** Get a direct chat with a single user + * This method may return synchronously or asynchoronously depending + * on whether a direct chat room with the respective person exists + * already. + * + * \sa directChatAvailable + */ + void requestDirectChat(const QString& userId); - /** Enumeration with flags defining the network job running policy - * So far only background/foreground flags are available. + /** Get a direct chat with a single user + * This method may return synchronously or asynchoronously depending + * on whether a direct chat room with the respective person exists + * already. * - * \sa Connection::callApi - */ - enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 }; - - class Connection: public QObject { - Q_OBJECT - - Q_PROPERTY(User* localUser READ user NOTIFY stateChanged) - Q_PROPERTY(QString localUserId READ userId NOTIFY stateChanged) - Q_PROPERTY(QString deviceId READ deviceId NOTIFY stateChanged) - Q_PROPERTY(QByteArray accessToken READ accessToken NOTIFY stateChanged) - Q_PROPERTY(QString defaultRoomVersion READ defaultRoomVersion NOTIFY capabilitiesLoaded) - Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) - Q_PROPERTY(QString domain READ domain NOTIFY homeserverChanged) - Q_PROPERTY(QVector<QMatrixClient::GetLoginFlowsJob::LoginFlow> loginFlows READ loginFlows NOTIFY loginFlowsChanged) - Q_PROPERTY(bool supportsSso READ supportsSso NOTIFY loginFlowsChanged) - Q_PROPERTY(bool supportsPasswordAuth READ supportsPasswordAuth NOTIFY loginFlowsChanged) - Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) - Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged) - - public: - // Room ids, rather than room pointers, are used in the direct chat - // map types because the library keeps Invite rooms separate from - // rooms in Join and Leave state; and direct chats in account data - // are stored with no regard to their state. - using DirectChatsMap = QMultiHash<const User*, QString>; - using DirectChatUsersMap = QMultiHash<QString, User*>; - using IgnoredUsersList = IgnoredUsersEvent::content_type; - - using UsersToDevicesToEvents = - std::unordered_map<QString, - std::unordered_map<QString, const Event&>>; - - enum RoomVisibility { PublishRoom, UnpublishRoom }; // FIXME: Should go inside CreateRoomJob - - explicit Connection(QObject* parent = nullptr); - explicit Connection(const QUrl& server, QObject* parent = nullptr); - virtual ~Connection(); - - /** Get all Invited and Joined rooms - * - * \deprecated - * Use allRooms(), roomsWithTag(), or rooms(JoinStates) instead - * \return a hashmap from a composite key - room name and whether - * it's an Invite rather than Join - to room pointers - */ - QHash<QPair<QString, bool>, Room*> roomMap() const; - - /** Get all rooms known within this Connection - * - * This includes Invite, Join and Leave rooms, in no particular order. - * \note Leave rooms will only show up in the list if they have been left - * in the same running session. The library doesn't cache left rooms - * between runs and it doesn't retrieve the full list of left rooms - * from the server. - * \sa rooms, room, roomsWithTag - */ - Q_INVOKABLE QVector<Room*> allRooms() const; - - /** Get rooms that have either of the given join state(s) - * - * This method returns, in no particular order, rooms which join state - * matches the mask passed in \p joinStates. - * \note Similar to allRooms(), this won't retrieve the full list of - * Leave rooms from the server. - * \sa allRooms, room, roomsWithTag - */ - Q_INVOKABLE QVector<Room*> rooms(JoinStates joinStates) const; - - /** Get the total number of rooms in the given join state(s) */ - Q_INVOKABLE int roomsCount(JoinStates joinStates) const; - - /** Check whether the account has data of the given type - * Direct chats map is not supported by this method _yet_. - */ - bool hasAccountData(const QString& type) const; - - /** Get a generic account data event of the given type - * This returns an account data event of the given type - * stored on the server. Direct chats map cannot be retrieved - * using this method _yet_; use directChats() instead. - */ - const EventPtr& accountData(const QString& type) const; - - /** Get a generic account data event of the given type - * This returns an account data event of the given type - * stored on the server. Direct chats map cannot be retrieved - * using this method _yet_; use directChats() instead. - */ - template <typename EventT> - const typename EventT::content_type accountData() const - { - if (const auto& eventPtr = accountData(EventT::matrixTypeId())) - return eventPtr->content(); - return {}; - } - - /** Get account data as a JSON object - * This returns the content part of the account data event - * of the given type. Direct chats map cannot be retrieved using - * this method _yet_; use directChats() instead. - */ - Q_INVOKABLE QJsonObject accountDataJson(const QString& type) const; - - /** Set a generic account data event of the given type */ - void setAccountData(EventPtr&& event); - - Q_INVOKABLE void setAccountData(const QString& type, - const QJsonObject& content); - - /** Get all Invited and Joined rooms grouped by tag - * \return a hashmap from tag name to a vector of room pointers, - * sorted by their order in the tag - details are at - * https://matrix.org/speculator/spec/drafts%2Fe2e/client_server/unstable.html#id95 - */ - QHash<QString, QVector<Room*>> tagsToRooms() const; - - /** Get all room tags known on this connection */ - QStringList tagNames() const; - - /** Get the list of rooms with the specified tag */ - QVector<Room*> roomsWithTag(const QString& tagName) const; - - /** Mark the room as a direct chat with the user - * This function marks \p room as a direct chat with \p user. - * Emits the signal synchronously, without waiting to complete - * synchronisation with the server. - * - * \sa directChatsListChanged - */ - void addToDirectChats(const Room* room, User* user); - - /** Unmark the room from direct chats - * This function removes the room id from direct chats either for - * a specific \p user or for all users if \p user in nullptr. - * The room id is used to allow removal of, e.g., ids of forgotten - * rooms; a Room object need not exist. Emits the signal - * immediately, without waiting to complete synchronisation with - * the server. - * - * \sa directChatsListChanged - */ - void removeFromDirectChats(const QString& roomId, - User* user = nullptr); - - /** Check whether the room id corresponds to a direct chat */ - bool isDirectChat(const QString& roomId) const; - - /** Get the whole map from users to direct chat rooms */ - DirectChatsMap directChats() const; - - /** Retrieve the list of users the room is a direct chat with - * @return The list of users for which this room is marked as - * a direct chat; an empty list if the room is not a direct chat - */ - QList<User*> directChatUsers(const Room* room) const; - - /** Check whether a particular user is in the ignore list */ - bool isIgnored(const User* user) const; - - /** Get the whole list of ignored users */ - IgnoredUsersList ignoredUsers() const; - - /** Add the user to the ignore list - * The change signal is emitted synchronously, without waiting - * to complete synchronisation with the server. - * - * \sa ignoredUsersListChanged - */ - void addToIgnoredUsers(const User* user); - - /** Remove the user from the ignore list */ - /** Similar to adding, the change signal is emitted synchronously. - * - * \sa ignoredUsersListChanged - */ - void removeFromIgnoredUsers(const User* user); - - /** Get the full list of users known to this account */ - QMap<QString, User*> users() const; - - /** Get the base URL of the homeserver to connect to */ - QUrl homeserver() const; - /** Get the domain name used for ids/aliases on the server */ - QString domain() const; - /** Get the list of supported login flows */ - QVector<GetLoginFlowsJob::LoginFlow> loginFlows() const; - /** Check whether the current homeserver supports password auth */ - bool supportsPasswordAuth() const; - /** Check whether the current homeserver supports SSO */ - bool supportsSso() const; - /** Find a room by its id and a mask of applicable states */ - Q_INVOKABLE Room* room(const QString& roomId, - JoinStates states = JoinState::Invite|JoinState::Join) const; - /** Find a room by its alias and a mask of applicable states */ - Q_INVOKABLE Room* roomByAlias(const QString& roomAlias, - JoinStates states = JoinState::Invite|JoinState::Join) const; - /** Update the internal map of room aliases to IDs */ - /// This is used for internal bookkeeping of rooms. Do NOT use - /// it to try change aliases, use Room::setAliases instead - void updateRoomAliases(const QString& roomId, - const QStringList& previousRoomAliases, - const QStringList& roomAliases); - Q_INVOKABLE Room* invitation(const QString& roomId) const; - Q_INVOKABLE User* user(const QString& userId); - const User* user() const; - User* user(); - QString userId() const; - QString deviceId() const; - QByteArray accessToken() const; - Q_INVOKABLE SyncJob* syncJob() const; - Q_INVOKABLE int millisToReconnect() const; - - [[deprecated("Use accessToken() instead")]] - Q_INVOKABLE QString token() const; - Q_INVOKABLE void getTurnServers(); - - struct SupportedRoomVersion - { - QString id; - QString status; - - static const QString StableTag; // "stable", as of CS API 0.5 - bool isStable() const { return status == StableTag; } - - friend QDebug operator<<(QDebug dbg, - const SupportedRoomVersion& v) - { - QDebugStateSaver _(dbg); - return dbg.nospace() << v.id << '/' << v.status; - } - }; - - /// Get the room version recommended by the server - /** Only works after server capabilities have been loaded. - * \sa loadingCapabilities */ - QString defaultRoomVersion() const; - /// Get the room version considered stable by the server - /** Only works after server capabilities have been loaded. - * \sa loadingCapabilities */ - QStringList stableRoomVersions() const; - /// Get all room versions supported by the server - /** Only works after server capabilities have been loaded. - * \sa loadingCapabilities */ - QVector<SupportedRoomVersion> availableRoomVersions() const; - - /** - * Call this before first sync to load from previously saved file. - * - * \param fromFile A local path to read the state from. Uses QUrl - * to be QML-friendly. Empty parameter means using a path - * defined by stateCachePath(). - */ - Q_INVOKABLE void loadState(); - /** - * This method saves the current state of rooms (but not messages - * in them) to a local cache file, so that it could be loaded by - * loadState() on a next run of the client. - * - * \param toFile A local path to save the state to. Uses QUrl to be - * QML-friendly. Empty parameter means using a path defined by - * stateCachePath(). - */ - Q_INVOKABLE void saveState() const; - - /// This method saves the current state of a single room. - void saveRoomState(Room* r) const; - - /** - * The default path to store the cached room state, defined as - * follows: - * QStandardPaths::writeableLocation(QStandardPaths::CacheLocation) + _safeUserId + "_state.json" - * where `_safeUserId` is userId() with `:` (colon) replaced with - * `_` (underscore) - * /see loadState(), saveState() - */ - Q_INVOKABLE QString stateCachePath() const; - - bool cacheState() const; - void setCacheState(bool newValue); - - bool lazyLoading() const; - void setLazyLoading(bool newValue); - - /** Start a job of a specified type with specified arguments and policy - * - * This is a universal method to start a job of a type passed - * as a template parameter. The policy allows to fine-tune the way - * the job is executed - as of this writing it means a choice - * between "foreground" and "background". - * - * \param runningPolicy controls how the job is executed - * \param jobArgs arguments to the job constructor - * - * \sa BaseJob::isBackground. QNetworkRequest::BackgroundRequestAttribute - */ - template <typename JobT, typename... JobArgTs> - JobT* callApi(RunningPolicy runningPolicy, - JobArgTs&&... jobArgs) const - { - auto job = new JobT(std::forward<JobArgTs>(jobArgs)...); - connect(job, &BaseJob::failure, this, &Connection::requestFailed); - job->start(connectionData(), runningPolicy&BackgroundRequest); - return job; - } - - /** Start a job of a specified type with specified arguments - * - * This is an overload that calls the job with "foreground" policy. - */ - template <typename JobT, typename... JobArgTs> - JobT* callApi(JobArgTs&&... jobArgs) const - { - return callApi<JobT>(ForegroundRequest, - std::forward<JobArgTs>(jobArgs)...); - } - - /** Get a request URL for a job with specified type and arguments - * - * This calls JobT::makeRequestUrl() prepending the connection's - * homeserver to the list of arguments. - */ - template <typename JobT, typename... JobArgTs> - QUrl getUrlForApi(JobArgTs&&... jobArgs) const - { - return JobT::makeRequestUrl(homeserver(), - std::forward<JobArgTs>(jobArgs)...); - } - - Q_INVOKABLE SsoSession* prepareForSso( - const QString& initialDeviceName, const QString& deviceId = {}); - - /** Generate a new transaction id. Transaction id's are unique within - * a single Connection object - */ - Q_INVOKABLE QByteArray generateTxnId() const; - - /// Set a room factory function - static void setRoomFactory(room_factory_t f); - - /// Set a user factory function - static void setUserFactory(user_factory_t f); - - /// Get a room factory function - static room_factory_t roomFactory(); - - /// Get a user factory function - static user_factory_t userFactory(); - - /// Set the room factory to default with the overriden room type - template <typename T> - static void setRoomType() { setRoomFactory(defaultRoomFactory<T>()); } - - /// Set the user factory to default with the overriden user type - template <typename T> - static void setUserType() { setUserFactory(defaultUserFactory<T>()); } - - public slots: - /** Set the homeserver base URL */ - void setHomeserver(const QUrl& baseUrl); - - /** Determine and set the homeserver from domain or MXID */ - void resolveServer(const QString& mxidOrDomain); - - void connectToServer(const QString& user, const QString& password, - const QString& initialDeviceName, - const QString& deviceId = {}); - void loginWithToken(const QByteArray& loginToken, - const QString& initialDeviceName, - const QString& deviceId = {}); - void assumeIdentity(const QString& userId, const QString& accessToken, - const QString& deviceId); - /*! @deprecated - * Use assumeIdentity() if you have an access token or - * loginWithToken() if you have a login token. - */ - void connectWithToken(const QString& userId, const QString& accessToken, - const QString& deviceId); - /// Explicitly request capabilities from the server - void reloadCapabilities(); - - /// Find out if capabilites are still loading from the server - bool loadingCapabilities() const; - - /** @deprecated Use stopSync() instead */ - void disconnectFromServer() { stopSync(); } - void logout(); - - void sync(int timeout = -1); - void syncLoop(int timeout = -1); - - void stopSync(); - QString nextBatchToken() const; - - virtual MediaThumbnailJob* getThumbnail(const QString& mediaId, - QSize requestedSize, RunningPolicy policy = BackgroundRequest) const; - MediaThumbnailJob* getThumbnail(const QUrl& url, - QSize requestedSize, RunningPolicy policy = BackgroundRequest) const; - MediaThumbnailJob* getThumbnail(const QUrl& url, - int requestedWidth, int requestedHeight, - RunningPolicy policy = BackgroundRequest) const; - - // QIODevice* should already be open - UploadContentJob* uploadContent(QIODevice* contentSource, - const QString& filename = {}, - const QString& overrideContentType = {}) const; - UploadContentJob* uploadFile(const QString& fileName, - const QString& overrideContentType = {}); - GetContentJob* getContent(const QString& mediaId) const; - GetContentJob* getContent(const QUrl& url) const; - // If localFilename is empty, a temporary file will be created - DownloadFileJob* downloadFile(const QUrl& url, - const QString& localFilename = {}) const; - - /** - * \brief Create a room (generic method) - * This method allows to customize room entirely to your liking, - * providing all the attributes the original CS API provides. - */ - CreateRoomJob* createRoom(RoomVisibility visibility, - const QString& alias, const QString& name, const QString& topic, - QStringList invites, const QString& presetName = {}, - const QString& roomVersion = {}, bool isDirect = false, - const QVector<CreateRoomJob::StateEvent>& initialState = {}, - const QVector<CreateRoomJob::Invite3pid>& invite3pids = {}, - const QJsonObject& creationContent = {}); - - /** Get a direct chat with a single user - * This method may return synchronously or asynchoronously depending - * on whether a direct chat room with the respective person exists - * already. - * - * \sa directChatAvailable - */ - void requestDirectChat(const QString& userId); - - /** Get a direct chat with a single user - * This method may return synchronously or asynchoronously depending - * on whether a direct chat room with the respective person exists - * already. - * - * \sa directChatAvailable - */ - void requestDirectChat(User* u); - - /** Run an operation in a direct chat with the user - * This method may return synchronously or asynchoronously depending - * on whether a direct chat room with the respective person exists - * already. Instead of emitting a signal it executes the passed - * function object with the direct chat room as its parameter. - */ - void doInDirectChat(const QString& userId, - const std::function<void(Room*)>& operation); - - /** Run an operation in a direct chat with the user - * This method may return synchronously or asynchoronously depending - * on whether a direct chat room with the respective person exists - * already. Instead of emitting a signal it executes the passed - * function object with the direct chat room as its parameter. - */ - void doInDirectChat(User* u, - const std::function<void(Room*)>& operation); - - /** Create a direct chat with a single user, optional name and topic - * A room will always be created, unlike in requestDirectChat. - * It is advised to use requestDirectChat as a default way of getting - * one-on-one with a person, and only use createDirectChat when - * a new creation is explicitly desired. - */ - CreateRoomJob* createDirectChat(const QString& userId, - const QString& topic = {}, const QString& name = {}); - - virtual JoinRoomJob* joinRoom(const QString& roomAlias, - const QStringList& serverNames = {}); - - /** Sends /forget to the server and also deletes room locally. - * This method is in Connection, not in Room, since it's a - * room lifecycle operation, and Connection is an acting room manager. - * It ensures that the local user is not a member of a room (running /leave, - * if necessary) then issues a /forget request and if that one doesn't fail - * deletion of the local Room object is ensured. - * \param id - the room id to forget - * \return - the ongoing /forget request to the server; note that the - * success() signal of this request is connected to deleteLater() - * of a respective room so by the moment this finishes, there might be no - * Room object anymore. - */ - ForgetRoomJob* forgetRoom(const QString& id); - - SendToDeviceJob* sendToDevices(const QString& eventType, - const UsersToDevicesToEvents& eventsMap) const; - - /** \deprecated This method is experimental and may be removed any time */ - SendMessageJob* sendMessage(const QString& roomId, - const RoomEvent& event) const; - - /** \deprecated Do not use this directly, use Room::leaveRoom() instead */ - virtual LeaveRoomJob* leaveRoom( Room* room ); - - // Old API that will be abolished any time soon. DO NOT USE. - - /** @deprecated Use callApi<PostReceiptJob>() or Room::postReceipt() instead */ - virtual PostReceiptJob* postReceipt(Room* room, - RoomEvent* event) const; - signals: - /** - * @deprecated - * This was a signal resulting from a successful resolveServer(). - * Since Connection now provides setHomeserver(), the HS URL - * may change even without resolveServer() invocation. Use - * homeserverChanged() instead of resolved(). You can also use - * connectToServer and connectWithToken without the HS URL set in - * advance (i.e. without calling resolveServer), as they now trigger - * server name resolution from MXID if the server URL is not valid. - */ - void resolved(); - void resolveError(QString error); - - void homeserverChanged(QUrl baseUrl); - void loginFlowsChanged(); - void capabilitiesLoaded(); - - void connected(); - void reconnected(); //< \deprecated Use connected() instead - void loggedOut(); - /** Login data or state have changed - * - * This is a common change signal for userId, deviceId and - * accessToken - these properties normally only change at - * a successful login and logout and are constant at other times. - */ - void stateChanged(); - void loginError(QString message, QString details); - - /** A network request (job) failed - * - * @param request - the pointer to the failed job - */ - void requestFailed(BaseJob* request); - - /** A network request (job) failed due to network problems - * - * This is _only_ emitted when the job will retry on its own; - * once it gives up, requestFailed() will be emitted. - * - * @param message - message about the network problem - * @param details - raw error details, if any available - * @param retriesTaken - how many retries have already been taken - * @param nextRetryInMilliseconds - when the job will retry again - */ - void networkError(QString message, QString details, - int retriesTaken, int nextRetryInMilliseconds); - - void syncDone(); - void syncError(QString message, QString details); - - void newUser(User* user); - - /** - * \group Signals emitted on room transitions - * - * Note: Rooms in Invite state are always stored separately from - * rooms in Join/Leave state, because of special treatment of - * invite_state in Matrix CS API (see The Spec on /sync for details). - * Therefore, objects below are: r - room in Join/Leave state; - * i - room in Invite state - * - * 1. none -> Invite: newRoom(r), invitedRoom(r,nullptr) - * 2. none -> Join: newRoom(r), joinedRoom(r,nullptr) - * 3. none -> Leave: newRoom(r), leftRoom(r,nullptr) - * 4. Invite -> Join: - * newRoom(r), joinedRoom(r,i), aboutToDeleteRoom(i) - * 4a. Leave and Invite -> Join: - * joinedRoom(r,i), aboutToDeleteRoom(i) - * 5. Invite -> Leave: - * newRoom(r), leftRoom(r,i), aboutToDeleteRoom(i) - * 5a. Leave and Invite -> Leave: - * leftRoom(r,i), aboutToDeleteRoom(i) - * 6. Join -> Leave: leftRoom(r) - * 7. Leave -> Invite: newRoom(i), invitedRoom(i,r) - * 8. Leave -> Join: joinedRoom(r) - * The following transitions are only possible via forgetRoom() - * so far; if a room gets forgotten externally, sync won't tell - * about it: - * 9. any -> none: as any -> Leave, then aboutToDeleteRoom(r) - */ - - /** A new room object has been created */ - void newRoom(Room* room); - - /** A room invitation is seen for the first time - * - * If the same room is in Left state, it's passed in prev. Beware - * that initial sync will trigger this signal for all rooms in - * Invite state. - */ - void invitedRoom(Room* room, Room* prev); - - /** A joined room is seen for the first time - * - * It's not the same as receiving a room in "join" section of sync - * response (rooms will be there even after joining); it's also - * not (exactly) the same as actual joining action of a user (all - * rooms coming in initial sync will trigger this signal too). If - * this room was in Invite state before, the respective object is - * passed in prev (and it will be deleted shortly afterwards). - */ - void joinedRoom(Room* room, Room* prev); - - /** A room has just been left - * - * If this room has been in Invite state (as in case of rejecting - * an invitation), the respective object will be passed in prev - * (and will be deleted shortly afterwards). Note that, similar - * to invitedRoom and joinedRoom, this signal is triggered for all - * Left rooms upon initial sync (not only those that were left - * right before the sync). - */ - void leftRoom(Room* room, Room* prev); - - /** The room object is about to be deleted */ - void aboutToDeleteRoom(Room* room); - - /** The room has just been created by createRoom or requestDirectChat - * - * This signal is not emitted in usual room state transitions, - * only as an outcome of room creation operations invoked by - * the client. - * \note requestDirectChat doesn't necessarily create a new chat; - * use directChatAvailable signal if you just need to obtain - * a direct chat room. - */ - void createdRoom(Room* room); - - /** The first sync for the room has been completed - * - * This signal is emitted after the room has been synced the first - * time. This is the right signal to connect to if you need to - * access the room state (name, aliases, members); state transition - * signals (newRoom, joinedRoom etc.) come earlier, when the room - * has just been created. - */ - void loadedRoomState(Room* room); - - /** Account data (except direct chats) have changed */ - void accountDataChanged(QString type); - - /** The direct chat room is ready for using - * This signal is emitted upon any successful outcome from - * requestDirectChat. - */ - void directChatAvailable(Room* directChat); - - /** The list of direct chats has changed - * This signal is emitted every time when the mapping of users - * to direct chat rooms is changed (because of either local updates - * or a different list arrived from the server). - */ - void directChatsListChanged(DirectChatsMap additions, - DirectChatsMap removals); - - void ignoredUsersListChanged(IgnoredUsersList additions, - IgnoredUsersList removals); - - void cacheStateChanged(); - void lazyLoadingChanged(); - void turnServersChanged(const QJsonObject& servers); - - protected: - /** - * @brief Access the underlying ConnectionData class - */ - const ConnectionData* connectionData() const; - - /** Get a Room object for the given id in the given state - * - * Use this method when you need a Room object in the local list - * of rooms, with the given state. Note that this does not interact - * with the server; in particular, does not automatically create - * rooms on the server. This call performs necessary join state - * transitions; e.g., if it finds a room in Invite but - * `joinState == JoinState::Join` then the Invite room object - * will be deleted and a new room object with Join state created. - * In contrast, switching between Join and Leave happens within - * the same object. - * \param roomId room id (not alias!) - * \param joinState desired (target) join state of the room; if - * omitted, any state will be found and return unchanged, or a - * new Join room created. - * @return a pointer to a Room object with the specified id and the - * specified state; nullptr if roomId is empty or if roomFactory() - * failed to create a Room object. - */ - Room* provideRoom(const QString& roomId, - Omittable<JoinState> joinState = none); - - /** - * Completes loading sync data. - */ - void onSyncSuccess(SyncData &&data, bool fromCache = false); - - protected slots: - void syncLoopIteration(); - - private: - class Private; - std::unique_ptr<Private> d; - - /** - * A single entry for functions that need to check whether the - * homeserver is valid before running. May either execute connectFn - * synchronously or asynchronously (if tryResolve is true and - * a DNS lookup is initiated); in case of errors, emits resolveError - * if the homeserver URL is not valid and cannot be resolved from - * userId. - * - * @param userId - fully-qualified MXID to resolve HS from - * @param connectFn - a function to execute once the HS URL is good - */ - void checkAndConnect(const QString& userId, - std::function<void()> connectFn); - void doConnectToServer(const QString& user, const QString& password, - const QString& initialDeviceName, - const QString& deviceId = {}); - - static room_factory_t _roomFactory; - static user_factory_t _userFactory; - }; -} // namespace QMatrixClient -Q_DECLARE_METATYPE(QMatrixClient::Connection*) + * \sa directChatAvailable + */ + void requestDirectChat(User* u); + + /** Run an operation in a direct chat with the user + * This method may return synchronously or asynchoronously depending + * on whether a direct chat room with the respective person exists + * already. Instead of emitting a signal it executes the passed + * function object with the direct chat room as its parameter. + */ + void doInDirectChat(const QString& userId, + const std::function<void(Room*)>& operation); + + /** Run an operation in a direct chat with the user + * This method may return synchronously or asynchoronously depending + * on whether a direct chat room with the respective person exists + * already. Instead of emitting a signal it executes the passed + * function object with the direct chat room as its parameter. + */ + void doInDirectChat(User* u, const std::function<void(Room*)>& operation); + + /** Create a direct chat with a single user, optional name and topic + * A room will always be created, unlike in requestDirectChat. + * It is advised to use requestDirectChat as a default way of getting + * one-on-one with a person, and only use createDirectChat when + * a new creation is explicitly desired. + */ + CreateRoomJob* createDirectChat(const QString& userId, + const QString& topic = {}, + const QString& name = {}); + + virtual JoinRoomJob* joinRoom(const QString& roomAlias, + const QStringList& serverNames = {}); + + /** Sends /forget to the server and also deletes room locally. + * This method is in Connection, not in Room, since it's a + * room lifecycle operation, and Connection is an acting room manager. + * It ensures that the local user is not a member of a room (running /leave, + * if necessary) then issues a /forget request and if that one doesn't fail + * deletion of the local Room object is ensured. + * \param id - the room id to forget + * \return - the ongoing /forget request to the server; note that the + * success() signal of this request is connected to deleteLater() + * of a respective room so by the moment this finishes, there might be no + * Room object anymore. + */ + ForgetRoomJob* forgetRoom(const QString& id); + + SendToDeviceJob* sendToDevices(const QString& eventType, + const UsersToDevicesToEvents& eventsMap); + + /** \deprecated This method is experimental and may be removed any time */ + SendMessageJob* sendMessage(const QString& roomId, const RoomEvent& event); + + /** \deprecated Do not use this directly, use Room::leaveRoom() instead */ + virtual LeaveRoomJob* leaveRoom(Room* room); + + // Old API that will be abolished any time soon. DO NOT USE. + + /** @deprecated Use callApi<PostReceiptJob>() or Room::postReceipt() instead + */ + virtual PostReceiptJob* postReceipt(Room* room, RoomEvent* event); + +signals: + /** + * @deprecated + * This was a signal resulting from a successful resolveServer(). + * Since Connection now provides setHomeserver(), the HS URL + * may change even without resolveServer() invocation. Use + * loginFLowsChanged() instead of resolved(). You can also use + * loginWith*() and assumeIdentity() without the HS URL set in + * advance (i.e. without calling resolveServer), as they trigger + * server name resolution from MXID if the server URL is not valid. + */ + void resolved(); + void resolveError(QString error); + + void homeserverChanged(QUrl baseUrl); + void loginFlowsChanged(); + void capabilitiesLoaded(); + + void connected(); + void reconnected(); //< \deprecated Use connected() instead + void loggedOut(); + /** Login data or state have changed + * + * This is a common change signal for userId, deviceId and + * accessToken - these properties normally only change at + * a successful login and logout and are constant at other times. + */ + void stateChanged(); + void loginError(QString message, QString details); + + /** A network request (job) failed + * + * @param request - the pointer to the failed job + */ + void requestFailed(Quotient::BaseJob* request); + + /** A network request (job) failed due to network problems + * + * This is _only_ emitted when the job will retry on its own; + * once it gives up, requestFailed() will be emitted. + * + * @param message - message about the network problem + * @param details - raw error details, if any available + * @param retriesTaken - how many retries have already been taken + * @param nextRetryInMilliseconds - when the job will retry again + */ + void networkError(QString message, QString details, int retriesTaken, + int nextRetryInMilliseconds); + + void syncDone(); + void syncError(QString message, QString details); + + void newUser(Quotient::User* user); + + /** + * \group Signals emitted on room transitions + * + * Note: Rooms in Invite state are always stored separately from + * rooms in Join/Leave state, because of special treatment of + * invite_state in Matrix CS API (see The Spec on /sync for details). + * Therefore, objects below are: r - room in Join/Leave state; + * i - room in Invite state + * + * 1. none -> Invite: newRoom(r), invitedRoom(r,nullptr) + * 2. none -> Join: newRoom(r), joinedRoom(r,nullptr) + * 3. none -> Leave: newRoom(r), leftRoom(r,nullptr) + * 4. Invite -> Join: + * newRoom(r), joinedRoom(r,i), aboutToDeleteRoom(i) + * 4a. Leave and Invite -> Join: + * joinedRoom(r,i), aboutToDeleteRoom(i) + * 5. Invite -> Leave: + * newRoom(r), leftRoom(r,i), aboutToDeleteRoom(i) + * 5a. Leave and Invite -> Leave: + * leftRoom(r,i), aboutToDeleteRoom(i) + * 6. Join -> Leave: leftRoom(r) + * 7. Leave -> Invite: newRoom(i), invitedRoom(i,r) + * 8. Leave -> Join: joinedRoom(r) + * The following transitions are only possible via forgetRoom() + * so far; if a room gets forgotten externally, sync won't tell + * about it: + * 9. any -> none: as any -> Leave, then aboutToDeleteRoom(r) + */ + + /** A new room object has been created */ + void newRoom(Quotient::Room* room); + + /** A room invitation is seen for the first time + * + * If the same room is in Left state, it's passed in prev. Beware + * that initial sync will trigger this signal for all rooms in + * Invite state. + */ + void invitedRoom(Quotient::Room* room, Quotient::Room* prev); + + /** A joined room is seen for the first time + * + * It's not the same as receiving a room in "join" section of sync + * response (rooms will be there even after joining); it's also + * not (exactly) the same as actual joining action of a user (all + * rooms coming in initial sync will trigger this signal too). If + * this room was in Invite state before, the respective object is + * passed in prev (and it will be deleted shortly afterwards). + */ + void joinedRoom(Quotient::Room* room, Quotient::Room* prev); + + /** A room has just been left + * + * If this room has been in Invite state (as in case of rejecting + * an invitation), the respective object will be passed in prev + * (and will be deleted shortly afterwards). Note that, similar + * to invitedRoom and joinedRoom, this signal is triggered for all + * Left rooms upon initial sync (not only those that were left + * right before the sync). + */ + void leftRoom(Quotient::Room* room, Quotient::Room* prev); + + /** The room object is about to be deleted */ + void aboutToDeleteRoom(Quotient::Room* room); + + /** The room has just been created by createRoom or requestDirectChat + * + * This signal is not emitted in usual room state transitions, + * only as an outcome of room creation operations invoked by + * the client. + * \note requestDirectChat doesn't necessarily create a new chat; + * use directChatAvailable signal if you just need to obtain + * a direct chat room. + */ + void createdRoom(Quotient::Room* room); + + /** The first sync for the room has been completed + * + * This signal is emitted after the room has been synced the first + * time. This is the right signal to connect to if you need to + * access the room state (name, aliases, members); state transition + * signals (newRoom, joinedRoom etc.) come earlier, when the room + * has just been created. + */ + void loadedRoomState(Quotient::Room* room); + + /** Account data (except direct chats) have changed */ + void accountDataChanged(QString type); + + /** The direct chat room is ready for using + * This signal is emitted upon any successful outcome from + * requestDirectChat. + */ + void directChatAvailable(Quotient::Room* directChat); + + /** The list of direct chats has changed + * This signal is emitted every time when the mapping of users + * to direct chat rooms is changed (because of either local updates + * or a different list arrived from the server). + */ + void directChatsListChanged(Quotient::DirectChatsMap additions, + Quotient::DirectChatsMap removals); + + void ignoredUsersListChanged(Quotient::IgnoredUsersList additions, + Quotient::IgnoredUsersList removals); + + void cacheStateChanged(); + void lazyLoadingChanged(); + void turnServersChanged(const QJsonObject& servers); + +protected: + /** + * @brief Access the underlying ConnectionData class + */ + const ConnectionData* connectionData() const; + + /** Get a Room object for the given id in the given state + * + * Use this method when you need a Room object in the local list + * of rooms, with the given state. Note that this does not interact + * with the server; in particular, does not automatically create + * rooms on the server. This call performs necessary join state + * transitions; e.g., if it finds a room in Invite but + * `joinState == JoinState::Join` then the Invite room object + * will be deleted and a new room object with Join state created. + * In contrast, switching between Join and Leave happens within + * the same object. + * \param roomId room id (not alias!) + * \param joinState desired (target) join state of the room; if + * omitted, any state will be found and return unchanged, or a + * new Join room created. + * @return a pointer to a Room object with the specified id and the + * specified state; nullptr if roomId is empty or if roomFactory() + * failed to create a Room object. + */ + Room* provideRoom(const QString& roomId, + Omittable<JoinState> joinState = none); + + /** + * Completes loading sync data. + */ + void onSyncSuccess(SyncData&& data, bool fromCache = false); + +protected slots: + void syncLoopIteration(); + +private: + class Private; + QScopedPointer<Private> d; + + static room_factory_t _roomFactory; + static user_factory_t _userFactory; +}; +} // namespace Quotient +Q_DECLARE_METATYPE(Quotient::DirectChatsMap) +Q_DECLARE_METATYPE(Quotient::IgnoredUsersList) diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp index 91cda09f..d57363d0 100644 --- a/lib/connectiondata.cpp +++ b/lib/connectiondata.cpp @@ -13,45 +13,109 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "connectiondata.h" -#include "networkaccessmanager.h" #include "logging.h" +#include "networkaccessmanager.h" +#include "jobs/basejob.h" -using namespace QMatrixClient; +#include <QtCore/QTimer> +#include <QtCore/QPointer> -struct ConnectionData::Private -{ - explicit Private(QUrl url) : baseUrl(std::move(url)) { } +#include <array> +#include <queue> + +using namespace Quotient; + +class ConnectionData::Private { +public: + explicit Private(QUrl url) : baseUrl(std::move(url)) + { + rateLimiter.setSingleShot(true); + } QUrl baseUrl; QByteArray accessToken; QString lastEvent; + QString userId; QString deviceId; + std::vector<QString> needToken; mutable unsigned int txnCounter = 0; - const qint64 id = QDateTime::currentMSecsSinceEpoch(); + const qint64 txnBase = QDateTime::currentMSecsSinceEpoch(); + + QString id() const { return userId + '/' + deviceId; } + + using job_queue_t = std::queue<QPointer<BaseJob>>; + std::array<job_queue_t, 2> jobs; // 0 - foreground, 1 - background + QTimer rateLimiter; }; ConnectionData::ConnectionData(QUrl baseUrl) : d(std::make_unique<Private>(std::move(baseUrl))) -{ } +{ + // Each lambda invocation below takes no more than one job from the + // queues (first foreground, then background) and resumes it; then + // restarts the rate limiter timer with duration 0, effectively yielding + // to the event loop and then resuming until both queues are empty. + QObject::connect(&d->rateLimiter, &QTimer::timeout, [this] { + // TODO: Consider moving out all job->sendRequest() invocations to + // a dedicated thread + d->rateLimiter.setInterval(0); + for (auto& q : d->jobs) + while (!q.empty()) { + const auto job = q.front(); + q.pop(); + if (!job || job->error() == BaseJob::Abandoned) + continue; + if (job->error() != BaseJob::Pending) { + qCCritical(MAIN) + << "Job" << job + << "is in the wrong status:" << job->status(); + Q_ASSERT(false); + job->setStatus(BaseJob::Pending); + } + job->sendRequest(); + d->rateLimiter.start(); + return; + } + qCDebug(MAIN) << d->id() << "job queues are empty"; + }); +} -ConnectionData::~ConnectionData() = default; +ConnectionData::~ConnectionData() +{ + d->rateLimiter.disconnect(); + d->rateLimiter.stop(); +} -QByteArray ConnectionData::accessToken() const +void ConnectionData::submit(BaseJob* job) { - return d->accessToken; + job->setStatus(BaseJob::Pending); + if (!d->rateLimiter.isActive()) { + QTimer::singleShot(0, job, &BaseJob::sendRequest); + return; + } + d->jobs[size_t(job->isBackground())].emplace(job); + qCDebug(MAIN) << job << "queued," << d->jobs.front().size() << "+" + << d->jobs.back().size() << "total jobs in" << d->id() + << "queues"; } -QUrl ConnectionData::baseUrl() const +void ConnectionData::limitRate(std::chrono::milliseconds nextCallAfter) { - return d->baseUrl; + qCDebug(MAIN) << "Jobs for" << (d->userId + "/" + d->deviceId) + << "suspended for" << nextCallAfter.count() << "ms"; + d->rateLimiter.start(nextCallAfter); } +QByteArray ConnectionData::accessToken() const { return d->accessToken; } + +QUrl ConnectionData::baseUrl() const { return d->baseUrl; } + QNetworkAccessManager* ConnectionData::nam() const { return NetworkAccessManager::instance(); @@ -80,22 +144,30 @@ void ConnectionData::setPort(int port) qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; } -const QString& ConnectionData::deviceId() const +const QString& ConnectionData::deviceId() const { return d->deviceId; } + +const QString& ConnectionData::userId() const { return d->userId; } + +bool ConnectionData::needsToken(const QString& requestName) const { - return d->deviceId; + return std::find(d->needToken.cbegin(), d->needToken.cend(), requestName) + != d->needToken.cend(); } void ConnectionData::setDeviceId(const QString& deviceId) { d->deviceId = deviceId; - qCDebug(MAIN) << "updated deviceId to" << d->deviceId; } -QString ConnectionData::lastEvent() const +void ConnectionData::setUserId(const QString& userId) { d->userId = userId; } + +void ConnectionData::setNeedsToken(const QString& requestName) { - return d->lastEvent; + d->needToken.push_back(requestName); } +QString ConnectionData::lastEvent() const { return d->lastEvent; } + void ConnectionData::setLastEvent(QString identifier) { d->lastEvent = std::move(identifier); @@ -103,6 +175,6 @@ void ConnectionData::setLastEvent(QString identifier) QByteArray ConnectionData::generateTxnId() const { - return QByteArray::number(d->id) + 'q' + - QByteArray::number(++d->txnCounter); + return d->deviceId.toLatin1() + QByteArray::number(d->txnBase) + + QByteArray::number(++d->txnCounter); } diff --git a/lib/connectiondata.h b/lib/connectiondata.h index 7a2f2e90..000099d1 100644 --- a/lib/connectiondata.h +++ b/lib/connectiondata.h @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once @@ -21,35 +21,45 @@ #include <QtCore/QUrl> #include <memory> +#include <chrono> class QNetworkAccessManager; -namespace QMatrixClient -{ - class ConnectionData - { - public: - explicit ConnectionData(QUrl baseUrl); - virtual ~ConnectionData(); - - QByteArray accessToken() const; - QUrl baseUrl() const; - const QString& deviceId() const; - - QNetworkAccessManager* nam() const; - void setBaseUrl(QUrl baseUrl); - void setToken(QByteArray accessToken); - void setHost( QString host ); - void setPort( int port ); - void setDeviceId(const QString& deviceId); - - QString lastEvent() const; - void setLastEvent( QString identifier ); - - QByteArray generateTxnId() const; - - private: - struct Private; - std::unique_ptr<Private> d; - }; -} // namespace QMatrixClient +namespace Quotient { +class BaseJob; + +class ConnectionData { +public: + explicit ConnectionData(QUrl baseUrl); + virtual ~ConnectionData(); + + void submit(BaseJob* job); + void limitRate(std::chrono::milliseconds nextCallAfter); + + QByteArray accessToken() const; + QUrl baseUrl() const; + const QString& deviceId() const; + const QString& userId() const; + bool needsToken(const QString& requestName) const; + QNetworkAccessManager* nam() const; + + void setBaseUrl(QUrl baseUrl); + void setToken(QByteArray accessToken); + [[deprecated("Use setBaseUrl() instead")]] + void setHost(QString host); + [[deprecated("Use setBaseUrl() instead")]] + void setPort(int port); + void setDeviceId(const QString& deviceId); + void setUserId(const QString& userId); + void setNeedsToken(const QString& requestName); + + QString lastEvent() const; + void setLastEvent(QString identifier); + + QByteArray generateTxnId() const; + +private: + class Private; + std::unique_ptr<Private> d; +}; +} // namespace Quotient diff --git a/lib/converters.cpp b/lib/converters.cpp index 88f5267e..9f570087 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -1,26 +1,26 @@ /****************************************************************************** -* Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ + * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ #include "converters.h" #include <QtCore/QVariant> -using namespace QMatrixClient; +using namespace Quotient; QJsonValue JsonConverter<QVariant>::dump(const QVariant& v) { @@ -32,24 +32,12 @@ QVariant JsonConverter<QVariant>::load(const QJsonValue& jv) return jv.toVariant(); } -QJsonObject JsonConverter<variant_map_t>::dump(const variant_map_t& map) +QJsonObject JsonConverter<QVariantHash>::dump(const QVariantHash& vh) { - return -#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) - QJsonObject::fromVariantHash -#else - QJsonObject::fromVariantMap -#endif - (map); + return QJsonObject::fromVariantHash(vh); } -variant_map_t JsonConverter<QVariantHash>::load(const QJsonValue& jv) +QVariantHash JsonConverter<QVariantHash>::load(const QJsonValue& jv) { - return jv.toObject(). -#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) - toVariantHash -#else - toVariantMap -#endif - (); + return jv.toObject().toVariantHash(); } diff --git a/lib/converters.h b/lib/converters.h index 5c31b93d..543e9496 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -1,431 +1,392 @@ /****************************************************************************** -* Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ + * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ #pragma once #include "util.h" -#include <QtCore/QJsonObject> +#include <QtCore/QDate> #include <QtCore/QJsonArray> // Includes <QtCore/QJsonValue> #include <QtCore/QJsonDocument> -#include <QtCore/QDate> -#include <QtCore/QUrlQuery> +#include <QtCore/QJsonObject> #include <QtCore/QSet> +#include <QtCore/QUrlQuery> #include <QtCore/QVector> -#include <unordered_map> #include <vector> -#if 0 // Waiting for C++17 -#include <experimental/optional> +class QVariant; + +namespace Quotient { template <typename T> -using optional = std::experimental::optional<T>; -#endif +struct JsonObjectConverter { + static void dumpTo(QJsonObject& jo, const T& pod) { jo = pod.toJson(); } + static void fillFrom(const QJsonObject& jo, T& pod) { pod = T(jo); } +}; -#if QT_VERSION < QT_VERSION_CHECK(5,14,0) -// Enable std::unordered_map<QString, T> -namespace std -{ - template <> struct hash<QString> +template <typename T> +struct JsonConverter { + static QJsonObject dump(const T& pod) { - size_t operator()(const QString& s) const Q_DECL_NOEXCEPT - { - return qHash(s -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - , uint(qGlobalQHashSeed()) -#endif - ); - } - }; -} // namespace std -#endif + QJsonObject jo; + JsonObjectConverter<T>::dumpTo(jo, pod); + return jo; + } + static T doLoad(const QJsonObject& jo) + { + T pod; + JsonObjectConverter<T>::fillFrom(jo, pod); + return pod; + } + static T load(const QJsonValue& jv) { return doLoad(jv.toObject()); } + static T load(const QJsonDocument& jd) { return doLoad(jd.object()); } +}; -class QVariant; +template <typename T> +inline auto toJson(const T& pod) +{ + return JsonConverter<T>::dump(pod); +} + +inline auto toJson(const QJsonObject& jo) { return jo; } -namespace QMatrixClient +template <typename T> +inline void fillJson(QJsonObject& json, const T& data) { - template <typename T> - struct JsonObjectConverter - { - static void dumpTo(QJsonObject& jo, const T& pod) { jo = pod; } - static void fillFrom(const QJsonObject& jo, T& pod) { pod = jo; } - }; + JsonObjectConverter<T>::dumpTo(json, data); +} - template <typename T> - struct JsonConverter - { - static QJsonObject dump(const T& pod) - { - QJsonObject jo; - JsonObjectConverter<T>::dumpTo(jo, pod); - return jo; - } - static T doLoad(const QJsonObject& jo) - { - T pod; - JsonObjectConverter<T>::fillFrom(jo, pod); - return pod; - } - static T load(const QJsonValue& jv) { return doLoad(jv.toObject()); } - static T load(const QJsonDocument& jd) { return doLoad(jd.object()); } - }; +template <typename T> +inline T fromJson(const QJsonValue& jv) +{ + return JsonConverter<T>::load(jv); +} - template <typename T> - inline auto toJson(const T& pod) - { - return JsonConverter<T>::dump(pod); - } +template <typename T> +inline T fromJson(const QJsonDocument& jd) +{ + return JsonConverter<T>::load(jd); +} - template <typename T> - inline auto fillJson(QJsonObject& json, const T& data) - { - JsonObjectConverter<T>::dumpTo(json, data); - } +// Convenience fromJson() overloads that deduce T instead of requiring +// the coder to explicitly type it. They still enforce the +// overwrite-everything semantics of fromJson(), unlike fillFromJson() + +template <typename T> +inline void fromJson(const QJsonValue& jv, T& pod) +{ + pod = jv.isUndefined() ? T() : fromJson<T>(jv); +} + +template <typename T> +inline void fromJson(const QJsonDocument& jd, T& pod) +{ + pod = fromJson<T>(jd); +} - template <typename T> - inline auto fromJson(const QJsonValue& jv) +template <typename T> +inline void fillFromJson(const QJsonValue& jv, T& pod) +{ + if (jv.isObject()) + JsonObjectConverter<T>::fillFrom(jv.toObject(), pod); + else if (!jv.isUndefined()) + pod = fromJson<T>(jv); +} + +// JsonConverter<> specialisations + +template <typename T> +struct TrivialJsonDumper { + // Works for: QJsonValue (and all things it can consume), + // QJsonObject, QJsonArray + static auto dump(const T& val) { return val; } +}; + +template <> +struct JsonConverter<bool> : public TrivialJsonDumper<bool> { + static auto load(const QJsonValue& jv) { return jv.toBool(); } +}; + +template <> +struct JsonConverter<int> : public TrivialJsonDumper<int> { + static auto load(const QJsonValue& jv) { return jv.toInt(); } +}; + +template <> +struct JsonConverter<double> : public TrivialJsonDumper<double> { + static auto load(const QJsonValue& jv) { return jv.toDouble(); } +}; + +template <> +struct JsonConverter<float> : public TrivialJsonDumper<float> { + static auto load(const QJsonValue& jv) { return float(jv.toDouble()); } +}; + +template <> +struct JsonConverter<qint64> : public TrivialJsonDumper<qint64> { + static auto load(const QJsonValue& jv) { return qint64(jv.toDouble()); } +}; + +template <> +struct JsonConverter<QString> : public TrivialJsonDumper<QString> { + static auto load(const QJsonValue& jv) { return jv.toString(); } +}; + +template <> +struct JsonConverter<QDateTime> { + static auto dump(const QDateTime& val) = delete; // not provided yet + static auto load(const QJsonValue& jv) { - return JsonConverter<T>::load(jv); + return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC); } +}; - template <typename T> - inline T fromJson(const QJsonDocument& jd) +template <> +struct JsonConverter<QDate> { + static auto dump(const QDate& val) = delete; // not provided yet + static auto load(const QJsonValue& jv) { - return JsonConverter<T>::load(jd); + return fromJson<QDateTime>(jv).date(); } +}; + +template <> +struct JsonConverter<QJsonArray> : public TrivialJsonDumper<QJsonArray> { + static auto load(const QJsonValue& jv) { return jv.toArray(); } +}; - template <typename T> - inline void fromJson(const QJsonValue& jv, T& pod) +template <> +struct JsonConverter<QByteArray> { + static QString dump(const QByteArray& ba) { return ba.constData(); } + static auto load(const QJsonValue& jv) { - if (!jv.isUndefined()) - pod = fromJson<T>(jv); + return fromJson<QString>(jv).toLatin1(); } +}; - template <typename T> - inline void fromJson(const QJsonDocument& jd, T& pod) +template <> +struct JsonConverter<QVariant> { + static QJsonValue dump(const QVariant& v); + static QVariant load(const QJsonValue& jv); +}; + +template <typename T> +struct JsonConverter<Omittable<T>> { + static QJsonValue dump(const Omittable<T>& from) { - pod = fromJson<T>(jd); + return from.has_value() ? toJson(from.value()) : QJsonValue(); } - - // Unfolds Omittable<> - template <typename T> - inline void fromJson(const QJsonValue& jv, Omittable<T>& pod) + static Omittable<T> load(const QJsonValue& jv) { if (jv.isUndefined()) - pod = none; - else - pod = fromJson<T>(jv); + return none; + return fromJson<T>(jv); } +}; - template <typename T> - inline void fillFromJson(const QJsonValue& jv, T& pod) +template <typename VectorT, typename T = typename VectorT::value_type> +struct JsonArrayConverter { + static void dumpTo(QJsonArray& ar, const VectorT& vals) { - if (jv.isObject()) - JsonObjectConverter<T>::fillFrom(jv.toObject(), pod); + for (const auto& v : vals) + ar.push_back(toJson(v)); } - - // JsonConverter<> specialisations - - template <typename T> - struct TrivialJsonDumper - { - // Works for: QJsonValue (and all things it can consume), - // QJsonObject, QJsonArray - static auto dump(const T& val) { return val; } - }; - - template <> struct JsonConverter<bool> : public TrivialJsonDumper<bool> - { - static auto load(const QJsonValue& jv) { return jv.toBool(); } - }; - - template <> struct JsonConverter<int> : public TrivialJsonDumper<int> + static auto dump(const VectorT& vals) { - static auto load(const QJsonValue& jv) { return jv.toInt(); } - }; - - template <> struct JsonConverter<double> - : public TrivialJsonDumper<double> + QJsonArray ja; + dumpTo(ja, vals); + return ja; + } + static auto load(const QJsonArray& ja) { - static auto load(const QJsonValue& jv) { return jv.toDouble(); } - }; + VectorT vect; + vect.reserve(typename VectorT::size_type(ja.size())); + for (const auto& i : ja) + vect.push_back(fromJson<T>(i)); + return vect; + } + static auto load(const QJsonValue& jv) { return load(jv.toArray()); } + static auto load(const QJsonDocument& jd) { return load(jd.array()); } +}; - template <> struct JsonConverter<float> : public TrivialJsonDumper<float> - { - static auto load(const QJsonValue& jv) { return float(jv.toDouble()); } - }; +template <typename T> +struct JsonConverter<std::vector<T>> + : public JsonArrayConverter<std::vector<T>> {}; - template <> struct JsonConverter<qint64> - : public TrivialJsonDumper<qint64> - { - static auto load(const QJsonValue& jv) { return qint64(jv.toDouble()); } - }; +template <typename T> +struct JsonConverter<QVector<T>> : public JsonArrayConverter<QVector<T>> {}; - template <> struct JsonConverter<QString> - : public TrivialJsonDumper<QString> - { - static auto load(const QJsonValue& jv) { return jv.toString(); } - }; +template <typename T> +struct JsonConverter<QList<T>> : public JsonArrayConverter<QList<T>> {}; - template <> struct JsonConverter<QDateTime> +template <> +struct JsonConverter<QStringList> : public JsonConverter<QList<QString>> { + static auto dump(const QStringList& sl) { - static auto dump(const QDateTime& val) = delete; // not provided yet - static auto load(const QJsonValue& jv) - { - return QDateTime::fromMSecsSinceEpoch( - fromJson<qint64>(jv), Qt::UTC); - } - }; + return QJsonArray::fromStringList(sl); + } +}; - template <> struct JsonConverter<QDate> +template <> +struct JsonObjectConverter<QSet<QString>> { + static void dumpTo(QJsonObject& json, const QSet<QString>& s) { - static auto dump(const QDate& val) = delete; // not provided yet - static auto load(const QJsonValue& jv) - { - return fromJson<QDateTime>(jv).date(); - } - }; - - template <> struct JsonConverter<QJsonArray> - : public TrivialJsonDumper<QJsonArray> + for (const auto& e : s) + json.insert(toJson(e), QJsonObject {}); + } + static auto fillFrom(const QJsonObject& json, QSet<QString>& s) { - static auto load(const QJsonValue& jv) { return jv.toArray(); } - }; + s.reserve(s.size() + json.size()); + for (auto it = json.begin(); it != json.end(); ++it) + s.insert(it.key()); + return s; + } +}; - template <> struct JsonConverter<QByteArray> +template <typename HashMapT> +struct HashMapFromJson { + static void dumpTo(QJsonObject& json, const HashMapT& hashMap) { - static QString dump(const QByteArray& ba) { return ba.constData(); } - static auto load(const QJsonValue& jv) - { - return fromJson<QString>(jv).toLatin1(); - } - }; - - template <> struct JsonConverter<QVariant> + for (auto it = hashMap.begin(); it != hashMap.end(); ++it) + json.insert(it.key(), toJson(it.value())); + } + static void fillFrom(const QJsonObject& jo, HashMapT& h) { - static QJsonValue dump(const QVariant& v); - static QVariant load(const QJsonValue& jv); - }; + h.reserve(jo.size()); + for (auto it = jo.begin(); it != jo.end(); ++it) + h[it.key()] = fromJson<typename HashMapT::mapped_type>(it.value()); + } +}; - template <typename VectorT, - typename T = typename VectorT::value_type> - struct JsonArrayConverter - { - static void dumpTo(QJsonArray& ar, const VectorT& vals) - { - for (const auto& v: vals) - ar.push_back(toJson(v)); - } - static auto dump(const VectorT& vals) - { - QJsonArray ja; - dumpTo(ja, vals); - return ja; - } - static auto load(const QJsonArray& ja) - { - VectorT vect; vect.reserve(typename VectorT::size_type(ja.size())); - for (const auto& i: ja) - vect.push_back(fromJson<T>(i)); - return vect; - } - static auto load(const QJsonValue& jv) { return load(jv.toArray()); } - static auto load(const QJsonDocument& jd) { return load(jd.array()); } - }; +template <typename T, typename HashT> +struct JsonObjectConverter<std::unordered_map<QString, T, HashT>> + : public HashMapFromJson<std::unordered_map<QString, T, HashT>> {}; - template <typename T> struct JsonConverter<std::vector<T>> - : public JsonArrayConverter<std::vector<T>> - { }; +template <typename T> +struct JsonObjectConverter<QHash<QString, T>> + : public HashMapFromJson<QHash<QString, T>> {}; - template <typename T> struct JsonConverter<QVector<T>> - : public JsonArrayConverter<QVector<T>> - { }; +template <> +struct JsonConverter<QVariantHash> { + static QJsonObject dump(const QVariantHash& vh); + static QVariantHash load(const QJsonValue& jv); +}; - template <typename T> struct JsonConverter<QList<T>> - : public JsonArrayConverter<QList<T>> - { }; +// Conditional insertion into a QJsonObject - template <> struct JsonConverter<QStringList> - : public JsonConverter<QList<QString>> +namespace _impl { + template <typename ValT> + inline void addTo(QJsonObject& o, const QString& k, ValT&& v) { - static auto dump(const QStringList& sl) - { - return QJsonArray::fromStringList(sl); - } - }; + o.insert(k, toJson(v)); + } - template <> struct JsonObjectConverter<QSet<QString>> + template <typename ValT> + inline void addTo(QUrlQuery& q, const QString& k, ValT&& v) { - static void dumpTo(QJsonObject& json, const QSet<QString>& s) - { - for (const auto& e: s) - json.insert(toJson(e), QJsonObject{}); - } - static auto fillFrom(const QJsonObject& json, QSet<QString>& s) - { - s.reserve(s.size() + json.size()); - for (auto it = json.begin(); it != json.end(); ++it) - s.insert(it.key()); - return s; - } - }; + q.addQueryItem(k, QStringLiteral("%1").arg(v)); + } - template <typename HashMapT> - struct HashMapFromJson + // OpenAPI is entirely JSON-based, which means representing bools as + // textual true/false, rather than 1/0. + inline void addTo(QUrlQuery& q, const QString& k, bool v) { - static void dumpTo(QJsonObject& json, const HashMapT& hashMap) - { - for (auto it = hashMap.begin(); it != hashMap.end(); ++it) - json.insert(it.key(), toJson(it.value())); - } - static void fillFrom(const QJsonObject& jo, HashMapT& h) - { - h.reserve(jo.size()); - for (auto it = jo.begin(); it != jo.end(); ++it) - h[it.key()] = - fromJson<typename HashMapT::mapped_type>(it.value()); - } - }; + q.addQueryItem(k, v ? QStringLiteral("true") : QStringLiteral("false")); + } - template <typename T> - struct JsonObjectConverter<std::unordered_map<QString, T>> - : public HashMapFromJson<std::unordered_map<QString, T>> - { }; - - template <typename T> - struct JsonObjectConverter<QHash<QString, T>> - : public HashMapFromJson<QHash<QString, T>> - { }; - - // We could use std::conditional<> below but QT_VERSION* macros in C++ code - // cause (kinda valid but useless and noisy) compiler warnings about - // bitwise operations on signed integers; so use the preprocessor for now. - using variant_map_t = -#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) - QVariantHash; -#else - QVariantMap; -#endif - template <> struct JsonConverter<variant_map_t> + inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals) { - static QJsonObject dump(const variant_map_t& vh); - static QVariantHash load(const QJsonValue& jv); - }; - - // Conditional insertion into a QJsonObject + for (const auto& v : vals) + q.addQueryItem(k, v); + } - namespace _impl + inline void addTo(QUrlQuery& q, const QString&, const QJsonObject& vals) { - template <typename ValT> - inline void addTo(QJsonObject& o, const QString& k, ValT&& v) - { o.insert(k, toJson(v)); } - - template <typename ValT> - inline void addTo(QUrlQuery& q, const QString& k, ValT&& v) - { q.addQueryItem(k, QStringLiteral("%1").arg(v)); } + for (auto it = vals.begin(); it != vals.end(); ++it) + q.addQueryItem(it.key(), it.value().toString()); + } - // OpenAPI is entirely JSON-based, which means representing bools as - // textual true/false, rather than 1/0. - inline void addTo(QUrlQuery& q, const QString& k, bool v) + // This one is for types that don't have isEmpty() and for all types + // when Force is true + template <typename ValT, bool Force = true, typename = bool> + struct AddNode { + template <typename ContT, typename ForwardedT> + static void impl(ContT& container, const QString& key, + ForwardedT&& value) { - q.addQueryItem(k, v ? QStringLiteral("true") - : QStringLiteral("false")); + addTo(container, key, std::forward<ForwardedT>(value)); } + }; - inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals) + // This one is for types that have isEmpty() when Force is false + template <typename ValT> + struct AddNode<ValT, false, decltype(std::declval<ValT>().isEmpty())> { + template <typename ContT, typename ForwardedT> + static void impl(ContT& container, const QString& key, + ForwardedT&& value) { - for (const auto& v: vals) - q.addQueryItem(k, v); + if (!value.isEmpty()) + addTo(container, key, std::forward<ForwardedT>(value)); } + }; - inline void addTo(QUrlQuery& q, const QString&, const QJsonObject& vals) + // This one unfolds Omittable<> (also only when Force is false) + template <typename ValT> + struct AddNode<Omittable<ValT>, false> { + template <typename ContT, typename OmittableT> + static void impl(ContT& container, const QString& key, + const OmittableT& value) { - for (auto it = vals.begin(); it != vals.end(); ++it) - q.addQueryItem(it.key(), it.value().toString()); + if (value) + addTo(container, key, *value); } - - // This one is for types that don't have isEmpty() - template <typename ValT, bool Force = true, typename = bool> - struct AddNode - { - template <typename ContT, typename ForwardedT> - static void impl(ContT& container, const QString& key, - ForwardedT&& value) - { - addTo(container, key, std::forward<ForwardedT>(value)); - } - }; - - // This one is for types that have isEmpty() - template <typename ValT> - struct AddNode<ValT, false, - decltype(std::declval<ValT>().isEmpty())> - { - template <typename ContT, typename ForwardedT> - static void impl(ContT& container, const QString& key, - ForwardedT&& value) - { - if (!value.isEmpty()) - AddNode<ValT>::impl(container, - key, std::forward<ForwardedT>(value)); - } - }; - - // This is a special one that unfolds Omittable<> - template <typename ValT, bool Force> - struct AddNode<Omittable<ValT>, Force> - { - template <typename ContT, typename OmittableT> - static void impl(ContT& container, - const QString& key, const OmittableT& value) - { - if (!value.omitted()) - AddNode<ValT>::impl(container, key, value.value()); - else if (Force) // Edge case, no value but must put something - AddNode<ValT>::impl(container, key, QString{}); - } - }; - -#if 0 - // This is a special one that unfolds optional<> - template <typename ValT, bool Force> - struct AddNode<optional<ValT>, Force> - { - template <typename ContT, typename OptionalT> - static void impl(ContT& container, - const QString& key, const OptionalT& value) - { - if (value) - AddNode<ValT>::impl(container, key, value.value()); - else if (Force) // Edge case, no value but must put something - AddNode<ValT>::impl(container, key, QString{}); - } - }; -#endif - - } // namespace _impl - - static constexpr bool IfNotEmpty = false; - - template <bool Force = true, typename ContT, typename ValT> - inline void addParam(ContT& container, const QString& key, ValT&& value) - { - _impl::AddNode<std::decay_t<ValT>, Force> - ::impl(container, key, std::forward<ValT>(value)); - } -} // namespace QMatrixClient + }; +} // namespace _impl + +static constexpr bool IfNotEmpty = false; + +/*! Add a key-value pair to QJsonObject or QUrlQuery + * + * Adds a key-value pair(s) specified by \p key and \p value to + * \p container, optionally (in case IfNotEmpty is passed for the first + * template parameter) taking into account the value "emptiness". + * With IfNotEmpty, \p value is NOT added to the container if and only if: + * - it has a method `isEmpty()` and `value.isEmpty() == true`, or + * - it's an `Omittable<>` and `value.omitted() == true`. + * + * If \p container is a QUrlQuery, an attempt to fit \p value into it is + * made as follows: + * - if \p value is a QJsonObject, \p key is ignored and pairs from \p value + * are copied to \p container, assuming that the value in each pair + * is a string; + * - if \p value is a QStringList, it is "exploded" into a list of key-value + * pairs with key equal to \p key and value taken from each list item; + * - if \p value is a bool, its OpenAPI (i.e. JSON) representation is added + * to the query (`true` or `false`, respectively). + * + * \tparam Force add the pair even if the value is empty. This is true + * by default; passing IfNotEmpty or false for this parameter + * enables emptiness checks as described above + */ +template <bool Force = true, typename ContT, typename ValT> +inline void addParam(ContT& container, const QString& key, ValT&& value) +{ + _impl::AddNode<std::decay_t<ValT>, Force>::impl(container, key, + std::forward<ValT>(value)); +} +} // namespace Quotient diff --git a/lib/csapi/account-data.cpp b/lib/csapi/account-data.cpp index 96b32a92..6a40e908 100644 --- a/lib/csapi/account-data.cpp +++ b/lib/csapi/account-data.cpp @@ -4,57 +4,59 @@ #include "account-data.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto SetAccountDataJobName = QStringLiteral("SetAccountDataJob"); +using namespace Quotient; -SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content) - : BaseJob(HttpVerb::Put, SetAccountDataJobName, - basePath % "/user/" % userId % "/account_data/" % type) +SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, + const QJsonObject& content) + : BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataJob"), + QStringLiteral("/_matrix/client/r0") % "/user/" % userId + % "/account_data/" % type) { setRequestData(Data(toJson(content))); } -QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type) +QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& type) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/user/" % userId % "/account_data/" % type); + QStringLiteral("/_matrix/client/r0") % "/user/" + % userId % "/account_data/" % type); } -static const auto GetAccountDataJobName = QStringLiteral("GetAccountDataJob"); - GetAccountDataJob::GetAccountDataJob(const QString& userId, const QString& type) - : BaseJob(HttpVerb::Get, GetAccountDataJobName, - basePath % "/user/" % userId % "/account_data/" % type) -{ -} - -static const auto SetAccountDataPerRoomJobName = QStringLiteral("SetAccountDataPerRoomJob"); - -SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content) - : BaseJob(HttpVerb::Put, SetAccountDataPerRoomJobName, - basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type) + : BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataJob"), + QStringLiteral("/_matrix/client/r0") % "/user/" % userId + % "/account_data/" % type) +{} + +SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, + const QString& roomId, + const QString& type, + const QJsonObject& content) + : BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataPerRoomJob"), + QStringLiteral("/_matrix/client/r0") % "/user/" % userId + % "/rooms/" % roomId % "/account_data/" % type) { setRequestData(Data(toJson(content))); } -QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& type) +QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, + const QString& userId, + const QString& roomId, + const QString& type) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type); -} - -static const auto GetAccountDataPerRoomJobName = QStringLiteral("GetAccountDataPerRoomJob"); - -GetAccountDataPerRoomJob::GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type) - : BaseJob(HttpVerb::Get, GetAccountDataPerRoomJobName, - basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type) -{ + QStringLiteral("/_matrix/client/r0") + % "/user/" % userId % "/rooms/" % roomId + % "/account_data/" % type); } +GetAccountDataPerRoomJob::GetAccountDataPerRoomJob(const QString& userId, + const QString& roomId, + const QString& type) + : BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataPerRoomJob"), + QStringLiteral("/_matrix/client/r0") % "/user/" % userId + % "/rooms/" % roomId % "/account_data/" % type) +{} diff --git a/lib/csapi/account-data.h b/lib/csapi/account-data.h index b067618f..9a31596f 100644 --- a/lib/csapi/account-data.h +++ b/lib/csapi/account-data.h @@ -6,109 +6,121 @@ #include "jobs/basejob.h" -#include <QtCore/QJsonObject> +namespace Quotient { -namespace QMatrixClient -{ - // Operations - - /// Set some account_data for the user. - /// - /// Set some account_data for the client. This config is only visible to the user - /// that set the account_data. The config will be synced to clients in the - /// top-level ``account_data``. - class SetAccountDataJob : public BaseJob - { - public: - /*! Set some account_data for the user. - * \param userId - * The ID of the user to set account_data for. The access token must be - * authorized to make requests for this user ID. - * \param type - * The event type of the account_data to set. Custom types should be - * namespaced to avoid clashes. - * \param content - * The content of the account_data - */ - explicit SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content = {}); - }; - - /// Get some account_data for the user. - /// - /// Get some account_data for the client. This config is only visible to the user - /// that set the account_data. - class GetAccountDataJob : public BaseJob - { - public: - /*! Get some account_data for the user. - * \param userId - * The ID of the user to get account_data for. The access token must be - * authorized to make requests for this user ID. - * \param type - * The event type of the account_data to get. Custom types should be - * namespaced to avoid clashes. - */ - explicit GetAccountDataJob(const QString& userId, const QString& type); +/*! \brief Set some account_data for the user. + * + * Set some account_data for the client. This config is only visible to the user + * that set the account_data. The config will be synced to clients in the + * top-level ``account_data``. + */ +class SetAccountDataJob : public BaseJob { +public: + /*! \brief Set some account_data for the user. + * + * \param userId + * The ID of the user to set account_data for. The access token must be + * authorized to make requests for this user ID. + * + * \param type + * The event type of the account_data to set. Custom types should be + * namespaced to avoid clashes. + * + * \param content + * The content of the account_data + */ + explicit SetAccountDataJob(const QString& userId, const QString& type, + const QJsonObject& content = {}); +}; - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetAccountDataJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type); +/*! \brief Get some account_data for the user. + * + * Get some account_data for the client. This config is only visible to the user + * that set the account_data. + */ +class GetAccountDataJob : public BaseJob { +public: + /*! \brief Get some account_data for the user. + * + * \param userId + * The ID of the user to get account_data for. The access token must be + * authorized to make requests for this user ID. + * + * \param type + * The event type of the account_data to get. Custom types should be + * namespaced to avoid clashes. + */ + explicit GetAccountDataJob(const QString& userId, const QString& type); - }; + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetAccountDataJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& type); +}; - /// Set some account_data for the user. - /// - /// Set some account_data for the client on a given room. This config is only - /// visible to the user that set the account_data. The config will be synced to - /// clients in the per-room ``account_data``. - class SetAccountDataPerRoomJob : public BaseJob - { - public: - /*! Set some account_data for the user. - * \param userId - * The ID of the user to set account_data for. The access token must be - * authorized to make requests for this user ID. - * \param roomId - * The ID of the room to set account_data on. - * \param type - * The event type of the account_data to set. Custom types should be - * namespaced to avoid clashes. - * \param content - * The content of the account_data - */ - explicit SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content = {}); - }; +/*! \brief Set some account_data for the user. + * + * Set some account_data for the client on a given room. This config is only + * visible to the user that set the account_data. The config will be synced to + * clients in the per-room ``account_data``. + */ +class SetAccountDataPerRoomJob : public BaseJob { +public: + /*! \brief Set some account_data for the user. + * + * \param userId + * The ID of the user to set account_data for. The access token must be + * authorized to make requests for this user ID. + * + * \param roomId + * The ID of the room to set account_data on. + * + * \param type + * The event type of the account_data to set. Custom types should be + * namespaced to avoid clashes. + * + * \param content + * The content of the account_data + */ + explicit SetAccountDataPerRoomJob(const QString& userId, + const QString& roomId, const QString& type, + const QJsonObject& content = {}); +}; - /// Get some account_data for the user. - /// - /// Get some account_data for the client on a given room. This config is only - /// visible to the user that set the account_data. - class GetAccountDataPerRoomJob : public BaseJob - { - public: - /*! Get some account_data for the user. - * \param userId - * The ID of the user to set account_data for. The access token must be - * authorized to make requests for this user ID. - * \param roomId - * The ID of the room to get account_data for. - * \param type - * The event type of the account_data to get. Custom types should be - * namespaced to avoid clashes. - */ - explicit GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type); +/*! \brief Get some account_data for the user. + * + * Get some account_data for the client on a given room. This config is only + * visible to the user that set the account_data. + */ +class GetAccountDataPerRoomJob : public BaseJob { +public: + /*! \brief Get some account_data for the user. + * + * \param userId + * The ID of the user to set account_data for. The access token must be + * authorized to make requests for this user ID. + * + * \param roomId + * The ID of the room to get account_data for. + * + * \param type + * The event type of the account_data to get. Custom types should be + * namespaced to avoid clashes. + */ + explicit GetAccountDataPerRoomJob(const QString& userId, + const QString& roomId, + const QString& type); - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetAccountDataPerRoomJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& type); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetAccountDataPerRoomJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& roomId, const QString& type); +}; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/admin.cpp b/lib/csapi/admin.cpp index ce06a56d..9619c441 100644 --- a/lib/csapi/admin.cpp +++ b/lib/csapi/admin.cpp @@ -4,84 +4,18 @@ #include "admin.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<GetWhoIsJob::ConnectionInfo> - { - static void fillFrom(const QJsonObject& jo, GetWhoIsJob::ConnectionInfo& result) - { - fromJson(jo.value("ip"_ls), result.ip); - fromJson(jo.value("last_seen"_ls), result.lastSeen); - fromJson(jo.value("user_agent"_ls), result.userAgent); - } - }; - - template <> struct JsonObjectConverter<GetWhoIsJob::SessionInfo> - { - static void fillFrom(const QJsonObject& jo, GetWhoIsJob::SessionInfo& result) - { - fromJson(jo.value("connections"_ls), result.connections); - } - }; - - template <> struct JsonObjectConverter<GetWhoIsJob::DeviceInfo> - { - static void fillFrom(const QJsonObject& jo, GetWhoIsJob::DeviceInfo& result) - { - fromJson(jo.value("sessions"_ls), result.sessions); - } - }; -} // namespace QMatrixClient - -class GetWhoIsJob::Private -{ - public: - QString userId; - QHash<QString, DeviceInfo> devices; -}; +using namespace Quotient; QUrl GetWhoIsJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/admin/whois/" % userId); + QStringLiteral("/_matrix/client/r0") + % "/admin/whois/" % userId); } -static const auto GetWhoIsJobName = QStringLiteral("GetWhoIsJob"); - GetWhoIsJob::GetWhoIsJob(const QString& userId) - : BaseJob(HttpVerb::Get, GetWhoIsJobName, - basePath % "/admin/whois/" % userId) - , d(new Private) -{ -} - -GetWhoIsJob::~GetWhoIsJob() = default; - -const QString& GetWhoIsJob::userId() const -{ - return d->userId; -} - -const QHash<QString, GetWhoIsJob::DeviceInfo>& GetWhoIsJob::devices() const -{ - return d->devices; -} - -BaseJob::Status GetWhoIsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("user_id"_ls), d->userId); - fromJson(json.value("devices"_ls), d->devices); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("GetWhoIsJob"), + QStringLiteral("/_matrix/client/r0") % "/admin/whois/" % userId) +{} diff --git a/lib/csapi/admin.h b/lib/csapi/admin.h index d35f3ee3..570bf24a 100644 --- a/lib/csapi/admin.h +++ b/lib/csapi/admin.h @@ -6,93 +6,108 @@ #include "jobs/basejob.h" -#include <QtCore/QVector> -#include <QtCore/QHash> -#include "converters.h" +namespace Quotient { + +/*! \brief Gets information about a particular user. + * + * Gets information about a particular user. + * + * This API may be restricted to only be called by the user being looked + * up, or by a server admin. Server-local administrator privileges are not + * specified in this document. + */ +class GetWhoIsJob : public BaseJob { +public: + // Inner data structures -namespace QMatrixClient -{ - // Operations + /// Gets information about a particular user. + /// + /// This API may be restricted to only be called by the user being looked + /// up, or by a server admin. Server-local administrator privileges are not + /// specified in this document. + struct ConnectionInfo { + /// Most recently seen IP address of the session. + QString ip; + /// Unix timestamp that the session was last active. + Omittable<qint64> lastSeen; + /// User agent string last seen in the session. + QString userAgent; + }; /// Gets information about a particular user. /// + /// This API may be restricted to only be called by the user being looked + /// up, or by a server admin. Server-local administrator privileges are not + /// specified in this document. + struct SessionInfo { + /// Information particular connections in the session. + QVector<ConnectionInfo> connections; + }; + /// Gets information about a particular user. - /// + /// /// This API may be restricted to only be called by the user being looked /// up, or by a server admin. Server-local administrator privileges are not /// specified in this document. - class GetWhoIsJob : public BaseJob - { - public: - // Inner data structures - - /// Gets information about a particular user. - /// - /// This API may be restricted to only be called by the user being looked - /// up, or by a server admin. Server-local administrator privileges are not - /// specified in this document. - struct ConnectionInfo - { - /// Most recently seen IP address of the session. - QString ip; - /// Unix timestamp that the session was last active. - Omittable<qint64> lastSeen; - /// User agent string last seen in the session. - QString userAgent; - }; - - /// Gets information about a particular user. - /// - /// This API may be restricted to only be called by the user being looked - /// up, or by a server admin. Server-local administrator privileges are not - /// specified in this document. - struct SessionInfo - { - /// Information particular connections in the session. - QVector<ConnectionInfo> connections; - }; - - /// Gets information about a particular user. - /// - /// This API may be restricted to only be called by the user being looked - /// up, or by a server admin. Server-local administrator privileges are not - /// specified in this document. - struct DeviceInfo - { - /// A user's sessions (i.e. what they did with an access token from one login). - QVector<SessionInfo> sessions; - }; - - // Construction/destruction - - /*! Gets information about a particular user. - * \param userId - * The user to look up. - */ - explicit GetWhoIsJob(const QString& userId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetWhoIsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - - ~GetWhoIsJob() override; - - // Result properties - - /// The Matrix user ID of the user. - const QString& userId() const; - /// Each key is an identitfier for one of the user's devices. - const QHash<QString, DeviceInfo>& devices() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; + struct DeviceInfo { + /// A user's sessions (i.e. what they did with an access token from one + /// login). + QVector<SessionInfo> sessions; }; -} // namespace QMatrixClient + + // Construction/destruction + + /*! \brief Gets information about a particular user. + * + * \param userId + * The user to look up. + */ + explicit GetWhoIsJob(const QString& userId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetWhoIsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); + + // Result properties + + /// The Matrix user ID of the user. + QString userId() const { return loadFromJson<QString>("user_id"_ls); } + + /// Each key is an identifier for one of the user's devices. + QHash<QString, DeviceInfo> devices() const + { + return loadFromJson<QHash<QString, DeviceInfo>>("devices"_ls); + } +}; + +template <> +struct JsonObjectConverter<GetWhoIsJob::ConnectionInfo> { + static void fillFrom(const QJsonObject& jo, + GetWhoIsJob::ConnectionInfo& result) + { + fromJson(jo.value("ip"_ls), result.ip); + fromJson(jo.value("last_seen"_ls), result.lastSeen); + fromJson(jo.value("user_agent"_ls), result.userAgent); + } +}; + +template <> +struct JsonObjectConverter<GetWhoIsJob::SessionInfo> { + static void fillFrom(const QJsonObject& jo, GetWhoIsJob::SessionInfo& result) + { + fromJson(jo.value("connections"_ls), result.connections); + } +}; + +template <> +struct JsonObjectConverter<GetWhoIsJob::DeviceInfo> { + static void fillFrom(const QJsonObject& jo, GetWhoIsJob::DeviceInfo& result) + { + fromJson(jo.value("sessions"_ls), result.sessions); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp index 11385dff..fa4f475a 100644 --- a/lib/csapi/administrative_contact.cpp +++ b/lib/csapi/administrative_contact.cpp @@ -4,172 +4,100 @@ #include "administrative_contact.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<GetAccount3PIDsJob::ThirdPartyIdentifier> - { - static void fillFrom(const QJsonObject& jo, GetAccount3PIDsJob::ThirdPartyIdentifier& result) - { - fromJson(jo.value("medium"_ls), result.medium); - fromJson(jo.value("address"_ls), result.address); - fromJson(jo.value("validated_at"_ls), result.validatedAt); - fromJson(jo.value("added_at"_ls), result.addedAt); - } - }; -} // namespace QMatrixClient - -class GetAccount3PIDsJob::Private -{ - public: - QVector<ThirdPartyIdentifier> threepids; -}; +using namespace Quotient; QUrl GetAccount3PIDsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/account/3pid"); + QStringLiteral("/_matrix/client/r0") + % "/account/3pid"); } -static const auto GetAccount3PIDsJobName = QStringLiteral("GetAccount3PIDsJob"); - GetAccount3PIDsJob::GetAccount3PIDsJob() - : BaseJob(HttpVerb::Get, GetAccount3PIDsJobName, - basePath % "/account/3pid") - , d(new Private) -{ -} - -GetAccount3PIDsJob::~GetAccount3PIDsJob() = default; + : BaseJob(HttpVerb::Get, QStringLiteral("GetAccount3PIDsJob"), + QStringLiteral("/_matrix/client/r0") % "/account/3pid") +{} -const QVector<GetAccount3PIDsJob::ThirdPartyIdentifier>& GetAccount3PIDsJob::threepids() const -{ - return d->threepids; -} - -BaseJob::Status GetAccount3PIDsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("threepids"_ls), d->threepids); - return Success; -} - -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<Post3PIDsJob::ThreePidCredentials> - { - static void dumpTo(QJsonObject& jo, const Post3PIDsJob::ThreePidCredentials& pod) - { - addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); - addParam<>(jo, QStringLiteral("id_server"), pod.idServer); - addParam<>(jo, QStringLiteral("sid"), pod.sid); - } - }; -} // namespace QMatrixClient - -static const auto Post3PIDsJobName = QStringLiteral("Post3PIDsJob"); - -Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds, Omittable<bool> bind) - : BaseJob(HttpVerb::Post, Post3PIDsJobName, - basePath % "/account/3pid") +Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds) + : BaseJob(HttpVerb::Post, QStringLiteral("Post3PIDsJob"), + QStringLiteral("/_matrix/client/r0") % "/account/3pid") { QJsonObject _data; addParam<>(_data, QStringLiteral("three_pid_creds"), threePidCreds); - addParam<IfNotEmpty>(_data, QStringLiteral("bind"), bind); - setRequestData(_data); + setRequestData(std::move(_data)); } -static const auto Delete3pidFromAccountJobName = QStringLiteral("Delete3pidFromAccountJob"); - -Delete3pidFromAccountJob::Delete3pidFromAccountJob(const QString& medium, const QString& address) - : BaseJob(HttpVerb::Post, Delete3pidFromAccountJobName, - basePath % "/account/3pid/delete") +Add3PIDJob::Add3PIDJob(const QString& clientSecret, const QString& sid, + const Omittable<AuthenticationData>& auth) + : BaseJob(HttpVerb::Post, QStringLiteral("Add3PIDJob"), + QStringLiteral("/_matrix/client/r0") % "/account/3pid/add") { QJsonObject _data; - addParam<>(_data, QStringLiteral("medium"), medium); - addParam<>(_data, QStringLiteral("address"), address); - setRequestData(_data); + addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); + addParam<>(_data, QStringLiteral("client_secret"), clientSecret); + addParam<>(_data, QStringLiteral("sid"), sid); + setRequestData(std::move(_data)); } -class RequestTokenTo3PIDEmailJob::Private -{ - public: - Sid data; -}; - -static const auto RequestTokenTo3PIDEmailJobName = QStringLiteral("RequestTokenTo3PIDEmailJob"); - -RequestTokenTo3PIDEmailJob::RequestTokenTo3PIDEmailJob(const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink) - : BaseJob(HttpVerb::Post, RequestTokenTo3PIDEmailJobName, - basePath % "/account/3pid/email/requestToken", false) - , d(new Private) +Bind3PIDJob::Bind3PIDJob(const QString& clientSecret, const QString& idServer, + const QString& idAccessToken, const QString& sid) + : BaseJob(HttpVerb::Post, QStringLiteral("Bind3PIDJob"), + QStringLiteral("/_matrix/client/r0") % "/account/3pid/bind") { QJsonObject _data; addParam<>(_data, QStringLiteral("client_secret"), clientSecret); - addParam<>(_data, QStringLiteral("email"), email); - addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); - addParam<IfNotEmpty>(_data, QStringLiteral("next_link"), nextLink); addParam<>(_data, QStringLiteral("id_server"), idServer); - setRequestData(_data); + addParam<>(_data, QStringLiteral("id_access_token"), idAccessToken); + addParam<>(_data, QStringLiteral("sid"), sid); + setRequestData(std::move(_data)); } -RequestTokenTo3PIDEmailJob::~RequestTokenTo3PIDEmailJob() = default; - -const Sid& RequestTokenTo3PIDEmailJob::data() const +Delete3pidFromAccountJob::Delete3pidFromAccountJob(const QString& medium, + const QString& address, + const QString& idServer) + : BaseJob(HttpVerb::Post, QStringLiteral("Delete3pidFromAccountJob"), + QStringLiteral("/_matrix/client/r0") % "/account/3pid/delete") { - return d->data; -} - -BaseJob::Status RequestTokenTo3PIDEmailJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; + QJsonObject _data; + addParam<IfNotEmpty>(_data, QStringLiteral("id_server"), idServer); + addParam<>(_data, QStringLiteral("medium"), medium); + addParam<>(_data, QStringLiteral("address"), address); + setRequestData(std::move(_data)); + addExpectedKey("id_server_unbind_result"); } -class RequestTokenTo3PIDMSISDNJob::Private -{ - public: - Sid data; -}; - -static const auto RequestTokenTo3PIDMSISDNJobName = QStringLiteral("RequestTokenTo3PIDMSISDNJob"); - -RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob(const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink) - : BaseJob(HttpVerb::Post, RequestTokenTo3PIDMSISDNJobName, - basePath % "/account/3pid/msisdn/requestToken", false) - , d(new Private) +Unbind3pidFromAccountJob::Unbind3pidFromAccountJob(const QString& medium, + const QString& address, + const QString& idServer) + : BaseJob(HttpVerb::Post, QStringLiteral("Unbind3pidFromAccountJob"), + QStringLiteral("/_matrix/client/r0") % "/account/3pid/unbind") { QJsonObject _data; - addParam<>(_data, QStringLiteral("client_secret"), clientSecret); - addParam<>(_data, QStringLiteral("country"), country); - addParam<>(_data, QStringLiteral("phone_number"), phoneNumber); - addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); - addParam<IfNotEmpty>(_data, QStringLiteral("next_link"), nextLink); - addParam<>(_data, QStringLiteral("id_server"), idServer); - setRequestData(_data); + addParam<IfNotEmpty>(_data, QStringLiteral("id_server"), idServer); + addParam<>(_data, QStringLiteral("medium"), medium); + addParam<>(_data, QStringLiteral("address"), address); + setRequestData(std::move(_data)); + addExpectedKey("id_server_unbind_result"); } -RequestTokenTo3PIDMSISDNJob::~RequestTokenTo3PIDMSISDNJob() = default; - -const Sid& RequestTokenTo3PIDMSISDNJob::data() const +RequestTokenTo3PIDEmailJob::RequestTokenTo3PIDEmailJob( + const EmailValidationData& body) + : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDEmailJob"), + QStringLiteral("/_matrix/client/r0") + % "/account/3pid/email/requestToken", + false) { - return d->data; + setRequestData(Data(toJson(body))); } -BaseJob::Status RequestTokenTo3PIDMSISDNJob::parseJson(const QJsonDocument& data) +RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob( + const MsisdnValidationData& body) + : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDMSISDNJob"), + QStringLiteral("/_matrix/client/r0") + % "/account/3pid/msisdn/requestToken", + false) { - fromJson(data, d->data); - return Success; + setRequestData(Data(toJson(body))); } - diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h index 02aeee4d..1966d533 100644 --- a/lib/csapi/administrative_contact.h +++ b/lib/csapi/administrative_contact.h @@ -4,234 +4,360 @@ #pragma once +#include "csapi/definitions/auth_data.h" +#include "csapi/definitions/request_email_validation.h" +#include "csapi/definitions/request_msisdn_validation.h" +#include "csapi/definitions/request_token_response.h" + #include "jobs/basejob.h" -#include "csapi/../identity/definitions/sid.h" -#include "converters.h" -#include <QtCore/QVector> +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Gets a list of a user's third party identifiers. + * + * Gets a list of the third party identifiers that the homeserver has + * associated with the user's account. + * + * This is *not* the same as the list of third party identifiers bound to + * the user's Matrix ID in identity servers. + * + * Identifiers in this list may be used by the homeserver as, for example, + * identifiers that it will accept to reset the user's account password. + */ +class GetAccount3PIDsJob : public BaseJob { +public: + // Inner data structures - /// Gets a list of a user's third party identifiers. - /// /// Gets a list of the third party identifiers that the homeserver has /// associated with the user's account. - /// + /// /// This is *not* the same as the list of third party identifiers bound to /// the user's Matrix ID in identity servers. - /// + /// /// Identifiers in this list may be used by the homeserver as, for example, /// identifiers that it will accept to reset the user's account password. - class GetAccount3PIDsJob : public BaseJob - { - public: - // Inner data structures - - /// Gets a list of the third party identifiers that the homeserver has - /// associated with the user's account. - /// - /// This is *not* the same as the list of third party identifiers bound to - /// the user's Matrix ID in identity servers. - /// - /// Identifiers in this list may be used by the homeserver as, for example, - /// identifiers that it will accept to reset the user's account password. - struct ThirdPartyIdentifier - { - /// The medium of the third party identifier. - QString medium; - /// The third party identifier address. - QString address; - /// The timestamp, in milliseconds, when the identifier was - /// validated by the identity server. - qint64 validatedAt; - /// The timestamp, in milliseconds, when the homeserver associated the third party identifier with the user. - qint64 addedAt; - }; - - // Construction/destruction - - explicit GetAccount3PIDsJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetAccount3PIDsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetAccount3PIDsJob() override; - - // Result properties - - /// Gets a list of the third party identifiers that the homeserver has - /// associated with the user's account. - /// - /// This is *not* the same as the list of third party identifiers bound to - /// the user's Matrix ID in identity servers. - /// - /// Identifiers in this list may be used by the homeserver as, for example, - /// identifiers that it will accept to reset the user's account password. - const QVector<ThirdPartyIdentifier>& threepids() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; + struct ThirdPartyIdentifier { + /// The medium of the third party identifier. + QString medium; + /// The third party identifier address. + QString address; + /// The timestamp, in milliseconds, when the identifier was + /// validated by the identity server. + qint64 validatedAt; + /// The timestamp, in milliseconds, when the homeserver associated the + /// third party identifier with the user. + qint64 addedAt; }; - /// Adds contact information to the user's account. + // Construction/destruction + + /// Gets a list of a user's third party identifiers. + explicit GetAccount3PIDsJob(); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetAccount3PIDsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + // Result properties + + /// Gets a list of the third party identifiers that the homeserver has + /// associated with the user's account. /// - /// Adds contact information to the user's account. - class Post3PIDsJob : public BaseJob + /// This is *not* the same as the list of third party identifiers bound to + /// the user's Matrix ID in identity servers. + /// + /// Identifiers in this list may be used by the homeserver as, for example, + /// identifiers that it will accept to reset the user's account password. + QVector<ThirdPartyIdentifier> threepids() const { - public: - // Inner data structures - - /// The third party credentials to associate with the account. - struct ThreePidCredentials - { - /// The client secret used in the session with the identity server. - QString clientSecret; - /// The identity server to use. - QString idServer; - /// The session identifier given by the identity server. - QString sid; - }; - - // Construction/destruction - - /*! Adds contact information to the user's account. - * \param threePidCreds - * The third party credentials to associate with the account. - * \param bind - * Whether the homeserver should also bind this third party - * identifier to the account's Matrix ID with the passed identity - * server. Default: ``false``. - */ - explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds, Omittable<bool> bind = none); - }; + return loadFromJson<QVector<ThirdPartyIdentifier>>("threepids"_ls); + } +}; - /// Deletes a third party identifier from the user's account - /// - /// Removes a third party identifier from the user's account. This might not - /// cause an unbind of the identifier from the identity server. - class Delete3pidFromAccountJob : public BaseJob +template <> +struct JsonObjectConverter<GetAccount3PIDsJob::ThirdPartyIdentifier> { + static void fillFrom(const QJsonObject& jo, + GetAccount3PIDsJob::ThirdPartyIdentifier& result) { - public: - /*! Deletes a third party identifier from the user's account - * \param medium - * The medium of the third party identifier being removed. - * \param address - * The third party address being removed. - */ - explicit Delete3pidFromAccountJob(const QString& medium, const QString& address); + fromJson(jo.value("medium"_ls), result.medium); + fromJson(jo.value("address"_ls), result.address); + fromJson(jo.value("validated_at"_ls), result.validatedAt); + fromJson(jo.value("added_at"_ls), result.addedAt); + } +}; + +/*! \brief Adds contact information to the user's account. + * + * Adds contact information to the user's account. + * + * This endpoint is deprecated in favour of the more specific ``/3pid/add`` + * and ``/3pid/bind`` endpoints. + * + * .. Note:: + * Previously this endpoint supported a ``bind`` parameter. This parameter + * has been removed, making this endpoint behave as though it was ``false``. + * This results in this endpoint being an equivalent to ``/3pid/bind`` rather + * than dual-purpose. + */ +class Post3PIDsJob : public BaseJob { +public: + // Inner data structures + + /// The third party credentials to associate with the account. + struct ThreePidCredentials { + /// The client secret used in the session with the identity server. + QString clientSecret; + /// The identity server to use. + QString idServer; + /// An access token previously registered with the identity server. + /// Servers can treat this as optional to distinguish between + /// r0.5-compatible clients and this specification version. + QString idAccessToken; + /// The session identifier given by the identity server. + QString sid; }; - /// Begins the validation process for an email address for association with the user's account. - /// - /// Proxies the Identity Service API ``validate/email/requestToken``, but - /// first checks that the given email address is **not** already associated - /// with an account on this homeserver. This API should be used to request - /// validation tokens when adding an email address to an account. This API's - /// parameters and response are identical to that of the |/register/email/requestToken|_ - /// endpoint. - class RequestTokenTo3PIDEmailJob : public BaseJob + // Construction/destruction + + /*! \brief Adds contact information to the user's account. + * + * \param threePidCreds + * The third party credentials to associate with the account. + */ + explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds); +}; + +template <> +struct JsonObjectConverter<Post3PIDsJob::ThreePidCredentials> { + static void dumpTo(QJsonObject& jo, + const Post3PIDsJob::ThreePidCredentials& pod) { - public: - /*! Begins the validation process for an email address for association with the user's account. - * \param clientSecret - * A unique string generated by the client, and used to identify the - * validation attempt. It must be a string consisting of the characters - * ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it - * must not be empty. - * \param email - * The email address to validate. - * \param sendAttempt - * The server will only send an email if the ``send_attempt`` - * is a number greater than the most recent one which it has seen, - * scoped to that ``email`` + ``client_secret`` pair. This is to - * avoid repeatedly sending the same email in the case of request - * retries between the POSTing user and the identity server. - * The client should increment this value if they desire a new - * email (e.g. a reminder) to be sent. - * \param idServer - * The hostname of the identity server to communicate with. May - * optionally include a port. - * \param nextLink - * Optional. When the validation is completed, the identity - * server will redirect the user to this URL. - */ - explicit RequestTokenTo3PIDEmailJob(const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink = {}); - ~RequestTokenTo3PIDEmailJob() override; - - // Result properties - - /// An email was sent to the given address. - const Sid& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; + addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); + addParam<>(jo, QStringLiteral("id_server"), pod.idServer); + addParam<>(jo, QStringLiteral("id_access_token"), pod.idAccessToken); + addParam<>(jo, QStringLiteral("sid"), pod.sid); + } +}; - /// Begins the validation process for a phone number for association with the user's account. - /// - /// Proxies the Identity Service API ``validate/msisdn/requestToken``, but - /// first checks that the given phone number is **not** already associated - /// with an account on this homeserver. This API should be used to request - /// validation tokens when adding a phone number to an account. This API's - /// parameters and response are identical to that of the |/register/msisdn/requestToken|_ - /// endpoint. - class RequestTokenTo3PIDMSISDNJob : public BaseJob +/*! \brief Adds contact information to the user's account. + * + * This API endpoint uses the `User-Interactive Authentication API`_. + * + * Adds contact information to the user's account. Homeservers should use 3PIDs + * added through this endpoint for password resets instead of relying on the + * identity server. + * + * Homeservers should prevent the caller from adding a 3PID to their account if + * it has already been added to another user's account on the homeserver. + */ +class Add3PIDJob : public BaseJob { +public: + /*! \brief Adds contact information to the user's account. + * + * \param clientSecret + * The client secret used in the session with the homeserver. + * + * \param sid + * The session identifier given by the homeserver. + * + * \param auth + * Additional authentication information for the + * user-interactive authentication API. + */ + explicit Add3PIDJob(const QString& clientSecret, const QString& sid, + const Omittable<AuthenticationData>& auth = none); +}; + +/*! \brief Binds a 3PID to the user's account through an Identity Service. + * + * Binds a 3PID to the user's account through the specified identity server. + * + * Homeservers should not prevent this request from succeeding if another user + * has bound the 3PID. Homeservers should simply proxy any errors received by + * the identity server to the caller. + * + * Homeservers should track successful binds so they can be unbound later. + */ +class Bind3PIDJob : public BaseJob { +public: + /*! \brief Binds a 3PID to the user's account through an Identity Service. + * + * \param clientSecret + * The client secret used in the session with the identity server. + * + * \param idServer + * The identity server to use. + * + * \param idAccessToken + * An access token previously registered with the identity server. + * + * \param sid + * The session identifier given by the identity server. + */ + explicit Bind3PIDJob(const QString& clientSecret, const QString& idServer, + const QString& idAccessToken, const QString& sid); +}; + +/*! \brief Deletes a third party identifier from the user's account + * + * Removes a third party identifier from the user's account. This might not + * cause an unbind of the identifier from the identity server. + * + * Unlike other endpoints, this endpoint does not take an ``id_access_token`` + * parameter because the homeserver is expected to sign the request to the + * identity server instead. + */ +class Delete3pidFromAccountJob : public BaseJob { +public: + /*! \brief Deletes a third party identifier from the user's account + * + * \param medium + * The medium of the third party identifier being removed. + * + * \param address + * The third party address being removed. + * + * \param idServer + * The identity server to unbind from. If not provided, the homeserver + * MUST use the ``id_server`` the identifier was added through. If the + * homeserver does not know the original ``id_server``, it MUST return + * a ``id_server_unbind_result`` of ``no-support``. + */ + explicit Delete3pidFromAccountJob(const QString& medium, + const QString& address, + const QString& idServer = {}); + + // Result properties + + /// An indicator as to whether or not the homeserver was able to unbind + /// the 3PID from the identity server. ``success`` indicates that the + /// indentity server has unbound the identifier whereas ``no-support`` + /// indicates that the identity server refuses to support the request + /// or the homeserver was not able to determine an identity server to + /// unbind from. + QString idServerUnbindResult() const { - public: - /*! Begins the validation process for a phone number for association with the user's account. - * \param clientSecret - * A unique string generated by the client, and used to identify the - * validation attempt. It must be a string consisting of the characters - * ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it - * must not be empty. - * \param country - * The two-letter uppercase ISO country code that the number in - * ``phone_number`` should be parsed as if it were dialled from. - * \param phoneNumber - * The phone number to validate. - * \param sendAttempt - * The server will only send an SMS if the ``send_attempt`` is a - * number greater than the most recent one which it has seen, - * scoped to that ``country`` + ``phone_number`` + ``client_secret`` - * triple. This is to avoid repeatedly sending the same SMS in - * the case of request retries between the POSTing user and the - * identity server. The client should increment this value if - * they desire a new SMS (e.g. a reminder) to be sent. - * \param idServer - * The hostname of the identity server to communicate with. May - * optionally include a port. - * \param nextLink - * Optional. When the validation is completed, the identity - * server will redirect the user to this URL. - */ - explicit RequestTokenTo3PIDMSISDNJob(const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink = {}); - ~RequestTokenTo3PIDMSISDNJob() override; - - // Result properties - - /// An SMS message was sent to the given phone number. - const Sid& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + return loadFromJson<QString>("id_server_unbind_result"_ls); + } +}; + +/*! \brief Removes a user's third party identifier from an identity server. + * + * Removes a user's third party identifier from the provided identity server + * without removing it from the homeserver. + * + * Unlike other endpoints, this endpoint does not take an ``id_access_token`` + * parameter because the homeserver is expected to sign the request to the + * identity server instead. + */ +class Unbind3pidFromAccountJob : public BaseJob { +public: + /*! \brief Removes a user's third party identifier from an identity server. + * + * \param medium + * The medium of the third party identifier being removed. + * + * \param address + * The third party address being removed. + * + * \param idServer + * The identity server to unbind from. If not provided, the homeserver + * MUST use the ``id_server`` the identifier was added through. If the + * homeserver does not know the original ``id_server``, it MUST return + * a ``id_server_unbind_result`` of ``no-support``. + */ + explicit Unbind3pidFromAccountJob(const QString& medium, + const QString& address, + const QString& idServer = {}); + + // Result properties + + /// An indicator as to whether or not the identity server was able to unbind + /// the 3PID. ``success`` indicates that the identity server has unbound the + /// identifier whereas ``no-support`` indicates that the identity server + /// refuses to support the request or the homeserver was not able to + /// determine an identity server to unbind from. + QString idServerUnbindResult() const + { + return loadFromJson<QString>("id_server_unbind_result"_ls); + } +}; + +/*! \brief Begins the validation process for an email address for association + * with the user's account. + * + * The homeserver must check that the given email address is **not** + * already associated with an account on this homeserver. This API should + * be used to request validation tokens when adding an email address to an + * account. This API's parameters and response are identical to that of + * the |/register/email/requestToken|_ endpoint. The homeserver should validate + * the email itself, either by sending a validation email itself or by using + * a service it has control over. + */ +class RequestTokenTo3PIDEmailJob : public BaseJob { +public: + /*! \brief Begins the validation process for an email address for + * association with the user's account. + * + * \param body + * The homeserver must check that the given email address is **not** + * already associated with an account on this homeserver. This API should + * be used to request validation tokens when adding an email address to an + * account. This API's parameters and response are identical to that of + * the |/register/email/requestToken|_ endpoint. The homeserver should + * validate the email itself, either by sending a validation email itself or + * by using a service it has control over. + */ + explicit RequestTokenTo3PIDEmailJob(const EmailValidationData& body); + + // Result properties + + /// An email was sent to the given address. Note that this may be an + /// email containing the validation token or it may be informing the + /// user of an error. + RequestTokenResponse response() const + { + return fromJson<RequestTokenResponse>(jsonData()); + } +}; + +/*! \brief Begins the validation process for a phone number for association with + * the user's account. + * + * The homeserver must check that the given phone number is **not** + * already associated with an account on this homeserver. This API should + * be used to request validation tokens when adding a phone number to an + * account. This API's parameters and response are identical to that of + * the |/register/msisdn/requestToken|_ endpoint. The homeserver should validate + * the phone number itself, either by sending a validation message itself or by + * using a service it has control over. + */ +class RequestTokenTo3PIDMSISDNJob : public BaseJob { +public: + /*! \brief Begins the validation process for a phone number for association + * with the user's account. + * + * \param body + * The homeserver must check that the given phone number is **not** + * already associated with an account on this homeserver. This API should + * be used to request validation tokens when adding a phone number to an + * account. This API's parameters and response are identical to that of + * the |/register/msisdn/requestToken|_ endpoint. The homeserver should + * validate the phone number itself, either by sending a validation message + * itself or by using a service it has control over. + */ + explicit RequestTokenTo3PIDMSISDNJob(const MsisdnValidationData& body); + + // Result properties + + /// An SMS message was sent to the given phone number. + RequestTokenResponse response() const + { + return fromJson<RequestTokenResponse>(jsonData()); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/appservice_room_directory.cpp b/lib/csapi/appservice_room_directory.cpp index f40e2f05..e8ec55bf 100644 --- a/lib/csapi/appservice_room_directory.cpp +++ b/lib/csapi/appservice_room_directory.cpp @@ -4,22 +4,18 @@ #include "appservice_room_directory.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; -static const auto UpdateAppserviceRoomDirectoryVsibilityJobName = QStringLiteral("UpdateAppserviceRoomDirectoryVsibilityJob"); - -UpdateAppserviceRoomDirectoryVsibilityJob::UpdateAppserviceRoomDirectoryVsibilityJob(const QString& networkId, const QString& roomId, const QString& visibility) - : BaseJob(HttpVerb::Put, UpdateAppserviceRoomDirectoryVsibilityJobName, - basePath % "/directory/list/appservice/" % networkId % "/" % roomId) +UpdateAppserviceRoomDirectoryVsibilityJob::UpdateAppserviceRoomDirectoryVsibilityJob( + const QString& networkId, const QString& roomId, const QString& visibility) + : BaseJob(HttpVerb::Put, + QStringLiteral("UpdateAppserviceRoomDirectoryVsibilityJob"), + QStringLiteral("/_matrix/client/r0") + % "/directory/list/appservice/" % networkId % "/" % roomId) { QJsonObject _data; addParam<>(_data, QStringLiteral("visibility"), visibility); - setRequestData(_data); + setRequestData(std::move(_data)); } - diff --git a/lib/csapi/appservice_room_directory.h b/lib/csapi/appservice_room_directory.h index f35198b3..3fa02a07 100644 --- a/lib/csapi/appservice_room_directory.h +++ b/lib/csapi/appservice_room_directory.h @@ -6,36 +6,41 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Updates a room's visibility in the application service's room + * directory. + * + * Updates the visibility of a given room on the application service's room + * directory. + * + * This API is similar to the room directory visibility API used by clients + * to update the homeserver's more general room directory. + * + * This API requires the use of an application service access token + * (``as_token``) instead of a typical client's access_token. This API cannot be + * invoked by users who are not identified as application services. + */ +class UpdateAppserviceRoomDirectoryVsibilityJob : public BaseJob { +public: + /*! \brief Updates a room's visibility in the application service's room + * directory. + * + * \param networkId + * The protocol (network) ID to update the room list for. This would + * have been provided by the application service as being listed as + * a supported protocol. + * + * \param roomId + * The room ID to add to the directory. + * + * \param visibility + * Whether the room should be visible (public) in the directory + * or not (private). + */ + explicit UpdateAppserviceRoomDirectoryVsibilityJob(const QString& networkId, + const QString& roomId, + const QString& visibility); +}; - /// Updates a room's visibility in the application service's room directory. - /// - /// Updates the visibility of a given room on the application service's room - /// directory. - /// - /// This API is similar to the room directory visibility API used by clients - /// to update the homeserver's more general room directory. - /// - /// This API requires the use of an application service access token (``as_token``) - /// instead of a typical client's access_token. This API cannot be invoked by - /// users who are not identified as application services. - class UpdateAppserviceRoomDirectoryVsibilityJob : public BaseJob - { - public: - /*! Updates a room's visibility in the application service's room directory. - * \param networkId - * The protocol (network) ID to update the room list for. This would - * have been provided by the application service as being listed as - * a supported protocol. - * \param roomId - * The room ID to add to the directory. - * \param visibility - * Whether the room should be visible (public) in the directory - * or not (private). - */ - explicit UpdateAppserviceRoomDirectoryVsibilityJob(const QString& networkId, const QString& roomId, const QString& visibility); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/banning.cpp b/lib/csapi/banning.cpp index 4065207b..8a8ec664 100644 --- a/lib/csapi/banning.cpp +++ b/lib/csapi/banning.cpp @@ -4,34 +4,27 @@ #include "banning.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto BanJobName = QStringLiteral("BanJob"); +using namespace Quotient; -BanJob::BanJob(const QString& roomId, const QString& userId, const QString& reason) - : BaseJob(HttpVerb::Post, BanJobName, - basePath % "/rooms/" % roomId % "/ban") +BanJob::BanJob(const QString& roomId, const QString& userId, + const QString& reason) + : BaseJob(HttpVerb::Post, QStringLiteral("BanJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/ban") { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); - setRequestData(_data); + setRequestData(std::move(_data)); } -static const auto UnbanJobName = QStringLiteral("UnbanJob"); - UnbanJob::UnbanJob(const QString& roomId, const QString& userId) - : BaseJob(HttpVerb::Post, UnbanJobName, - basePath % "/rooms/" % roomId % "/unban") + : BaseJob(HttpVerb::Post, QStringLiteral("UnbanJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/unban") { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); - setRequestData(_data); + setRequestData(std::move(_data)); } - diff --git a/lib/csapi/banning.h b/lib/csapi/banning.h index 237bd2a0..37ae91ee 100644 --- a/lib/csapi/banning.h +++ b/lib/csapi/banning.h @@ -6,47 +6,56 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Ban a user in the room. + * + * Ban a user in the room. If the user is currently in the room, also kick them. + * + * When a user is banned from a room, they may not join it or be invited to it + * until they are unbanned. + * + * The caller must have the required power level in order to perform this + * operation. + */ +class BanJob : public BaseJob { +public: + /*! \brief Ban a user in the room. + * + * \param roomId + * The room identifier (not alias) from which the user should be banned. + * + * \param userId + * The fully qualified user ID of the user being banned. + * + * \param reason + * The reason the user has been banned. This will be supplied as the + * ``reason`` on the target's updated `m.room.member`_ event. + */ + explicit BanJob(const QString& roomId, const QString& userId, + const QString& reason = {}); +}; - /// Ban a user in the room. - /// - /// Ban a user in the room. If the user is currently in the room, also kick them. - /// - /// When a user is banned from a room, they may not join it or be invited to it until they are unbanned. - /// - /// The caller must have the required power level in order to perform this operation. - class BanJob : public BaseJob - { - public: - /*! Ban a user in the room. - * \param roomId - * The room identifier (not alias) from which the user should be banned. - * \param userId - * The fully qualified user ID of the user being banned. - * \param reason - * The reason the user has been banned. This will be supplied as the ``reason`` on the target's updated `m.room.member`_ event. - */ - explicit BanJob(const QString& roomId, const QString& userId, const QString& reason = {}); - }; +/*! \brief Unban a user from the room. + * + * Unban a user from the room. This allows them to be invited to the room, + * and join if they would otherwise be allowed to join according to its join + * rules. + * + * The caller must have the required power level in order to perform this + * operation. + */ +class UnbanJob : public BaseJob { +public: + /*! \brief Unban a user from the room. + * + * \param roomId + * The room identifier (not alias) from which the user should be unbanned. + * + * \param userId + * The fully qualified user ID of the user being unbanned. + */ + explicit UnbanJob(const QString& roomId, const QString& userId); +}; - /// Unban a user from the room. - /// - /// Unban a user from the room. This allows them to be invited to the room, - /// and join if they would otherwise be allowed to join according to its join rules. - /// - /// The caller must have the required power level in order to perform this operation. - class UnbanJob : public BaseJob - { - public: - /*! Unban a user from the room. - * \param roomId - * The room identifier (not alias) from which the user should be unbanned. - * \param userId - * The fully qualified user ID of the user being unbanned. - */ - explicit UnbanJob(const QString& roomId, const QString& userId); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/capabilities.cpp b/lib/csapi/capabilities.cpp index 210423f5..33a53cad 100644 --- a/lib/csapi/capabilities.cpp +++ b/lib/csapi/capabilities.cpp @@ -4,81 +4,20 @@ #include "capabilities.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<GetCapabilitiesJob::ChangePasswordCapability> - { - static void fillFrom(const QJsonObject& jo, GetCapabilitiesJob::ChangePasswordCapability& result) - { - fromJson(jo.value("enabled"_ls), result.enabled); - } - }; - - template <> struct JsonObjectConverter<GetCapabilitiesJob::RoomVersionsCapability> - { - static void fillFrom(const QJsonObject& jo, GetCapabilitiesJob::RoomVersionsCapability& result) - { - fromJson(jo.value("default"_ls), result.defaultVersion); - fromJson(jo.value("available"_ls), result.available); - } - }; - - template <> struct JsonObjectConverter<GetCapabilitiesJob::Capabilities> - { - static void fillFrom(QJsonObject jo, GetCapabilitiesJob::Capabilities& result) - { - fromJson(jo.take("m.change_password"_ls), result.changePassword); - fromJson(jo.take("m.room_versions"_ls), result.roomVersions); - fromJson(jo, result.additionalProperties); - } - }; -} // namespace QMatrixClient - -class GetCapabilitiesJob::Private -{ - public: - Capabilities capabilities; -}; +using namespace Quotient; QUrl GetCapabilitiesJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/capabilities"); + QStringLiteral("/_matrix/client/r0") + % "/capabilities"); } -static const auto GetCapabilitiesJobName = QStringLiteral("GetCapabilitiesJob"); - GetCapabilitiesJob::GetCapabilitiesJob() - : BaseJob(HttpVerb::Get, GetCapabilitiesJobName, - basePath % "/capabilities") - , d(new Private) -{ -} - -GetCapabilitiesJob::~GetCapabilitiesJob() = default; - -const GetCapabilitiesJob::Capabilities& GetCapabilitiesJob::capabilities() const + : BaseJob(HttpVerb::Get, QStringLiteral("GetCapabilitiesJob"), + QStringLiteral("/_matrix/client/r0") % "/capabilities") { - return d->capabilities; + addExpectedKey("capabilities"); } - -BaseJob::Status GetCapabilitiesJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("capabilities"_ls)) - return { JsonParseError, - "The key 'capabilities' not found in the response" }; - fromJson(json.value("capabilities"_ls), d->capabilities); - return Success; -} - diff --git a/lib/csapi/capabilities.h b/lib/csapi/capabilities.h index 06a8bf0d..da50c8c1 100644 --- a/lib/csapi/capabilities.h +++ b/lib/csapi/capabilities.h @@ -6,77 +6,93 @@ #include "jobs/basejob.h" -#include <QtCore/QJsonObject> -#include "converters.h" -#include <QtCore/QHash> +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Gets information about the server's capabilities. + * + * Gets information about the server's supported feature set + * and other relevant capabilities. + */ +class GetCapabilitiesJob : public BaseJob { +public: + // Inner data structures + + /// Capability to indicate if the user can change their password. + struct ChangePasswordCapability { + /// True if the user can change their password, false otherwise. + bool enabled; + }; + + /// The room versions the server supports. + struct RoomVersionsCapability { + /// The default room version the server is using for new rooms. + QString defaultVersion; + /// A detailed description of the room versions the server supports. + QHash<QString, QString> available; + }; + + /// The custom capabilities the server supports, using the + /// Java package naming convention. + struct Capabilities { + /// Capability to indicate if the user can change their password. + Omittable<ChangePasswordCapability> changePassword; + /// The room versions the server supports. + Omittable<RoomVersionsCapability> roomVersions; + /// The custom capabilities the server supports, using the + /// Java package naming convention. + QHash<QString, QJsonObject> additionalProperties; + }; + + // Construction/destruction /// Gets information about the server's capabilities. - /// - /// Gets information about the server's supported feature set - /// and other relevant capabilities. - class GetCapabilitiesJob : public BaseJob + explicit GetCapabilitiesJob(); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetCapabilitiesJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + // Result properties + + /// The custom capabilities the server supports, using the + /// Java package naming convention. + Capabilities capabilities() const { - public: - // Inner data structures - - /// Capability to indicate if the user can change their password. - struct ChangePasswordCapability - { - /// True if the user can change their password, false otherwise. - bool enabled; - }; - - /// The room versions the server supports. - struct RoomVersionsCapability - { - /// The default room version the server is using for new rooms. - QString defaultVersion; - /// A detailed description of the room versions the server supports. - QHash<QString, QString> available; - }; - - /// The custom capabilities the server supports, using the - /// Java package naming convention. - struct Capabilities - { - /// Capability to indicate if the user can change their password. - Omittable<ChangePasswordCapability> changePassword; - /// The room versions the server supports. - Omittable<RoomVersionsCapability> roomVersions; - /// The custom capabilities the server supports, using the - /// Java package naming convention. - QHash<QString, QJsonObject> additionalProperties; - }; - - // Construction/destruction - - explicit GetCapabilitiesJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetCapabilitiesJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetCapabilitiesJob() override; - - // Result properties - - /// The custom capabilities the server supports, using the - /// Java package naming convention. - const Capabilities& capabilities() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + return loadFromJson<Capabilities>("capabilities"_ls); + } +}; + +template <> +struct JsonObjectConverter<GetCapabilitiesJob::ChangePasswordCapability> { + static void fillFrom(const QJsonObject& jo, + GetCapabilitiesJob::ChangePasswordCapability& result) + { + fromJson(jo.value("enabled"_ls), result.enabled); + } +}; + +template <> +struct JsonObjectConverter<GetCapabilitiesJob::RoomVersionsCapability> { + static void fillFrom(const QJsonObject& jo, + GetCapabilitiesJob::RoomVersionsCapability& result) + { + fromJson(jo.value("default"_ls), result.defaultVersion); + fromJson(jo.value("available"_ls), result.available); + } +}; + +template <> +struct JsonObjectConverter<GetCapabilitiesJob::Capabilities> { + static void fillFrom(QJsonObject jo, + GetCapabilitiesJob::Capabilities& result) + { + fromJson(jo.take("m.change_password"_ls), result.changePassword); + fromJson(jo.take("m.room_versions"_ls), result.roomVersions); + fromJson(jo, result.additionalProperties); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp index 22223985..7ae89739 100644 --- a/lib/csapi/content-repo.cpp +++ b/lib/csapi/content-repo.cpp @@ -4,184 +4,89 @@ #include "content-repo.h" -#include "converters.h" - -#include <QtNetwork/QNetworkReply> #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/media/r0"); - -class UploadContentJob::Private -{ - public: - QString contentUri; -}; +using namespace Quotient; -BaseJob::Query queryToUploadContent(const QString& filename) +auto queryToUploadContent(const QString& filename) { BaseJob::Query _q; addParam<IfNotEmpty>(_q, QStringLiteral("filename"), filename); return _q; } -static const auto UploadContentJobName = QStringLiteral("UploadContentJob"); - -UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename, const QString& contentType) - : BaseJob(HttpVerb::Post, UploadContentJobName, - basePath % "/upload", - queryToUploadContent(filename)) - , d(new Private) +UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename, + const QString& contentType) + : BaseJob(HttpVerb::Post, QStringLiteral("UploadContentJob"), + QStringLiteral("/_matrix/media/r0") % "/upload", + queryToUploadContent(filename)) { setRequestHeader("Content-Type", contentType.toLatin1()); - setRequestData(Data(content)); + addExpectedKey("content_uri"); } -UploadContentJob::~UploadContentJob() = default; - -const QString& UploadContentJob::contentUri() const -{ - return d->contentUri; -} - -BaseJob::Status UploadContentJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("content_uri"_ls)) - return { JsonParseError, - "The key 'content_uri' not found in the response" }; - fromJson(json.value("content_uri"_ls), d->contentUri); - return Success; -} - -class GetContentJob::Private -{ - public: - QString contentType; - QString contentDisposition; - QIODevice* data; -}; - -BaseJob::Query queryToGetContent(bool allowRemote) +auto queryToGetContent(bool allowRemote) { BaseJob::Query _q; addParam<IfNotEmpty>(_q, QStringLiteral("allow_remote"), allowRemote); return _q; } -QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, bool allowRemote) +QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, + const QString& mediaId, bool allowRemote) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/download/" % serverName % "/" % mediaId, - queryToGetContent(allowRemote)); + QStringLiteral("/_matrix/media/r0") + % "/download/" % serverName % "/" + % mediaId, + queryToGetContent(allowRemote)); } -static const auto GetContentJobName = QStringLiteral("GetContentJob"); - -GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId, bool allowRemote) - : BaseJob(HttpVerb::Get, GetContentJobName, - basePath % "/download/" % serverName % "/" % mediaId, - queryToGetContent(allowRemote), - {}, false) - , d(new Private) +GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId, + bool allowRemote) + : BaseJob(HttpVerb::Get, QStringLiteral("GetContentJob"), + QStringLiteral("/_matrix/media/r0") % "/download/" % serverName + % "/" % mediaId, + queryToGetContent(allowRemote), {}, false) { setExpectedContentTypes({ "*/*" }); } -GetContentJob::~GetContentJob() = default; - -const QString& GetContentJob::contentType() const -{ - return d->contentType; -} - -const QString& GetContentJob::contentDisposition() const -{ - return d->contentDisposition; -} - -QIODevice* GetContentJob::data() const -{ - return d->data; -} - -BaseJob::Status GetContentJob::parseReply(QNetworkReply* reply) -{ - d->contentType = reply->rawHeader("Content-Type"); - d->contentDisposition = reply->rawHeader("Content-Disposition"); - d->data = reply; - return Success; -} - -class GetContentOverrideNameJob::Private -{ - public: - QString contentType; - QString contentDisposition; - QIODevice* data; -}; - -BaseJob::Query queryToGetContentOverrideName(bool allowRemote) +auto queryToGetContentOverrideName(bool allowRemote) { BaseJob::Query _q; addParam<IfNotEmpty>(_q, QStringLiteral("allow_remote"), allowRemote); return _q; } -QUrl GetContentOverrideNameJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, const QString& fileName, bool allowRemote) +QUrl GetContentOverrideNameJob::makeRequestUrl(QUrl baseUrl, + const QString& serverName, + const QString& mediaId, + const QString& fileName, + bool allowRemote) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/download/" % serverName % "/" % mediaId % "/" % fileName, - queryToGetContentOverrideName(allowRemote)); + QStringLiteral("/_matrix/media/r0") + % "/download/" % serverName % "/" + % mediaId % "/" % fileName, + queryToGetContentOverrideName(allowRemote)); } -static const auto GetContentOverrideNameJobName = QStringLiteral("GetContentOverrideNameJob"); - -GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName, const QString& mediaId, const QString& fileName, bool allowRemote) - : BaseJob(HttpVerb::Get, GetContentOverrideNameJobName, - basePath % "/download/" % serverName % "/" % mediaId % "/" % fileName, - queryToGetContentOverrideName(allowRemote), - {}, false) - , d(new Private) +GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName, + const QString& mediaId, + const QString& fileName, + bool allowRemote) + : BaseJob(HttpVerb::Get, QStringLiteral("GetContentOverrideNameJob"), + QStringLiteral("/_matrix/media/r0") % "/download/" % serverName + % "/" % mediaId % "/" % fileName, + queryToGetContentOverrideName(allowRemote), {}, false) { setExpectedContentTypes({ "*/*" }); } -GetContentOverrideNameJob::~GetContentOverrideNameJob() = default; - -const QString& GetContentOverrideNameJob::contentType() const -{ - return d->contentType; -} - -const QString& GetContentOverrideNameJob::contentDisposition() const -{ - return d->contentDisposition; -} - -QIODevice* GetContentOverrideNameJob::data() const -{ - return d->data; -} - -BaseJob::Status GetContentOverrideNameJob::parseReply(QNetworkReply* reply) -{ - d->contentType = reply->rawHeader("Content-Type"); - d->contentDisposition = reply->rawHeader("Content-Disposition"); - d->data = reply; - return Success; -} - -class GetContentThumbnailJob::Private -{ - public: - QString contentType; - QIODevice* data; -}; - -BaseJob::Query queryToGetContentThumbnail(int width, int height, const QString& method, bool allowRemote) +auto queryToGetContentThumbnail(int width, int height, const QString& method, + bool allowRemote) { BaseJob::Query _q; addParam<>(_q, QStringLiteral("width"), width); @@ -191,52 +96,33 @@ BaseJob::Query queryToGetContentThumbnail(int width, int height, const QString& return _q; } -QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, int width, int height, const QString& method, bool allowRemote) +QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl, + const QString& serverName, + const QString& mediaId, int width, + int height, const QString& method, + bool allowRemote) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/thumbnail/" % serverName % "/" % mediaId, - queryToGetContentThumbnail(width, height, method, allowRemote)); + return BaseJob::makeRequestUrl( + std::move(baseUrl), + QStringLiteral("/_matrix/media/r0") % "/thumbnail/" % serverName % "/" + % mediaId, + queryToGetContentThumbnail(width, height, method, allowRemote)); } -static const auto GetContentThumbnailJobName = QStringLiteral("GetContentThumbnailJob"); - -GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName, const QString& mediaId, int width, int height, const QString& method, bool allowRemote) - : BaseJob(HttpVerb::Get, GetContentThumbnailJobName, - basePath % "/thumbnail/" % serverName % "/" % mediaId, - queryToGetContentThumbnail(width, height, method, allowRemote), - {}, false) - , d(new Private) +GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName, + const QString& mediaId, int width, + int height, const QString& method, + bool allowRemote) + : BaseJob(HttpVerb::Get, QStringLiteral("GetContentThumbnailJob"), + QStringLiteral("/_matrix/media/r0") % "/thumbnail/" % serverName + % "/" % mediaId, + queryToGetContentThumbnail(width, height, method, allowRemote), + {}, false) { setExpectedContentTypes({ "image/jpeg", "image/png" }); } -GetContentThumbnailJob::~GetContentThumbnailJob() = default; - -const QString& GetContentThumbnailJob::contentType() const -{ - return d->contentType; -} - -QIODevice* GetContentThumbnailJob::data() const -{ - return d->data; -} - -BaseJob::Status GetContentThumbnailJob::parseReply(QNetworkReply* reply) -{ - d->contentType = reply->rawHeader("Content-Type"); - d->data = reply; - return Success; -} - -class GetUrlPreviewJob::Private -{ - public: - Omittable<qint64> matrixImageSize; - QString ogImage; -}; - -BaseJob::Query queryToGetUrlPreview(const QString& url, Omittable<qint64> ts) +auto queryToGetUrlPreview(const QString& url, Omittable<qint64> ts) { BaseJob::Query _q; addParam<>(_q, QStringLiteral("url"), url); @@ -244,75 +130,29 @@ BaseJob::Query queryToGetUrlPreview(const QString& url, Omittable<qint64> ts) return _q; } -QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QString& url, Omittable<qint64> ts) +QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QString& url, + Omittable<qint64> ts) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/preview_url", - queryToGetUrlPreview(url, ts)); + QStringLiteral("/_matrix/media/r0") + % "/preview_url", + queryToGetUrlPreview(url, ts)); } -static const auto GetUrlPreviewJobName = QStringLiteral("GetUrlPreviewJob"); - GetUrlPreviewJob::GetUrlPreviewJob(const QString& url, Omittable<qint64> ts) - : BaseJob(HttpVerb::Get, GetUrlPreviewJobName, - basePath % "/preview_url", - queryToGetUrlPreview(url, ts)) - , d(new Private) -{ -} - -GetUrlPreviewJob::~GetUrlPreviewJob() = default; - -Omittable<qint64> GetUrlPreviewJob::matrixImageSize() const -{ - return d->matrixImageSize; -} - -const QString& GetUrlPreviewJob::ogImage() const -{ - return d->ogImage; -} - -BaseJob::Status GetUrlPreviewJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("matrix:image:size"_ls), d->matrixImageSize); - fromJson(json.value("og:image"_ls), d->ogImage); - return Success; -} - -class GetConfigJob::Private -{ - public: - Omittable<qint64> uploadSize; -}; + : BaseJob(HttpVerb::Get, QStringLiteral("GetUrlPreviewJob"), + QStringLiteral("/_matrix/media/r0") % "/preview_url", + queryToGetUrlPreview(url, ts)) +{} QUrl GetConfigJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/config"); + QStringLiteral("/_matrix/media/r0") + % "/config"); } -static const auto GetConfigJobName = QStringLiteral("GetConfigJob"); - GetConfigJob::GetConfigJob() - : BaseJob(HttpVerb::Get, GetConfigJobName, - basePath % "/config") - , d(new Private) -{ -} - -GetConfigJob::~GetConfigJob() = default; - -Omittable<qint64> GetConfigJob::uploadSize() const -{ - return d->uploadSize; -} - -BaseJob::Status GetConfigJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("m.upload.size"_ls), d->uploadSize); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("GetConfigJob"), + QStringLiteral("/_matrix/media/r0") % "/config") +{} diff --git a/lib/csapi/content-repo.h b/lib/csapi/content-repo.h index 5ef2e0d6..ed67485c 100644 --- a/lib/csapi/content-repo.h +++ b/lib/csapi/content-repo.h @@ -6,255 +6,273 @@ #include "jobs/basejob.h" -#include "converters.h" #include <QtCore/QIODevice> +#include <QtNetwork/QNetworkReply> -namespace QMatrixClient -{ - // Operations +namespace Quotient { - /// Upload some content to the content repository. - class UploadContentJob : public BaseJob - { - public: - /*! Upload some content to the content repository. - * \param content - * \param filename - * The name of the file being uploaded - * \param contentType - * The content type of the file being uploaded - */ - explicit UploadContentJob(QIODevice* content, const QString& filename = {}, const QString& contentType = {}); - ~UploadContentJob() override; - - // Result properties - - /// The MXC URI to the uploaded content. - const QString& contentUri() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Download content from the content repository. - class GetContentJob : public BaseJob - { - public: - /*! Download content from the content repository. - * \param serverName - * The server name from the ``mxc://`` URI (the authoritory component) - * \param mediaId - * The media ID from the ``mxc://`` URI (the path component) - * \param allowRemote - * Indicates to the server that it should not attempt to fetch the media if it is deemed - * remote. This is to prevent routing loops where the server contacts itself. Defaults to - * true if not provided. - */ - explicit GetContentJob(const QString& serverName, const QString& mediaId, bool allowRemote = true); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetContentJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, bool allowRemote = true); - - ~GetContentJob() override; - - // Result properties - - /// The content type of the file that was previously uploaded. - const QString& contentType() const; - /// The name of the file that was previously uploaded, if set. - const QString& contentDisposition() const; - /// The content that was previously uploaded. - QIODevice* data() const; - - protected: - Status parseReply(QNetworkReply* reply) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Download content from the content repository as a given filename. - class GetContentOverrideNameJob : public BaseJob +/*! \brief Upload some content to the content repository. + * + */ +class UploadContentJob : public BaseJob { +public: + /*! \brief Upload some content to the content repository. + * + * \param content + * The content to be uploaded. + * + * \param filename + * The name of the file being uploaded + * + * \param contentType + * The content type of the file being uploaded + */ + explicit UploadContentJob(QIODevice* content, const QString& filename = {}, + const QString& contentType = {}); + + // Result properties + + /// The `MXC URI`_ to the uploaded content. + QString contentUri() const { - public: - /*! Download content from the content repository as a given filename. - * \param serverName - * The server name from the ``mxc://`` URI (the authoritory component) - * \param mediaId - * The media ID from the ``mxc://`` URI (the path component) - * \param fileName - * The filename to give in the Content-Disposition - * \param allowRemote - * Indicates to the server that it should not attempt to fetch the media if it is deemed - * remote. This is to prevent routing loops where the server contacts itself. Defaults to - * true if not provided. - */ - explicit GetContentOverrideNameJob(const QString& serverName, const QString& mediaId, const QString& fileName, bool allowRemote = true); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetContentOverrideNameJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, const QString& fileName, bool allowRemote = true); - - ~GetContentOverrideNameJob() override; - - // Result properties - - /// The content type of the file that was previously uploaded. - const QString& contentType() const; - /// The name of file given in the request - const QString& contentDisposition() const; - /// The content that was previously uploaded. - QIODevice* data() const; - - protected: - Status parseReply(QNetworkReply* reply) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Download a thumbnail of the content from the content repository. - class GetContentThumbnailJob : public BaseJob + return loadFromJson<QString>("content_uri"_ls); + } +}; + +/*! \brief Download content from the content repository. + * + */ +class GetContentJob : public BaseJob { +public: + /*! \brief Download content from the content repository. + * + * \param serverName + * The server name from the ``mxc://`` URI (the authoritory component) + * + * \param mediaId + * The media ID from the ``mxc://`` URI (the path component) + * + * \param allowRemote + * Indicates to the server that it should not attempt to fetch the media + * if it is deemed remote. This is to prevent routing loops where the server + * contacts itself. Defaults to true if not provided. + */ + explicit GetContentJob(const QString& serverName, const QString& mediaId, + bool allowRemote = true); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetContentJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, + const QString& mediaId, bool allowRemote = true); + + // Result properties + + /// The content type of the file that was previously uploaded. + QString contentType() const { return reply()->rawHeader("Content-Type"); } + + /// The name of the file that was previously uploaded, if set. + QString contentDisposition() const { - public: - /*! Download a thumbnail of the content from the content repository. - * \param serverName - * The server name from the ``mxc://`` URI (the authoritory component) - * \param mediaId - * The media ID from the ``mxc://`` URI (the path component) - * \param width - * The *desired* width of the thumbnail. The actual thumbnail may not - * match the size specified. - * \param height - * The *desired* height of the thumbnail. The actual thumbnail may not - * match the size specified. - * \param method - * The desired resizing method. - * \param allowRemote - * Indicates to the server that it should not attempt to fetch the media if it is deemed - * remote. This is to prevent routing loops where the server contacts itself. Defaults to - * true if not provided. - */ - explicit GetContentThumbnailJob(const QString& serverName, const QString& mediaId, int width, int height, const QString& method = {}, bool allowRemote = true); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetContentThumbnailJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, int width, int height, const QString& method = {}, bool allowRemote = true); - - ~GetContentThumbnailJob() override; - - // Result properties - - /// The content type of the thumbnail. - const QString& contentType() const; - /// A thumbnail of the requested content. - QIODevice* data() const; - - protected: - Status parseReply(QNetworkReply* reply) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Get information about a URL for a client - class GetUrlPreviewJob : public BaseJob + return reply()->rawHeader("Content-Disposition"); + } + + /// The content that was previously uploaded. + QIODevice* data() { return reply(); } +}; + +/*! \brief Download content from the content repository overriding the file name + * + * This will download content from the content repository (same as + * the previous endpoint) but replace the target file name with the one + * provided by the caller. + */ +class GetContentOverrideNameJob : public BaseJob { +public: + /*! \brief Download content from the content repository overriding the file + * name + * + * \param serverName + * The server name from the ``mxc://`` URI (the authoritory component) + * + * \param mediaId + * The media ID from the ``mxc://`` URI (the path component) + * + * \param fileName + * A filename to give in the ``Content-Disposition`` header. + * + * \param allowRemote + * Indicates to the server that it should not attempt to fetch the media + * if it is deemed remote. This is to prevent routing loops where the server + * contacts itself. Defaults to true if not provided. + */ + explicit GetContentOverrideNameJob(const QString& serverName, + const QString& mediaId, + const QString& fileName, + bool allowRemote = true); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetContentOverrideNameJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, + const QString& mediaId, const QString& fileName, + bool allowRemote = true); + + // Result properties + + /// The content type of the file that was previously uploaded. + QString contentType() const { return reply()->rawHeader("Content-Type"); } + + /// The ``fileName`` requested or the name of the file that was previously + /// uploaded, if set. + QString contentDisposition() const { - public: - /*! Get information about a URL for a client - * \param url - * The URL to get a preview of - * \param ts - * The preferred point in time to return a preview for. The server may - * return a newer version if it does not have the requested version - * available. - */ - explicit GetUrlPreviewJob(const QString& url, Omittable<qint64> ts = none); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetUrlPreviewJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& url, Omittable<qint64> ts = none); - - ~GetUrlPreviewJob() override; - - // Result properties - - /// The byte-size of the image. Omitted if there is no image attached. - Omittable<qint64> matrixImageSize() const; - /// An MXC URI to the image. Omitted if there is no image. - const QString& ogImage() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; + return reply()->rawHeader("Content-Disposition"); + } + /// The content that was previously uploaded. + QIODevice* data() { return reply(); } +}; + +/*! \brief Download a thumbnail of content from the content repository + * + * Download a thumbnail of content from the content repository. + * See the `thumbnailing <#thumbnails>`_ section for more information. + */ +class GetContentThumbnailJob : public BaseJob { +public: + /*! \brief Download a thumbnail of content from the content repository + * + * \param serverName + * The server name from the ``mxc://`` URI (the authoritory component) + * + * \param mediaId + * The media ID from the ``mxc://`` URI (the path component) + * + * \param width + * The *desired* width of the thumbnail. The actual thumbnail may be + * larger than the size specified. + * + * \param height + * The *desired* height of the thumbnail. The actual thumbnail may be + * larger than the size specified. + * + * \param method + * The desired resizing method. See the `thumbnailing <#thumbnails>`_ + * section for more information. + * + * \param allowRemote + * Indicates to the server that it should not attempt to fetch + * the media if it is deemed remote. This is to prevent routing loops + * where the server contacts itself. Defaults to true if not provided. + */ + explicit GetContentThumbnailJob(const QString& serverName, + const QString& mediaId, int width, + int height, const QString& method = {}, + bool allowRemote = true); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetContentThumbnailJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, + const QString& mediaId, int width, int height, + const QString& method = {}, + bool allowRemote = true); + + // Result properties + + /// The content type of the thumbnail. + QString contentType() const { return reply()->rawHeader("Content-Type"); } + + /// A thumbnail of the requested content. + QIODevice* data() { return reply(); } +}; + +/*! \brief Get information about a URL for a client + * + * Get information about a URL for the client. Typically this is called when a + * client sees a URL in a message and wants to render a preview for the user. + * + * .. Note:: + * Clients should consider avoiding this endpoint for URLs posted in encrypted + * rooms. Encrypted rooms often contain more sensitive information the users + * do not want to share with the homeserver, and this can mean that the URLs + * being shared should also not be shared with the homeserver. + */ +class GetUrlPreviewJob : public BaseJob { +public: + /*! \brief Get information about a URL for a client + * + * \param url + * The URL to get a preview of. + * + * \param ts + * The preferred point in time to return a preview for. The server may + * return a newer version if it does not have the requested version + * available. + */ + explicit GetUrlPreviewJob(const QString& url, Omittable<qint64> ts = none); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetUrlPreviewJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& url, + Omittable<qint64> ts = none); + + // Result properties + + /// The byte-size of the image. Omitted if there is no image attached. + Omittable<qint64> matrixImageSize() const + { + return loadFromJson<Omittable<qint64>>("matrix:image:size"_ls); + } + + /// An `MXC URI`_ to the image. Omitted if there is no image. + QString ogImage() const { return loadFromJson<QString>("og:image"_ls); } +}; + +/*! \brief Get the configuration for the content repository. + * + * This endpoint allows clients to retrieve the configuration of the content + * repository, such as upload limitations. + * Clients SHOULD use this as a guide when using content repository endpoints. + * All values are intentionally left optional. Clients SHOULD follow + * the advice given in the field description when the field is not available. + * + * **NOTE:** Both clients and server administrators should be aware that proxies + * between the client and the server may affect the apparent behaviour of + * content repository APIs, for example, proxies may enforce a lower upload size + * limit than is advertised by the server on this endpoint. + */ +class GetConfigJob : public BaseJob { +public: /// Get the configuration for the content repository. - /// - /// This endpoint allows clients to retrieve the configuration of the content - /// repository, such as upload limitations. - /// Clients SHOULD use this as a guide when using content repository endpoints. - /// All values are intentionally left optional. Clients SHOULD follow - /// the advice given in the field description when the field is not available. - /// - /// **NOTE:** Both clients and server administrators should be aware that proxies - /// between the client and the server may affect the apparent behaviour of content - /// repository APIs, for example, proxies may enforce a lower upload size limit - /// than is advertised by the server on this endpoint. - class GetConfigJob : public BaseJob + explicit GetConfigJob(); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetConfigJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + // Result properties + + /// The maximum size an upload can be in bytes. + /// Clients SHOULD use this as a guide when uploading content. + /// If not listed or null, the size limit should be treated as unknown. + Omittable<qint64> uploadSize() const { - public: - explicit GetConfigJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetConfigJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetConfigJob() override; - - // Result properties - - /// The maximum size an upload can be in bytes. - /// Clients SHOULD use this as a guide when uploading content. - /// If not listed or null, the size limit should be treated as unknown. - Omittable<qint64> uploadSize() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + return loadFromJson<Omittable<qint64>>("m.upload.size"_ls); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp index 448547ae..a94f9951 100644 --- a/lib/csapi/create_room.cpp +++ b/lib/csapi/create_room.cpp @@ -4,82 +4,38 @@ #include "create_room.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<CreateRoomJob::Invite3pid> - { - static void dumpTo(QJsonObject& jo, const CreateRoomJob::Invite3pid& pod) - { - addParam<>(jo, QStringLiteral("id_server"), pod.idServer); - addParam<>(jo, QStringLiteral("medium"), pod.medium); - addParam<>(jo, QStringLiteral("address"), pod.address); - } - }; - - template <> struct JsonObjectConverter<CreateRoomJob::StateEvent> - { - static void dumpTo(QJsonObject& jo, const CreateRoomJob::StateEvent& pod) - { - addParam<>(jo, QStringLiteral("type"), pod.type); - addParam<IfNotEmpty>(jo, QStringLiteral("state_key"), pod.stateKey); - addParam<>(jo, QStringLiteral("content"), pod.content); - } - }; -} // namespace QMatrixClient - -class CreateRoomJob::Private -{ - public: - QString roomId; -}; - -static const auto CreateRoomJobName = QStringLiteral("CreateRoomJob"); - -CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& roomAliasName, const QString& name, const QString& topic, const QStringList& invite, const QVector<Invite3pid>& invite3pid, const QString& roomVersion, const QJsonObject& creationContent, const QVector<StateEvent>& initialState, const QString& preset, Omittable<bool> isDirect, const QJsonObject& powerLevelContentOverride) - : BaseJob(HttpVerb::Post, CreateRoomJobName, - basePath % "/createRoom") - , d(new Private) +using namespace Quotient; + +CreateRoomJob::CreateRoomJob(const QString& visibility, + const QString& roomAliasName, const QString& name, + const QString& topic, const QStringList& invite, + const QVector<Invite3pid>& invite3pid, + const QString& roomVersion, + const QJsonObject& creationContent, + const QVector<StateEvent>& initialState, + const QString& preset, Omittable<bool> isDirect, + const QJsonObject& powerLevelContentOverride) + : BaseJob(HttpVerb::Post, QStringLiteral("CreateRoomJob"), + QStringLiteral("/_matrix/client/r0") % "/createRoom") { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("visibility"), visibility); - addParam<IfNotEmpty>(_data, QStringLiteral("room_alias_name"), roomAliasName); + addParam<IfNotEmpty>(_data, QStringLiteral("room_alias_name"), + roomAliasName); addParam<IfNotEmpty>(_data, QStringLiteral("name"), name); addParam<IfNotEmpty>(_data, QStringLiteral("topic"), topic); addParam<IfNotEmpty>(_data, QStringLiteral("invite"), invite); addParam<IfNotEmpty>(_data, QStringLiteral("invite_3pid"), invite3pid); addParam<IfNotEmpty>(_data, QStringLiteral("room_version"), roomVersion); - addParam<IfNotEmpty>(_data, QStringLiteral("creation_content"), creationContent); + addParam<IfNotEmpty>(_data, QStringLiteral("creation_content"), + creationContent); addParam<IfNotEmpty>(_data, QStringLiteral("initial_state"), initialState); addParam<IfNotEmpty>(_data, QStringLiteral("preset"), preset); addParam<IfNotEmpty>(_data, QStringLiteral("is_direct"), isDirect); - addParam<IfNotEmpty>(_data, QStringLiteral("power_level_content_override"), powerLevelContentOverride); - setRequestData(_data); -} - -CreateRoomJob::~CreateRoomJob() = default; - -const QString& CreateRoomJob::roomId() const -{ - return d->roomId; + addParam<IfNotEmpty>(_data, QStringLiteral("power_level_content_override"), + powerLevelContentOverride); + setRequestData(std::move(_data)); + addExpectedKey("room_id"); } - -BaseJob::Status CreateRoomJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("room_id"_ls)) - return { JsonParseError, - "The key 'room_id' not found in the response" }; - fromJson(json.value("room_id"_ls), d->roomId); - return Success; -} - diff --git a/lib/csapi/create_room.h b/lib/csapi/create_room.h index d7c01d00..6a718ff4 100644 --- a/lib/csapi/create_room.h +++ b/lib/csapi/create_room.h @@ -6,229 +6,305 @@ #include "jobs/basejob.h" -#include <QtCore/QJsonObject> -#include "converters.h" -#include <QtCore/QVector> +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Create a new room + * + * Create a new room with various configuration options. + * + * The server MUST apply the normal state resolution rules when creating + * the new room, including checking power levels for each event. It MUST + * apply the events implied by the request in the following order: + * + * 1. The ``m.room.create`` event itself. Must be the first event in the + * room. + * + * 2. An ``m.room.member`` event for the creator to join the room. This is + * needed so the remaining events can be sent. + * + * 3. A default ``m.room.power_levels`` event, giving the room creator + * (and not other members) permission to send state events. Overridden + * by the ``power_level_content_override`` parameter. + * + * 4. Events set by the ``preset``. Currently these are the + * ``m.room.join_rules``, + * ``m.room.history_visibility``, and ``m.room.guest_access`` state events. + * + * 5. Events listed in ``initial_state``, in the order that they are + * listed. + * + * 6. Events implied by ``name`` and ``topic`` (``m.room.name`` and + * ``m.room.topic`` state events). + * + * 7. Invite events implied by ``invite`` and ``invite_3pid`` (``m.room.member`` + * with + * ``membership: invite`` and ``m.room.third_party_invite``). + * + * The available presets do the following with respect to room state: + * + * ======================== ============== ====================== + * ================ ========= Preset ``join_rules`` + * ``history_visibility`` ``guest_access`` Other + * ======================== ============== ====================== + * ================ ========= + * ``private_chat`` ``invite`` ``shared`` ``can_join`` + * ``trusted_private_chat`` ``invite`` ``shared`` ``can_join`` All + * invitees are given the same power level as the room creator. + * ``public_chat`` ``public`` ``shared`` ``forbidden`` + * ======================== ============== ====================== + * ================ ========= + * + * The server will create a ``m.room.create`` event in the room with the + * requesting user as the creator, alongside other keys provided in the + * ``creation_content``. + */ +class CreateRoomJob : public BaseJob { +public: + // Inner data structures - /// Create a new room - /// /// Create a new room with various configuration options. - /// + /// /// The server MUST apply the normal state resolution rules when creating /// the new room, including checking power levels for each event. It MUST /// apply the events implied by the request in the following order: - /// - /// 0. A default ``m.room.power_levels`` event, giving the room creator + /// + /// 1. The ``m.room.create`` event itself. Must be the first event in the + /// room. + /// + /// 2. An ``m.room.member`` event for the creator to join the room. This is + /// needed so the remaining events can be sent. + /// + /// 3. A default ``m.room.power_levels`` event, giving the room creator /// (and not other members) permission to send state events. Overridden /// by the ``power_level_content_override`` parameter. - /// - /// 1. Events set by the ``preset``. Currently these are the ``m.room.join_rules``, - /// ``m.room.history_visibility``, and ``m.room.guest_access`` state events. - /// - /// 2. Events listed in ``initial_state``, in the order that they are + /// + /// 4. Events set by the ``preset``. Currently these are the + /// ``m.room.join_rules``, + /// ``m.room.history_visibility``, and ``m.room.guest_access`` state + /// events. + /// + /// 5. Events listed in ``initial_state``, in the order that they are /// listed. - /// - /// 3. Events implied by ``name`` and ``topic`` (``m.room.name`` and ``m.room.topic`` + /// + /// 6. Events implied by ``name`` and ``topic`` (``m.room.name`` and + /// ``m.room.topic`` /// state events). - /// - /// 4. Invite events implied by ``invite`` and ``invite_3pid`` (``m.room.member`` with + /// + /// 7. Invite events implied by ``invite`` and ``invite_3pid`` + /// (``m.room.member`` with /// ``membership: invite`` and ``m.room.third_party_invite``). - /// + /// /// The available presets do the following with respect to room state: - /// - /// ======================== ============== ====================== ================ ========= - /// Preset ``join_rules`` ``history_visibility`` ``guest_access`` Other - /// ======================== ============== ====================== ================ ========= - /// ``private_chat`` ``invite`` ``shared`` ``can_join`` - /// ``trusted_private_chat`` ``invite`` ``shared`` ``can_join`` All invitees are given the same power level as the room creator. - /// ``public_chat`` ``public`` ``shared`` ``forbidden`` - /// ======================== ============== ====================== ================ ========= - /// + /// + /// ======================== ============== ====================== + /// ================ ========= + /// Preset ``join_rules`` ``history_visibility`` + /// ``guest_access`` Other + /// ======================== ============== ====================== + /// ================ ========= + /// ``private_chat`` ``invite`` ``shared`` ``can_join`` + /// ``trusted_private_chat`` ``invite`` ``shared`` ``can_join`` All + /// invitees are given the same power level as the room creator. + /// ``public_chat`` ``public`` ``shared`` ``forbidden`` + /// ======================== ============== ====================== + /// ================ ========= + /// /// The server will create a ``m.room.create`` event in the room with the /// requesting user as the creator, alongside other keys provided in the /// ``creation_content``. - class CreateRoomJob : public BaseJob - { - public: - // Inner data structures + struct Invite3pid { + /// The hostname+port of the identity server which should be used for + /// third party identifier lookups. + QString idServer; + /// An access token previously registered with the identity server. + /// Servers can treat this as optional to distinguish between + /// r0.5-compatible clients and this specification version. + QString idAccessToken; + /// The kind of address being passed in the address field, for example + /// ``email``. + QString medium; + /// The invitee's third party identifier. + QString address; + }; - /// Create a new room with various configuration options. - /// - /// The server MUST apply the normal state resolution rules when creating - /// the new room, including checking power levels for each event. It MUST - /// apply the events implied by the request in the following order: - /// - /// 0. A default ``m.room.power_levels`` event, giving the room creator - /// (and not other members) permission to send state events. Overridden - /// by the ``power_level_content_override`` parameter. - /// - /// 1. Events set by the ``preset``. Currently these are the ``m.room.join_rules``, - /// ``m.room.history_visibility``, and ``m.room.guest_access`` state events. - /// - /// 2. Events listed in ``initial_state``, in the order that they are - /// listed. - /// - /// 3. Events implied by ``name`` and ``topic`` (``m.room.name`` and ``m.room.topic`` - /// state events). - /// - /// 4. Invite events implied by ``invite`` and ``invite_3pid`` (``m.room.member`` with - /// ``membership: invite`` and ``m.room.third_party_invite``). - /// - /// The available presets do the following with respect to room state: - /// - /// ======================== ============== ====================== ================ ========= - /// Preset ``join_rules`` ``history_visibility`` ``guest_access`` Other - /// ======================== ============== ====================== ================ ========= - /// ``private_chat`` ``invite`` ``shared`` ``can_join`` - /// ``trusted_private_chat`` ``invite`` ``shared`` ``can_join`` All invitees are given the same power level as the room creator. - /// ``public_chat`` ``public`` ``shared`` ``forbidden`` - /// ======================== ============== ====================== ================ ========= - /// - /// The server will create a ``m.room.create`` event in the room with the - /// requesting user as the creator, alongside other keys provided in the - /// ``creation_content``. - struct Invite3pid - { - /// The hostname+port of the identity server which should be used for third party identifier lookups. - QString idServer; - /// The kind of address being passed in the address field, for example ``email``. - QString medium; - /// The invitee's third party identifier. - QString address; - }; + /// Create a new room with various configuration options. + /// + /// The server MUST apply the normal state resolution rules when creating + /// the new room, including checking power levels for each event. It MUST + /// apply the events implied by the request in the following order: + /// + /// 1. The ``m.room.create`` event itself. Must be the first event in the + /// room. + /// + /// 2. An ``m.room.member`` event for the creator to join the room. This is + /// needed so the remaining events can be sent. + /// + /// 3. A default ``m.room.power_levels`` event, giving the room creator + /// (and not other members) permission to send state events. Overridden + /// by the ``power_level_content_override`` parameter. + /// + /// 4. Events set by the ``preset``. Currently these are the + /// ``m.room.join_rules``, + /// ``m.room.history_visibility``, and ``m.room.guest_access`` state + /// events. + /// + /// 5. Events listed in ``initial_state``, in the order that they are + /// listed. + /// + /// 6. Events implied by ``name`` and ``topic`` (``m.room.name`` and + /// ``m.room.topic`` + /// state events). + /// + /// 7. Invite events implied by ``invite`` and ``invite_3pid`` + /// (``m.room.member`` with + /// ``membership: invite`` and ``m.room.third_party_invite``). + /// + /// The available presets do the following with respect to room state: + /// + /// ======================== ============== ====================== + /// ================ ========= + /// Preset ``join_rules`` ``history_visibility`` + /// ``guest_access`` Other + /// ======================== ============== ====================== + /// ================ ========= + /// ``private_chat`` ``invite`` ``shared`` ``can_join`` + /// ``trusted_private_chat`` ``invite`` ``shared`` ``can_join`` All + /// invitees are given the same power level as the room creator. + /// ``public_chat`` ``public`` ``shared`` ``forbidden`` + /// ======================== ============== ====================== + /// ================ ========= + /// + /// The server will create a ``m.room.create`` event in the room with the + /// requesting user as the creator, alongside other keys provided in the + /// ``creation_content``. + struct StateEvent { + /// The type of event to send. + QString type; + /// The state_key of the state event. Defaults to an empty string. + QString stateKey; + /// The content of the event. + QJsonObject content; + }; - /// Create a new room with various configuration options. - /// - /// The server MUST apply the normal state resolution rules when creating - /// the new room, including checking power levels for each event. It MUST - /// apply the events implied by the request in the following order: - /// - /// 0. A default ``m.room.power_levels`` event, giving the room creator - /// (and not other members) permission to send state events. Overridden - /// by the ``power_level_content_override`` parameter. - /// - /// 1. Events set by the ``preset``. Currently these are the ``m.room.join_rules``, - /// ``m.room.history_visibility``, and ``m.room.guest_access`` state events. - /// - /// 2. Events listed in ``initial_state``, in the order that they are - /// listed. - /// - /// 3. Events implied by ``name`` and ``topic`` (``m.room.name`` and ``m.room.topic`` - /// state events). - /// - /// 4. Invite events implied by ``invite`` and ``invite_3pid`` (``m.room.member`` with - /// ``membership: invite`` and ``m.room.third_party_invite``). - /// - /// The available presets do the following with respect to room state: - /// - /// ======================== ============== ====================== ================ ========= - /// Preset ``join_rules`` ``history_visibility`` ``guest_access`` Other - /// ======================== ============== ====================== ================ ========= - /// ``private_chat`` ``invite`` ``shared`` ``can_join`` - /// ``trusted_private_chat`` ``invite`` ``shared`` ``can_join`` All invitees are given the same power level as the room creator. - /// ``public_chat`` ``public`` ``shared`` ``forbidden`` - /// ======================== ============== ====================== ================ ========= - /// - /// The server will create a ``m.room.create`` event in the room with the - /// requesting user as the creator, alongside other keys provided in the - /// ``creation_content``. - struct StateEvent - { - /// The type of event to send. - QString type; - /// The state_key of the state event. Defaults to an empty string. - QString stateKey; - /// The content of the event. - QJsonObject content; - }; + // Construction/destruction - // Construction/destruction + /*! \brief Create a new room + * + * \param visibility + * A ``public`` visibility indicates that the room will be shown + * in the published room list. A ``private`` visibility will hide + * the room from the published room list. Rooms default to + * ``private`` visibility if this key is not included. NB: This + * should not be confused with ``join_rules`` which also uses the + * word ``public``. + * + * \param roomAliasName + * The desired room alias **local part**. If this is included, a + * room alias will be created and mapped to the newly created + * room. The alias will belong on the *same* homeserver which + * created the room. For example, if this was set to "foo" and + * sent to the homeserver "example.com" the complete room alias + * would be ``#foo:example.com``. + * + * The complete room alias will become the canonical alias for + * the room. + * + * \param name + * If this is included, an ``m.room.name`` event will be sent + * into the room to indicate the name of the room. See Room + * Events for more information on ``m.room.name``. + * + * \param topic + * If this is included, an ``m.room.topic`` event will be sent + * into the room to indicate the topic for the room. See Room + * Events for more information on ``m.room.topic``. + * + * \param invite + * A list of user IDs to invite to the room. This will tell the + * server to invite everyone in the list to the newly created room. + * + * \param invite3pid + * A list of objects representing third party IDs to invite into + * the room. + * + * \param roomVersion + * The room version to set for the room. If not provided, the homeserver + * is to use its configured default. If provided, the homeserver will return + * a 400 error with the errcode ``M_UNSUPPORTED_ROOM_VERSION`` if it does + * not support the room version. + * + * \param creationContent + * Extra keys, such as ``m.federate``, to be added to the content + * of the `m.room.create`_ event. The server will clobber the following + * keys: ``creator``, ``room_version``. Future versions of the + * specification may allow the server to clobber other keys. + * + * \param initialState + * A list of state events to set in the new room. This allows + * the user to override the default state events set in the new + * room. The expected format of the state events are an object + * with type, state_key and content keys set. + * + * Takes precedence over events set by ``preset``, but gets + * overriden by ``name`` and ``topic`` keys. + * + * \param preset + * Convenience parameter for setting various default state events + * based on a preset. + * + * If unspecified, the server should use the ``visibility`` to determine + * which preset to use. A visbility of ``public`` equates to a preset of + * ``public_chat`` and ``private`` visibility equates to a preset of + * ``private_chat``. + * + * \param isDirect + * This flag makes the server set the ``is_direct`` flag on the + * ``m.room.member`` events sent to the users in ``invite`` and + * ``invite_3pid``. See `Direct Messaging`_ for more information. + * + * \param powerLevelContentOverride + * The power level content to override in the default power level + * event. This object is applied on top of the generated + * `m.room.power_levels`_ event content prior to it being sent to the room. + * Defaults to overriding nothing. + */ + explicit CreateRoomJob(const QString& visibility = {}, + const QString& roomAliasName = {}, + const QString& name = {}, const QString& topic = {}, + const QStringList& invite = {}, + const QVector<Invite3pid>& invite3pid = {}, + const QString& roomVersion = {}, + const QJsonObject& creationContent = {}, + const QVector<StateEvent>& initialState = {}, + const QString& preset = {}, + Omittable<bool> isDirect = none, + const QJsonObject& powerLevelContentOverride = {}); - /*! Create a new room - * \param visibility - * A ``public`` visibility indicates that the room will be shown - * in the published room list. A ``private`` visibility will hide - * the room from the published room list. Rooms default to - * ``private`` visibility if this key is not included. NB: This - * should not be confused with ``join_rules`` which also uses the - * word ``public``. - * \param roomAliasName - * The desired room alias **local part**. If this is included, a - * room alias will be created and mapped to the newly created - * room. The alias will belong on the *same* homeserver which - * created the room. For example, if this was set to "foo" and - * sent to the homeserver "example.com" the complete room alias - * would be ``#foo:example.com``. - * - * The complete room alias will become the canonical alias for - * the room. - * \param name - * If this is included, an ``m.room.name`` event will be sent - * into the room to indicate the name of the room. See Room - * Events for more information on ``m.room.name``. - * \param topic - * If this is included, an ``m.room.topic`` event will be sent - * into the room to indicate the topic for the room. See Room - * Events for more information on ``m.room.topic``. - * \param invite - * A list of user IDs to invite to the room. This will tell the - * server to invite everyone in the list to the newly created room. - * \param invite3pid - * A list of objects representing third party IDs to invite into - * the room. - * \param roomVersion - * The room version to set for the room. If not provided, the homeserver is - * to use its configured default. If provided, the homeserver will return a - * 400 error with the errcode ``M_UNSUPPORTED_ROOM_VERSION`` if it does not - * support the room version. - * \param creationContent - * Extra keys, such as ``m.federate``, to be added to the content - * of the `m.room.create`_ event. The server will clobber the following - * keys: ``creator``, ``room_version``. Future versions of the specification - * may allow the server to clobber other keys. - * \param initialState - * A list of state events to set in the new room. This allows - * the user to override the default state events set in the new - * room. The expected format of the state events are an object - * with type, state_key and content keys set. - * - * Takes precedence over events set by ``preset``, but gets - * overriden by ``name`` and ``topic`` keys. - * \param preset - * Convenience parameter for setting various default state events - * based on a preset. - * - * If unspecified, the server should use the ``visibility`` to determine - * which preset to use. A visbility of ``public`` equates to a preset of - * ``public_chat`` and ``private`` visibility equates to a preset of - * ``private_chat``. - * \param isDirect - * This flag makes the server set the ``is_direct`` flag on the - * ``m.room.member`` events sent to the users in ``invite`` and - * ``invite_3pid``. See `Direct Messaging`_ for more information. - * \param powerLevelContentOverride - * The power level content to override in the default power level - * event. This object is applied on top of the generated `m.room.power_levels`_ - * event content prior to it being sent to the room. Defaults to - * overriding nothing. - */ - explicit CreateRoomJob(const QString& visibility = {}, const QString& roomAliasName = {}, const QString& name = {}, const QString& topic = {}, const QStringList& invite = {}, const QVector<Invite3pid>& invite3pid = {}, const QString& roomVersion = {}, const QJsonObject& creationContent = {}, const QVector<StateEvent>& initialState = {}, const QString& preset = {}, Omittable<bool> isDirect = none, const QJsonObject& powerLevelContentOverride = {}); - ~CreateRoomJob() override; + // Result properties - // Result properties + /// The created room's ID. + QString roomId() const { return loadFromJson<QString>("room_id"_ls); } +}; - /// The created room's ID. - const QString& roomId() const; +template <> +struct JsonObjectConverter<CreateRoomJob::Invite3pid> { + static void dumpTo(QJsonObject& jo, const CreateRoomJob::Invite3pid& pod) + { + addParam<>(jo, QStringLiteral("id_server"), pod.idServer); + addParam<>(jo, QStringLiteral("id_access_token"), pod.idAccessToken); + addParam<>(jo, QStringLiteral("medium"), pod.medium); + addParam<>(jo, QStringLiteral("address"), pod.address); + } +}; - protected: - Status parseJson(const QJsonDocument& data) override; +template <> +struct JsonObjectConverter<CreateRoomJob::StateEvent> { + static void dumpTo(QJsonObject& jo, const CreateRoomJob::StateEvent& pod) + { + addParam<>(jo, QStringLiteral("type"), pod.type); + addParam<IfNotEmpty>(jo, QStringLiteral("state_key"), pod.stateKey); + addParam<>(jo, QStringLiteral("content"), pod.content); + } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/auth_data.cpp b/lib/csapi/definitions/auth_data.cpp deleted file mode 100644 index 006b8c7e..00000000 --- a/lib/csapi/definitions/auth_data.cpp +++ /dev/null @@ -1,25 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "auth_data.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<AuthenticationData>::dumpTo( - QJsonObject& jo, const AuthenticationData& pod) -{ - fillJson(jo, pod.authInfo); - addParam<>(jo, QStringLiteral("type"), pod.type); - addParam<IfNotEmpty>(jo, QStringLiteral("session"), pod.session); -} - -void JsonObjectConverter<AuthenticationData>::fillFrom( - QJsonObject jo, AuthenticationData& result) -{ - fromJson(jo.take("type"_ls), result.type); - fromJson(jo.take("session"_ls), result.session); - - fromJson(jo, result.authInfo); -} - diff --git a/lib/csapi/definitions/auth_data.h b/lib/csapi/definitions/auth_data.h index 26eb205c..e92596d0 100644 --- a/lib/csapi/definitions/auth_data.h +++ b/lib/csapi/definitions/auth_data.h @@ -6,27 +6,34 @@ #include "converters.h" -#include <QtCore/QJsonObject> -#include <QtCore/QHash> +namespace Quotient { +/// Used by clients to submit authentication information to the +/// interactive-authentication API +struct AuthenticationData { + /// The login type that the client is attempting to complete. + QString type; -namespace QMatrixClient -{ - // Data structures + /// The value of the session key given by the homeserver. + QString session; - /// Used by clients to submit authentication information to the interactive-authentication API - struct AuthenticationData + /// Keys dependent on the login type + QHash<QString, QJsonObject> authInfo; +}; + +template <> +struct JsonObjectConverter<AuthenticationData> { + static void dumpTo(QJsonObject& jo, const AuthenticationData& pod) { - /// The login type that the client is attempting to complete. - QString type; - /// The value of the session key given by the homeserver. - QString session; - /// Keys dependent on the login type - QHash<QString, QJsonObject> authInfo; - }; - template <> struct JsonObjectConverter<AuthenticationData> + fillJson(jo, pod.authInfo); + addParam<>(jo, QStringLiteral("type"), pod.type); + addParam<IfNotEmpty>(jo, QStringLiteral("session"), pod.session); + } + static void fillFrom(QJsonObject jo, AuthenticationData& pod) { - static void dumpTo(QJsonObject& jo, const AuthenticationData& pod); - static void fillFrom(QJsonObject jo, AuthenticationData& pod); - }; + fromJson(jo.take("type"_ls), pod.type); + fromJson(jo.take("session"_ls), pod.session); + fromJson(jo, pod.authInfo); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/client_device.cpp b/lib/csapi/definitions/client_device.cpp deleted file mode 100644 index 752b806a..00000000 --- a/lib/csapi/definitions/client_device.cpp +++ /dev/null @@ -1,26 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "client_device.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<Device>::dumpTo( - QJsonObject& jo, const Device& pod) -{ - addParam<>(jo, QStringLiteral("device_id"), pod.deviceId); - addParam<IfNotEmpty>(jo, QStringLiteral("display_name"), pod.displayName); - addParam<IfNotEmpty>(jo, QStringLiteral("last_seen_ip"), pod.lastSeenIp); - addParam<IfNotEmpty>(jo, QStringLiteral("last_seen_ts"), pod.lastSeenTs); -} - -void JsonObjectConverter<Device>::fillFrom( - const QJsonObject& jo, Device& result) -{ - fromJson(jo.value("device_id"_ls), result.deviceId); - fromJson(jo.value("display_name"_ls), result.displayName); - fromJson(jo.value("last_seen_ip"_ls), result.lastSeenIp); - fromJson(jo.value("last_seen_ts"_ls), result.lastSeenTs); -} - diff --git a/lib/csapi/definitions/client_device.h b/lib/csapi/definitions/client_device.h index a6224f71..a5ab1bfc 100644 --- a/lib/csapi/definitions/client_device.h +++ b/lib/csapi/definitions/client_device.h @@ -6,32 +6,43 @@ #include "converters.h" -#include "converters.h" +namespace Quotient { +/// A client device +struct Device { + /// Identifier of this device. + QString deviceId; + + /// Display name set by the user for this device. Absent if no name has been + /// set. + QString displayName; + + /// The IP address where this device was last seen. (May be a few minutes + /// out of date, for efficiency reasons). + QString lastSeenIp; -namespace QMatrixClient -{ - // Data structures + /// The timestamp (in milliseconds since the unix epoch) when this devices + /// was last seen. (May be a few minutes out of date, for efficiency + /// reasons). + Omittable<qint64> lastSeenTs; +}; - /// A client device - struct Device +template <> +struct JsonObjectConverter<Device> { + static void dumpTo(QJsonObject& jo, const Device& pod) { - /// Identifier of this device. - QString deviceId; - /// Display name set by the user for this device. Absent if no name has been - /// set. - QString displayName; - /// The IP address where this device was last seen. (May be a few minutes out - /// of date, for efficiency reasons). - QString lastSeenIp; - /// The timestamp (in milliseconds since the unix epoch) when this devices - /// was last seen. (May be a few minutes out of date, for efficiency - /// reasons). - Omittable<qint64> lastSeenTs; - }; - template <> struct JsonObjectConverter<Device> + addParam<>(jo, QStringLiteral("device_id"), pod.deviceId); + addParam<IfNotEmpty>(jo, QStringLiteral("display_name"), + pod.displayName); + addParam<IfNotEmpty>(jo, QStringLiteral("last_seen_ip"), pod.lastSeenIp); + addParam<IfNotEmpty>(jo, QStringLiteral("last_seen_ts"), pod.lastSeenTs); + } + static void fillFrom(const QJsonObject& jo, Device& pod) { - static void dumpTo(QJsonObject& jo, const Device& pod); - static void fillFrom(const QJsonObject& jo, Device& pod); - }; + fromJson(jo.value("device_id"_ls), pod.deviceId); + fromJson(jo.value("display_name"_ls), pod.displayName); + fromJson(jo.value("last_seen_ip"_ls), pod.lastSeenIp); + fromJson(jo.value("last_seen_ts"_ls), pod.lastSeenTs); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/device_keys.cpp b/lib/csapi/definitions/device_keys.cpp deleted file mode 100644 index 1e79499f..00000000 --- a/lib/csapi/definitions/device_keys.cpp +++ /dev/null @@ -1,28 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "device_keys.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<DeviceKeys>::dumpTo( - QJsonObject& jo, const DeviceKeys& pod) -{ - addParam<>(jo, QStringLiteral("user_id"), pod.userId); - addParam<>(jo, QStringLiteral("device_id"), pod.deviceId); - addParam<>(jo, QStringLiteral("algorithms"), pod.algorithms); - addParam<>(jo, QStringLiteral("keys"), pod.keys); - addParam<>(jo, QStringLiteral("signatures"), pod.signatures); -} - -void JsonObjectConverter<DeviceKeys>::fillFrom( - const QJsonObject& jo, DeviceKeys& result) -{ - fromJson(jo.value("user_id"_ls), result.userId); - fromJson(jo.value("device_id"_ls), result.deviceId); - fromJson(jo.value("algorithms"_ls), result.algorithms); - fromJson(jo.value("keys"_ls), result.keys); - fromJson(jo.value("signatures"_ls), result.signatures); -} - diff --git a/lib/csapi/definitions/device_keys.h b/lib/csapi/definitions/device_keys.h index 8ebe1125..3065f218 100644 --- a/lib/csapi/definitions/device_keys.h +++ b/lib/csapi/definitions/device_keys.h @@ -6,38 +6,51 @@ #include "converters.h" -#include <QtCore/QHash> +namespace Quotient { +/// Device identity keys +struct DeviceKeys { + /// The ID of the user the device belongs to. Must match the user ID used + /// when logging in. + QString userId; -namespace QMatrixClient -{ - // Data structures + /// The ID of the device these keys belong to. Must match the device ID used + /// when logging in. + QString deviceId; - /// Device identity keys - struct DeviceKeys + /// The encryption algorithms supported by this device. + QStringList algorithms; + + /// Public identity keys. The names of the properties should be in the + /// format ``<algorithm>:<device_id>``. The keys themselves should be + /// encoded as specified by the key algorithm. + QHash<QString, QString> keys; + + /// Signatures for the device key object. A map from user ID, to a map from + /// ``<algorithm>:<device_id>`` to the signature. + /// + /// The signature is calculated using the process described at `Signing + /// JSON`_. + QHash<QString, QHash<QString, QString>> signatures; +}; + +template <> +struct JsonObjectConverter<DeviceKeys> { + static void dumpTo(QJsonObject& jo, const DeviceKeys& pod) { - /// The ID of the user the device belongs to. Must match the user ID used - /// when logging in. - QString userId; - /// The ID of the device these keys belong to. Must match the device ID used - /// when logging in. - QString deviceId; - /// The encryption algorithms supported by this device. - QStringList algorithms; - /// Public identity keys. The names of the properties should be in the - /// format ``<algorithm>:<device_id>``. The keys themselves should be - /// encoded as specified by the key algorithm. - QHash<QString, QString> keys; - /// Signatures for the device key object. A map from user ID, to a map from - /// ``<algorithm>:<device_id>`` to the signature. - /// - /// The signature is calculated using the process described at `Signing - /// JSON`_. - QHash<QString, QHash<QString, QString>> signatures; - }; - template <> struct JsonObjectConverter<DeviceKeys> + addParam<>(jo, QStringLiteral("user_id"), pod.userId); + addParam<>(jo, QStringLiteral("device_id"), pod.deviceId); + addParam<>(jo, QStringLiteral("algorithms"), pod.algorithms); + addParam<>(jo, QStringLiteral("keys"), pod.keys); + addParam<>(jo, QStringLiteral("signatures"), pod.signatures); + } + static void fillFrom(const QJsonObject& jo, DeviceKeys& pod) { - static void dumpTo(QJsonObject& jo, const DeviceKeys& pod); - static void fillFrom(const QJsonObject& jo, DeviceKeys& pod); - }; + fromJson(jo.value("user_id"_ls), pod.userId); + fromJson(jo.value("device_id"_ls), pod.deviceId); + fromJson(jo.value("algorithms"_ls), pod.algorithms); + fromJson(jo.value("keys"_ls), pod.keys); + fromJson(jo.value("signatures"_ls), pod.signatures); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/event_filter.cpp b/lib/csapi/definitions/event_filter.cpp deleted file mode 100644 index b20d7807..00000000 --- a/lib/csapi/definitions/event_filter.cpp +++ /dev/null @@ -1,28 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "event_filter.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<EventFilter>::dumpTo( - QJsonObject& jo, const EventFilter& pod) -{ - addParam<IfNotEmpty>(jo, QStringLiteral("limit"), pod.limit); - addParam<IfNotEmpty>(jo, QStringLiteral("not_senders"), pod.notSenders); - addParam<IfNotEmpty>(jo, QStringLiteral("not_types"), pod.notTypes); - addParam<IfNotEmpty>(jo, QStringLiteral("senders"), pod.senders); - addParam<IfNotEmpty>(jo, QStringLiteral("types"), pod.types); -} - -void JsonObjectConverter<EventFilter>::fillFrom( - const QJsonObject& jo, EventFilter& result) -{ - fromJson(jo.value("limit"_ls), result.limit); - fromJson(jo.value("not_senders"_ls), result.notSenders); - fromJson(jo.value("not_types"_ls), result.notTypes); - fromJson(jo.value("senders"_ls), result.senders); - fromJson(jo.value("types"_ls), result.types); -} - diff --git a/lib/csapi/definitions/event_filter.h b/lib/csapi/definitions/event_filter.h index 6de1fe79..67497412 100644 --- a/lib/csapi/definitions/event_filter.h +++ b/lib/csapi/definitions/event_filter.h @@ -6,29 +6,51 @@ #include "converters.h" -#include "converters.h" +namespace Quotient { + +struct EventFilter { + /// The maximum number of events to return. + Omittable<int> limit; + + /// A list of sender IDs to exclude. If this list is absent then no senders + /// are excluded. A matching sender will be excluded even if it is listed in + /// the ``'senders'`` filter. + QStringList notSenders; + + /// A list of event types to exclude. If this list is absent then no event + /// types are excluded. A matching type will be excluded even if it is + /// listed in the ``'types'`` filter. A '*' can be used as a wildcard to + /// match any sequence of characters. + QStringList notTypes; + + /// A list of senders IDs to include. If this list is absent then all + /// senders are included. + QStringList senders; -namespace QMatrixClient -{ - // Data structures + /// A list of event types to include. If this list is absent then all event + /// types are included. A ``'*'`` can be used as a wildcard to match any + /// sequence of characters. + QStringList types; +}; - struct EventFilter +template <> +struct JsonObjectConverter<EventFilter> { + static void dumpTo(QJsonObject& jo, const EventFilter& pod) { - /// The maximum number of events to return. - Omittable<int> limit; - /// A list of sender IDs to exclude. If this list is absent then no senders are excluded. A matching sender will be excluded even if it is listed in the ``'senders'`` filter. - QStringList notSenders; - /// A list of event types to exclude. If this list is absent then no event types are excluded. A matching type will be excluded even if it is listed in the ``'types'`` filter. A '*' can be used as a wildcard to match any sequence of characters. - QStringList notTypes; - /// A list of senders IDs to include. If this list is absent then all senders are included. - QStringList senders; - /// A list of event types to include. If this list is absent then all event types are included. A ``'*'`` can be used as a wildcard to match any sequence of characters. - QStringList types; - }; - template <> struct JsonObjectConverter<EventFilter> + addParam<IfNotEmpty>(jo, QStringLiteral("limit"), pod.limit); + addParam<IfNotEmpty>(jo, QStringLiteral("not_senders"), pod.notSenders); + addParam<IfNotEmpty>(jo, QStringLiteral("not_types"), pod.notTypes); + addParam<IfNotEmpty>(jo, QStringLiteral("senders"), pod.senders); + addParam<IfNotEmpty>(jo, QStringLiteral("types"), pod.types); + } + static void fillFrom(const QJsonObject& jo, EventFilter& pod) { - static void dumpTo(QJsonObject& jo, const EventFilter& pod); - static void fillFrom(const QJsonObject& jo, EventFilter& pod); - }; + fromJson(jo.value("limit"_ls), pod.limit); + fromJson(jo.value("not_senders"_ls), pod.notSenders); + fromJson(jo.value("not_types"_ls), pod.notTypes); + fromJson(jo.value("senders"_ls), pod.senders); + fromJson(jo.value("types"_ls), pod.types); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/openid_token.h b/lib/csapi/definitions/openid_token.h new file mode 100644 index 00000000..5e68c376 --- /dev/null +++ b/lib/csapi/definitions/openid_token.h @@ -0,0 +1,48 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +namespace Quotient { + +struct OpenidToken { + /// An access token the consumer may use to verify the identity of + /// the person who generated the token. This is given to the federation + /// API ``GET /openid/userinfo`` to verify the user's identity. + QString accessToken; + + /// The string ``Bearer``. + QString tokenType; + + /// The homeserver domain the consumer should use when attempting to + /// verify the user's identity. + QString matrixServerName; + + /// The number of seconds before this token expires and a new one must + /// be generated. + int expiresIn; +}; + +template <> +struct JsonObjectConverter<OpenidToken> { + static void dumpTo(QJsonObject& jo, const OpenidToken& pod) + { + addParam<>(jo, QStringLiteral("access_token"), pod.accessToken); + addParam<>(jo, QStringLiteral("token_type"), pod.tokenType); + addParam<>(jo, QStringLiteral("matrix_server_name"), + pod.matrixServerName); + addParam<>(jo, QStringLiteral("expires_in"), pod.expiresIn); + } + static void fillFrom(const QJsonObject& jo, OpenidToken& pod) + { + fromJson(jo.value("access_token"_ls), pod.accessToken); + fromJson(jo.value("token_type"_ls), pod.tokenType); + fromJson(jo.value("matrix_server_name"_ls), pod.matrixServerName); + fromJson(jo.value("expires_in"_ls), pod.expiresIn); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/public_rooms_response.cpp b/lib/csapi/definitions/public_rooms_response.cpp deleted file mode 100644 index 0d26662c..00000000 --- a/lib/csapi/definitions/public_rooms_response.cpp +++ /dev/null @@ -1,54 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "public_rooms_response.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<PublicRoomsChunk>::dumpTo( - QJsonObject& jo, const PublicRoomsChunk& pod) -{ - addParam<IfNotEmpty>(jo, QStringLiteral("aliases"), pod.aliases); - addParam<IfNotEmpty>(jo, QStringLiteral("canonical_alias"), pod.canonicalAlias); - addParam<IfNotEmpty>(jo, QStringLiteral("name"), pod.name); - addParam<>(jo, QStringLiteral("num_joined_members"), pod.numJoinedMembers); - addParam<>(jo, QStringLiteral("room_id"), pod.roomId); - addParam<IfNotEmpty>(jo, QStringLiteral("topic"), pod.topic); - addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable); - addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin); - addParam<IfNotEmpty>(jo, QStringLiteral("avatar_url"), pod.avatarUrl); -} - -void JsonObjectConverter<PublicRoomsChunk>::fillFrom( - const QJsonObject& jo, PublicRoomsChunk& result) -{ - fromJson(jo.value("aliases"_ls), result.aliases); - fromJson(jo.value("canonical_alias"_ls), result.canonicalAlias); - fromJson(jo.value("name"_ls), result.name); - fromJson(jo.value("num_joined_members"_ls), result.numJoinedMembers); - fromJson(jo.value("room_id"_ls), result.roomId); - fromJson(jo.value("topic"_ls), result.topic); - fromJson(jo.value("world_readable"_ls), result.worldReadable); - fromJson(jo.value("guest_can_join"_ls), result.guestCanJoin); - fromJson(jo.value("avatar_url"_ls), result.avatarUrl); -} - -void JsonObjectConverter<PublicRoomsResponse>::dumpTo( - QJsonObject& jo, const PublicRoomsResponse& pod) -{ - addParam<>(jo, QStringLiteral("chunk"), pod.chunk); - addParam<IfNotEmpty>(jo, QStringLiteral("next_batch"), pod.nextBatch); - addParam<IfNotEmpty>(jo, QStringLiteral("prev_batch"), pod.prevBatch); - addParam<IfNotEmpty>(jo, QStringLiteral("total_room_count_estimate"), pod.totalRoomCountEstimate); -} - -void JsonObjectConverter<PublicRoomsResponse>::fillFrom( - const QJsonObject& jo, PublicRoomsResponse& result) -{ - fromJson(jo.value("chunk"_ls), result.chunk); - fromJson(jo.value("next_batch"_ls), result.nextBatch); - fromJson(jo.value("prev_batch"_ls), result.prevBatch); - fromJson(jo.value("total_room_count_estimate"_ls), result.totalRoomCountEstimate); -} - diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h index 4c54ac25..8f30e607 100644 --- a/lib/csapi/definitions/public_rooms_response.h +++ b/lib/csapi/definitions/public_rooms_response.h @@ -6,63 +6,107 @@ #include "converters.h" -#include <QtCore/QVector> -#include "converters.h" +namespace Quotient { + +struct PublicRoomsChunk { + /// Aliases of the room. May be empty. + QStringList aliases; + + /// The canonical alias of the room, if any. + QString canonicalAlias; + + /// The name of the room, if any. + QString name; + + /// The number of members joined to the room. + int numJoinedMembers; + + /// The ID of the room. + QString roomId; + + /// The topic of the room, if any. + QString topic; -namespace QMatrixClient -{ - // Data structures + /// Whether the room may be viewed by guest users without joining. + bool worldReadable; - struct PublicRoomsChunk + /// Whether guest users may join the room and participate in it. + /// If they can, they will be subject to ordinary power level + /// rules like any other user. + bool guestCanJoin; + + /// The URL for the room's avatar, if one is set. + QString avatarUrl; +}; + +template <> +struct JsonObjectConverter<PublicRoomsChunk> { + static void dumpTo(QJsonObject& jo, const PublicRoomsChunk& pod) { - /// Aliases of the room. May be empty. - QStringList aliases; - /// The canonical alias of the room, if any. - QString canonicalAlias; - /// The name of the room, if any. - QString name; - /// The number of members joined to the room. - int numJoinedMembers; - /// The ID of the room. - QString roomId; - /// The topic of the room, if any. - QString topic; - /// Whether the room may be viewed by guest users without joining. - bool worldReadable; - /// Whether guest users may join the room and participate in it. - /// If they can, they will be subject to ordinary power level - /// rules like any other user. - bool guestCanJoin; - /// The URL for the room's avatar, if one is set. - QString avatarUrl; - }; - template <> struct JsonObjectConverter<PublicRoomsChunk> + addParam<IfNotEmpty>(jo, QStringLiteral("aliases"), pod.aliases); + addParam<IfNotEmpty>(jo, QStringLiteral("canonical_alias"), + pod.canonicalAlias); + addParam<IfNotEmpty>(jo, QStringLiteral("name"), pod.name); + addParam<>(jo, QStringLiteral("num_joined_members"), + pod.numJoinedMembers); + addParam<>(jo, QStringLiteral("room_id"), pod.roomId); + addParam<IfNotEmpty>(jo, QStringLiteral("topic"), pod.topic); + addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable); + addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin); + addParam<IfNotEmpty>(jo, QStringLiteral("avatar_url"), pod.avatarUrl); + } + static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod) { - static void dumpTo(QJsonObject& jo, const PublicRoomsChunk& pod); - static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod); - }; + fromJson(jo.value("aliases"_ls), pod.aliases); + fromJson(jo.value("canonical_alias"_ls), pod.canonicalAlias); + fromJson(jo.value("name"_ls), pod.name); + fromJson(jo.value("num_joined_members"_ls), pod.numJoinedMembers); + fromJson(jo.value("room_id"_ls), pod.roomId); + fromJson(jo.value("topic"_ls), pod.topic); + fromJson(jo.value("world_readable"_ls), pod.worldReadable); + fromJson(jo.value("guest_can_join"_ls), pod.guestCanJoin); + fromJson(jo.value("avatar_url"_ls), pod.avatarUrl); + } +}; + +/// A list of the rooms on the server. +struct PublicRoomsResponse { + /// A paginated chunk of public rooms. + QVector<PublicRoomsChunk> chunk; + + /// A pagination token for the response. The absence of this token + /// means there are no more results to fetch and the client should + /// stop paginating. + QString nextBatch; + + /// A pagination token that allows fetching previous results. The + /// absence of this token means there are no results before this + /// batch, i.e. this is the first batch. + QString prevBatch; + + /// An estimate on the total number of public rooms, if the + /// server has an estimate. + Omittable<int> totalRoomCountEstimate; +}; - /// A list of the rooms on the server. - struct PublicRoomsResponse +template <> +struct JsonObjectConverter<PublicRoomsResponse> { + static void dumpTo(QJsonObject& jo, const PublicRoomsResponse& pod) { - /// A paginated chunk of public rooms. - QVector<PublicRoomsChunk> chunk; - /// A pagination token for the response. The absence of this token - /// means there are no more results to fetch and the client should - /// stop paginating. - QString nextBatch; - /// A pagination token that allows fetching previous results. The - /// absence of this token means there are no results before this - /// batch, i.e. this is the first batch. - QString prevBatch; - /// An estimate on the total number of public rooms, if the - /// server has an estimate. - Omittable<int> totalRoomCountEstimate; - }; - template <> struct JsonObjectConverter<PublicRoomsResponse> + addParam<>(jo, QStringLiteral("chunk"), pod.chunk); + addParam<IfNotEmpty>(jo, QStringLiteral("next_batch"), pod.nextBatch); + addParam<IfNotEmpty>(jo, QStringLiteral("prev_batch"), pod.prevBatch); + addParam<IfNotEmpty>(jo, QStringLiteral("total_room_count_estimate"), + pod.totalRoomCountEstimate); + } + static void fillFrom(const QJsonObject& jo, PublicRoomsResponse& pod) { - static void dumpTo(QJsonObject& jo, const PublicRoomsResponse& pod); - static void fillFrom(const QJsonObject& jo, PublicRoomsResponse& pod); - }; + fromJson(jo.value("chunk"_ls), pod.chunk); + fromJson(jo.value("next_batch"_ls), pod.nextBatch); + fromJson(jo.value("prev_batch"_ls), pod.prevBatch); + fromJson(jo.value("total_room_count_estimate"_ls), + pod.totalRoomCountEstimate); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/push_condition.cpp b/lib/csapi/definitions/push_condition.cpp deleted file mode 100644 index ace02755..00000000 --- a/lib/csapi/definitions/push_condition.cpp +++ /dev/null @@ -1,26 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "push_condition.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<PushCondition>::dumpTo( - QJsonObject& jo, const PushCondition& pod) -{ - addParam<>(jo, QStringLiteral("kind"), pod.kind); - addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key); - addParam<IfNotEmpty>(jo, QStringLiteral("pattern"), pod.pattern); - addParam<IfNotEmpty>(jo, QStringLiteral("is"), pod.is); -} - -void JsonObjectConverter<PushCondition>::fillFrom( - const QJsonObject& jo, PushCondition& result) -{ - fromJson(jo.value("kind"_ls), result.kind); - fromJson(jo.value("key"_ls), result.key); - fromJson(jo.value("pattern"_ls), result.pattern); - fromJson(jo.value("is"_ls), result.is); -} - diff --git a/lib/csapi/definitions/push_condition.h b/lib/csapi/definitions/push_condition.h index e45526d2..a6decf1b 100644 --- a/lib/csapi/definitions/push_condition.h +++ b/lib/csapi/definitions/push_condition.h @@ -6,32 +6,51 @@ #include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Data structures +struct PushCondition { + /// The kind of condition to apply. See `conditions <#conditions>`_ for + /// more information on the allowed kinds and how they work. + QString kind; - struct PushCondition + /// Required for ``event_match`` conditions. The dot-separated field of the + /// event to match. + /// + /// Required for ``sender_notification_permission`` conditions. The field in + /// the power level event the user needs a minimum power level for. Fields + /// must be specified under the ``notifications`` property in the power + /// level event's ``content``. + QString key; + + /// Required for ``event_match`` conditions. The glob-style pattern to + /// match against. Patterns with no special glob characters should be + /// treated as having asterisks prepended and appended when testing the + /// condition. + QString pattern; + + /// Required for ``room_member_count`` conditions. A decimal integer + /// optionally prefixed by one of, ==, <, >, >= or <=. A prefix of < matches + /// rooms where the member count is strictly less than the given number and + /// so forth. If no prefix is present, this parameter defaults to ==. + QString is; +}; + +template <> +struct JsonObjectConverter<PushCondition> { + static void dumpTo(QJsonObject& jo, const PushCondition& pod) { - QString kind; - /// Required for ``event_match`` conditions. The dot-separated field of the - /// event to match. - QString key; - /// Required for ``event_match`` conditions. The glob-style pattern to - /// match against. Patterns with no special glob characters should be - /// treated as having asterisks prepended and appended when testing the - /// condition. - QString pattern; - /// Required for ``room_member_count`` conditions. A decimal integer - /// optionally prefixed by one of, ==, <, >, >= or <=. A prefix of < matches - /// rooms where the member count is strictly less than the given number and - /// so forth. If no prefix is present, this parameter defaults to ==. - QString is; - }; - template <> struct JsonObjectConverter<PushCondition> + addParam<>(jo, QStringLiteral("kind"), pod.kind); + addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key); + addParam<IfNotEmpty>(jo, QStringLiteral("pattern"), pod.pattern); + addParam<IfNotEmpty>(jo, QStringLiteral("is"), pod.is); + } + static void fillFrom(const QJsonObject& jo, PushCondition& pod) { - static void dumpTo(QJsonObject& jo, const PushCondition& pod); - static void fillFrom(const QJsonObject& jo, PushCondition& pod); - }; + fromJson(jo.value("kind"_ls), pod.kind); + fromJson(jo.value("key"_ls), pod.key); + fromJson(jo.value("pattern"_ls), pod.pattern); + fromJson(jo.value("is"_ls), pod.is); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/push_rule.cpp b/lib/csapi/definitions/push_rule.cpp deleted file mode 100644 index abbb04b5..00000000 --- a/lib/csapi/definitions/push_rule.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "push_rule.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<PushRule>::dumpTo( - QJsonObject& jo, const PushRule& pod) -{ - addParam<>(jo, QStringLiteral("actions"), pod.actions); - addParam<>(jo, QStringLiteral("default"), pod.isDefault); - addParam<>(jo, QStringLiteral("enabled"), pod.enabled); - addParam<>(jo, QStringLiteral("rule_id"), pod.ruleId); - addParam<IfNotEmpty>(jo, QStringLiteral("conditions"), pod.conditions); - addParam<IfNotEmpty>(jo, QStringLiteral("pattern"), pod.pattern); -} - -void JsonObjectConverter<PushRule>::fillFrom( - const QJsonObject& jo, PushRule& result) -{ - fromJson(jo.value("actions"_ls), result.actions); - fromJson(jo.value("default"_ls), result.isDefault); - fromJson(jo.value("enabled"_ls), result.enabled); - fromJson(jo.value("rule_id"_ls), result.ruleId); - fromJson(jo.value("conditions"_ls), result.conditions); - fromJson(jo.value("pattern"_ls), result.pattern); -} - diff --git a/lib/csapi/definitions/push_rule.h b/lib/csapi/definitions/push_rule.h index bea13e96..43749bae 100644 --- a/lib/csapi/definitions/push_rule.h +++ b/lib/csapi/definitions/push_rule.h @@ -7,37 +7,52 @@ #include "converters.h" #include "csapi/definitions/push_condition.h" -#include <QtCore/QJsonObject> -#include <QtCore/QVector> -#include <QtCore/QVariant> -#include "converters.h" -namespace QMatrixClient -{ - // Data structures +namespace Quotient { + +struct PushRule { + /// The actions to perform when this rule is matched. + QVector<QVariant> actions; + + /// Whether this is a default rule, or has been set explicitly. + bool isDefault; + + /// Whether the push rule is enabled or not. + bool enabled; + + /// The ID of this rule. + QString ruleId; + + /// The conditions that must hold true for an event in order for a rule to + /// be applied to an event. A rule with no conditions always matches. Only + /// applicable to ``underride`` and ``override`` rules. + QVector<PushCondition> conditions; + + /// The glob-style pattern to match against. Only applicable to ``content`` + /// rules. + QString pattern; +}; - struct PushRule +template <> +struct JsonObjectConverter<PushRule> { + static void dumpTo(QJsonObject& jo, const PushRule& pod) { - /// The actions to perform when this rule is matched. - QVector<QVariant> actions; - /// Whether this is a default rule, or has been set explicitly. - bool isDefault; - /// Whether the push rule is enabled or not. - bool enabled; - /// The ID of this rule. - QString ruleId; - /// The conditions that must hold true for an event in order for a rule to be - /// applied to an event. A rule with no conditions always matches. Only - /// applicable to ``underride`` and ``override`` rules. - QVector<PushCondition> conditions; - /// The glob-style pattern to match against. Only applicable to ``content`` - /// rules. - QString pattern; - }; - template <> struct JsonObjectConverter<PushRule> + addParam<>(jo, QStringLiteral("actions"), pod.actions); + addParam<>(jo, QStringLiteral("default"), pod.isDefault); + addParam<>(jo, QStringLiteral("enabled"), pod.enabled); + addParam<>(jo, QStringLiteral("rule_id"), pod.ruleId); + addParam<IfNotEmpty>(jo, QStringLiteral("conditions"), pod.conditions); + addParam<IfNotEmpty>(jo, QStringLiteral("pattern"), pod.pattern); + } + static void fillFrom(const QJsonObject& jo, PushRule& pod) { - static void dumpTo(QJsonObject& jo, const PushRule& pod); - static void fillFrom(const QJsonObject& jo, PushRule& pod); - }; + fromJson(jo.value("actions"_ls), pod.actions); + fromJson(jo.value("default"_ls), pod.isDefault); + fromJson(jo.value("enabled"_ls), pod.enabled); + fromJson(jo.value("rule_id"_ls), pod.ruleId); + fromJson(jo.value("conditions"_ls), pod.conditions); + fromJson(jo.value("pattern"_ls), pod.pattern); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/push_ruleset.cpp b/lib/csapi/definitions/push_ruleset.cpp deleted file mode 100644 index f1bad882..00000000 --- a/lib/csapi/definitions/push_ruleset.cpp +++ /dev/null @@ -1,28 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "push_ruleset.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<PushRuleset>::dumpTo( - QJsonObject& jo, const PushRuleset& pod) -{ - addParam<IfNotEmpty>(jo, QStringLiteral("content"), pod.content); - addParam<IfNotEmpty>(jo, QStringLiteral("override"), pod.override); - addParam<IfNotEmpty>(jo, QStringLiteral("room"), pod.room); - addParam<IfNotEmpty>(jo, QStringLiteral("sender"), pod.sender); - addParam<IfNotEmpty>(jo, QStringLiteral("underride"), pod.underride); -} - -void JsonObjectConverter<PushRuleset>::fillFrom( - const QJsonObject& jo, PushRuleset& result) -{ - fromJson(jo.value("content"_ls), result.content); - fromJson(jo.value("override"_ls), result.override); - fromJson(jo.value("room"_ls), result.room); - fromJson(jo.value("sender"_ls), result.sender); - fromJson(jo.value("underride"_ls), result.underride); -} - diff --git a/lib/csapi/definitions/push_ruleset.h b/lib/csapi/definitions/push_ruleset.h index f2d937c0..ba780a33 100644 --- a/lib/csapi/definitions/push_ruleset.h +++ b/lib/csapi/definitions/push_ruleset.h @@ -6,26 +6,40 @@ #include "converters.h" -#include <QtCore/QVector> -#include "converters.h" #include "csapi/definitions/push_rule.h" -namespace QMatrixClient -{ - // Data structures +namespace Quotient { + +struct PushRuleset { + QVector<PushRule> content; + + QVector<PushRule> override; + + QVector<PushRule> room; + + QVector<PushRule> sender; + + QVector<PushRule> underride; +}; - struct PushRuleset +template <> +struct JsonObjectConverter<PushRuleset> { + static void dumpTo(QJsonObject& jo, const PushRuleset& pod) { - QVector<PushRule> content; - QVector<PushRule> override; - QVector<PushRule> room; - QVector<PushRule> sender; - QVector<PushRule> underride; - }; - template <> struct JsonObjectConverter<PushRuleset> + addParam<IfNotEmpty>(jo, QStringLiteral("content"), pod.content); + addParam<IfNotEmpty>(jo, QStringLiteral("override"), pod.override); + addParam<IfNotEmpty>(jo, QStringLiteral("room"), pod.room); + addParam<IfNotEmpty>(jo, QStringLiteral("sender"), pod.sender); + addParam<IfNotEmpty>(jo, QStringLiteral("underride"), pod.underride); + } + static void fillFrom(const QJsonObject& jo, PushRuleset& pod) { - static void dumpTo(QJsonObject& jo, const PushRuleset& pod); - static void fillFrom(const QJsonObject& jo, PushRuleset& pod); - }; + fromJson(jo.value("content"_ls), pod.content); + fromJson(jo.value("override"_ls), pod.override); + fromJson(jo.value("room"_ls), pod.room); + fromJson(jo.value("sender"_ls), pod.sender); + fromJson(jo.value("underride"_ls), pod.underride); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/request_email_validation.h b/lib/csapi/definitions/request_email_validation.h new file mode 100644 index 00000000..ab34862e --- /dev/null +++ b/lib/csapi/definitions/request_email_validation.h @@ -0,0 +1,48 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +#include "csapi/definitions/../../identity/definitions/request_email_validation.h" + +namespace Quotient { + +struct EmailValidationData : RequestEmailValidation { + /// The hostname of the identity server to communicate with. May optionally + /// include a port. This parameter is ignored when the homeserver handles + /// 3PID verification. + /// + /// This parameter is deprecated with a plan to be removed in a future + /// specification version for ``/account/password`` and ``/register`` + /// requests. + QString idServer; + + /// An access token previously registered with the identity server. Servers + /// can treat this as optional to distinguish between r0.5-compatible + /// clients and this specification version. + /// + /// Required if an ``id_server`` is supplied. + QString idAccessToken; +}; + +template <> +struct JsonObjectConverter<EmailValidationData> { + static void dumpTo(QJsonObject& jo, const EmailValidationData& pod) + { + fillJson<RequestEmailValidation>(jo, pod); + addParam<IfNotEmpty>(jo, QStringLiteral("id_server"), pod.idServer); + addParam<IfNotEmpty>(jo, QStringLiteral("id_access_token"), + pod.idAccessToken); + } + static void fillFrom(const QJsonObject& jo, EmailValidationData& pod) + { + fillFromJson<RequestEmailValidation>(jo, pod); + fromJson(jo.value("id_server"_ls), pod.idServer); + fromJson(jo.value("id_access_token"_ls), pod.idAccessToken); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/request_msisdn_validation.h b/lib/csapi/definitions/request_msisdn_validation.h new file mode 100644 index 00000000..8539cd98 --- /dev/null +++ b/lib/csapi/definitions/request_msisdn_validation.h @@ -0,0 +1,48 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +#include "csapi/definitions/../../identity/definitions/request_msisdn_validation.h" + +namespace Quotient { + +struct MsisdnValidationData : RequestMsisdnValidation { + /// The hostname of the identity server to communicate with. May optionally + /// include a port. This parameter is ignored when the homeserver handles + /// 3PID verification. + /// + /// This parameter is deprecated with a plan to be removed in a future + /// specification version for ``/account/password`` and ``/register`` + /// requests. + QString idServer; + + /// An access token previously registered with the identity server. Servers + /// can treat this as optional to distinguish between r0.5-compatible + /// clients and this specification version. + /// + /// Required if an ``id_server`` is supplied. + QString idAccessToken; +}; + +template <> +struct JsonObjectConverter<MsisdnValidationData> { + static void dumpTo(QJsonObject& jo, const MsisdnValidationData& pod) + { + fillJson<RequestMsisdnValidation>(jo, pod); + addParam<IfNotEmpty>(jo, QStringLiteral("id_server"), pod.idServer); + addParam<IfNotEmpty>(jo, QStringLiteral("id_access_token"), + pod.idAccessToken); + } + static void fillFrom(const QJsonObject& jo, MsisdnValidationData& pod) + { + fillFromJson<RequestMsisdnValidation>(jo, pod); + fromJson(jo.value("id_server"_ls), pod.idServer); + fromJson(jo.value("id_access_token"_ls), pod.idAccessToken); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/request_token_response.h b/lib/csapi/definitions/request_token_response.h new file mode 100644 index 00000000..00222839 --- /dev/null +++ b/lib/csapi/definitions/request_token_response.h @@ -0,0 +1,45 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +namespace Quotient { + +struct RequestTokenResponse { + /// The session ID. Session IDs are opaque strings that must consist + /// entirely of the characters ``[0-9a-zA-Z.=_-]``. Their length must not + /// exceed 255 characters and they must not be empty. + QString sid; + + /// An optional field containing a URL where the client must submit the + /// validation token to, with identical parameters to the Identity Service + /// API's ``POST /validate/email/submitToken`` endpoint (without the + /// requirement for an access token). The homeserver must send this token to + /// the user (if applicable), who should then be prompted to provide it to + /// the client. + /// + /// If this field is not present, the client can assume that verification + /// will happen without the client's involvement provided the homeserver + /// advertises this specification version in the ``/versions`` response + /// (ie: r0.5.0). + QString submitUrl; +}; + +template <> +struct JsonObjectConverter<RequestTokenResponse> { + static void dumpTo(QJsonObject& jo, const RequestTokenResponse& pod) + { + addParam<>(jo, QStringLiteral("sid"), pod.sid); + addParam<IfNotEmpty>(jo, QStringLiteral("submit_url"), pod.submitUrl); + } + static void fillFrom(const QJsonObject& jo, RequestTokenResponse& pod) + { + fromJson(jo.value("sid"_ls), pod.sid); + fromJson(jo.value("submit_url"_ls), pod.submitUrl); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/room_event_filter.cpp b/lib/csapi/definitions/room_event_filter.cpp deleted file mode 100644 index df92e684..00000000 --- a/lib/csapi/definitions/room_event_filter.cpp +++ /dev/null @@ -1,26 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "room_event_filter.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<RoomEventFilter>::dumpTo( - QJsonObject& jo, const RoomEventFilter& pod) -{ - fillJson<EventFilter>(jo, pod); - addParam<IfNotEmpty>(jo, QStringLiteral("not_rooms"), pod.notRooms); - addParam<IfNotEmpty>(jo, QStringLiteral("rooms"), pod.rooms); - addParam<IfNotEmpty>(jo, QStringLiteral("contains_url"), pod.containsUrl); -} - -void JsonObjectConverter<RoomEventFilter>::fillFrom( - const QJsonObject& jo, RoomEventFilter& result) -{ - fillFromJson<EventFilter>(jo, result); - fromJson(jo.value("not_rooms"_ls), result.notRooms); - fromJson(jo.value("rooms"_ls), result.rooms); - fromJson(jo.value("contains_url"_ls), result.containsUrl); -} - diff --git a/lib/csapi/definitions/room_event_filter.h b/lib/csapi/definitions/room_event_filter.h index 6eb9a390..11e87fde 100644 --- a/lib/csapi/definitions/room_event_filter.h +++ b/lib/csapi/definitions/room_event_filter.h @@ -7,25 +7,61 @@ #include "converters.h" #include "csapi/definitions/event_filter.h" -#include "converters.h" -namespace QMatrixClient -{ - // Data structures +namespace Quotient { + +struct RoomEventFilter : EventFilter { + /// If ``true``, enables lazy-loading of membership events. See + /// `Lazy-loading room members <#lazy-loading-room-members>`_ + /// for more information. Defaults to ``false``. + Omittable<bool> lazyLoadMembers; + + /// If ``true``, sends all membership events for all events, even if they + /// have already been sent to the client. Does not apply unless + /// ``lazy_load_members`` is ``true``. See `Lazy-loading room members + /// <#lazy-loading-room-members>`_ for more information. Defaults to + /// ``false``. + Omittable<bool> includeRedundantMembers; + + /// A list of room IDs to exclude. If this list is absent then no rooms are + /// excluded. A matching room will be excluded even if it is listed in the + /// ``'rooms'`` filter. + QStringList notRooms; + + /// A list of room IDs to include. If this list is absent then all rooms are + /// included. + QStringList rooms; + + /// If ``true``, includes only events with a ``url`` key in their content. + /// If ``false``, excludes those events. If omitted, ``url`` key is not + /// considered for filtering. + Omittable<bool> containsUrl; +}; - struct RoomEventFilter : EventFilter +template <> +struct JsonObjectConverter<RoomEventFilter> { + static void dumpTo(QJsonObject& jo, const RoomEventFilter& pod) { - /// A list of room IDs to exclude. If this list is absent then no rooms are excluded. A matching room will be excluded even if it is listed in the ``'rooms'`` filter. - QStringList notRooms; - /// A list of room IDs to include. If this list is absent then all rooms are included. - QStringList rooms; - /// If ``true``, includes only events with a ``url`` key in their content. If ``false``, excludes those events. If omitted, ``url`` key is not considered for filtering. - Omittable<bool> containsUrl; - }; - template <> struct JsonObjectConverter<RoomEventFilter> + fillJson<EventFilter>(jo, pod); + addParam<IfNotEmpty>(jo, QStringLiteral("lazy_load_members"), + pod.lazyLoadMembers); + addParam<IfNotEmpty>(jo, QStringLiteral("include_redundant_members"), + pod.includeRedundantMembers); + addParam<IfNotEmpty>(jo, QStringLiteral("not_rooms"), pod.notRooms); + addParam<IfNotEmpty>(jo, QStringLiteral("rooms"), pod.rooms); + addParam<IfNotEmpty>(jo, QStringLiteral("contains_url"), + pod.containsUrl); + } + static void fillFrom(const QJsonObject& jo, RoomEventFilter& pod) { - static void dumpTo(QJsonObject& jo, const RoomEventFilter& pod); - static void fillFrom(const QJsonObject& jo, RoomEventFilter& pod); - }; + fillFromJson<EventFilter>(jo, pod); + fromJson(jo.value("lazy_load_members"_ls), pod.lazyLoadMembers); + fromJson(jo.value("include_redundant_members"_ls), + pod.includeRedundantMembers); + fromJson(jo.value("not_rooms"_ls), pod.notRooms); + fromJson(jo.value("rooms"_ls), pod.rooms); + fromJson(jo.value("contains_url"_ls), pod.containsUrl); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/sync_filter.cpp b/lib/csapi/definitions/sync_filter.cpp deleted file mode 100644 index 32752d1f..00000000 --- a/lib/csapi/definitions/sync_filter.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "sync_filter.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<StateFilter>::dumpTo( - QJsonObject& jo, const StateFilter& pod) -{ - fillJson<RoomEventFilter>(jo, pod); - addParam<IfNotEmpty>(jo, QStringLiteral("lazy_load_members"), pod.lazyLoadMembers); - addParam<IfNotEmpty>(jo, QStringLiteral("include_redundant_members"), pod.includeRedundantMembers); -} - -void JsonObjectConverter<StateFilter>::fillFrom( - const QJsonObject& jo, StateFilter& result) -{ - fillFromJson<RoomEventFilter>(jo, result); - fromJson(jo.value("lazy_load_members"_ls), result.lazyLoadMembers); - fromJson(jo.value("include_redundant_members"_ls), result.includeRedundantMembers); -} - -void JsonObjectConverter<RoomFilter>::dumpTo( - QJsonObject& jo, const RoomFilter& pod) -{ - addParam<IfNotEmpty>(jo, QStringLiteral("not_rooms"), pod.notRooms); - addParam<IfNotEmpty>(jo, QStringLiteral("rooms"), pod.rooms); - addParam<IfNotEmpty>(jo, QStringLiteral("ephemeral"), pod.ephemeral); - addParam<IfNotEmpty>(jo, QStringLiteral("include_leave"), pod.includeLeave); - addParam<IfNotEmpty>(jo, QStringLiteral("state"), pod.state); - addParam<IfNotEmpty>(jo, QStringLiteral("timeline"), pod.timeline); - addParam<IfNotEmpty>(jo, QStringLiteral("account_data"), pod.accountData); -} - -void JsonObjectConverter<RoomFilter>::fillFrom( - const QJsonObject& jo, RoomFilter& result) -{ - fromJson(jo.value("not_rooms"_ls), result.notRooms); - fromJson(jo.value("rooms"_ls), result.rooms); - fromJson(jo.value("ephemeral"_ls), result.ephemeral); - fromJson(jo.value("include_leave"_ls), result.includeLeave); - fromJson(jo.value("state"_ls), result.state); - fromJson(jo.value("timeline"_ls), result.timeline); - fromJson(jo.value("account_data"_ls), result.accountData); -} - -void JsonObjectConverter<Filter>::dumpTo( - QJsonObject& jo, const Filter& pod) -{ - addParam<IfNotEmpty>(jo, QStringLiteral("event_fields"), pod.eventFields); - addParam<IfNotEmpty>(jo, QStringLiteral("event_format"), pod.eventFormat); - addParam<IfNotEmpty>(jo, QStringLiteral("presence"), pod.presence); - addParam<IfNotEmpty>(jo, QStringLiteral("account_data"), pod.accountData); - addParam<IfNotEmpty>(jo, QStringLiteral("room"), pod.room); -} - -void JsonObjectConverter<Filter>::fillFrom( - const QJsonObject& jo, Filter& result) -{ - fromJson(jo.value("event_fields"_ls), result.eventFields); - fromJson(jo.value("event_format"_ls), result.eventFormat); - fromJson(jo.value("presence"_ls), result.presence); - fromJson(jo.value("account_data"_ls), result.accountData); - fromJson(jo.value("room"_ls), result.room); -} - diff --git a/lib/csapi/definitions/sync_filter.h b/lib/csapi/definitions/sync_filter.h index d94c74d7..9c8f08d5 100644 --- a/lib/csapi/definitions/sync_filter.h +++ b/lib/csapi/definitions/sync_filter.h @@ -6,86 +6,110 @@ #include "converters.h" -#include "csapi/definitions/room_event_filter.h" -#include "converters.h" #include "csapi/definitions/event_filter.h" +#include "csapi/definitions/room_event_filter.h" + +namespace Quotient { +/// Filters to be applied to room data. +struct RoomFilter { + /// A list of room IDs to exclude. If this list is absent then no rooms are + /// excluded. A matching room will be excluded even if it is listed in the + /// ``'rooms'`` filter. This filter is applied before the filters in + /// ``ephemeral``, ``state``, ``timeline`` or ``account_data`` + QStringList notRooms; -namespace QMatrixClient -{ - // Data structures + /// A list of room IDs to include. If this list is absent then all rooms are + /// included. This filter is applied before the filters in ``ephemeral``, + /// ``state``, ``timeline`` or ``account_data`` + QStringList rooms; + + /// The events that aren't recorded in the room history, e.g. typing and + /// receipts, to include for rooms. + RoomEventFilter ephemeral; + + /// Include rooms that the user has left in the sync, default false + Omittable<bool> includeLeave; /// The state events to include for rooms. - struct StateFilter : RoomEventFilter + RoomEventFilter state; + + /// The message and state update events to include for rooms. + RoomEventFilter timeline; + + /// The per user account data to include for rooms. + RoomEventFilter accountData; +}; + +template <> +struct JsonObjectConverter<RoomFilter> { + static void dumpTo(QJsonObject& jo, const RoomFilter& pod) { - /// If ``true``, the only ``m.room.member`` events returned in - /// the ``state`` section of the ``/sync`` response are those - /// which are definitely necessary for a client to display - /// the ``sender`` of the timeline events in that response. - /// If ``false``, ``m.room.member`` events are not filtered. - /// By default, servers should suppress duplicate redundant - /// lazy-loaded ``m.room.member`` events from being sent to a given - /// client across multiple calls to ``/sync``, given that most clients - /// cache membership events (see ``include_redundant_members`` - /// to change this behaviour). - Omittable<bool> lazyLoadMembers; - /// If ``true``, the ``state`` section of the ``/sync`` response will - /// always contain the ``m.room.member`` events required to display - /// the ``sender`` of the timeline events in that response, assuming - /// ``lazy_load_members`` is enabled. This means that redundant - /// duplicate member events may be returned across multiple calls to - /// ``/sync``. This is useful for naive clients who never track - /// membership data. If ``false``, duplicate ``m.room.member`` events - /// may be suppressed by the server across multiple calls to ``/sync``. - /// If ``lazy_load_members`` is ``false`` this field is ignored. - Omittable<bool> includeRedundantMembers; - }; - template <> struct JsonObjectConverter<StateFilter> + addParam<IfNotEmpty>(jo, QStringLiteral("not_rooms"), pod.notRooms); + addParam<IfNotEmpty>(jo, QStringLiteral("rooms"), pod.rooms); + addParam<IfNotEmpty>(jo, QStringLiteral("ephemeral"), pod.ephemeral); + addParam<IfNotEmpty>(jo, QStringLiteral("include_leave"), + pod.includeLeave); + addParam<IfNotEmpty>(jo, QStringLiteral("state"), pod.state); + addParam<IfNotEmpty>(jo, QStringLiteral("timeline"), pod.timeline); + addParam<IfNotEmpty>(jo, QStringLiteral("account_data"), + pod.accountData); + } + static void fillFrom(const QJsonObject& jo, RoomFilter& pod) { - static void dumpTo(QJsonObject& jo, const StateFilter& pod); - static void fillFrom(const QJsonObject& jo, StateFilter& pod); - }; + fromJson(jo.value("not_rooms"_ls), pod.notRooms); + fromJson(jo.value("rooms"_ls), pod.rooms); + fromJson(jo.value("ephemeral"_ls), pod.ephemeral); + fromJson(jo.value("include_leave"_ls), pod.includeLeave); + fromJson(jo.value("state"_ls), pod.state); + fromJson(jo.value("timeline"_ls), pod.timeline); + fromJson(jo.value("account_data"_ls), pod.accountData); + } +}; + +struct Filter { + /// List of event fields to include. If this list is absent then all fields + /// are included. The entries may include '.' characters to indicate + /// sub-fields. So ['content.body'] will include the 'body' field of the + /// 'content' object. A literal '.' character in a field name may be escaped + /// using a '\\'. A server may include more fields than were requested. + QStringList eventFields; + + /// The format to use for events. 'client' will return the events in a + /// format suitable for clients. 'federation' will return the raw event as + /// received over federation. The default is 'client'. + QString eventFormat; + + /// The presence updates to include. + EventFilter presence; + + /// The user account data that isn't associated with rooms to include. + EventFilter accountData; /// Filters to be applied to room data. - struct RoomFilter - { - /// A list of room IDs to exclude. If this list is absent then no rooms are excluded. A matching room will be excluded even if it is listed in the ``'rooms'`` filter. This filter is applied before the filters in ``ephemeral``, ``state``, ``timeline`` or ``account_data`` - QStringList notRooms; - /// A list of room IDs to include. If this list is absent then all rooms are included. This filter is applied before the filters in ``ephemeral``, ``state``, ``timeline`` or ``account_data`` - QStringList rooms; - /// The events that aren't recorded in the room history, e.g. typing and receipts, to include for rooms. - Omittable<RoomEventFilter> ephemeral; - /// Include rooms that the user has left in the sync, default false - Omittable<bool> includeLeave; - /// The state events to include for rooms. - Omittable<StateFilter> state; - /// The message and state update events to include for rooms. - Omittable<RoomEventFilter> timeline; - /// The per user account data to include for rooms. - Omittable<RoomEventFilter> accountData; - }; - template <> struct JsonObjectConverter<RoomFilter> - { - static void dumpTo(QJsonObject& jo, const RoomFilter& pod); - static void fillFrom(const QJsonObject& jo, RoomFilter& pod); - }; + RoomFilter room; +}; - struct Filter +template <> +struct JsonObjectConverter<Filter> { + static void dumpTo(QJsonObject& jo, const Filter& pod) { - /// List of event fields to include. If this list is absent then all fields are included. The entries may include '.' charaters to indicate sub-fields. So ['content.body'] will include the 'body' field of the 'content' object. A literal '.' character in a field name may be escaped using a '\\'. A server may include more fields than were requested. - QStringList eventFields; - /// The format to use for events. 'client' will return the events in a format suitable for clients. 'federation' will return the raw event as receieved over federation. The default is 'client'. - QString eventFormat; - /// The presence updates to include. - Omittable<EventFilter> presence; - /// The user account data that isn't associated with rooms to include. - Omittable<EventFilter> accountData; - /// Filters to be applied to room data. - Omittable<RoomFilter> room; - }; - template <> struct JsonObjectConverter<Filter> + addParam<IfNotEmpty>(jo, QStringLiteral("event_fields"), + pod.eventFields); + addParam<IfNotEmpty>(jo, QStringLiteral("event_format"), + pod.eventFormat); + addParam<IfNotEmpty>(jo, QStringLiteral("presence"), pod.presence); + addParam<IfNotEmpty>(jo, QStringLiteral("account_data"), + pod.accountData); + addParam<IfNotEmpty>(jo, QStringLiteral("room"), pod.room); + } + static void fillFrom(const QJsonObject& jo, Filter& pod) { - static void dumpTo(QJsonObject& jo, const Filter& pod); - static void fillFrom(const QJsonObject& jo, Filter& pod); - }; + fromJson(jo.value("event_fields"_ls), pod.eventFields); + fromJson(jo.value("event_format"_ls), pod.eventFormat); + fromJson(jo.value("presence"_ls), pod.presence); + fromJson(jo.value("account_data"_ls), pod.accountData); + fromJson(jo.value("room"_ls), pod.room); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/third_party_signed.h b/lib/csapi/definitions/third_party_signed.h new file mode 100644 index 00000000..526545d0 --- /dev/null +++ b/lib/csapi/definitions/third_party_signed.h @@ -0,0 +1,44 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +namespace Quotient { +/// A signature of an ``m.third_party_invite`` token to prove that this user +/// owns a third party identity which has been invited to the room. +struct ThirdPartySigned { + /// The Matrix ID of the user who issued the invite. + QString sender; + + /// The Matrix ID of the invitee. + QString mxid; + + /// The state key of the m.third_party_invite event. + QString token; + + /// A signatures object containing a signature of the entire signed object. + QHash<QString, QHash<QString, QString>> signatures; +}; + +template <> +struct JsonObjectConverter<ThirdPartySigned> { + static void dumpTo(QJsonObject& jo, const ThirdPartySigned& pod) + { + addParam<>(jo, QStringLiteral("sender"), pod.sender); + addParam<>(jo, QStringLiteral("mxid"), pod.mxid); + addParam<>(jo, QStringLiteral("token"), pod.token); + addParam<>(jo, QStringLiteral("signatures"), pod.signatures); + } + static void fillFrom(const QJsonObject& jo, ThirdPartySigned& pod) + { + fromJson(jo.value("sender"_ls), pod.sender); + fromJson(jo.value("mxid"_ls), pod.mxid); + fromJson(jo.value("token"_ls), pod.token); + fromJson(jo.value("signatures"_ls), pod.signatures); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/user_identifier.cpp b/lib/csapi/definitions/user_identifier.cpp deleted file mode 100644 index 05a27c1c..00000000 --- a/lib/csapi/definitions/user_identifier.cpp +++ /dev/null @@ -1,23 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "user_identifier.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<UserIdentifier>::dumpTo( - QJsonObject& jo, const UserIdentifier& pod) -{ - fillJson(jo, pod.additionalProperties); - addParam<>(jo, QStringLiteral("type"), pod.type); -} - -void JsonObjectConverter<UserIdentifier>::fillFrom( - QJsonObject jo, UserIdentifier& result) -{ - fromJson(jo.take("type"_ls), result.type); - - fromJson(jo, result.additionalProperties); -} - diff --git a/lib/csapi/definitions/user_identifier.h b/lib/csapi/definitions/user_identifier.h index cbb1550f..dadf6f97 100644 --- a/lib/csapi/definitions/user_identifier.h +++ b/lib/csapi/definitions/user_identifier.h @@ -6,24 +6,29 @@ #include "converters.h" -#include <QtCore/QVariant> - -namespace QMatrixClient -{ - // Data structures +namespace Quotient { +/// Identification information for a user +struct UserIdentifier { + /// The type of identification. See `Identifier types`_ for supported + /// values and additional property descriptions. + QString type; /// Identification information for a user - struct UserIdentifier + QVariantHash additionalProperties; +}; + +template <> +struct JsonObjectConverter<UserIdentifier> { + static void dumpTo(QJsonObject& jo, const UserIdentifier& pod) { - /// The type of identification. See `Identifier types`_ for supported values and additional property descriptions. - QString type; - /// Identification information for a user - QVariantHash additionalProperties; - }; - template <> struct JsonObjectConverter<UserIdentifier> + fillJson(jo, pod.additionalProperties); + addParam<>(jo, QStringLiteral("type"), pod.type); + } + static void fillFrom(QJsonObject jo, UserIdentifier& pod) { - static void dumpTo(QJsonObject& jo, const UserIdentifier& pod); - static void fillFrom(QJsonObject jo, UserIdentifier& pod); - }; + fromJson(jo.take("type"_ls), pod.type); + fromJson(jo, pod.additionalProperties); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/wellknown/full.cpp b/lib/csapi/definitions/wellknown/full.cpp deleted file mode 100644 index 5ecef34f..00000000 --- a/lib/csapi/definitions/wellknown/full.cpp +++ /dev/null @@ -1,25 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "full.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<DiscoveryInformation>::dumpTo( - QJsonObject& jo, const DiscoveryInformation& pod) -{ - fillJson(jo, pod.additionalProperties); - addParam<>(jo, QStringLiteral("m.homeserver"), pod.homeserver); - addParam<IfNotEmpty>(jo, QStringLiteral("m.identity_server"), pod.identityServer); -} - -void JsonObjectConverter<DiscoveryInformation>::fillFrom( - QJsonObject jo, DiscoveryInformation& result) -{ - fromJson(jo.take("m.homeserver"_ls), result.homeserver); - fromJson(jo.take("m.identity_server"_ls), result.identityServer); - - fromJson(jo, result.additionalProperties); -} - diff --git a/lib/csapi/definitions/wellknown/full.h b/lib/csapi/definitions/wellknown/full.h index d9346acb..a0ef2076 100644 --- a/lib/csapi/definitions/wellknown/full.h +++ b/lib/csapi/definitions/wellknown/full.h @@ -6,33 +6,40 @@ #include "converters.h" -#include <QtCore/QJsonObject> -#include "converters.h" #include "csapi/definitions/wellknown/homeserver.h" #include "csapi/definitions/wellknown/identity_server.h" -#include <QtCore/QHash> -namespace QMatrixClient -{ - // Data structures +namespace Quotient { +/// Used by clients to determine the homeserver, identity server, and other +/// optional components they should be interacting with. +struct DiscoveryInformation { + /// Used by clients to determine the homeserver, identity server, and other + /// optional components they should be interacting with. + HomeserverInformation homeserver; /// Used by clients to determine the homeserver, identity server, and other /// optional components they should be interacting with. - struct DiscoveryInformation + Omittable<IdentityServerInformation> identityServer; + + /// Application-dependent keys using Java package naming convention. + QHash<QString, QJsonObject> additionalProperties; +}; + +template <> +struct JsonObjectConverter<DiscoveryInformation> { + static void dumpTo(QJsonObject& jo, const DiscoveryInformation& pod) { - /// Used by clients to determine the homeserver, identity server, and other - /// optional components they should be interacting with. - HomeserverInformation homeserver; - /// Used by clients to determine the homeserver, identity server, and other - /// optional components they should be interacting with. - Omittable<IdentityServerInformation> identityServer; - /// Application-dependent keys using Java package naming convention. - QHash<QString, QJsonObject> additionalProperties; - }; - template <> struct JsonObjectConverter<DiscoveryInformation> + fillJson(jo, pod.additionalProperties); + addParam<>(jo, QStringLiteral("m.homeserver"), pod.homeserver); + addParam<IfNotEmpty>(jo, QStringLiteral("m.identity_server"), + pod.identityServer); + } + static void fillFrom(QJsonObject jo, DiscoveryInformation& pod) { - static void dumpTo(QJsonObject& jo, const DiscoveryInformation& pod); - static void fillFrom(QJsonObject jo, DiscoveryInformation& pod); - }; + fromJson(jo.take("m.homeserver"_ls), pod.homeserver); + fromJson(jo.take("m.identity_server"_ls), pod.identityServer); + fromJson(jo, pod.additionalProperties); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/wellknown/homeserver.cpp b/lib/csapi/definitions/wellknown/homeserver.cpp deleted file mode 100644 index 0783f11b..00000000 --- a/lib/csapi/definitions/wellknown/homeserver.cpp +++ /dev/null @@ -1,20 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "homeserver.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<HomeserverInformation>::dumpTo( - QJsonObject& jo, const HomeserverInformation& pod) -{ - addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl); -} - -void JsonObjectConverter<HomeserverInformation>::fillFrom( - const QJsonObject& jo, HomeserverInformation& result) -{ - fromJson(jo.value("base_url"_ls), result.baseUrl); -} - diff --git a/lib/csapi/definitions/wellknown/homeserver.h b/lib/csapi/definitions/wellknown/homeserver.h index f6761c30..5cfaca24 100644 --- a/lib/csapi/definitions/wellknown/homeserver.h +++ b/lib/csapi/definitions/wellknown/homeserver.h @@ -6,21 +6,23 @@ #include "converters.h" +namespace Quotient { +/// Used by clients to discover homeserver information. +struct HomeserverInformation { + /// The base URL for the homeserver for client-server connections. + QString baseUrl; +}; -namespace QMatrixClient -{ - // Data structures - - /// Used by clients to discover homeserver information. - struct HomeserverInformation +template <> +struct JsonObjectConverter<HomeserverInformation> { + static void dumpTo(QJsonObject& jo, const HomeserverInformation& pod) { - /// The base URL for the homeserver for client-server connections. - QString baseUrl; - }; - template <> struct JsonObjectConverter<HomeserverInformation> + addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl); + } + static void fillFrom(const QJsonObject& jo, HomeserverInformation& pod) { - static void dumpTo(QJsonObject& jo, const HomeserverInformation& pod); - static void fillFrom(const QJsonObject& jo, HomeserverInformation& pod); - }; + fromJson(jo.value("base_url"_ls), pod.baseUrl); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/definitions/wellknown/identity_server.cpp b/lib/csapi/definitions/wellknown/identity_server.cpp deleted file mode 100644 index 99f36641..00000000 --- a/lib/csapi/definitions/wellknown/identity_server.cpp +++ /dev/null @@ -1,20 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "identity_server.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<IdentityServerInformation>::dumpTo( - QJsonObject& jo, const IdentityServerInformation& pod) -{ - addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl); -} - -void JsonObjectConverter<IdentityServerInformation>::fillFrom( - const QJsonObject& jo, IdentityServerInformation& result) -{ - fromJson(jo.value("base_url"_ls), result.baseUrl); -} - diff --git a/lib/csapi/definitions/wellknown/identity_server.h b/lib/csapi/definitions/wellknown/identity_server.h index 67d8b08d..3bd07bd1 100644 --- a/lib/csapi/definitions/wellknown/identity_server.h +++ b/lib/csapi/definitions/wellknown/identity_server.h @@ -6,21 +6,23 @@ #include "converters.h" +namespace Quotient { +/// Used by clients to discover identity server information. +struct IdentityServerInformation { + /// The base URL for the identity server for client-server connections. + QString baseUrl; +}; -namespace QMatrixClient -{ - // Data structures - - /// Used by clients to discover identity server information. - struct IdentityServerInformation +template <> +struct JsonObjectConverter<IdentityServerInformation> { + static void dumpTo(QJsonObject& jo, const IdentityServerInformation& pod) { - /// The base URL for the identity server for client-server connections. - QString baseUrl; - }; - template <> struct JsonObjectConverter<IdentityServerInformation> + addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl); + } + static void fillFrom(const QJsonObject& jo, IdentityServerInformation& pod) { - static void dumpTo(QJsonObject& jo, const IdentityServerInformation& pod); - static void fillFrom(const QJsonObject& jo, IdentityServerInformation& pod); - }; + fromJson(jo.value("base_url"_ls), pod.baseUrl); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/device_management.cpp b/lib/csapi/device_management.cpp index 9c31db5d..eac9a545 100644 --- a/lib/csapi/device_management.cpp +++ b/lib/csapi/device_management.cpp @@ -4,114 +4,61 @@ #include "device_management.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetDevicesJob::Private -{ - public: - QVector<Device> devices; -}; +using namespace Quotient; QUrl GetDevicesJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/devices"); + QStringLiteral("/_matrix/client/r0") + % "/devices"); } -static const auto GetDevicesJobName = QStringLiteral("GetDevicesJob"); - GetDevicesJob::GetDevicesJob() - : BaseJob(HttpVerb::Get, GetDevicesJobName, - basePath % "/devices") - , d(new Private) -{ -} - -GetDevicesJob::~GetDevicesJob() = default; - -const QVector<Device>& GetDevicesJob::devices() const -{ - return d->devices; -} - -BaseJob::Status GetDevicesJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("devices"_ls), d->devices); - return Success; -} - -class GetDeviceJob::Private -{ - public: - Device data; -}; + : BaseJob(HttpVerb::Get, QStringLiteral("GetDevicesJob"), + QStringLiteral("/_matrix/client/r0") % "/devices") +{} QUrl GetDeviceJob::makeRequestUrl(QUrl baseUrl, const QString& deviceId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/devices/" % deviceId); + QStringLiteral("/_matrix/client/r0") + % "/devices/" % deviceId); } -static const auto GetDeviceJobName = QStringLiteral("GetDeviceJob"); - GetDeviceJob::GetDeviceJob(const QString& deviceId) - : BaseJob(HttpVerb::Get, GetDeviceJobName, - basePath % "/devices/" % deviceId) - , d(new Private) -{ -} - -GetDeviceJob::~GetDeviceJob() = default; - -const Device& GetDeviceJob::data() const -{ - return d->data; -} - -BaseJob::Status GetDeviceJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - -static const auto UpdateDeviceJobName = QStringLiteral("UpdateDeviceJob"); - -UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId, const QString& displayName) - : BaseJob(HttpVerb::Put, UpdateDeviceJobName, - basePath % "/devices/" % deviceId) + : BaseJob(HttpVerb::Get, QStringLiteral("GetDeviceJob"), + QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId) +{} + +UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId, + const QString& displayName) + : BaseJob(HttpVerb::Put, QStringLiteral("UpdateDeviceJob"), + QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("display_name"), displayName); - setRequestData(_data); + setRequestData(std::move(_data)); } -static const auto DeleteDeviceJobName = QStringLiteral("DeleteDeviceJob"); - -DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId, const Omittable<AuthenticationData>& auth) - : BaseJob(HttpVerb::Delete, DeleteDeviceJobName, - basePath % "/devices/" % deviceId) +DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId, + const Omittable<AuthenticationData>& auth) + : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), + QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); - setRequestData(_data); + setRequestData(std::move(_data)); } -static const auto DeleteDevicesJobName = QStringLiteral("DeleteDevicesJob"); - -DeleteDevicesJob::DeleteDevicesJob(const QStringList& devices, const Omittable<AuthenticationData>& auth) - : BaseJob(HttpVerb::Post, DeleteDevicesJobName, - basePath % "/delete_devices") +DeleteDevicesJob::DeleteDevicesJob(const QStringList& devices, + const Omittable<AuthenticationData>& auth) + : BaseJob(HttpVerb::Post, QStringLiteral("DeleteDevicesJob"), + QStringLiteral("/_matrix/client/r0") % "/delete_devices") { QJsonObject _data; addParam<>(_data, QStringLiteral("devices"), devices); addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); - setRequestData(_data); + setRequestData(std::move(_data)); } - diff --git a/lib/csapi/device_management.h b/lib/csapi/device_management.h index f41efdbc..47dc7ec8 100644 --- a/lib/csapi/device_management.h +++ b/lib/csapi/device_management.h @@ -4,132 +4,124 @@ #pragma once -#include "jobs/basejob.h" - #include "csapi/definitions/auth_data.h" -#include <QtCore/QVector> -#include "converters.h" #include "csapi/definitions/client_device.h" -namespace QMatrixClient -{ - // Operations - - /// List registered devices for the current user - /// - /// Gets information about all devices for the current user. - class GetDevicesJob : public BaseJob - { - public: - explicit GetDevicesJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetDevicesJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetDevicesJob() override; +#include "jobs/basejob.h" - // Result properties +namespace Quotient { - /// A list of all registered devices for this user. - const QVector<Device>& devices() const; +/*! \brief List registered devices for the current user + * + * Gets information about all devices for the current user. + */ +class GetDevicesJob : public BaseJob { +public: + /// List registered devices for the current user + explicit GetDevicesJob(); - protected: - Status parseJson(const QJsonDocument& data) override; + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetDevicesJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); - private: - class Private; - QScopedPointer<Private> d; - }; + // Result properties - /// Get a single device - /// - /// Gets information on a single device, by device id. - class GetDeviceJob : public BaseJob - { - public: - /*! Get a single device - * \param deviceId - * The device to retrieve. - */ - explicit GetDeviceJob(const QString& deviceId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetDeviceJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& deviceId); - - ~GetDeviceJob() override; - - // Result properties - - /// Device information - const Device& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Update a device - /// - /// Updates the metadata on the given device. - class UpdateDeviceJob : public BaseJob - { - public: - /*! Update a device - * \param deviceId - * The device to update. - * \param displayName - * The new display name for this device. If not given, the - * display name is unchanged. - */ - explicit UpdateDeviceJob(const QString& deviceId, const QString& displayName = {}); - }; - - /// Delete a device - /// - /// This API endpoint uses the `User-Interactive Authentication API`_. - /// - /// Deletes the given device, and invalidates any access token associated with it. - class DeleteDeviceJob : public BaseJob + /// A list of all registered devices for this user. + QVector<Device> devices() const { - public: - /*! Delete a device - * \param deviceId - * The device to delete. - * \param auth - * Additional authentication information for the - * user-interactive authentication API. - */ - explicit DeleteDeviceJob(const QString& deviceId, const Omittable<AuthenticationData>& auth = none); - }; - - /// Bulk deletion of devices - /// - /// This API endpoint uses the `User-Interactive Authentication API`_. - /// - /// Deletes the given devices, and invalidates any access token associated with them. - class DeleteDevicesJob : public BaseJob - { - public: - /*! Bulk deletion of devices - * \param devices - * The list of device IDs to delete. - * \param auth - * Additional authentication information for the - * user-interactive authentication API. - */ - explicit DeleteDevicesJob(const QStringList& devices, const Omittable<AuthenticationData>& auth = none); - }; -} // namespace QMatrixClient + return loadFromJson<QVector<Device>>("devices"_ls); + } +}; + +/*! \brief Get a single device + * + * Gets information on a single device, by device id. + */ +class GetDeviceJob : public BaseJob { +public: + /*! \brief Get a single device + * + * \param deviceId + * The device to retrieve. + */ + explicit GetDeviceJob(const QString& deviceId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetDeviceJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& deviceId); + + // Result properties + + /// Device information + Device device() const { return fromJson<Device>(jsonData()); } +}; + +/*! \brief Update a device + * + * Updates the metadata on the given device. + */ +class UpdateDeviceJob : public BaseJob { +public: + /*! \brief Update a device + * + * \param deviceId + * The device to update. + * + * \param displayName + * The new display name for this device. If not given, the + * display name is unchanged. + */ + explicit UpdateDeviceJob(const QString& deviceId, + const QString& displayName = {}); +}; + +/*! \brief Delete a device + * + * This API endpoint uses the `User-Interactive Authentication API`_. + * + * Deletes the given device, and invalidates any access token associated with it. + */ +class DeleteDeviceJob : public BaseJob { +public: + /*! \brief Delete a device + * + * \param deviceId + * The device to delete. + * + * \param auth + * Additional authentication information for the + * user-interactive authentication API. + */ + explicit DeleteDeviceJob(const QString& deviceId, + const Omittable<AuthenticationData>& auth = none); +}; + +/*! \brief Bulk deletion of devices + * + * This API endpoint uses the `User-Interactive Authentication API`_. + * + * Deletes the given devices, and invalidates any access token associated with + * them. + */ +class DeleteDevicesJob : public BaseJob { +public: + /*! \brief Bulk deletion of devices + * + * \param devices + * The list of device IDs to delete. + * + * \param auth + * Additional authentication information for the + * user-interactive authentication API. + */ + explicit DeleteDevicesJob(const QStringList& devices, + const Omittable<AuthenticationData>& auth = none); +}; + +} // namespace Quotient diff --git a/lib/csapi/directory.cpp b/lib/csapi/directory.cpp index 4af86f7b..25ea82e2 100644 --- a/lib/csapi/directory.cpp +++ b/lib/csapi/directory.cpp @@ -4,78 +4,58 @@ #include "directory.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0/directory"); - -static const auto SetRoomAliasJobName = QStringLiteral("SetRoomAliasJob"); +using namespace Quotient; SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId) - : BaseJob(HttpVerb::Put, SetRoomAliasJobName, - basePath % "/room/" % roomAlias) + : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomAliasJob"), + QStringLiteral("/_matrix/client/r0") % "/directory/room/" + % roomAlias) { QJsonObject _data; addParam<>(_data, QStringLiteral("room_id"), roomId); - setRequestData(_data); + setRequestData(std::move(_data)); } -class GetRoomIdByAliasJob::Private -{ - public: - QString roomId; - QStringList servers; -}; - QUrl GetRoomIdByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/room/" % roomAlias); + QStringLiteral("/_matrix/client/r0") + % "/directory/room/" % roomAlias); } -static const auto GetRoomIdByAliasJobName = QStringLiteral("GetRoomIdByAliasJob"); - GetRoomIdByAliasJob::GetRoomIdByAliasJob(const QString& roomAlias) - : BaseJob(HttpVerb::Get, GetRoomIdByAliasJobName, - basePath % "/room/" % roomAlias, false) - , d(new Private) -{ -} - -GetRoomIdByAliasJob::~GetRoomIdByAliasJob() = default; + : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomIdByAliasJob"), + QStringLiteral("/_matrix/client/r0") % "/directory/room/" + % roomAlias, + false) +{} -const QString& GetRoomIdByAliasJob::roomId() const -{ - return d->roomId; -} - -const QStringList& GetRoomIdByAliasJob::servers() const +QUrl DeleteRoomAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) { - return d->servers; + return BaseJob::makeRequestUrl(std::move(baseUrl), + QStringLiteral("/_matrix/client/r0") + % "/directory/room/" % roomAlias); } -BaseJob::Status GetRoomIdByAliasJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("room_id"_ls), d->roomId); - fromJson(json.value("servers"_ls), d->servers); - return Success; -} +DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias) + : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomAliasJob"), + QStringLiteral("/_matrix/client/r0") % "/directory/room/" + % roomAlias) +{} -QUrl DeleteRoomAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) +QUrl GetLocalAliasesJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/room/" % roomAlias); + QStringLiteral("/_matrix/client/r0") + % "/rooms/" % roomId % "/aliases"); } -static const auto DeleteRoomAliasJobName = QStringLiteral("DeleteRoomAliasJob"); - -DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias) - : BaseJob(HttpVerb::Delete, DeleteRoomAliasJobName, - basePath % "/room/" % roomAlias) +GetLocalAliasesJob::GetLocalAliasesJob(const QString& roomId) + : BaseJob(HttpVerb::Get, QStringLiteral("GetLocalAliasesJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/aliases") { + addExpectedKey("aliases"); } - diff --git a/lib/csapi/directory.h b/lib/csapi/directory.h index 39e86635..9b109aad 100644 --- a/lib/csapi/directory.h +++ b/lib/csapi/directory.h @@ -6,86 +6,135 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Create a new mapping from room alias to room ID. + * + */ +class SetRoomAliasJob : public BaseJob { +public: + /*! \brief Create a new mapping from room alias to room ID. + * + * \param roomAlias + * The room alias to set. + * + * \param roomId + * The room ID to set. + */ + explicit SetRoomAliasJob(const QString& roomAlias, const QString& roomId); +}; - /// Create a new mapping from room alias to room ID. - class SetRoomAliasJob : public BaseJob - { - public: - /*! Create a new mapping from room alias to room ID. - * \param roomAlias - * The room alias to set. - * \param roomId - * The room ID to set. - */ - explicit SetRoomAliasJob(const QString& roomAlias, const QString& roomId); - }; - - /// Get the room ID corresponding to this room alias. - /// - /// Requests that the server resolve a room alias to a room ID. - /// - /// The server will use the federation API to resolve the alias if the - /// domain part of the alias does not correspond to the server's own - /// domain. - class GetRoomIdByAliasJob : public BaseJob +/*! \brief Get the room ID corresponding to this room alias. + * + * Requests that the server resolve a room alias to a room ID. + * + * The server will use the federation API to resolve the alias if the + * domain part of the alias does not correspond to the server's own + * domain. + */ +class GetRoomIdByAliasJob : public BaseJob { +public: + /*! \brief Get the room ID corresponding to this room alias. + * + * \param roomAlias + * The room alias. + */ + explicit GetRoomIdByAliasJob(const QString& roomAlias); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRoomIdByAliasJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomAlias); + + // Result properties + + /// The room ID for this room alias. + QString roomId() const { return loadFromJson<QString>("room_id"_ls); } + + /// A list of servers that are aware of this room alias. + QStringList servers() const { - public: - /*! Get the room ID corresponding to this room alias. - * \param roomAlias - * The room alias. - */ - explicit GetRoomIdByAliasJob(const QString& roomAlias); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetRoomIdByAliasJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomAlias); - - ~GetRoomIdByAliasJob() override; - - // Result properties - - /// The room ID for this room alias. - const QString& roomId() const; - /// A list of servers that are aware of this room alias. - const QStringList& servers() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Remove a mapping of room alias to room ID. - /// - /// Remove a mapping of room alias to room ID. - /// - /// Servers may choose to implement additional access control checks here, for instance that room aliases can only be deleted by their creator or a server administrator. - class DeleteRoomAliasJob : public BaseJob + return loadFromJson<QStringList>("servers"_ls); + } +}; + +/*! \brief Remove a mapping of room alias to room ID. + * + * Remove a mapping of room alias to room ID. + * + * Servers may choose to implement additional access control checks here, for + * instance that room aliases can only be deleted by their creator or a server + * administrator. + * + * .. Note:: + * Servers may choose to update the ``alt_aliases`` for the + * ``m.room.canonical_alias`` state event in the room when an alias is removed. + * Servers which choose to update the canonical alias event are recommended to, + * in addition to their other relevant permission checks, delete the alias and + * return a successful response even if the user does not have permission to + * update the ``m.room.canonical_alias`` event. + */ +class DeleteRoomAliasJob : public BaseJob { +public: + /*! \brief Remove a mapping of room alias to room ID. + * + * \param roomAlias + * The room alias to remove. + */ + explicit DeleteRoomAliasJob(const QString& roomAlias); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for DeleteRoomAliasJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomAlias); +}; + +/*! \brief Get a list of local aliases on a given room. + * + * Get a list of aliases maintained by the local server for the + * given room. + * + * This endpoint can be called by users who are in the room (external + * users receive an ``M_FORBIDDEN`` error response). If the room's + * ``m.room.history_visibility`` maps to ``world_readable``, any + * user can call this endpoint. + * + * Servers may choose to implement additional access control checks here, + * such as allowing server administrators to view aliases regardless of + * membership. + * + * .. Note:: + * Clients are recommended not to display this list of aliases prominently + * as they are not curated, unlike those listed in the + * ``m.room.canonical_alias`` state event. + */ +class GetLocalAliasesJob : public BaseJob { +public: + /*! \brief Get a list of local aliases on a given room. + * + * \param roomId + * The room ID to find local aliases of. + */ + explicit GetLocalAliasesJob(const QString& roomId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetLocalAliasesJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); + + // Result properties + + /// The server's local aliases on the room. Can be empty. + QStringList aliases() const { - public: - /*! Remove a mapping of room alias to room ID. - * \param roomAlias - * The room alias to remove. - */ - explicit DeleteRoomAliasJob(const QString& roomAlias); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * DeleteRoomAliasJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomAlias); - - }; -} // namespace QMatrixClient + return loadFromJson<QStringList>("aliases"_ls); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/event_context.cpp b/lib/csapi/event_context.cpp index bb1f5301..d2a5f522 100644 --- a/lib/csapi/event_context.cpp +++ b/lib/csapi/event_context.cpp @@ -4,90 +4,36 @@ #include "event_context.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetEventContextJob::Private -{ - public: - QString begin; - QString end; - RoomEvents eventsBefore; - RoomEventPtr event; - RoomEvents eventsAfter; - StateEvents state; -}; +using namespace Quotient; -BaseJob::Query queryToGetEventContext(Omittable<int> limit) +auto queryToGetEventContext(Omittable<int> limit, const QString& filter) { BaseJob::Query _q; addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); + addParam<IfNotEmpty>(_q, QStringLiteral("filter"), filter); return _q; } -QUrl GetEventContextJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId, Omittable<int> limit) +QUrl GetEventContextJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, + Omittable<int> limit, + const QString& filter) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/context/" % eventId, - queryToGetEventContext(limit)); -} - -static const auto GetEventContextJobName = QStringLiteral("GetEventContextJob"); - -GetEventContextJob::GetEventContextJob(const QString& roomId, const QString& eventId, Omittable<int> limit) - : BaseJob(HttpVerb::Get, GetEventContextJobName, - basePath % "/rooms/" % roomId % "/context/" % eventId, - queryToGetEventContext(limit)) - , d(new Private) -{ -} - -GetEventContextJob::~GetEventContextJob() = default; - -const QString& GetEventContextJob::begin() const -{ - return d->begin; -} - -const QString& GetEventContextJob::end() const -{ - return d->end; -} - -RoomEvents&& GetEventContextJob::eventsBefore() -{ - return std::move(d->eventsBefore); -} - -RoomEventPtr&& GetEventContextJob::event() -{ - return std::move(d->event); -} - -RoomEvents&& GetEventContextJob::eventsAfter() -{ - return std::move(d->eventsAfter); -} - -StateEvents&& GetEventContextJob::state() -{ - return std::move(d->state); -} - -BaseJob::Status GetEventContextJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("start"_ls), d->begin); - fromJson(json.value("end"_ls), d->end); - fromJson(json.value("events_before"_ls), d->eventsBefore); - fromJson(json.value("event"_ls), d->event); - fromJson(json.value("events_after"_ls), d->eventsAfter); - fromJson(json.value("state"_ls), d->state); - return Success; -} - + QStringLiteral("/_matrix/client/r0") + % "/rooms/" % roomId % "/context/" + % eventId, + queryToGetEventContext(limit, filter)); +} + +GetEventContextJob::GetEventContextJob(const QString& roomId, + const QString& eventId, + Omittable<int> limit, + const QString& filter) + : BaseJob(HttpVerb::Get, QStringLiteral("GetEventContextJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/context/" % eventId, + queryToGetEventContext(limit, filter)) +{} diff --git a/lib/csapi/event_context.h b/lib/csapi/event_context.h index a5fda7ea..d82d16ab 100644 --- a/lib/csapi/event_context.h +++ b/lib/csapi/event_context.h @@ -4,65 +4,84 @@ #pragma once +#include "events/eventloader.h" #include "jobs/basejob.h" -#include "events/eventloader.h" -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Get events and state around the specified event. + * + * This API returns a number of events that happened just before and + * after the specified event. This allows clients to get the context + * surrounding an event. + * + * *Note*: This endpoint supports lazy-loading of room member events. See + * `Lazy-loading room members <#lazy-loading-room-members>`_ for more + * information. + */ +class GetEventContextJob : public BaseJob { +public: + /*! \brief Get events and state around the specified event. + * + * \param roomId + * The room to get events from. + * + * \param eventId + * The event to get context around. + * + * \param limit + * The maximum number of events to return. Default: 10. + * + * \param filter + * A JSON ``RoomEventFilter`` to filter the returned events with. The + * filter is only applied to ``events_before``, ``events_after``, and + * ``state``. It is not applied to the ``event`` itself. The filter may + * be applied before or/and after the ``limit`` parameter - whichever the + * homeserver prefers. + * + * See `Filtering <#filtering>`_ for more information. + */ + explicit GetEventContextJob(const QString& roomId, const QString& eventId, + Omittable<int> limit = none, + const QString& filter = {}); - /// Get events and state around the specified event. - /// - /// This API returns a number of events that happened just before and - /// after the specified event. This allows clients to get the context - /// surrounding an event. - class GetEventContextJob : public BaseJob - { - public: - /*! Get events and state around the specified event. - * \param roomId - * The room to get events from. - * \param eventId - * The event to get context around. - * \param limit - * The maximum number of events to return. Default: 10. - */ - explicit GetEventContextJob(const QString& roomId, const QString& eventId, Omittable<int> limit = none); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetEventContextJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, + Omittable<int> limit = none, + const QString& filter = {}); - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetEventContextJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId, Omittable<int> limit = none); + // Result properties - ~GetEventContextJob() override; + /// A token that can be used to paginate backwards with. + QString begin() const { return loadFromJson<QString>("start"_ls); } - // Result properties + /// A token that can be used to paginate forwards with. + QString end() const { return loadFromJson<QString>("end"_ls); } - /// A token that can be used to paginate backwards with. - const QString& begin() const; - /// A token that can be used to paginate forwards with. - const QString& end() const; - /// A list of room events that happened just before the - /// requested event, in reverse-chronological order. - RoomEvents&& eventsBefore(); - /// Details of the requested event. - RoomEventPtr&& event(); - /// A list of room events that happened just after the - /// requested event, in chronological order. - RoomEvents&& eventsAfter(); - /// The state of the room at the last event returned. - StateEvents&& state(); + /// A list of room events that happened just before the + /// requested event, in reverse-chronological order. + RoomEvents eventsBefore() + { + return takeFromJson<RoomEvents>("events_before"_ls); + } + + /// Details of the requested event. + RoomEventPtr event() { return takeFromJson<RoomEventPtr>("event"_ls); } + + /// A list of room events that happened just after the + /// requested event, in chronological order. + RoomEvents eventsAfter() + { + return takeFromJson<RoomEvents>("events_after"_ls); + } - protected: - Status parseJson(const QJsonDocument& data) override; + /// The state of the room at the last event returned. + StateEvents state() { return takeFromJson<StateEvents>("state"_ls); } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp index 982e60b5..bb3a893f 100644 --- a/lib/csapi/filter.cpp +++ b/lib/csapi/filter.cpp @@ -4,78 +4,29 @@ #include "filter.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class DefineFilterJob::Private -{ - public: - QString filterId; -}; - -static const auto DefineFilterJobName = QStringLiteral("DefineFilterJob"); +using namespace Quotient; DefineFilterJob::DefineFilterJob(const QString& userId, const Filter& filter) - : BaseJob(HttpVerb::Post, DefineFilterJobName, - basePath % "/user/" % userId % "/filter") - , d(new Private) + : BaseJob(HttpVerb::Post, QStringLiteral("DefineFilterJob"), + QStringLiteral("/_matrix/client/r0") % "/user/" % userId + % "/filter") { setRequestData(Data(toJson(filter))); + addExpectedKey("filter_id"); } -DefineFilterJob::~DefineFilterJob() = default; - -const QString& DefineFilterJob::filterId() const -{ - return d->filterId; -} - -BaseJob::Status DefineFilterJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("filter_id"_ls)) - return { JsonParseError, - "The key 'filter_id' not found in the response" }; - fromJson(json.value("filter_id"_ls), d->filterId); - return Success; -} - -class GetFilterJob::Private -{ - public: - Filter data; -}; - -QUrl GetFilterJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& filterId) +QUrl GetFilterJob::makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& filterId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/user/" % userId % "/filter/" % filterId); + QStringLiteral("/_matrix/client/r0") % "/user/" + % userId % "/filter/" % filterId); } -static const auto GetFilterJobName = QStringLiteral("GetFilterJob"); - GetFilterJob::GetFilterJob(const QString& userId, const QString& filterId) - : BaseJob(HttpVerb::Get, GetFilterJobName, - basePath % "/user/" % userId % "/filter/" % filterId) - , d(new Private) -{ -} - -GetFilterJob::~GetFilterJob() = default; - -const Filter& GetFilterJob::data() const -{ - return d->data; -} - -BaseJob::Status GetFilterJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("GetFilterJob"), + QStringLiteral("/_matrix/client/r0") % "/user/" % userId + % "/filter/" % filterId) +{} diff --git a/lib/csapi/filter.h b/lib/csapi/filter.h index 0ca7e953..f07b489c 100644 --- a/lib/csapi/filter.h +++ b/lib/csapi/filter.h @@ -4,82 +4,67 @@ #pragma once -#include "jobs/basejob.h" - -#include "converters.h" #include "csapi/definitions/sync_filter.h" -namespace QMatrixClient -{ - // Operations - - /// Upload a new filter. - /// - /// Uploads a new filter definition to the homeserver. - /// Returns a filter ID that may be used in future requests to - /// restrict which events are returned to the client. - class DefineFilterJob : public BaseJob - { - public: - /*! Upload a new filter. - * \param userId - * The id of the user uploading the filter. The access token must be authorized to make requests for this user id. - * \param filter - * Uploads a new filter definition to the homeserver. - * Returns a filter ID that may be used in future requests to - * restrict which events are returned to the client. - */ - explicit DefineFilterJob(const QString& userId, const Filter& filter); - ~DefineFilterJob() override; - - // Result properties - - /// The ID of the filter that was created. Cannot start - /// with a ``{`` as this character is used to determine - /// if the filter provided is inline JSON or a previously - /// declared filter by homeservers on some APIs. - const QString& filterId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Download a filter - class GetFilterJob : public BaseJob - { - public: - /*! Download a filter - * \param userId - * The user ID to download a filter for. - * \param filterId - * The filter ID to download. - */ - explicit GetFilterJob(const QString& userId, const QString& filterId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetFilterJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& filterId); - - ~GetFilterJob() override; - - // Result properties - - /// "The filter defintion" - const Filter& data() const; +#include "jobs/basejob.h" - protected: - Status parseJson(const QJsonDocument& data) override; +namespace Quotient { - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +/*! \brief Upload a new filter. + * + * Uploads a new filter definition to the homeserver. + * Returns a filter ID that may be used in future requests to + * restrict which events are returned to the client. + */ +class DefineFilterJob : public BaseJob { +public: + /*! \brief Upload a new filter. + * + * \param userId + * The id of the user uploading the filter. The access token must be + * authorized to make requests for this user id. + * + * \param filter + * The filter to upload. + */ + explicit DefineFilterJob(const QString& userId, const Filter& filter); + + // Result properties + + /// The ID of the filter that was created. Cannot start + /// with a ``{`` as this character is used to determine + /// if the filter provided is inline JSON or a previously + /// declared filter by homeservers on some APIs. + QString filterId() const { return loadFromJson<QString>("filter_id"_ls); } +}; + +/*! \brief Download a filter + * + */ +class GetFilterJob : public BaseJob { +public: + /*! \brief Download a filter + * + * \param userId + * The user ID to download a filter for. + * + * \param filterId + * The filter ID to download. + */ + explicit GetFilterJob(const QString& userId, const QString& filterId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetFilterJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& filterId); + + // Result properties + + /// The filter definition. + Filter filter() const { return fromJson<Filter>(jsonData()); } +}; + +} // namespace Quotient diff --git a/lib/csapi/gtad.yaml b/lib/csapi/gtad.yaml deleted file mode 100644 index a44f803a..00000000 --- a/lib/csapi/gtad.yaml +++ /dev/null @@ -1,145 +0,0 @@ -analyzer: - subst: - "%CLIENT_RELEASE_LABEL%": r0 - "%CLIENT_MAJOR_VERSION%": r0 - identifiers: - signed: signedData - unsigned: unsignedData - PushRule/default: isDefault - default: defaultVersion # getCapabilities/RoomVersionsCapability - origin_server_ts: originServerTimestamp # Instead of originServerTs - start: begin # Because start() is a method in BaseJob - m.upload.size: uploadSize - m.homeserver: homeserver - m.identity_server: identityServer - m.change_password: changePassword - m.room_versions: roomVersions - AuthenticationData/additionalProperties: authInfo - - # Structure inside `types`: - # - swaggerType: <targetTypeSpec> - # OR - # - swaggerType: - # - swaggerFormat: <targetTypeSpec> - # - /swaggerFormatRegEx/: <targetTypeSpec> - # - //: <targetTypeSpec> # default, if the format doesn't mach anything above - # WHERE - # targetTypeSpec = targetType OR - # { type: targetType, imports: <filename OR [ filenames... ]>, <other attributes...> } - # swaggerType can be +set/+on pair; attributes from the map under +set - # are added to each type from the sequence under +on. - types: - - +set: &UseOmittable - useOmittable: - imports: [ '"converters.h"' ] - omittedValue: 'none' # See `none` in converters.h - +on: - - integer: - - int64: qint64 - - int32: qint32 - - //: int - - number: - - float: float - - //: double - - boolean: bool - - string: - - byte: &ByteStream - type: QIODevice* - imports: <QtCore/QIODevice> - - binary: *ByteStream - - +set: { avoidCopy: } - +on: - - date: - type: QDate - initializer: QDate::fromString("{{defaultValue}}") - imports: <QtCore/QDate> - - dateTime: - type: QDateTime - initializer: QDateTime::fromString("{{defaultValue}}") - imports: <QtCore/QDateTime> - - //: &QString - type: QString - initializer: QStringLiteral("{{defaultValue}}") - isString: - - file: *ByteStream - - +set: { avoidCopy: } - +on: - - object: &QJsonObject { type: QJsonObject, imports: <QtCore/QJsonObject> } - - $ref: - - +set: { moveOnly: } - +on: - - /state_event.yaml$/: - { type: StateEventPtr, imports: '"events/eventloader.h"' } - - /room_event.yaml$/: - { type: RoomEventPtr, imports: '"events/eventloader.h"' } - - /event.yaml$/: - { type: EventPtr, imports: '"events/eventloader.h"' } - - /m\.room\.member$/: pass # This $ref is only used in an array, see below - - //: *UseOmittable # Also apply "avoidCopy" to all other ref'ed types - - schema: # Properties of inline structure definitions - - TurnServerCredentials: *QJsonObject # Because it's used as is - - //: *UseOmittable - - array: - - string: QStringList - - +set: { moveOnly: } - +on: - - /^Notification|Result$/: - type: "std::vector<{{1}}>" - imports: '"events/eventloader.h"' - - /m\.room\.member$/: - type: "EventsArray<RoomMemberEvent>" - imports: '"events/roommemberevent.h"' - - /state_event.yaml$/: StateEvents - - /room_event.yaml$/: RoomEvents - - /event.yaml$/: Events - - //: { type: "QVector<{{1}}>", imports: <QtCore/QVector> } - - map: # `additionalProperties` in OpenAPI - - RoomState: - type: "std::unordered_map<QString, {{1}}>" - moveOnly: - imports: <unordered_map> - - /.+/: - type: "QHash<QString, {{1}}>" - imports: <QtCore/QHash> - - //: - type: QVariantHash - imports: <QtCore/QVariant> - - variant: # A sequence `type` (multitype) in OpenAPI - - /^string,null|null,string$/: *QString - - //: { type: QVariant, imports: <QtCore/QVariant> } - - #operations: - -mustache: - constants: - # Syntax elements used by GTAD -# _quote: '"' # Common quote for left and right -# _leftQuote: '"' -# _rightQuote: '"' -# _joinChar: ',' # The character used by {{_join}} - not working yet - _comment: '//' - partials: - _typeRenderer: "{{#scope}}{{scopeCamelCase}}Job::{{/scope}}{{>name}}" - omittedValue: '{}' # default value to initialize omitted parameters with - initializer: '{{defaultValue}}' - cjoin: '{{#hasMore}}, {{/hasMore}}' - openOmittable: "{{^required?}}{{#useOmittable}}{{^defaultValue}}Omittable<{{/defaultValue}}{{/useOmittable}}{{/required?}}" - closeOmittable: "{{^required?}}{{#useOmittable}}{{^defaultValue}}>{{/defaultValue}}{{/useOmittable}}{{/required?}}" - maybeOmittableType: "{{>openOmittable}}{{dataType.name}}{{>closeOmittable}}" - qualifiedMaybeOmittableType: "{{>openOmittable}}{{dataType.qualifiedName}}{{>closeOmittable}}" - maybeCrefType: "{{#avoidCopy}}const {{/avoidCopy}}{{>maybeOmittableType}}{{#avoidCopy}}&{{/avoidCopy}}{{#moveOnly}}&&{{/moveOnly}}" - qualifiedMaybeCrefType: - "{{#avoidCopy}}const {{/avoidCopy}}{{>qualifiedMaybeOmittableType}}{{#avoidCopy}}&{{/avoidCopy}}{{#moveOnly}}&&{{/moveOnly}}" - initializeDefaultValue: "{{#defaultValue}}{{>initializer}}{{/defaultValue}}{{^defaultValue}}{{>omittedValue}}{{/defaultValue}}" - joinedParamDecl: '{{>maybeCrefType}} {{paramName}}{{^required?}} = {{>initializeDefaultValue}}{{/required?}}{{>cjoin}}' - joinedParamDef: '{{>maybeCrefType}} {{paramName}}{{>cjoin}}' - passQueryParams: '{{#queryParams}}{{paramName}}{{>cjoin}}{{/queryParams}}' - copyrightName: Kitsune Ral - copyrightEmail: <kitsune-ral@users.sf.net> - - templates: - - "{{base}}.h.mustache" - - "{{base}}.cpp.mustache" - - #outFilesList: apifiles.txt - diff --git a/lib/csapi/inviting.cpp b/lib/csapi/inviting.cpp index 7dc33b18..01620f9e 100644 --- a/lib/csapi/inviting.cpp +++ b/lib/csapi/inviting.cpp @@ -4,22 +4,16 @@ #include "inviting.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto InviteUserJobName = QStringLiteral("InviteUserJob"); +using namespace Quotient; InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId) - : BaseJob(HttpVerb::Post, InviteUserJobName, - basePath % "/rooms/" % roomId % "/invite") + : BaseJob(HttpVerb::Post, QStringLiteral("InviteUserJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/invite") { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); - setRequestData(_data); + setRequestData(std::move(_data)); } - diff --git a/lib/csapi/inviting.h b/lib/csapi/inviting.h index 6d5d2e99..59a61b89 100644 --- a/lib/csapi/inviting.h +++ b/lib/csapi/inviting.h @@ -6,40 +6,40 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Invite a user to participate in a particular room. + * + * .. _invite-by-user-id-endpoint: + * + * *Note that there are two forms of this API, which are documented separately. + * This version of the API requires that the inviter knows the Matrix + * identifier of the invitee. The other is documented in the* + * `third party invites section`_. + * + * This API invites a user to participate in a particular room. + * They do not start participating in the room until they actually join the + * room. + * + * Only users currently in a particular room can invite other users to + * join that room. + * + * If the user was invited to the room, the homeserver will append a + * ``m.room.member`` event to the room. + * + * .. _third party invites section: `invite-by-third-party-id-endpoint`_ + */ +class InviteUserJob : public BaseJob { +public: + /*! \brief Invite a user to participate in a particular room. + * + * \param roomId + * The room identifier (not alias) to which to invite the user. + * + * \param userId + * The fully qualified user ID of the invitee. + */ + explicit InviteUserJob(const QString& roomId, const QString& userId); +}; - /// Invite a user to participate in a particular room. - /// - /// .. _invite-by-user-id-endpoint: - /// - /// *Note that there are two forms of this API, which are documented separately. - /// This version of the API requires that the inviter knows the Matrix - /// identifier of the invitee. The other is documented in the* - /// `third party invites section`_. - /// - /// This API invites a user to participate in a particular room. - /// They do not start participating in the room until they actually join the - /// room. - /// - /// Only users currently in a particular room can invite other users to - /// join that room. - /// - /// If the user was invited to the room, the homeserver will append a - /// ``m.room.member`` event to the room. - /// - /// .. _third party invites section: `invite-by-third-party-id-endpoint`_ - class InviteUserJob : public BaseJob - { - public: - /*! Invite a user to participate in a particular room. - * \param roomId - * The room identifier (not alias) to which to invite the user. - * \param userId - * The fully qualified user ID of the invitee. - */ - explicit InviteUserJob(const QString& roomId, const QString& userId); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp index 00d930fa..4761e949 100644 --- a/lib/csapi/joining.cpp +++ b/lib/csapi/joining.cpp @@ -4,129 +4,39 @@ #include "joining.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<JoinRoomByIdJob::ThirdPartySigned> - { - static void dumpTo(QJsonObject& jo, const JoinRoomByIdJob::ThirdPartySigned& pod) - { - addParam<>(jo, QStringLiteral("sender"), pod.sender); - addParam<>(jo, QStringLiteral("mxid"), pod.mxid); - addParam<>(jo, QStringLiteral("token"), pod.token); - addParam<>(jo, QStringLiteral("signatures"), pod.signatures); - } - }; -} // namespace QMatrixClient - -class JoinRoomByIdJob::Private -{ - public: - QString roomId; -}; - -static const auto JoinRoomByIdJobName = QStringLiteral("JoinRoomByIdJob"); +using namespace Quotient; -JoinRoomByIdJob::JoinRoomByIdJob(const QString& roomId, const Omittable<ThirdPartySigned>& thirdPartySigned) - : BaseJob(HttpVerb::Post, JoinRoomByIdJobName, - basePath % "/rooms/" % roomId % "/join") - , d(new Private) +JoinRoomByIdJob::JoinRoomByIdJob( + const QString& roomId, const Omittable<ThirdPartySigned>& thirdPartySigned) + : BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomByIdJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/join") { QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("third_party_signed"), thirdPartySigned); - setRequestData(_data); -} - -JoinRoomByIdJob::~JoinRoomByIdJob() = default; - -const QString& JoinRoomByIdJob::roomId() const -{ - return d->roomId; -} - -BaseJob::Status JoinRoomByIdJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("room_id"_ls)) - return { JsonParseError, - "The key 'room_id' not found in the response" }; - fromJson(json.value("room_id"_ls), d->roomId); - return Success; + addParam<IfNotEmpty>(_data, QStringLiteral("third_party_signed"), + thirdPartySigned); + setRequestData(std::move(_data)); + addExpectedKey("room_id"); } -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<JoinRoomJob::Signed> - { - static void dumpTo(QJsonObject& jo, const JoinRoomJob::Signed& pod) - { - addParam<>(jo, QStringLiteral("sender"), pod.sender); - addParam<>(jo, QStringLiteral("mxid"), pod.mxid); - addParam<>(jo, QStringLiteral("token"), pod.token); - addParam<>(jo, QStringLiteral("signatures"), pod.signatures); - } - }; - - template <> struct JsonObjectConverter<JoinRoomJob::ThirdPartySigned> - { - static void dumpTo(QJsonObject& jo, const JoinRoomJob::ThirdPartySigned& pod) - { - addParam<>(jo, QStringLiteral("signed"), pod.signedData); - } - }; -} // namespace QMatrixClient - -class JoinRoomJob::Private -{ - public: - QString roomId; -}; - -BaseJob::Query queryToJoinRoom(const QStringList& serverName) +auto queryToJoinRoom(const QStringList& serverName) { BaseJob::Query _q; addParam<IfNotEmpty>(_q, QStringLiteral("server_name"), serverName); return _q; } -static const auto JoinRoomJobName = QStringLiteral("JoinRoomJob"); - -JoinRoomJob::JoinRoomJob(const QString& roomIdOrAlias, const QStringList& serverName, const Omittable<ThirdPartySigned>& thirdPartySigned) - : BaseJob(HttpVerb::Post, JoinRoomJobName, - basePath % "/join/" % roomIdOrAlias, - queryToJoinRoom(serverName)) - , d(new Private) +JoinRoomJob::JoinRoomJob(const QString& roomIdOrAlias, + const QStringList& serverName, + const Omittable<ThirdPartySigned>& thirdPartySigned) + : BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomJob"), + QStringLiteral("/_matrix/client/r0") % "/join/" % roomIdOrAlias, + queryToJoinRoom(serverName)) { QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("third_party_signed"), thirdPartySigned); - setRequestData(_data); + addParam<IfNotEmpty>(_data, QStringLiteral("third_party_signed"), + thirdPartySigned); + setRequestData(std::move(_data)); + addExpectedKey("room_id"); } - -JoinRoomJob::~JoinRoomJob() = default; - -const QString& JoinRoomJob::roomId() const -{ - return d->roomId; -} - -BaseJob::Status JoinRoomJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("room_id"_ls)) - return { JsonParseError, - "The key 'room_id' not found in the response" }; - fromJson(json.value("room_id"_ls), d->roomId); - return Success; -} - diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h index 52c8ea42..dd936f92 100644 --- a/lib/csapi/joining.h +++ b/lib/csapi/joining.h @@ -4,148 +4,84 @@ #pragma once -#include "jobs/basejob.h" - -#include "converters.h" -#include <QtCore/QJsonObject> - -namespace QMatrixClient -{ - // Operations - - /// Start the requesting user participating in a particular room. - /// - /// *Note that this API requires a room ID, not alias.* ``/join/{roomIdOrAlias}`` *exists if you have a room alias.* - /// - /// This API starts a user participating in a particular room, if that user - /// is allowed to participate in that room. After this call, the client is - /// allowed to see all current state events in the room, and all subsequent - /// events associated with the room until the user leaves the room. - /// - /// After a user has joined a room, the room will appear as an entry in the - /// response of the |/initialSync|_ and |/sync|_ APIs. - /// - /// If a ``third_party_signed`` was supplied, the homeserver must verify - /// that it matches a pending ``m.room.third_party_invite`` event in the - /// room, and perform key validity checking if required by the event. - class JoinRoomByIdJob : public BaseJob - { - public: - // Inner data structures - - /// A signature of an ``m.third_party_invite`` token to prove that this user owns a third party identity which has been invited to the room. - struct ThirdPartySigned - { - /// The Matrix ID of the user who issued the invite. - QString sender; - /// The Matrix ID of the invitee. - QString mxid; - /// The state key of the m.third_party_invite event. - QString token; - /// A signatures object containing a signature of the entire signed object. - QJsonObject signatures; - }; - - // Construction/destruction - - /*! Start the requesting user participating in a particular room. - * \param roomId - * The room identifier (not alias) to join. - * \param thirdPartySigned - * A signature of an ``m.third_party_invite`` token to prove that this user owns a third party identity which has been invited to the room. - */ - explicit JoinRoomByIdJob(const QString& roomId, const Omittable<ThirdPartySigned>& thirdPartySigned = none); - ~JoinRoomByIdJob() override; - - // Result properties - - /// The joined room ID. - const QString& roomId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; +#include "csapi/definitions/third_party_signed.h" - /// Start the requesting user participating in a particular room. - /// - /// *Note that this API takes either a room ID or alias, unlike* ``/room/{roomId}/join``. - /// - /// This API starts a user participating in a particular room, if that user - /// is allowed to participate in that room. After this call, the client is - /// allowed to see all current state events in the room, and all subsequent - /// events associated with the room until the user leaves the room. - /// - /// After a user has joined a room, the room will appear as an entry in the - /// response of the |/initialSync|_ and |/sync|_ APIs. - /// - /// If a ``third_party_signed`` was supplied, the homeserver must verify - /// that it matches a pending ``m.room.third_party_invite`` event in the - /// room, and perform key validity checking if required by the event. - class JoinRoomJob : public BaseJob - { - public: - // Inner data structures - - /// *Note that this API takes either a room ID or alias, unlike* ``/room/{roomId}/join``. - /// - /// This API starts a user participating in a particular room, if that user - /// is allowed to participate in that room. After this call, the client is - /// allowed to see all current state events in the room, and all subsequent - /// events associated with the room until the user leaves the room. - /// - /// After a user has joined a room, the room will appear as an entry in the - /// response of the |/initialSync|_ and |/sync|_ APIs. - /// - /// If a ``third_party_signed`` was supplied, the homeserver must verify - /// that it matches a pending ``m.room.third_party_invite`` event in the - /// room, and perform key validity checking if required by the event. - struct Signed - { - /// The Matrix ID of the user who issued the invite. - QString sender; - /// The Matrix ID of the invitee. - QString mxid; - /// The state key of the m.third_party_invite event. - QString token; - /// A signatures object containing a signature of the entire signed object. - QJsonObject signatures; - }; - - /// A signature of an ``m.third_party_invite`` token to prove that this user owns a third party identity which has been invited to the room. - struct ThirdPartySigned - { - /// A signature of an ``m.third_party_invite`` token to prove that this user owns a third party identity which has been invited to the room. - Signed signedData; - }; - - // Construction/destruction - - /*! Start the requesting user participating in a particular room. - * \param roomIdOrAlias - * The room identifier or alias to join. - * \param serverName - * The servers to attempt to join the room through. One of the servers - * must be participating in the room. - * \param thirdPartySigned - * A signature of an ``m.third_party_invite`` token to prove that this user owns a third party identity which has been invited to the room. - */ - explicit JoinRoomJob(const QString& roomIdOrAlias, const QStringList& serverName = {}, const Omittable<ThirdPartySigned>& thirdPartySigned = none); - ~JoinRoomJob() override; - - // Result properties - - /// The joined room ID. - const QString& roomId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; +#include "jobs/basejob.h" - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +namespace Quotient { + +/*! \brief Start the requesting user participating in a particular room. + * + * *Note that this API requires a room ID, not alias.* + * ``/join/{roomIdOrAlias}`` *exists if you have a room alias.* + * + * This API starts a user participating in a particular room, if that user + * is allowed to participate in that room. After this call, the client is + * allowed to see all current state events in the room, and all subsequent + * events associated with the room until the user leaves the room. + * + * After a user has joined a room, the room will appear as an entry in the + * response of the |/initialSync|_ and |/sync|_ APIs. + */ +class JoinRoomByIdJob : public BaseJob { +public: + /*! \brief Start the requesting user participating in a particular room. + * + * \param roomId + * The room identifier (not alias) to join. + * + * \param thirdPartySigned + * If supplied, the homeserver must verify that it matches a pending + * ``m.room.third_party_invite`` event in the room, and perform + * key validity checking if required by the event. + */ + explicit JoinRoomByIdJob( + const QString& roomId, + const Omittable<ThirdPartySigned>& thirdPartySigned = none); + + // Result properties + + /// The joined room ID. + QString roomId() const { return loadFromJson<QString>("room_id"_ls); } +}; + +/*! \brief Start the requesting user participating in a particular room. + * + * *Note that this API takes either a room ID or alias, unlike* + * ``/room/{roomId}/join``. + * + * This API starts a user participating in a particular room, if that user + * is allowed to participate in that room. After this call, the client is + * allowed to see all current state events in the room, and all subsequent + * events associated with the room until the user leaves the room. + * + * After a user has joined a room, the room will appear as an entry in the + * response of the |/initialSync|_ and |/sync|_ APIs. + */ +class JoinRoomJob : public BaseJob { +public: + /*! \brief Start the requesting user participating in a particular room. + * + * \param roomIdOrAlias + * The room identifier or alias to join. + * + * \param serverName + * The servers to attempt to join the room through. One of the servers + * must be participating in the room. + * + * \param thirdPartySigned + * If a ``third_party_signed`` was supplied, the homeserver must verify + * that it matches a pending ``m.room.third_party_invite`` event in the + * room, and perform key validity checking if required by the event. + */ + explicit JoinRoomJob( + const QString& roomIdOrAlias, const QStringList& serverName = {}, + const Omittable<ThirdPartySigned>& thirdPartySigned = none); + + // Result properties + + /// The joined room ID. + QString roomId() const { return loadFromJson<QString>("room_id"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/keys.cpp b/lib/csapi/keys.cpp index 6c16a8a3..34ab47c9 100644 --- a/lib/csapi/keys.cpp +++ b/lib/csapi/keys.cpp @@ -4,161 +4,48 @@ #include "keys.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class UploadKeysJob::Private -{ - public: - QHash<QString, int> oneTimeKeyCounts; -}; - -static const auto UploadKeysJobName = QStringLiteral("UploadKeysJob"); +using namespace Quotient; -UploadKeysJob::UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys, const QHash<QString, QVariant>& oneTimeKeys) - : BaseJob(HttpVerb::Post, UploadKeysJobName, - basePath % "/keys/upload") - , d(new Private) +UploadKeysJob::UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys, + const QHash<QString, QVariant>& oneTimeKeys) + : BaseJob(HttpVerb::Post, QStringLiteral("UploadKeysJob"), + QStringLiteral("/_matrix/client/r0") % "/keys/upload") { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("device_keys"), deviceKeys); addParam<IfNotEmpty>(_data, QStringLiteral("one_time_keys"), oneTimeKeys); - setRequestData(_data); + setRequestData(std::move(_data)); + addExpectedKey("one_time_key_counts"); } -UploadKeysJob::~UploadKeysJob() = default; - -const QHash<QString, int>& UploadKeysJob::oneTimeKeyCounts() const -{ - return d->oneTimeKeyCounts; -} - -BaseJob::Status UploadKeysJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("one_time_key_counts"_ls)) - return { JsonParseError, - "The key 'one_time_key_counts' not found in the response" }; - fromJson(json.value("one_time_key_counts"_ls), d->oneTimeKeyCounts); - return Success; -} - -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<QueryKeysJob::UnsignedDeviceInfo> - { - static void fillFrom(const QJsonObject& jo, QueryKeysJob::UnsignedDeviceInfo& result) - { - fromJson(jo.value("device_display_name"_ls), result.deviceDisplayName); - } - }; - - template <> struct JsonObjectConverter<QueryKeysJob::DeviceInformation> - { - static void fillFrom(const QJsonObject& jo, QueryKeysJob::DeviceInformation& result) - { - fillFromJson<DeviceKeys>(jo, result); - fromJson(jo.value("unsigned"_ls), result.unsignedData); - } - }; -} // namespace QMatrixClient - -class QueryKeysJob::Private -{ - public: - QHash<QString, QJsonObject> failures; - QHash<QString, QHash<QString, DeviceInformation>> deviceKeys; -}; - -static const auto QueryKeysJobName = QStringLiteral("QueryKeysJob"); - -QueryKeysJob::QueryKeysJob(const QHash<QString, QStringList>& deviceKeys, Omittable<int> timeout, const QString& token) - : BaseJob(HttpVerb::Post, QueryKeysJobName, - basePath % "/keys/query") - , d(new Private) +QueryKeysJob::QueryKeysJob(const QHash<QString, QStringList>& deviceKeys, + Omittable<int> timeout, const QString& token) + : BaseJob(HttpVerb::Post, QStringLiteral("QueryKeysJob"), + QStringLiteral("/_matrix/client/r0") % "/keys/query") { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout); addParam<>(_data, QStringLiteral("device_keys"), deviceKeys); addParam<IfNotEmpty>(_data, QStringLiteral("token"), token); - setRequestData(_data); -} - -QueryKeysJob::~QueryKeysJob() = default; - -const QHash<QString, QJsonObject>& QueryKeysJob::failures() const -{ - return d->failures; -} - -const QHash<QString, QHash<QString, QueryKeysJob::DeviceInformation>>& QueryKeysJob::deviceKeys() const -{ - return d->deviceKeys; -} - -BaseJob::Status QueryKeysJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("failures"_ls), d->failures); - fromJson(json.value("device_keys"_ls), d->deviceKeys); - return Success; + setRequestData(std::move(_data)); } -class ClaimKeysJob::Private -{ - public: - QHash<QString, QJsonObject> failures; - QHash<QString, QHash<QString, QVariant>> oneTimeKeys; -}; - -static const auto ClaimKeysJobName = QStringLiteral("ClaimKeysJob"); - -ClaimKeysJob::ClaimKeysJob(const QHash<QString, QHash<QString, QString>>& oneTimeKeys, Omittable<int> timeout) - : BaseJob(HttpVerb::Post, ClaimKeysJobName, - basePath % "/keys/claim") - , d(new Private) +ClaimKeysJob::ClaimKeysJob( + const QHash<QString, QHash<QString, QString>>& oneTimeKeys, + Omittable<int> timeout) + : BaseJob(HttpVerb::Post, QStringLiteral("ClaimKeysJob"), + QStringLiteral("/_matrix/client/r0") % "/keys/claim") { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout); addParam<>(_data, QStringLiteral("one_time_keys"), oneTimeKeys); - setRequestData(_data); -} - -ClaimKeysJob::~ClaimKeysJob() = default; - -const QHash<QString, QJsonObject>& ClaimKeysJob::failures() const -{ - return d->failures; + setRequestData(std::move(_data)); + addExpectedKey("one_time_keys"); } -const QHash<QString, QHash<QString, QVariant>>& ClaimKeysJob::oneTimeKeys() const -{ - return d->oneTimeKeys; -} - -BaseJob::Status ClaimKeysJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("failures"_ls), d->failures); - fromJson(json.value("one_time_keys"_ls), d->oneTimeKeys); - return Success; -} - -class GetKeysChangesJob::Private -{ - public: - QStringList changed; - QStringList left; -}; - -BaseJob::Query queryToGetKeysChanges(const QString& from, const QString& to) +auto queryToGetKeysChanges(const QString& from, const QString& to) { BaseJob::Query _q; addParam<>(_q, QStringLiteral("from"), from); @@ -166,40 +53,17 @@ BaseJob::Query queryToGetKeysChanges(const QString& from, const QString& to) return _q; } -QUrl GetKeysChangesJob::makeRequestUrl(QUrl baseUrl, const QString& from, const QString& to) +QUrl GetKeysChangesJob::makeRequestUrl(QUrl baseUrl, const QString& from, + const QString& to) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/keys/changes", - queryToGetKeysChanges(from, to)); + QStringLiteral("/_matrix/client/r0") + % "/keys/changes", + queryToGetKeysChanges(from, to)); } -static const auto GetKeysChangesJobName = QStringLiteral("GetKeysChangesJob"); - GetKeysChangesJob::GetKeysChangesJob(const QString& from, const QString& to) - : BaseJob(HttpVerb::Get, GetKeysChangesJobName, - basePath % "/keys/changes", - queryToGetKeysChanges(from, to)) - , d(new Private) -{ -} - -GetKeysChangesJob::~GetKeysChangesJob() = default; - -const QStringList& GetKeysChangesJob::changed() const -{ - return d->changed; -} - -const QStringList& GetKeysChangesJob::left() const -{ - return d->left; -} - -BaseJob::Status GetKeysChangesJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("changed"_ls), d->changed); - fromJson(json.value("left"_ls), d->left); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("GetKeysChangesJob"), + QStringLiteral("/_matrix/client/r0") % "/keys/changes", + queryToGetKeysChanges(from, to)) +{} diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h index e59b1dae..8f6c8cc9 100644 --- a/lib/csapi/keys.h +++ b/lib/csapi/keys.h @@ -4,216 +4,236 @@ #pragma once -#include "jobs/basejob.h" - #include "csapi/definitions/device_keys.h" -#include <QtCore/QHash> -#include "converters.h" -#include <QtCore/QVariant> -#include <QtCore/QJsonObject> -namespace QMatrixClient -{ - // Operations +#include "jobs/basejob.h" + +namespace Quotient { - /// Upload end-to-end encryption keys. - /// - /// Publishes end-to-end encryption keys for the device. - class UploadKeysJob : public BaseJob +/*! \brief Upload end-to-end encryption keys. + * + * Publishes end-to-end encryption keys for the device. + */ +class UploadKeysJob : public BaseJob { +public: + /*! \brief Upload end-to-end encryption keys. + * + * \param deviceKeys + * Identity keys for the device. May be absent if no new + * identity keys are required. + * + * \param oneTimeKeys + * One-time public keys for "pre-key" messages. The names of + * the properties should be in the format + * ``<algorithm>:<key_id>``. The format of the key is determined + * by the `key algorithm <#key-algorithms>`_. + * + * May be absent if no new one-time keys are required. + */ + explicit UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys = none, + const QHash<QString, QVariant>& oneTimeKeys = {}); + + // Result properties + + /// For each key algorithm, the number of unclaimed one-time keys + /// of that type currently held on the server for this device. + QHash<QString, int> oneTimeKeyCounts() const { - public: - /*! Upload end-to-end encryption keys. - * \param deviceKeys - * Identity keys for the device. May be absent if no new - * identity keys are required. - * \param oneTimeKeys - * One-time public keys for "pre-key" messages. The names of - * the properties should be in the format - * ``<algorithm>:<key_id>``. The format of the key is determined - * by the key algorithm. - * - * May be absent if no new one-time keys are required. - */ - explicit UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys = none, const QHash<QString, QVariant>& oneTimeKeys = {}); - ~UploadKeysJob() override; - - // Result properties - - /// For each key algorithm, the number of unclaimed one-time keys - /// of that type currently held on the server for this device. - const QHash<QString, int>& oneTimeKeyCounts() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; + return loadFromJson<QHash<QString, int>>("one_time_key_counts"_ls); + } +}; + +/*! \brief Download device identity keys. + * + * Returns the current devices and identity keys for the given users. + */ +class QueryKeysJob : public BaseJob { +public: + // Inner data structures + + /// Additional data added to the device key information + /// by intermediate servers, and not covered by the + /// signatures. + struct UnsignedDeviceInfo { + /// The display name which the user set on the device. + QString deviceDisplayName; }; - /// Download device identity keys. - /// /// Returns the current devices and identity keys for the given users. - class QueryKeysJob : public BaseJob - { - public: - // Inner data structures - - /// Additional data added to the device key information - /// by intermediate servers, and not covered by the - /// signatures. - struct UnsignedDeviceInfo - { - /// The display name which the user set on the device. - QString deviceDisplayName; - }; - - /// Returns the current devices and identity keys for the given users. - struct DeviceInformation : DeviceKeys - { - /// Additional data added to the device key information - /// by intermediate servers, and not covered by the - /// signatures. - Omittable<UnsignedDeviceInfo> unsignedData; - }; - - // Construction/destruction - - /*! Download device identity keys. - * \param deviceKeys - * The keys to be downloaded. A map from user ID, to a list of - * device IDs, or to an empty list to indicate all devices for the - * corresponding user. - * \param timeout - * The time (in milliseconds) to wait when downloading keys from - * remote servers. 10 seconds is the recommended default. - * \param token - * If the client is fetching keys as a result of a device update received - * in a sync request, this should be the 'since' token of that sync request, - * or any later sync token. This allows the server to ensure its response - * contains the keys advertised by the notification in that sync. - */ - explicit QueryKeysJob(const QHash<QString, QStringList>& deviceKeys, Omittable<int> timeout = none, const QString& token = {}); - ~QueryKeysJob() override; - - // Result properties - - /// If any remote homeservers could not be reached, they are - /// recorded here. The names of the properties are the names of - /// the unreachable servers. - /// - /// If the homeserver could be reached, but the user or device - /// was unknown, no failure is recorded. Instead, the corresponding - /// user or device is missing from the ``device_keys`` result. - const QHash<QString, QJsonObject>& failures() const; - /// Information on the queried devices. A map from user ID, to a - /// map from device ID to device information. For each device, - /// the information returned will be the same as uploaded via - /// ``/keys/upload``, with the addition of an ``unsigned`` - /// property. - const QHash<QString, QHash<QString, DeviceInformation>>& deviceKeys() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; + struct DeviceInformation : DeviceKeys { + /// Additional data added to the device key information + /// by intermediate servers, and not covered by the + /// signatures. + Omittable<UnsignedDeviceInfo> unsignedData; }; - /// Claim one-time encryption keys. + // Construction/destruction + + /*! \brief Download device identity keys. + * + * \param deviceKeys + * The keys to be downloaded. A map from user ID, to a list of + * device IDs, or to an empty list to indicate all devices for the + * corresponding user. + * + * \param timeout + * The time (in milliseconds) to wait when downloading keys from + * remote servers. 10 seconds is the recommended default. + * + * \param token + * If the client is fetching keys as a result of a device update received + * in a sync request, this should be the 'since' token of that sync + * request, or any later sync token. This allows the server to ensure its + * response contains the keys advertised by the notification in that sync. + */ + explicit QueryKeysJob(const QHash<QString, QStringList>& deviceKeys, + Omittable<int> timeout = none, + const QString& token = {}); + + // Result properties + + /// If any remote homeservers could not be reached, they are + /// recorded here. The names of the properties are the names of + /// the unreachable servers. /// - /// Claims one-time keys for use in pre-key messages. - class ClaimKeysJob : public BaseJob + /// If the homeserver could be reached, but the user or device + /// was unknown, no failure is recorded. Instead, the corresponding + /// user or device is missing from the ``device_keys`` result. + QHash<QString, QJsonObject> failures() const { - public: - /*! Claim one-time encryption keys. - * \param oneTimeKeys - * The keys to be claimed. A map from user ID, to a map from - * device ID to algorithm name. - * \param timeout - * The time (in milliseconds) to wait when downloading keys from - * remote servers. 10 seconds is the recommended default. - */ - explicit ClaimKeysJob(const QHash<QString, QHash<QString, QString>>& oneTimeKeys, Omittable<int> timeout = none); - ~ClaimKeysJob() override; - - // Result properties - - /// If any remote homeservers could not be reached, they are - /// recorded here. The names of the properties are the names of - /// the unreachable servers. - /// - /// If the homeserver could be reached, but the user or device - /// was unknown, no failure is recorded. Instead, the corresponding - /// user or device is missing from the ``one_time_keys`` result. - const QHash<QString, QJsonObject>& failures() const; - /// One-time keys for the queried devices. A map from user ID, to a - /// map from devices to a map from ``<algorithm>:<key_id>`` to the key object. - const QHash<QString, QHash<QString, QVariant>>& oneTimeKeys() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; + return loadFromJson<QHash<QString, QJsonObject>>("failures"_ls); + } + + /// Information on the queried devices. A map from user ID, to a + /// map from device ID to device information. For each device, + /// the information returned will be the same as uploaded via + /// ``/keys/upload``, with the addition of an ``unsigned`` + /// property. + QHash<QString, QHash<QString, DeviceInformation>> deviceKeys() const + { + return loadFromJson<QHash<QString, QHash<QString, DeviceInformation>>>( + "device_keys"_ls); + } +}; + +template <> +struct JsonObjectConverter<QueryKeysJob::UnsignedDeviceInfo> { + static void fillFrom(const QJsonObject& jo, + QueryKeysJob::UnsignedDeviceInfo& result) + { + fromJson(jo.value("device_display_name"_ls), result.deviceDisplayName); + } +}; + +template <> +struct JsonObjectConverter<QueryKeysJob::DeviceInformation> { + static void fillFrom(const QJsonObject& jo, + QueryKeysJob::DeviceInformation& result) + { + fillFromJson<DeviceKeys>(jo, result); + fromJson(jo.value("unsigned"_ls), result.unsignedData); + } +}; + +/*! \brief Claim one-time encryption keys. + * + * Claims one-time keys for use in pre-key messages. + */ +class ClaimKeysJob : public BaseJob { +public: + /*! \brief Claim one-time encryption keys. + * + * \param oneTimeKeys + * The keys to be claimed. A map from user ID, to a map from + * device ID to algorithm name. + * + * \param timeout + * The time (in milliseconds) to wait when downloading keys from + * remote servers. 10 seconds is the recommended default. + */ + explicit ClaimKeysJob( + const QHash<QString, QHash<QString, QString>>& oneTimeKeys, + Omittable<int> timeout = none); + + // Result properties + + /// If any remote homeservers could not be reached, they are + /// recorded here. The names of the properties are the names of + /// the unreachable servers. + /// + /// If the homeserver could be reached, but the user or device + /// was unknown, no failure is recorded. Instead, the corresponding + /// user or device is missing from the ``one_time_keys`` result. + QHash<QString, QJsonObject> failures() const + { + return loadFromJson<QHash<QString, QJsonObject>>("failures"_ls); + } - /// Query users with recent device key updates. + /// One-time keys for the queried devices. A map from user ID, to a + /// map from devices to a map from ``<algorithm>:<key_id>`` to the key + /// object. /// - /// Gets a list of users who have updated their device identity keys since a - /// previous sync token. - /// - /// The server should include in the results any users who: - /// - /// * currently share a room with the calling user (ie, both users have - /// membership state ``join``); *and* - /// * added new device identity keys or removed an existing device with - /// identity keys, between ``from`` and ``to``. - class GetKeysChangesJob : public BaseJob + /// See the `key algorithms <#key-algorithms>`_ section for information + /// on the Key Object format. + QHash<QString, QHash<QString, QVariant>> oneTimeKeys() const { - public: - /*! Query users with recent device key updates. - * \param from - * The desired start point of the list. Should be the ``next_batch`` field - * from a response to an earlier call to |/sync|. Users who have not - * uploaded new device identity keys since this point, nor deleted - * existing devices with identity keys since then, will be excluded - * from the results. - * \param to - * The desired end point of the list. Should be the ``next_batch`` - * field from a recent call to |/sync| - typically the most recent - * such call. This may be used by the server as a hint to check its - * caches are up to date. - */ - explicit GetKeysChangesJob(const QString& from, const QString& to); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetKeysChangesJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& from, const QString& to); - - ~GetKeysChangesJob() override; - - // Result properties - - /// The Matrix User IDs of all users who updated their device - /// identity keys. - const QStringList& changed() const; - /// The Matrix User IDs of all users who may have left all - /// the end-to-end encrypted rooms they previously shared - /// with the user. - const QStringList& left() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + return loadFromJson<QHash<QString, QHash<QString, QVariant>>>( + "one_time_keys"_ls); + } +}; + +/*! \brief Query users with recent device key updates. + * + * Gets a list of users who have updated their device identity keys since a + * previous sync token. + * + * The server should include in the results any users who: + * + * * currently share a room with the calling user (ie, both users have + * membership state ``join``); *and* + * * added new device identity keys or removed an existing device with + * identity keys, between ``from`` and ``to``. + */ +class GetKeysChangesJob : public BaseJob { +public: + /*! \brief Query users with recent device key updates. + * + * \param from + * The desired start point of the list. Should be the ``next_batch`` field + * from a response to an earlier call to |/sync|. Users who have not + * uploaded new device identity keys since this point, nor deleted + * existing devices with identity keys since then, will be excluded + * from the results. + * + * \param to + * The desired end point of the list. Should be the ``next_batch`` + * field from a recent call to |/sync| - typically the most recent + * such call. This may be used by the server as a hint to check its + * caches are up to date. + */ + explicit GetKeysChangesJob(const QString& from, const QString& to); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetKeysChangesJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& from, + const QString& to); + + // Result properties + + /// The Matrix User IDs of all users who updated their device + /// identity keys. + QStringList changed() const + { + return loadFromJson<QStringList>("changed"_ls); + } + + /// The Matrix User IDs of all users who may have left all + /// the end-to-end encrypted rooms they previously shared + /// with the user. + QStringList left() const { return loadFromJson<QStringList>("left"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/kicking.cpp b/lib/csapi/kicking.cpp index 1d6d5543..7de5ce01 100644 --- a/lib/csapi/kicking.cpp +++ b/lib/csapi/kicking.cpp @@ -4,23 +4,17 @@ #include "kicking.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; -static const auto KickJobName = QStringLiteral("KickJob"); - -KickJob::KickJob(const QString& roomId, const QString& userId, const QString& reason) - : BaseJob(HttpVerb::Post, KickJobName, - basePath % "/rooms/" % roomId % "/kick") +KickJob::KickJob(const QString& roomId, const QString& userId, + const QString& reason) + : BaseJob(HttpVerb::Post, QStringLiteral("KickJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/kick") { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); - setRequestData(_data); + setRequestData(std::move(_data)); } - diff --git a/lib/csapi/kicking.h b/lib/csapi/kicking.h index 714079cf..2645a54f 100644 --- a/lib/csapi/kicking.h +++ b/lib/csapi/kicking.h @@ -6,32 +6,36 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Kick a user from the room. + * + * Kick a user from the room. + * + * The caller must have the required power level in order to perform this + * operation. + * + * Kicking a user adjusts the target member's membership state to be ``leave`` + * with an optional ``reason``. Like with other membership changes, a user can + * directly adjust the target member's state by making a request to + * ``/rooms/<room id>/state/m.room.member/<user id>``. + */ +class KickJob : public BaseJob { +public: + /*! \brief Kick a user from the room. + * + * \param roomId + * The room identifier (not alias) from which the user should be kicked. + * + * \param userId + * The fully qualified user ID of the user being kicked. + * + * \param reason + * The reason the user has been kicked. This will be supplied as the + * ``reason`` on the target's updated `m.room.member`_ event. + */ + explicit KickJob(const QString& roomId, const QString& userId, + const QString& reason = {}); +}; - /// Kick a user from the room. - /// - /// Kick a user from the room. - /// - /// The caller must have the required power level in order to perform this operation. - /// - /// Kicking a user adjusts the target member's membership state to be ``leave`` with an - /// optional ``reason``. Like with other membership changes, a user can directly adjust - /// the target member's state by making a request to ``/rooms/<room id>/state/m.room.member/<user id>``. - class KickJob : public BaseJob - { - public: - /*! Kick a user from the room. - * \param roomId - * The room identifier (not alias) from which the user should be kicked. - * \param userId - * The fully qualified user ID of the user being kicked. - * \param reason - * The reason the user has been kicked. This will be supplied as the - * ``reason`` on the target's updated `m.room.member`_ event. - */ - explicit KickJob(const QString& roomId, const QString& userId, const QString& reason = {}); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/leaving.cpp b/lib/csapi/leaving.cpp index 09e5f83b..8bd170bf 100644 --- a/lib/csapi/leaving.cpp +++ b/lib/csapi/leaving.cpp @@ -4,39 +4,32 @@ #include "leaving.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; QUrl LeaveRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/leave"); + QStringLiteral("/_matrix/client/r0") + % "/rooms/" % roomId % "/leave"); } -static const auto LeaveRoomJobName = QStringLiteral("LeaveRoomJob"); - LeaveRoomJob::LeaveRoomJob(const QString& roomId) - : BaseJob(HttpVerb::Post, LeaveRoomJobName, - basePath % "/rooms/" % roomId % "/leave") -{ -} + : BaseJob(HttpVerb::Post, QStringLiteral("LeaveRoomJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/leave") +{} QUrl ForgetRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/forget"); + QStringLiteral("/_matrix/client/r0") + % "/rooms/" % roomId % "/forget"); } -static const auto ForgetRoomJobName = QStringLiteral("ForgetRoomJob"); - ForgetRoomJob::ForgetRoomJob(const QString& roomId) - : BaseJob(HttpVerb::Post, ForgetRoomJobName, - basePath % "/rooms/" % roomId % "/forget") -{ -} - + : BaseJob(HttpVerb::Post, QStringLiteral("ForgetRoomJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/forget") +{} diff --git a/lib/csapi/leaving.h b/lib/csapi/leaving.h index 3a340034..1bea7e41 100644 --- a/lib/csapi/leaving.h +++ b/lib/csapi/leaving.h @@ -6,70 +6,66 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations - - /// Stop the requesting user participating in a particular room. - /// - /// This API stops a user participating in a particular room. - /// - /// If the user was already in the room, they will no longer be able to see - /// new events in the room. If the room requires an invite to join, they - /// will need to be re-invited before they can re-join. - /// - /// If the user was invited to the room, but had not joined, this call - /// serves to reject the invite. - /// - /// The user will still be allowed to retrieve history from the room which - /// they were previously allowed to see. - class LeaveRoomJob : public BaseJob - { - public: - /*! Stop the requesting user participating in a particular room. - * \param roomId - * The room identifier to leave. - */ - explicit LeaveRoomJob(const QString& roomId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * LeaveRoomJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); +/*! \brief Stop the requesting user participating in a particular room. + * + * This API stops a user participating in a particular room. + * + * If the user was already in the room, they will no longer be able to see + * new events in the room. If the room requires an invite to join, they + * will need to be re-invited before they can re-join. + * + * If the user was invited to the room, but had not joined, this call + * serves to reject the invite. + * + * The user will still be allowed to retrieve history from the room which + * they were previously allowed to see. + */ +class LeaveRoomJob : public BaseJob { +public: + /*! \brief Stop the requesting user participating in a particular room. + * + * \param roomId + * The room identifier to leave. + */ + explicit LeaveRoomJob(const QString& roomId); - }; + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for LeaveRoomJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); +}; - /// Stop the requesting user remembering about a particular room. - /// - /// This API stops a user remembering about a particular room. - /// - /// In general, history is a first class citizen in Matrix. After this API - /// is called, however, a user will no longer be able to retrieve history - /// for this room. If all users on a homeserver forget a room, the room is - /// eligible for deletion from that homeserver. - /// - /// If the user is currently joined to the room, they must leave the room - /// before calling this API. - class ForgetRoomJob : public BaseJob - { - public: - /*! Stop the requesting user remembering about a particular room. - * \param roomId - * The room identifier to forget. - */ - explicit ForgetRoomJob(const QString& roomId); +/*! \brief Stop the requesting user remembering about a particular room. + * + * This API stops a user remembering about a particular room. + * + * In general, history is a first class citizen in Matrix. After this API + * is called, however, a user will no longer be able to retrieve history + * for this room. If all users on a homeserver forget a room, the room is + * eligible for deletion from that homeserver. + * + * If the user is currently joined to the room, they must leave the room + * before calling this API. + */ +class ForgetRoomJob : public BaseJob { +public: + /*! \brief Stop the requesting user remembering about a particular room. + * + * \param roomId + * The room identifier to forget. + */ + explicit ForgetRoomJob(const QString& roomId); - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * ForgetRoomJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for ForgetRoomJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); +}; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/list_joined_rooms.cpp b/lib/csapi/list_joined_rooms.cpp index 85a9cae4..8d7e267f 100644 --- a/lib/csapi/list_joined_rooms.cpp +++ b/lib/csapi/list_joined_rooms.cpp @@ -4,49 +4,20 @@ #include "list_joined_rooms.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetJoinedRoomsJob::Private -{ - public: - QStringList joinedRooms; -}; +using namespace Quotient; QUrl GetJoinedRoomsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/joined_rooms"); + QStringLiteral("/_matrix/client/r0") + % "/joined_rooms"); } -static const auto GetJoinedRoomsJobName = QStringLiteral("GetJoinedRoomsJob"); - GetJoinedRoomsJob::GetJoinedRoomsJob() - : BaseJob(HttpVerb::Get, GetJoinedRoomsJobName, - basePath % "/joined_rooms") - , d(new Private) + : BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedRoomsJob"), + QStringLiteral("/_matrix/client/r0") % "/joined_rooms") { + addExpectedKey("joined_rooms"); } - -GetJoinedRoomsJob::~GetJoinedRoomsJob() = default; - -const QStringList& GetJoinedRoomsJob::joinedRooms() const -{ - return d->joinedRooms; -} - -BaseJob::Status GetJoinedRoomsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("joined_rooms"_ls)) - return { JsonParseError, - "The key 'joined_rooms' not found in the response" }; - fromJson(json.value("joined_rooms"_ls), d->joinedRooms); - return Success; -} - diff --git a/lib/csapi/list_joined_rooms.h b/lib/csapi/list_joined_rooms.h index 881a97b4..1034aa7b 100644 --- a/lib/csapi/list_joined_rooms.h +++ b/lib/csapi/list_joined_rooms.h @@ -6,39 +6,31 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations - +/*! \brief Lists the user's current rooms. + * + * This API returns a list of the user's current rooms. + */ +class GetJoinedRoomsJob : public BaseJob { +public: /// Lists the user's current rooms. - /// - /// This API returns a list of the user's current rooms. - class GetJoinedRoomsJob : public BaseJob - { - public: - explicit GetJoinedRoomsJob(); + explicit GetJoinedRoomsJob(); - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetJoinedRoomsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetJoinedRoomsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); - ~GetJoinedRoomsJob() override; + // Result properties - // Result properties - - /// The ID of each room in which the user has ``joined`` membership. - const QStringList& joinedRooms() const; - - protected: - Status parseJson(const QJsonDocument& data) override; + /// The ID of each room in which the user has ``joined`` membership. + QStringList joinedRooms() const + { + return loadFromJson<QStringList>("joined_rooms"_ls); + } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp index 71b3c541..415d816c 100644 --- a/lib/csapi/list_public_rooms.cpp +++ b/lib/csapi/list_public_rooms.cpp @@ -4,67 +4,39 @@ #include "list_public_rooms.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; -class GetRoomVisibilityOnDirectoryJob::Private -{ - public: - QString visibility; -}; - -QUrl GetRoomVisibilityOnDirectoryJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) +QUrl GetRoomVisibilityOnDirectoryJob::makeRequestUrl(QUrl baseUrl, + const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/directory/list/room/" % roomId); -} - -static const auto GetRoomVisibilityOnDirectoryJobName = QStringLiteral("GetRoomVisibilityOnDirectoryJob"); - -GetRoomVisibilityOnDirectoryJob::GetRoomVisibilityOnDirectoryJob(const QString& roomId) - : BaseJob(HttpVerb::Get, GetRoomVisibilityOnDirectoryJobName, - basePath % "/directory/list/room/" % roomId, false) - , d(new Private) -{ -} - -GetRoomVisibilityOnDirectoryJob::~GetRoomVisibilityOnDirectoryJob() = default; - -const QString& GetRoomVisibilityOnDirectoryJob::visibility() const -{ - return d->visibility; -} - -BaseJob::Status GetRoomVisibilityOnDirectoryJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("visibility"_ls), d->visibility); - return Success; + QStringLiteral("/_matrix/client/r0") + % "/directory/list/room/" % roomId); } -static const auto SetRoomVisibilityOnDirectoryJobName = QStringLiteral("SetRoomVisibilityOnDirectoryJob"); +GetRoomVisibilityOnDirectoryJob::GetRoomVisibilityOnDirectoryJob( + const QString& roomId) + : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomVisibilityOnDirectoryJob"), + QStringLiteral("/_matrix/client/r0") % "/directory/list/room/" + % roomId, + false) +{} -SetRoomVisibilityOnDirectoryJob::SetRoomVisibilityOnDirectoryJob(const QString& roomId, const QString& visibility) - : BaseJob(HttpVerb::Put, SetRoomVisibilityOnDirectoryJobName, - basePath % "/directory/list/room/" % roomId) +SetRoomVisibilityOnDirectoryJob::SetRoomVisibilityOnDirectoryJob( + const QString& roomId, const QString& visibility) + : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomVisibilityOnDirectoryJob"), + QStringLiteral("/_matrix/client/r0") % "/directory/list/room/" + % roomId) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("visibility"), visibility); - setRequestData(_data); + setRequestData(std::move(_data)); } -class GetPublicRoomsJob::Private -{ - public: - PublicRoomsResponse data; -}; - -BaseJob::Query queryToGetPublicRooms(Omittable<int> limit, const QString& since, const QString& server) +auto queryToGetPublicRooms(Omittable<int> limit, const QString& since, + const QString& server) { BaseJob::Query _q; addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); @@ -73,90 +45,50 @@ BaseJob::Query queryToGetPublicRooms(Omittable<int> limit, const QString& since, return _q; } -QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, Omittable<int> limit, const QString& since, const QString& server) +QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, Omittable<int> limit, + const QString& since, + const QString& server) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/publicRooms", - queryToGetPublicRooms(limit, since, server)); + QStringLiteral("/_matrix/client/r0") + % "/publicRooms", + queryToGetPublicRooms(limit, since, server)); } -static const auto GetPublicRoomsJobName = QStringLiteral("GetPublicRoomsJob"); - -GetPublicRoomsJob::GetPublicRoomsJob(Omittable<int> limit, const QString& since, const QString& server) - : BaseJob(HttpVerb::Get, GetPublicRoomsJobName, - basePath % "/publicRooms", - queryToGetPublicRooms(limit, since, server), - {}, false) - , d(new Private) +GetPublicRoomsJob::GetPublicRoomsJob(Omittable<int> limit, const QString& since, + const QString& server) + : BaseJob(HttpVerb::Get, QStringLiteral("GetPublicRoomsJob"), + QStringLiteral("/_matrix/client/r0") % "/publicRooms", + queryToGetPublicRooms(limit, since, server), {}, false) { + addExpectedKey("chunk"); } -GetPublicRoomsJob::~GetPublicRoomsJob() = default; - -const PublicRoomsResponse& GetPublicRoomsJob::data() const -{ - return d->data; -} - -BaseJob::Status GetPublicRoomsJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<QueryPublicRoomsJob::Filter> - { - static void dumpTo(QJsonObject& jo, const QueryPublicRoomsJob::Filter& pod) - { - addParam<IfNotEmpty>(jo, QStringLiteral("generic_search_term"), pod.genericSearchTerm); - } - }; -} // namespace QMatrixClient - -class QueryPublicRoomsJob::Private -{ - public: - PublicRoomsResponse data; -}; - -BaseJob::Query queryToQueryPublicRooms(const QString& server) +auto queryToQueryPublicRooms(const QString& server) { BaseJob::Query _q; addParam<IfNotEmpty>(_q, QStringLiteral("server"), server); return _q; } -static const auto QueryPublicRoomsJobName = QStringLiteral("QueryPublicRoomsJob"); - -QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, Omittable<int> limit, const QString& since, const Omittable<Filter>& filter, Omittable<bool> includeAllNetworks, const QString& thirdPartyInstanceId) - : BaseJob(HttpVerb::Post, QueryPublicRoomsJobName, - basePath % "/publicRooms", - queryToQueryPublicRooms(server)) - , d(new Private) +QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, + Omittable<int> limit, + const QString& since, + const Omittable<Filter>& filter, + Omittable<bool> includeAllNetworks, + const QString& thirdPartyInstanceId) + : BaseJob(HttpVerb::Post, QStringLiteral("QueryPublicRoomsJob"), + QStringLiteral("/_matrix/client/r0") % "/publicRooms", + queryToQueryPublicRooms(server)) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("limit"), limit); addParam<IfNotEmpty>(_data, QStringLiteral("since"), since); addParam<IfNotEmpty>(_data, QStringLiteral("filter"), filter); - addParam<IfNotEmpty>(_data, QStringLiteral("include_all_networks"), includeAllNetworks); - addParam<IfNotEmpty>(_data, QStringLiteral("third_party_instance_id"), thirdPartyInstanceId); - setRequestData(_data); -} - -QueryPublicRoomsJob::~QueryPublicRoomsJob() = default; - -const PublicRoomsResponse& QueryPublicRoomsJob::data() const -{ - return d->data; + addParam<IfNotEmpty>(_data, QStringLiteral("include_all_networks"), + includeAllNetworks); + addParam<IfNotEmpty>(_data, QStringLiteral("third_party_instance_id"), + thirdPartyInstanceId); + setRequestData(std::move(_data)); + addExpectedKey("chunk"); } - -BaseJob::Status QueryPublicRoomsJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h index a6498745..b0cb79d2 100644 --- a/lib/csapi/list_public_rooms.h +++ b/lib/csapi/list_public_rooms.h @@ -4,171 +4,214 @@ #pragma once -#include "jobs/basejob.h" - #include "csapi/definitions/public_rooms_response.h" -#include "converters.h" -namespace QMatrixClient -{ - // Operations +#include "jobs/basejob.h" - /// Gets the visibility of a room in the directory - /// - /// Gets the visibility of a given room on the server's public room directory. - class GetRoomVisibilityOnDirectoryJob : public BaseJob - { - public: - /*! Gets the visibility of a room in the directory - * \param roomId - * The room ID. - */ - explicit GetRoomVisibilityOnDirectoryJob(const QString& roomId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetRoomVisibilityOnDirectoryJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); - - ~GetRoomVisibilityOnDirectoryJob() override; - - // Result properties - - /// The visibility of the room in the directory. - const QString& visibility() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; +namespace Quotient { - /// Sets the visibility of a room in the room directory - /// - /// Sets the visibility of a given room in the server's public room - /// directory. - /// - /// Servers may choose to implement additional access control checks - /// here, for instance that room visibility can only be changed by - /// the room creator or a server administrator. - class SetRoomVisibilityOnDirectoryJob : public BaseJob +/*! \brief Gets the visibility of a room in the directory + * + * Gets the visibility of a given room on the server's public room directory. + */ +class GetRoomVisibilityOnDirectoryJob : public BaseJob { +public: + /*! \brief Gets the visibility of a room in the directory + * + * \param roomId + * The room ID. + */ + explicit GetRoomVisibilityOnDirectoryJob(const QString& roomId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRoomVisibilityOnDirectoryJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); + + // Result properties + + /// The visibility of the room in the directory. + QString visibility() const { - public: - /*! Sets the visibility of a room in the room directory - * \param roomId - * The room ID. - * \param visibility - * The new visibility setting for the room. - * Defaults to 'public'. - */ - explicit SetRoomVisibilityOnDirectoryJob(const QString& roomId, const QString& visibility = {}); + return loadFromJson<QString>("visibility"_ls); + } +}; + +/*! \brief Sets the visibility of a room in the room directory + * + * Sets the visibility of a given room in the server's public room + * directory. + * + * Servers may choose to implement additional access control checks + * here, for instance that room visibility can only be changed by + * the room creator or a server administrator. + */ +class SetRoomVisibilityOnDirectoryJob : public BaseJob { +public: + /*! \brief Sets the visibility of a room in the room directory + * + * \param roomId + * The room ID. + * + * \param visibility + * The new visibility setting for the room. + * Defaults to 'public'. + */ + explicit SetRoomVisibilityOnDirectoryJob(const QString& roomId, + const QString& visibility = {}); +}; + +/*! \brief Lists the public rooms on the server. + * + * Lists the public rooms on the server. + * + * This API returns paginated responses. The rooms are ordered by the number + * of joined members, with the largest rooms first. + */ +class GetPublicRoomsJob : public BaseJob { +public: + /*! \brief Lists the public rooms on the server. + * + * \param limit + * Limit the number of results returned. + * + * \param since + * A pagination token from a previous request, allowing clients to + * get the next (or previous) batch of rooms. + * The direction of pagination is specified solely by which token + * is supplied, rather than via an explicit flag. + * + * \param server + * The server to fetch the public room lists from. Defaults to the + * local server. + */ + explicit GetPublicRoomsJob(Omittable<int> limit = none, + const QString& since = {}, + const QString& server = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetPublicRoomsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, Omittable<int> limit = none, + const QString& since = {}, + const QString& server = {}); + + // Result properties + + /// A paginated chunk of public rooms. + QVector<PublicRoomsChunk> chunk() const + { + return loadFromJson<QVector<PublicRoomsChunk>>("chunk"_ls); + } + + /// A pagination token for the response. The absence of this token + /// means there are no more results to fetch and the client should + /// stop paginating. + QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); } + + /// A pagination token that allows fetching previous results. The + /// absence of this token means there are no results before this + /// batch, i.e. this is the first batch. + QString prevBatch() const { return loadFromJson<QString>("prev_batch"_ls); } + + /// An estimate on the total number of public rooms, if the + /// server has an estimate. + Omittable<int> totalRoomCountEstimate() const + { + return loadFromJson<Omittable<int>>("total_room_count_estimate"_ls); + } +}; + +/*! \brief Lists the public rooms on the server with optional filter. + * + * Lists the public rooms on the server, with optional filter. + * + * This API returns paginated responses. The rooms are ordered by the number + * of joined members, with the largest rooms first. + */ +class QueryPublicRoomsJob : public BaseJob { +public: + // Inner data structures + + /// Filter to apply to the results. + struct Filter { + /// A string to search for in the room metadata, e.g. name, + /// topic, canonical alias etc. (Optional). + QString genericSearchTerm; }; - /// Lists the public rooms on the server. - /// - /// Lists the public rooms on the server. - /// - /// This API returns paginated responses. The rooms are ordered by the number - /// of joined members, with the largest rooms first. - class GetPublicRoomsJob : public BaseJob + // Construction/destruction + + /*! \brief Lists the public rooms on the server with optional filter. + * + * \param server + * The server to fetch the public room lists from. Defaults to the + * local server. + * + * \param limit + * Limit the number of results returned. + * + * \param since + * A pagination token from a previous request, allowing clients + * to get the next (or previous) batch of rooms. The direction + * of pagination is specified solely by which token is supplied, + * rather than via an explicit flag. + * + * \param filter + * Filter to apply to the results. + * + * \param includeAllNetworks + * Whether or not to include all known networks/protocols from + * application services on the homeserver. Defaults to false. + * + * \param thirdPartyInstanceId + * The specific third party network/protocol to request from the + * homeserver. Can only be used if ``include_all_networks`` is false. + */ + explicit QueryPublicRoomsJob(const QString& server = {}, + Omittable<int> limit = none, + const QString& since = {}, + const Omittable<Filter>& filter = none, + Omittable<bool> includeAllNetworks = none, + const QString& thirdPartyInstanceId = {}); + + // Result properties + + /// A paginated chunk of public rooms. + QVector<PublicRoomsChunk> chunk() const { - public: - /*! Lists the public rooms on the server. - * \param limit - * Limit the number of results returned. - * \param since - * A pagination token from a previous request, allowing clients to - * get the next (or previous) batch of rooms. - * The direction of pagination is specified solely by which token - * is supplied, rather than via an explicit flag. - * \param server - * The server to fetch the public room lists from. Defaults to the - * local server. - */ - explicit GetPublicRoomsJob(Omittable<int> limit = none, const QString& since = {}, const QString& server = {}); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetPublicRoomsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, Omittable<int> limit = none, const QString& since = {}, const QString& server = {}); - - ~GetPublicRoomsJob() override; - - // Result properties - - /// A list of the rooms on the server. - const PublicRoomsResponse& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; + return loadFromJson<QVector<PublicRoomsChunk>>("chunk"_ls); + } + + /// A pagination token for the response. The absence of this token + /// means there are no more results to fetch and the client should + /// stop paginating. + QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); } + + /// A pagination token that allows fetching previous results. The + /// absence of this token means there are no results before this + /// batch, i.e. this is the first batch. + QString prevBatch() const { return loadFromJson<QString>("prev_batch"_ls); } + + /// An estimate on the total number of public rooms, if the + /// server has an estimate. + Omittable<int> totalRoomCountEstimate() const + { + return loadFromJson<Omittable<int>>("total_room_count_estimate"_ls); + } +}; - /// Lists the public rooms on the server with optional filter. - /// - /// Lists the public rooms on the server, with optional filter. - /// - /// This API returns paginated responses. The rooms are ordered by the number - /// of joined members, with the largest rooms first. - class QueryPublicRoomsJob : public BaseJob +template <> +struct JsonObjectConverter<QueryPublicRoomsJob::Filter> { + static void dumpTo(QJsonObject& jo, const QueryPublicRoomsJob::Filter& pod) { - public: - // Inner data structures - - /// Filter to apply to the results. - struct Filter - { - /// A string to search for in the room metadata, e.g. name, - /// topic, canonical alias etc. (Optional). - QString genericSearchTerm; - }; - - // Construction/destruction - - /*! Lists the public rooms on the server with optional filter. - * \param server - * The server to fetch the public room lists from. Defaults to the - * local server. - * \param limit - * Limit the number of results returned. - * \param since - * A pagination token from a previous request, allowing clients - * to get the next (or previous) batch of rooms. The direction - * of pagination is specified solely by which token is supplied, - * rather than via an explicit flag. - * \param filter - * Filter to apply to the results. - * \param includeAllNetworks - * Whether or not to include all known networks/protocols from - * application services on the homeserver. Defaults to false. - * \param thirdPartyInstanceId - * The specific third party network/protocol to request from the - * homeserver. Can only be used if ``include_all_networks`` is false. - */ - explicit QueryPublicRoomsJob(const QString& server = {}, Omittable<int> limit = none, const QString& since = {}, const Omittable<Filter>& filter = none, Omittable<bool> includeAllNetworks = none, const QString& thirdPartyInstanceId = {}); - ~QueryPublicRoomsJob() override; - - // Result properties - - /// A list of the rooms on the server. - const PublicRoomsResponse& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + addParam<IfNotEmpty>(jo, QStringLiteral("generic_search_term"), + pod.genericSearchTerm); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp index 5e369b9a..a5bac9ea 100644 --- a/lib/csapi/login.cpp +++ b/lib/csapi/login.cpp @@ -4,78 +4,29 @@ #include "login.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<GetLoginFlowsJob::LoginFlow> - { - static void fillFrom(const QJsonObject& jo, GetLoginFlowsJob::LoginFlow& result) - { - fromJson(jo.value("type"_ls), result.type); - } - }; -} // namespace QMatrixClient - -class GetLoginFlowsJob::Private -{ - public: - QVector<LoginFlow> flows; -}; +using namespace Quotient; QUrl GetLoginFlowsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/login"); + QStringLiteral("/_matrix/client/r0") + % "/login"); } -static const auto GetLoginFlowsJobName = QStringLiteral("GetLoginFlowsJob"); - GetLoginFlowsJob::GetLoginFlowsJob() - : BaseJob(HttpVerb::Get, GetLoginFlowsJobName, - basePath % "/login", false) - , d(new Private) -{ -} - -GetLoginFlowsJob::~GetLoginFlowsJob() = default; - -const QVector<GetLoginFlowsJob::LoginFlow>& GetLoginFlowsJob::flows() const -{ - return d->flows; -} - -BaseJob::Status GetLoginFlowsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("flows"_ls), d->flows); - return Success; -} - -class LoginJob::Private -{ - public: - QString userId; - QString accessToken; - QString homeServer; - QString deviceId; - Omittable<DiscoveryInformation> wellKnown; -}; - -static const auto LoginJobName = QStringLiteral("LoginJob"); - -LoginJob::LoginJob(const QString& type, const Omittable<UserIdentifier>& identifier, const QString& password, const QString& token, const QString& deviceId, const QString& initialDeviceDisplayName, const QString& user, const QString& medium, const QString& address) - : BaseJob(HttpVerb::Post, LoginJobName, - basePath % "/login", false) - , d(new Private) + : BaseJob(HttpVerb::Get, QStringLiteral("GetLoginFlowsJob"), + QStringLiteral("/_matrix/client/r0") % "/login", false) +{} + +LoginJob::LoginJob(const QString& type, + const Omittable<UserIdentifier>& identifier, + const QString& password, const QString& token, + const QString& deviceId, + const QString& initialDeviceDisplayName) + : BaseJob(HttpVerb::Post, QStringLiteral("LoginJob"), + QStringLiteral("/_matrix/client/r0") % "/login", false) { QJsonObject _data; addParam<>(_data, QStringLiteral("type"), type); @@ -83,48 +34,7 @@ LoginJob::LoginJob(const QString& type, const Omittable<UserIdentifier>& identif addParam<IfNotEmpty>(_data, QStringLiteral("password"), password); addParam<IfNotEmpty>(_data, QStringLiteral("token"), token); addParam<IfNotEmpty>(_data, QStringLiteral("device_id"), deviceId); - addParam<IfNotEmpty>(_data, QStringLiteral("initial_device_display_name"), initialDeviceDisplayName); - addParam<IfNotEmpty>(_data, QStringLiteral("user"), user); - addParam<IfNotEmpty>(_data, QStringLiteral("medium"), medium); - addParam<IfNotEmpty>(_data, QStringLiteral("address"), address); - setRequestData(_data); + addParam<IfNotEmpty>(_data, QStringLiteral("initial_device_display_name"), + initialDeviceDisplayName); + setRequestData(std::move(_data)); } - -LoginJob::~LoginJob() = default; - -const QString& LoginJob::userId() const -{ - return d->userId; -} - -const QString& LoginJob::accessToken() const -{ - return d->accessToken; -} - -const QString& LoginJob::homeServer() const -{ - return d->homeServer; -} - -const QString& LoginJob::deviceId() const -{ - return d->deviceId; -} - -const Omittable<DiscoveryInformation>& LoginJob::wellKnown() const -{ - return d->wellKnown; -} - -BaseJob::Status LoginJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("user_id"_ls), d->userId); - fromJson(json.value("access_token"_ls), d->accessToken); - fromJson(json.value("home_server"_ls), d->homeServer); - fromJson(json.value("device_id"_ls), d->deviceId); - fromJson(json.value("well_known"_ls), d->wellKnown); - return Success; -} - diff --git a/lib/csapi/login.h b/lib/csapi/login.h index 648316df..a406fc79 100644 --- a/lib/csapi/login.h +++ b/lib/csapi/login.h @@ -4,132 +4,151 @@ #pragma once -#include "jobs/basejob.h" - -#include <QtCore/QVector> -#include "csapi/definitions/wellknown/full.h" #include "csapi/definitions/user_identifier.h" -#include "converters.h" - -namespace QMatrixClient -{ - // Operations - - /// Get the supported login types to authenticate users - /// - /// Gets the homeserver's supported login types to authenticate users. Clients - /// should pick one of these and supply it as the ``type`` when logging in. - class GetLoginFlowsJob : public BaseJob - { - public: - // Inner data structures - - /// Gets the homeserver's supported login types to authenticate users. Clients - /// should pick one of these and supply it as the ``type`` when logging in. - struct LoginFlow - { - /// The login type. This is supplied as the ``type`` when - /// logging in. - QString type; - }; +#include "csapi/definitions/wellknown/full.h" - // Construction/destruction +#include "jobs/basejob.h" - explicit GetLoginFlowsJob(); +namespace Quotient { - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetLoginFlowsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); +/*! \brief Get the supported login types to authenticate users + * + * Gets the homeserver's supported login types to authenticate users. Clients + * should pick one of these and supply it as the ``type`` when logging in. + */ +class GetLoginFlowsJob : public BaseJob { +public: + // Inner data structures + + /// Gets the homeserver's supported login types to authenticate users. + /// Clients should pick one of these and supply it as the ``type`` when + /// logging in. + struct LoginFlow { + /// The login type. This is supplied as the ``type`` when + /// logging in. + QString type; + }; - ~GetLoginFlowsJob() override; + // Construction/destruction - // Result properties + /// Get the supported login types to authenticate users + explicit GetLoginFlowsJob(); - /// The homeserver's supported login types - const QVector<LoginFlow>& flows() const; + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetLoginFlowsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); - protected: - Status parseJson(const QJsonDocument& data) override; + // Result properties - private: - class Private; - QScopedPointer<Private> d; - }; + /// The homeserver's supported login types + QVector<LoginFlow> flows() const + { + return loadFromJson<QVector<LoginFlow>>("flows"_ls); + } +}; + +template <> +struct JsonObjectConverter<GetLoginFlowsJob::LoginFlow> { + static void fillFrom(const QJsonObject& jo, + GetLoginFlowsJob::LoginFlow& result) + { + fromJson(jo.value("type"_ls), result.type); + } +}; + +/*! \brief Authenticates the user. + * + * Authenticates the user, and issues an access token they can + * use to authorize themself in subsequent requests. + * + * If the client does not supply a ``device_id``, the server must + * auto-generate one. + * + * The returned access token must be associated with the ``device_id`` + * supplied by the client or generated by the server. The server may + * invalidate any access token previously associated with that device. See + * `Relationship between access tokens and devices`_. + */ +class LoginJob : public BaseJob { +public: + /*! \brief Authenticates the user. + * + * \param type + * The login type being used. + * + * \param identifier + * Authenticates the user, and issues an access token they can + * use to authorize themself in subsequent requests. + * + * If the client does not supply a ``device_id``, the server must + * auto-generate one. + * + * The returned access token must be associated with the ``device_id`` + * supplied by the client or generated by the server. The server may + * invalidate any access token previously associated with that device. See + * `Relationship between access tokens and devices`_. + * + * \param password + * Required when ``type`` is ``m.login.password``. The user's + * password. + * + * \param token + * Required when ``type`` is ``m.login.token``. Part of `Token-based`_ + * login. + * + * \param deviceId + * ID of the client device. If this does not correspond to a + * known client device, a new device will be created. The server + * will auto-generate a device_id if this is not specified. + * + * \param initialDeviceDisplayName + * A display name to assign to the newly-created device. Ignored + * if ``device_id`` corresponds to a known device. + */ + explicit LoginJob(const QString& type, + const Omittable<UserIdentifier>& identifier = none, + const QString& password = {}, const QString& token = {}, + const QString& deviceId = {}, + const QString& initialDeviceDisplayName = {}); + + // Result properties + + /// The fully-qualified Matrix ID for the account. + QString userId() const { return loadFromJson<QString>("user_id"_ls); } + + /// An access token for the account. + /// This access token can then be used to authorize other requests. + QString accessToken() const + { + return loadFromJson<QString>("access_token"_ls); + } - /// Authenticates the user. + /// The server_name of the homeserver on which the account has + /// been registered. /// - /// Authenticates the user, and issues an access token they can - /// use to authorize themself in subsequent requests. - /// - /// If the client does not supply a ``device_id``, the server must - /// auto-generate one. - /// - /// The returned access token must be associated with the ``device_id`` - /// supplied by the client or generated by the server. The server may - /// invalidate any access token previously associated with that device. See - /// `Relationship between access tokens and devices`_. - class LoginJob : public BaseJob + /// **Deprecated**. Clients should extract the server_name from + /// ``user_id`` (by splitting at the first colon) if they require + /// it. Note also that ``homeserver`` is not spelt this way. + QString homeServer() const { - public: - /*! Authenticates the user. - * \param type - * The login type being used. - * \param identifier - * Identification information for the user. - * \param password - * Required when ``type`` is ``m.login.password``. The user's - * password. - * \param token - * Required when ``type`` is ``m.login.token``. Part of `Token-based`_ login. - * \param deviceId - * ID of the client device. If this does not correspond to a - * known client device, a new device will be created. The server - * will auto-generate a device_id if this is not specified. - * \param initialDeviceDisplayName - * A display name to assign to the newly-created device. Ignored - * if ``device_id`` corresponds to a known device. - * \param user - * The fully qualified user ID or just local part of the user ID, to log in. Deprecated in favour of ``identifier``. - * \param medium - * When logging in using a third party identifier, the medium of the identifier. Must be 'email'. Deprecated in favour of ``identifier``. - * \param address - * Third party identifier for the user. Deprecated in favour of ``identifier``. - */ - explicit LoginJob(const QString& type, const Omittable<UserIdentifier>& identifier = none, const QString& password = {}, const QString& token = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}, const QString& user = {}, const QString& medium = {}, const QString& address = {}); - ~LoginJob() override; - - // Result properties - - /// The fully-qualified Matrix ID that has been registered. - const QString& userId() const; - /// An access token for the account. - /// This access token can then be used to authorize other requests. - const QString& accessToken() const; - /// The server_name of the homeserver on which the account has - /// been registered. - /// - /// **Deprecated**. Clients should extract the server_name from - /// ``user_id`` (by splitting at the first colon) if they require - /// it. Note also that ``homeserver`` is not spelt this way. - const QString& homeServer() const; - /// ID of the logged-in device. Will be the same as the - /// corresponding parameter in the request, if one was specified. - const QString& deviceId() const; - /// Optional client configuration provided by the server. If present, - /// clients SHOULD use the provided object to reconfigure themselves, - /// optionally validating the URLs within. This object takes the same - /// form as the one returned from .well-known autodiscovery. - const Omittable<DiscoveryInformation>& wellKnown() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + return loadFromJson<QString>("home_server"_ls); + } + + /// ID of the logged-in device. Will be the same as the + /// corresponding parameter in the request, if one was specified. + QString deviceId() const { return loadFromJson<QString>("device_id"_ls); } + + /// Optional client configuration provided by the server. If present, + /// clients SHOULD use the provided object to reconfigure themselves, + /// optionally validating the URLs within. This object takes the same + /// form as the one returned from .well-known autodiscovery. + Omittable<DiscoveryInformation> wellKnown() const + { + return loadFromJson<Omittable<DiscoveryInformation>>("well_known"_ls); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/logout.cpp b/lib/csapi/logout.cpp index 6e209e07..9583b8ec 100644 --- a/lib/csapi/logout.cpp +++ b/lib/csapi/logout.cpp @@ -4,39 +4,30 @@ #include "logout.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; QUrl LogoutJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/logout"); + QStringLiteral("/_matrix/client/r0") + % "/logout"); } -static const auto LogoutJobName = QStringLiteral("LogoutJob"); - LogoutJob::LogoutJob() - : BaseJob(HttpVerb::Post, LogoutJobName, - basePath % "/logout") -{ -} + : BaseJob(HttpVerb::Post, QStringLiteral("LogoutJob"), + QStringLiteral("/_matrix/client/r0") % "/logout") +{} QUrl LogoutAllJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/logout/all"); + QStringLiteral("/_matrix/client/r0") + % "/logout/all"); } -static const auto LogoutAllJobName = QStringLiteral("LogoutAllJob"); - LogoutAllJob::LogoutAllJob() - : BaseJob(HttpVerb::Post, LogoutAllJobName, - basePath % "/logout/all") -{ -} - + : BaseJob(HttpVerb::Post, QStringLiteral("LogoutAllJob"), + QStringLiteral("/_matrix/client/r0") % "/logout/all") +{} diff --git a/lib/csapi/logout.h b/lib/csapi/logout.h index 3ef3c656..78f14e40 100644 --- a/lib/csapi/logout.h +++ b/lib/csapi/logout.h @@ -6,52 +6,52 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations - +/*! \brief Invalidates a user access token + * + * Invalidates an existing access token, so that it can no longer be used for + * authorization. The device associated with the access token is also deleted. + * `Device keys <#device-keys>`_ for the device are deleted alongside the device. + */ +class LogoutJob : public BaseJob { +public: /// Invalidates a user access token - /// - /// Invalidates an existing access token, so that it can no longer be used for - /// authorization. - class LogoutJob : public BaseJob - { - public: - explicit LogoutJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * LogoutJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - }; - + explicit LogoutJob(); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for LogoutJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); +}; + +/*! \brief Invalidates all access tokens for a user + * + * Invalidates all access tokens for a user, so that they can no longer be used + * for authorization. This includes the access token that made this request. All + * devices for the user are also deleted. `Device keys <#device-keys>`_ for the + * device are deleted alongside the device. + * + * This endpoint does not use the `User-Interactive Authentication API`_ because + * User-Interactive Authentication is designed to protect against attacks where + * the someone gets hold of a single access token then takes over the account. + * This endpoint invalidates all access tokens for the user, including the token + * used in the request, and therefore the attacker is unable to take over the + * account in this way. + */ +class LogoutAllJob : public BaseJob { +public: /// Invalidates all access tokens for a user - /// - /// Invalidates all access tokens for a user, so that they can no longer be used for - /// authorization. This includes the access token that made this request. - /// - /// This endpoint does not require UI authorization because UI authorization is - /// designed to protect against attacks where the someone gets hold of a single access - /// token then takes over the account. This endpoint invalidates all access tokens for - /// the user, including the token used in the request, and therefore the attacker is - /// unable to take over the account in this way. - class LogoutAllJob : public BaseJob - { - public: - explicit LogoutAllJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * LogoutAllJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - }; -} // namespace QMatrixClient + explicit LogoutAllJob(); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for LogoutAllJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); +}; + +} // namespace Quotient diff --git a/lib/csapi/message_pagination.cpp b/lib/csapi/message_pagination.cpp index 9aca7ec9..855c051f 100644 --- a/lib/csapi/message_pagination.cpp +++ b/lib/csapi/message_pagination.cpp @@ -4,23 +4,13 @@ #include "message_pagination.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetRoomEventsJob::Private -{ - public: - QString begin; - QString end; - RoomEvents chunk; -}; +using namespace Quotient; -BaseJob::Query queryToGetRoomEvents(const QString& from, const QString& to, const QString& dir, Omittable<int> limit, const QString& filter) +auto queryToGetRoomEvents(const QString& from, const QString& to, + const QString& dir, Omittable<int> limit, + const QString& filter) { BaseJob::Query _q; addParam<>(_q, QStringLiteral("from"), from); @@ -31,46 +21,22 @@ BaseJob::Query queryToGetRoomEvents(const QString& from, const QString& to, cons return _q; } -QUrl GetRoomEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& from, const QString& dir, const QString& to, Omittable<int> limit, const QString& filter) -{ - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/messages", - queryToGetRoomEvents(from, to, dir, limit, filter)); -} - -static const auto GetRoomEventsJobName = QStringLiteral("GetRoomEventsJob"); - -GetRoomEventsJob::GetRoomEventsJob(const QString& roomId, const QString& from, const QString& dir, const QString& to, Omittable<int> limit, const QString& filter) - : BaseJob(HttpVerb::Get, GetRoomEventsJobName, - basePath % "/rooms/" % roomId % "/messages", - queryToGetRoomEvents(from, to, dir, limit, filter)) - , d(new Private) -{ -} - -GetRoomEventsJob::~GetRoomEventsJob() = default; - -const QString& GetRoomEventsJob::begin() const -{ - return d->begin; -} - -const QString& GetRoomEventsJob::end() const -{ - return d->end; -} - -RoomEvents&& GetRoomEventsJob::chunk() -{ - return std::move(d->chunk); -} - -BaseJob::Status GetRoomEventsJob::parseJson(const QJsonDocument& data) +QUrl GetRoomEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& from, const QString& dir, + const QString& to, Omittable<int> limit, + const QString& filter) { - auto json = data.object(); - fromJson(json.value("start"_ls), d->begin); - fromJson(json.value("end"_ls), d->end); - fromJson(json.value("chunk"_ls), d->chunk); - return Success; + return BaseJob::makeRequestUrl( + std::move(baseUrl), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/messages", + queryToGetRoomEvents(from, to, dir, limit, filter)); } +GetRoomEventsJob::GetRoomEventsJob(const QString& roomId, const QString& from, + const QString& dir, const QString& to, + Omittable<int> limit, const QString& filter) + : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomEventsJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/messages", + queryToGetRoomEvents(from, to, dir, limit, filter)) +{} diff --git a/lib/csapi/message_pagination.h b/lib/csapi/message_pagination.h index 12544f0c..286d4237 100644 --- a/lib/csapi/message_pagination.h +++ b/lib/csapi/message_pagination.h @@ -4,70 +4,89 @@ #pragma once +#include "events/eventloader.h" #include "jobs/basejob.h" -#include "events/eventloader.h" -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Get a list of events for this room + * + * This API returns a list of message and state events for a room. It uses + * pagination query parameters to paginate history in the room. + * + * *Note*: This endpoint supports lazy-loading of room member events. See + * `Lazy-loading room members <#lazy-loading-room-members>`_ for more + * information. + */ +class GetRoomEventsJob : public BaseJob { +public: + /*! \brief Get a list of events for this room + * + * \param roomId + * The room to get events from. + * + * \param from + * The token to start returning events from. This token can be obtained + * from a ``prev_batch`` token returned for each room by the sync API, + * or from a ``start`` or ``end`` token returned by a previous request + * to this endpoint. + * + * \param dir + * The direction to return events from. + * + * \param to + * The token to stop returning events at. This token can be obtained from + * a ``prev_batch`` token returned for each room by the sync endpoint, + * or from a ``start`` or ``end`` token returned by a previous request to + * this endpoint. + * + * \param limit + * The maximum number of events to return. Default: 10. + * + * \param filter + * A JSON RoomEventFilter to filter returned events with. + */ + explicit GetRoomEventsJob(const QString& roomId, const QString& from, + const QString& dir, const QString& to = {}, + Omittable<int> limit = none, + const QString& filter = {}); - /// Get a list of events for this room - /// - /// This API returns a list of message and state events for a room. It uses - /// pagination query parameters to paginate history in the room. - class GetRoomEventsJob : public BaseJob - { - public: - /*! Get a list of events for this room - * \param roomId - * The room to get events from. - * \param from - * The token to start returning events from. This token can be obtained - * from a ``prev_batch`` token returned for each room by the sync API, - * or from a ``start`` or ``end`` token returned by a previous request - * to this endpoint. - * \param dir - * The direction to return events from. - * \param to - * The token to stop returning events at. This token can be obtained from - * a ``prev_batch`` token returned for each room by the sync endpoint, - * or from a ``start`` or ``end`` token returned by a previous request to - * this endpoint. - * \param limit - * The maximum number of events to return. Default: 10. - * \param filter - * A JSON RoomEventFilter to filter returned events with. - */ - explicit GetRoomEventsJob(const QString& roomId, const QString& from, const QString& dir, const QString& to = {}, Omittable<int> limit = none, const QString& filter = {}); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRoomEventsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& from, const QString& dir, + const QString& to = {}, + Omittable<int> limit = none, + const QString& filter = {}); - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetRoomEventsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& from, const QString& dir, const QString& to = {}, Omittable<int> limit = none, const QString& filter = {}); + // Result properties - ~GetRoomEventsJob() override; + /// The token the pagination starts from. If ``dir=b`` this will be + /// the token supplied in ``from``. + QString begin() const { return loadFromJson<QString>("start"_ls); } - // Result properties + /// The token the pagination ends at. If ``dir=b`` this token should + /// be used again to request even earlier events. + QString end() const { return loadFromJson<QString>("end"_ls); } - /// The token the pagination starts from. If ``dir=b`` this will be - /// the token supplied in ``from``. - const QString& begin() const; - /// The token the pagination ends at. If ``dir=b`` this token should - /// be used again to request even earlier events. - const QString& end() const; - /// A list of room events. - RoomEvents&& chunk(); + /// A list of room events. The order depends on the ``dir`` parameter. + /// For ``dir=b`` events will be in reverse-chronological order, + /// for ``dir=f`` in chronological order, so that events start + /// at the ``from`` point. + RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); } - protected: - Status parseJson(const QJsonDocument& data) override; + /// A list of state events relevant to showing the ``chunk``. For example, if + /// ``lazy_load_members`` is enabled in the filter then this may contain + /// the membership events for the senders of events in the ``chunk``. + /// + /// Unless ``include_redundant_members`` is ``true``, the server + /// may remove membership events which would have already been + /// sent to the client in prior calls to this endpoint, assuming + /// the membership of those members has not changed. + StateEvents state() { return takeFromJson<StateEvents>("state"_ls); } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/notifications.cpp b/lib/csapi/notifications.cpp index c00b7cb0..a479d500 100644 --- a/lib/csapi/notifications.cpp +++ b/lib/csapi/notifications.cpp @@ -4,40 +4,12 @@ #include "notifications.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<GetNotificationsJob::Notification> - { - static void fillFrom(const QJsonObject& jo, GetNotificationsJob::Notification& result) - { - fromJson(jo.value("actions"_ls), result.actions); - fromJson(jo.value("event"_ls), result.event); - fromJson(jo.value("profile_tag"_ls), result.profileTag); - fromJson(jo.value("read"_ls), result.read); - fromJson(jo.value("room_id"_ls), result.roomId); - fromJson(jo.value("ts"_ls), result.ts); - } - }; -} // namespace QMatrixClient +using namespace Quotient; -class GetNotificationsJob::Private -{ - public: - QString nextToken; - std::vector<Notification> notifications; -}; - -BaseJob::Query queryToGetNotifications(const QString& from, Omittable<int> limit, const QString& only) +auto queryToGetNotifications(const QString& from, Omittable<int> limit, + const QString& only) { BaseJob::Query _q; addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); @@ -46,43 +18,22 @@ BaseJob::Query queryToGetNotifications(const QString& from, Omittable<int> limit return _q; } -QUrl GetNotificationsJob::makeRequestUrl(QUrl baseUrl, const QString& from, Omittable<int> limit, const QString& only) +QUrl GetNotificationsJob::makeRequestUrl(QUrl baseUrl, const QString& from, + Omittable<int> limit, + const QString& only) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/notifications", - queryToGetNotifications(from, limit, only)); + QStringLiteral("/_matrix/client/r0") + % "/notifications", + queryToGetNotifications(from, limit, only)); } -static const auto GetNotificationsJobName = QStringLiteral("GetNotificationsJob"); - -GetNotificationsJob::GetNotificationsJob(const QString& from, Omittable<int> limit, const QString& only) - : BaseJob(HttpVerb::Get, GetNotificationsJobName, - basePath % "/notifications", - queryToGetNotifications(from, limit, only)) - , d(new Private) +GetNotificationsJob::GetNotificationsJob(const QString& from, + Omittable<int> limit, + const QString& only) + : BaseJob(HttpVerb::Get, QStringLiteral("GetNotificationsJob"), + QStringLiteral("/_matrix/client/r0") % "/notifications", + queryToGetNotifications(from, limit, only)) { + addExpectedKey("notifications"); } - -GetNotificationsJob::~GetNotificationsJob() = default; - -const QString& GetNotificationsJob::nextToken() const -{ - return d->nextToken; -} - -std::vector<GetNotificationsJob::Notification>&& GetNotificationsJob::notifications() -{ - return std::move(d->notifications); -} - -BaseJob::Status GetNotificationsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("next_token"_ls), d->nextToken); - if (!json.contains("notifications"_ls)) - return { JsonParseError, - "The key 'notifications' not found in the response" }; - fromJson(json.value("notifications"_ls), d->notifications); - return Success; -} - diff --git a/lib/csapi/notifications.h b/lib/csapi/notifications.h index 898b5154..ff499c7a 100644 --- a/lib/csapi/notifications.h +++ b/lib/csapi/notifications.h @@ -4,86 +4,94 @@ #pragma once +#include "events/eventloader.h" #include "jobs/basejob.h" -#include "events/eventloader.h" -#include "converters.h" -#include <QtCore/QVector> -#include <QtCore/QVariant> -#include <QtCore/QJsonObject> +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Gets a list of events that the user has been notified about + * + * This API is used to paginate through the list of events that the + * user has been, or would have been notified about. + */ +class GetNotificationsJob : public BaseJob { +public: + // Inner data structures - /// Gets a list of events that the user has been notified about - /// /// This API is used to paginate through the list of events that the /// user has been, or would have been notified about. - class GetNotificationsJob : public BaseJob - { - public: - // Inner data structures - - /// This API is used to paginate through the list of events that the - /// user has been, or would have been notified about. - struct Notification - { - /// The action(s) to perform when the conditions for this rule are met. - /// See `Push Rules: API`_. - QVector<QVariant> actions; - /// The Event object for the event that triggered the notification. - EventPtr event; - /// The profile tag of the rule that matched this event. - QString profileTag; - /// Indicates whether the user has sent a read receipt indicating - /// that they have read this message. - bool read; - /// The ID of the room in which the event was posted. - QString roomId; - /// The unix timestamp at which the event notification was sent, - /// in milliseconds. - int ts; - }; + struct Notification { + /// The action(s) to perform when the conditions for this rule are met. + /// See `Push Rules: API`_. + QVector<QVariant> actions; + /// The Event object for the event that triggered the notification. + EventPtr event; + /// The profile tag of the rule that matched this event. + QString profileTag; + /// Indicates whether the user has sent a read receipt indicating + /// that they have read this message. + bool read; + /// The ID of the room in which the event was posted. + QString roomId; + /// The unix timestamp at which the event notification was sent, + /// in milliseconds. + int ts; + }; - // Construction/destruction + // Construction/destruction - /*! Gets a list of events that the user has been notified about - * \param from - * Pagination token given to retrieve the next set of events. - * \param limit - * Limit on the number of events to return in this request. - * \param only - * Allows basic filtering of events returned. Supply ``highlight`` - * to return only events where the notification had the highlight - * tweak set. - */ - explicit GetNotificationsJob(const QString& from = {}, Omittable<int> limit = none, const QString& only = {}); + /*! \brief Gets a list of events that the user has been notified about + * + * \param from + * Pagination token given to retrieve the next set of events. + * + * \param limit + * Limit on the number of events to return in this request. + * + * \param only + * Allows basic filtering of events returned. Supply ``highlight`` + * to return only events where the notification had the highlight + * tweak set. + */ + explicit GetNotificationsJob(const QString& from = {}, + Omittable<int> limit = none, + const QString& only = {}); - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetNotificationsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& from = {}, Omittable<int> limit = none, const QString& only = {}); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetNotificationsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& from = {}, + Omittable<int> limit = none, + const QString& only = {}); - ~GetNotificationsJob() override; + // Result properties - // Result properties + /// The token to supply in the ``from`` param of the next + /// ``/notifications`` request in order to request more + /// events. If this is absent, there are no more results. + QString nextToken() const { return loadFromJson<QString>("next_token"_ls); } - /// The token to supply in the ``from`` param of the next - /// ``/notifications`` request in order to request more - /// events. If this is absent, there are no more results. - const QString& nextToken() const; - /// The list of events that triggered notifications. - std::vector<Notification>&& notifications(); + /// The list of events that triggered notifications. + std::vector<Notification> notifications() + { + return takeFromJson<std::vector<Notification>>("notifications"_ls); + } +}; - protected: - Status parseJson(const QJsonDocument& data) override; +template <> +struct JsonObjectConverter<GetNotificationsJob::Notification> { + static void fillFrom(const QJsonObject& jo, + GetNotificationsJob::Notification& result) + { + fromJson(jo.value("actions"_ls), result.actions); + fromJson(jo.value("event"_ls), result.event); + fromJson(jo.value("profile_tag"_ls), result.profileTag); + fromJson(jo.value("read"_ls), result.read); + fromJson(jo.value("room_id"_ls), result.roomId); + fromJson(jo.value("ts"_ls), result.ts); + } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/openid.cpp b/lib/csapi/openid.cpp index b27fe0b8..3941e9c0 100644 --- a/lib/csapi/openid.cpp +++ b/lib/csapi/openid.cpp @@ -4,74 +4,15 @@ #include "openid.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class RequestOpenIdTokenJob::Private -{ - public: - QString accessToken; - QString tokenType; - QString matrixServerName; - int expiresIn; -}; - -static const auto RequestOpenIdTokenJobName = QStringLiteral("RequestOpenIdTokenJob"); +using namespace Quotient; -RequestOpenIdTokenJob::RequestOpenIdTokenJob(const QString& userId, const QJsonObject& body) - : BaseJob(HttpVerb::Post, RequestOpenIdTokenJobName, - basePath % "/user/" % userId % "/openid/request_token") - , d(new Private) +RequestOpenIdTokenJob::RequestOpenIdTokenJob(const QString& userId, + const QJsonObject& body) + : BaseJob(HttpVerb::Post, QStringLiteral("RequestOpenIdTokenJob"), + QStringLiteral("/_matrix/client/r0") % "/user/" % userId + % "/openid/request_token") { setRequestData(Data(toJson(body))); } - -RequestOpenIdTokenJob::~RequestOpenIdTokenJob() = default; - -const QString& RequestOpenIdTokenJob::accessToken() const -{ - return d->accessToken; -} - -const QString& RequestOpenIdTokenJob::tokenType() const -{ - return d->tokenType; -} - -const QString& RequestOpenIdTokenJob::matrixServerName() const -{ - return d->matrixServerName; -} - -int RequestOpenIdTokenJob::expiresIn() const -{ - return d->expiresIn; -} - -BaseJob::Status RequestOpenIdTokenJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("access_token"_ls)) - return { JsonParseError, - "The key 'access_token' not found in the response" }; - fromJson(json.value("access_token"_ls), d->accessToken); - if (!json.contains("token_type"_ls)) - return { JsonParseError, - "The key 'token_type' not found in the response" }; - fromJson(json.value("token_type"_ls), d->tokenType); - if (!json.contains("matrix_server_name"_ls)) - return { JsonParseError, - "The key 'matrix_server_name' not found in the response" }; - fromJson(json.value("matrix_server_name"_ls), d->matrixServerName); - if (!json.contains("expires_in"_ls)) - return { JsonParseError, - "The key 'expires_in' not found in the response" }; - fromJson(json.value("expires_in"_ls), d->expiresIn); - return Success; -} - diff --git a/lib/csapi/openid.h b/lib/csapi/openid.h index 807801fb..efb5f623 100644 --- a/lib/csapi/openid.h +++ b/lib/csapi/openid.h @@ -4,58 +4,45 @@ #pragma once +#include "csapi/definitions/openid_token.h" + #include "jobs/basejob.h" -#include "converters.h" -#include <QtCore/QJsonObject> - -namespace QMatrixClient -{ - // Operations - - /// Get an OpenID token object to verify the requester's identity. - /// - /// Gets an OpenID token object that the requester may supply to another - /// service to verify their identity in Matrix. The generated token is only - /// valid for exchanging for user information from the federation API for - /// OpenID. - /// - /// The access token generated is only valid for the OpenID API. It cannot - /// be used to request another OpenID access token or call ``/sync``, for - /// example. - class RequestOpenIdTokenJob : public BaseJob - { - public: - /*! Get an OpenID token object to verify the requester's identity. - * \param userId - * The user to request and OpenID token for. Should be the user who - * is authenticated for the request. - * \param body - * An empty object. Reserved for future expansion. - */ - explicit RequestOpenIdTokenJob(const QString& userId, const QJsonObject& body = {}); - ~RequestOpenIdTokenJob() override; - - // Result properties - - /// An access token the consumer may use to verify the identity of - /// the person who generated the token. This is given to the federation - /// API ``GET /openid/userinfo``. - const QString& accessToken() const; - /// The string ``Bearer``. - const QString& tokenType() const; - /// The homeserver domain the consumer should use when attempting to - /// verify the user's identity. - const QString& matrixServerName() const; - /// The number of seconds before this token expires and a new one must - /// be generated. - int expiresIn() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +namespace Quotient { + +/*! \brief Get an OpenID token object to verify the requester's identity. + * + * Gets an OpenID token object that the requester may supply to another + * service to verify their identity in Matrix. The generated token is only + * valid for exchanging for user information from the federation API for + * OpenID. + * + * The access token generated is only valid for the OpenID API. It cannot + * be used to request another OpenID access token or call ``/sync``, for + * example. + */ +class RequestOpenIdTokenJob : public BaseJob { +public: + /*! \brief Get an OpenID token object to verify the requester's identity. + * + * \param userId + * The user to request and OpenID token for. Should be the user who + * is authenticated for the request. + * + * \param body + * An empty object. Reserved for future expansion. + */ + explicit RequestOpenIdTokenJob(const QString& userId, + const QJsonObject& body = {}); + + // Result properties + + /// OpenID token information. This response is nearly compatible with the + /// response documented in the `OpenID Connect 1.0 Specification + /// <http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse>`_ + /// with the only difference being the lack of an ``id_token``. Instead, + /// the Matrix homeserver's name is provided. + OpenidToken tokenData() const { return fromJson<OpenidToken>(jsonData()); } +}; + +} // namespace Quotient diff --git a/lib/csapi/peeking_events.cpp b/lib/csapi/peeking_events.cpp index 3208d48d..70a5b6f3 100644 --- a/lib/csapi/peeking_events.cpp +++ b/lib/csapi/peeking_events.cpp @@ -4,23 +4,12 @@ #include "peeking_events.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class PeekEventsJob::Private -{ - public: - QString begin; - QString end; - RoomEvents chunk; -}; +using namespace Quotient; -BaseJob::Query queryToPeekEvents(const QString& from, Omittable<int> timeout, const QString& roomId) +auto queryToPeekEvents(const QString& from, Omittable<int> timeout, + const QString& roomId) { BaseJob::Query _q; addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); @@ -29,46 +18,18 @@ BaseJob::Query queryToPeekEvents(const QString& from, Omittable<int> timeout, co return _q; } -QUrl PeekEventsJob::makeRequestUrl(QUrl baseUrl, const QString& from, Omittable<int> timeout, const QString& roomId) +QUrl PeekEventsJob::makeRequestUrl(QUrl baseUrl, const QString& from, + Omittable<int> timeout, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/events", - queryToPeekEvents(from, timeout, roomId)); -} - -static const auto PeekEventsJobName = QStringLiteral("PeekEventsJob"); - -PeekEventsJob::PeekEventsJob(const QString& from, Omittable<int> timeout, const QString& roomId) - : BaseJob(HttpVerb::Get, PeekEventsJobName, - basePath % "/events", - queryToPeekEvents(from, timeout, roomId)) - , d(new Private) -{ -} - -PeekEventsJob::~PeekEventsJob() = default; - -const QString& PeekEventsJob::begin() const -{ - return d->begin; -} - -const QString& PeekEventsJob::end() const -{ - return d->end; -} - -RoomEvents&& PeekEventsJob::chunk() -{ - return std::move(d->chunk); -} - -BaseJob::Status PeekEventsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("start"_ls), d->begin); - fromJson(json.value("end"_ls), d->end); - fromJson(json.value("chunk"_ls), d->chunk); - return Success; + QStringLiteral("/_matrix/client/r0") + % "/events", + queryToPeekEvents(from, timeout, roomId)); } +PeekEventsJob::PeekEventsJob(const QString& from, Omittable<int> timeout, + const QString& roomId) + : BaseJob(HttpVerb::Get, QStringLiteral("PeekEventsJob"), + QStringLiteral("/_matrix/client/r0") % "/events", + queryToPeekEvents(from, timeout, roomId)) +{} diff --git a/lib/csapi/peeking_events.h b/lib/csapi/peeking_events.h index 5a6e513c..cecd9f2d 100644 --- a/lib/csapi/peeking_events.h +++ b/lib/csapi/peeking_events.h @@ -4,67 +4,63 @@ #pragma once -#include "jobs/basejob.h" - #include "events/eventloader.h" -#include "converters.h" +#include "jobs/basejob.h" -namespace QMatrixClient -{ - // Operations +namespace Quotient { - /// Listen on the event stream. - /// - /// This will listen for new events related to a particular room and return - /// them to the caller. This will block until an event is received, or until - /// the ``timeout`` is reached. - /// - /// This API is the same as the normal ``/events`` endpoint, but can be - /// called by users who have not joined the room. - /// - /// Note that the normal ``/events`` endpoint has been deprecated. This - /// API will also be deprecated at some point, but its replacement is not - /// yet known. - class PeekEventsJob : public BaseJob - { - public: - /*! Listen on the event stream. - * \param from - * The token to stream from. This token is either from a previous - * request to this API or from the initial sync API. - * \param timeout - * The maximum time in milliseconds to wait for an event. - * \param roomId - * The room ID for which events should be returned. - */ - explicit PeekEventsJob(const QString& from = {}, Omittable<int> timeout = none, const QString& roomId = {}); +/*! \brief Listen on the event stream. + * + * This will listen for new events related to a particular room and return + * them to the caller. This will block until an event is received, or until + * the ``timeout`` is reached. + * + * This API is the same as the normal ``/events`` endpoint, but can be + * called by users who have not joined the room. + * + * Note that the normal ``/events`` endpoint has been deprecated. This + * API will also be deprecated at some point, but its replacement is not + * yet known. + */ +class PeekEventsJob : public BaseJob { +public: + /*! \brief Listen on the event stream. + * + * \param from + * The token to stream from. This token is either from a previous + * request to this API or from the initial sync API. + * + * \param timeout + * The maximum time in milliseconds to wait for an event. + * + * \param roomId + * The room ID for which events should be returned. + */ + explicit PeekEventsJob(const QString& from = {}, + Omittable<int> timeout = none, + const QString& roomId = {}); - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * PeekEventsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& from = {}, Omittable<int> timeout = none, const QString& roomId = {}); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for PeekEventsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& from = {}, + Omittable<int> timeout = none, + const QString& roomId = {}); - ~PeekEventsJob() override; + // Result properties - // Result properties + /// A token which correlates to the first value in ``chunk``. This + /// is usually the same token supplied to ``from=``. + QString begin() const { return loadFromJson<QString>("start"_ls); } - /// A token which correlates to the first value in ``chunk``. This - /// is usually the same token supplied to ``from=``. - const QString& begin() const; - /// A token which correlates to the last value in ``chunk``. This - /// token should be used in the next request to ``/events``. - const QString& end() const; - /// An array of events. - RoomEvents&& chunk(); + /// A token which correlates to the last value in ``chunk``. This + /// token should be used in the next request to ``/events``. + QString end() const { return loadFromJson<QString>("end"_ls); } - protected: - Status parseJson(const QJsonDocument& data) override; + /// An array of events. + RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/preamble.mustache b/lib/csapi/preamble.mustache deleted file mode 100644 index 3ba87d61..00000000 --- a/lib/csapi/preamble.mustache +++ /dev/null @@ -1,3 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp index 024d7a34..58d0d157 100644 --- a/lib/csapi/presence.cpp +++ b/lib/csapi/presence.cpp @@ -4,82 +4,33 @@ #include "presence.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto SetPresenceJobName = QStringLiteral("SetPresenceJob"); +using namespace Quotient; -SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence, const QString& statusMsg) - : BaseJob(HttpVerb::Put, SetPresenceJobName, - basePath % "/presence/" % userId % "/status") +SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence, + const QString& statusMsg) + : BaseJob(HttpVerb::Put, QStringLiteral("SetPresenceJob"), + QStringLiteral("/_matrix/client/r0") % "/presence/" % userId + % "/status") { QJsonObject _data; addParam<>(_data, QStringLiteral("presence"), presence); addParam<IfNotEmpty>(_data, QStringLiteral("status_msg"), statusMsg); - setRequestData(_data); + setRequestData(std::move(_data)); } -class GetPresenceJob::Private -{ - public: - QString presence; - Omittable<int> lastActiveAgo; - QString statusMsg; - Omittable<bool> currentlyActive; -}; - QUrl GetPresenceJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/presence/" % userId % "/status"); + QStringLiteral("/_matrix/client/r0") + % "/presence/" % userId % "/status"); } -static const auto GetPresenceJobName = QStringLiteral("GetPresenceJob"); - GetPresenceJob::GetPresenceJob(const QString& userId) - : BaseJob(HttpVerb::Get, GetPresenceJobName, - basePath % "/presence/" % userId % "/status") - , d(new Private) + : BaseJob(HttpVerb::Get, QStringLiteral("GetPresenceJob"), + QStringLiteral("/_matrix/client/r0") % "/presence/" % userId + % "/status") { + addExpectedKey("presence"); } - -GetPresenceJob::~GetPresenceJob() = default; - -const QString& GetPresenceJob::presence() const -{ - return d->presence; -} - -Omittable<int> GetPresenceJob::lastActiveAgo() const -{ - return d->lastActiveAgo; -} - -const QString& GetPresenceJob::statusMsg() const -{ - return d->statusMsg; -} - -Omittable<bool> GetPresenceJob::currentlyActive() const -{ - return d->currentlyActive; -} - -BaseJob::Status GetPresenceJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("presence"_ls)) - return { JsonParseError, - "The key 'presence' not found in the response" }; - fromJson(json.value("presence"_ls), d->presence); - fromJson(json.value("last_active_ago"_ls), d->lastActiveAgo); - fromJson(json.value("status_msg"_ls), d->statusMsg); - fromJson(json.value("currently_active"_ls), d->currentlyActive); - return Success; -} - diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h index 5e132d24..a885bf4f 100644 --- a/lib/csapi/presence.h +++ b/lib/csapi/presence.h @@ -6,71 +6,72 @@ #include "jobs/basejob.h" -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Update this user's presence state. + * + * This API sets the given user's presence state. When setting the status, + * the activity time is updated to reflect that activity; the client does + * not need to specify the ``last_active_ago`` field. You cannot set the + * presence state of another user. + */ +class SetPresenceJob : public BaseJob { +public: + /*! \brief Update this user's presence state. + * + * \param userId + * The user whose presence state to update. + * + * \param presence + * The new presence state. + * + * \param statusMsg + * The status message to attach to this state. + */ + explicit SetPresenceJob(const QString& userId, const QString& presence, + const QString& statusMsg = {}); +}; - /// Update this user's presence state. - /// - /// This API sets the given user's presence state. When setting the status, - /// the activity time is updated to reflect that activity; the client does - /// not need to specify the ``last_active_ago`` field. You cannot set the - /// presence state of another user. - class SetPresenceJob : public BaseJob - { - public: - /*! Update this user's presence state. - * \param userId - * The user whose presence state to update. - * \param presence - * The new presence state. - * \param statusMsg - * The status message to attach to this state. - */ - explicit SetPresenceJob(const QString& userId, const QString& presence, const QString& statusMsg = {}); - }; +/*! \brief Get this user's presence state. + * + * Get the given user's presence state. + */ +class GetPresenceJob : public BaseJob { +public: + /*! \brief Get this user's presence state. + * + * \param userId + * The user whose presence state to get. + */ + explicit GetPresenceJob(const QString& userId); - /// Get this user's presence state. - /// - /// Get the given user's presence state. - class GetPresenceJob : public BaseJob - { - public: - /*! Get this user's presence state. - * \param userId - * The user whose presence state to get. - */ - explicit GetPresenceJob(const QString& userId); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetPresenceJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetPresenceJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); + // Result properties - ~GetPresenceJob() override; + /// This user's presence. + QString presence() const { return loadFromJson<QString>("presence"_ls); } - // Result properties + /// The length of time in milliseconds since an action was performed + /// by this user. + Omittable<int> lastActiveAgo() const + { + return loadFromJson<Omittable<int>>("last_active_ago"_ls); + } - /// This user's presence. - const QString& presence() const; - /// The length of time in milliseconds since an action was performed - /// by this user. - Omittable<int> lastActiveAgo() const; - /// The state message for this user if one was set. - const QString& statusMsg() const; - /// Whether the user is currently active - Omittable<bool> currentlyActive() const; + /// The state message for this user if one was set. + QString statusMsg() const { return loadFromJson<QString>("status_msg"_ls); } - protected: - Status parseJson(const QJsonDocument& data) override; + /// Whether the user is currently active + Omittable<bool> currentlyActive() const + { + return loadFromJson<Omittable<bool>>("currently_active"_ls); + } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp index 4ed3ad9b..8436b8e6 100644 --- a/lib/csapi/profile.cpp +++ b/lib/csapi/profile.cpp @@ -4,145 +4,67 @@ #include "profile.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -static const auto SetDisplayNameJobName = QStringLiteral("SetDisplayNameJob"); +using namespace Quotient; -SetDisplayNameJob::SetDisplayNameJob(const QString& userId, const QString& displayname) - : BaseJob(HttpVerb::Put, SetDisplayNameJobName, - basePath % "/profile/" % userId % "/displayname") +SetDisplayNameJob::SetDisplayNameJob(const QString& userId, + const QString& displayname) + : BaseJob(HttpVerb::Put, QStringLiteral("SetDisplayNameJob"), + QStringLiteral("/_matrix/client/r0") % "/profile/" % userId + % "/displayname") { QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("displayname"), displayname); - setRequestData(_data); + addParam<>(_data, QStringLiteral("displayname"), displayname); + setRequestData(std::move(_data)); } -class GetDisplayNameJob::Private -{ - public: - QString displayname; -}; - QUrl GetDisplayNameJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/profile/" % userId % "/displayname"); + QStringLiteral("/_matrix/client/r0") + % "/profile/" % userId % "/displayname"); } -static const auto GetDisplayNameJobName = QStringLiteral("GetDisplayNameJob"); - GetDisplayNameJob::GetDisplayNameJob(const QString& userId) - : BaseJob(HttpVerb::Get, GetDisplayNameJobName, - basePath % "/profile/" % userId % "/displayname", false) - , d(new Private) -{ -} - -GetDisplayNameJob::~GetDisplayNameJob() = default; - -const QString& GetDisplayNameJob::displayname() const -{ - return d->displayname; -} - -BaseJob::Status GetDisplayNameJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("displayname"_ls), d->displayname); - return Success; -} - -static const auto SetAvatarUrlJobName = QStringLiteral("SetAvatarUrlJob"); + : BaseJob(HttpVerb::Get, QStringLiteral("GetDisplayNameJob"), + QStringLiteral("/_matrix/client/r0") % "/profile/" % userId + % "/displayname", + false) +{} SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QString& avatarUrl) - : BaseJob(HttpVerb::Put, SetAvatarUrlJobName, - basePath % "/profile/" % userId % "/avatar_url") + : BaseJob(HttpVerb::Put, QStringLiteral("SetAvatarUrlJob"), + QStringLiteral("/_matrix/client/r0") % "/profile/" % userId + % "/avatar_url") { QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("avatar_url"), avatarUrl); - setRequestData(_data); + addParam<>(_data, QStringLiteral("avatar_url"), avatarUrl); + setRequestData(std::move(_data)); } -class GetAvatarUrlJob::Private -{ - public: - QString avatarUrl; -}; - QUrl GetAvatarUrlJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/profile/" % userId % "/avatar_url"); + QStringLiteral("/_matrix/client/r0") + % "/profile/" % userId % "/avatar_url"); } -static const auto GetAvatarUrlJobName = QStringLiteral("GetAvatarUrlJob"); - GetAvatarUrlJob::GetAvatarUrlJob(const QString& userId) - : BaseJob(HttpVerb::Get, GetAvatarUrlJobName, - basePath % "/profile/" % userId % "/avatar_url", false) - , d(new Private) -{ -} - -GetAvatarUrlJob::~GetAvatarUrlJob() = default; - -const QString& GetAvatarUrlJob::avatarUrl() const -{ - return d->avatarUrl; -} - -BaseJob::Status GetAvatarUrlJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("avatar_url"_ls), d->avatarUrl); - return Success; -} - -class GetUserProfileJob::Private -{ - public: - QString avatarUrl; - QString displayname; -}; + : BaseJob(HttpVerb::Get, QStringLiteral("GetAvatarUrlJob"), + QStringLiteral("/_matrix/client/r0") % "/profile/" % userId + % "/avatar_url", + false) +{} QUrl GetUserProfileJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/profile/" % userId); + QStringLiteral("/_matrix/client/r0") + % "/profile/" % userId); } -static const auto GetUserProfileJobName = QStringLiteral("GetUserProfileJob"); - GetUserProfileJob::GetUserProfileJob(const QString& userId) - : BaseJob(HttpVerb::Get, GetUserProfileJobName, - basePath % "/profile/" % userId, false) - , d(new Private) -{ -} - -GetUserProfileJob::~GetUserProfileJob() = default; - -const QString& GetUserProfileJob::avatarUrl() const -{ - return d->avatarUrl; -} - -const QString& GetUserProfileJob::displayname() const -{ - return d->displayname; -} - -BaseJob::Status GetUserProfileJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("avatar_url"_ls), d->avatarUrl); - fromJson(json.value("displayname"_ls), d->displayname); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("GetUserProfileJob"), + QStringLiteral("/_matrix/client/r0") % "/profile/" % userId, false) +{} diff --git a/lib/csapi/profile.h b/lib/csapi/profile.h index 23094aff..3858fab2 100644 --- a/lib/csapi/profile.h +++ b/lib/csapi/profile.h @@ -6,154 +6,137 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations - - /// Set the user's display name. - /// - /// This API sets the given user's display name. You must have permission to - /// set this user's display name, e.g. you need to have their ``access_token``. - class SetDisplayNameJob : public BaseJob - { - public: - /*! Set the user's display name. - * \param userId - * The user whose display name to set. - * \param displayname - * The new display name for this user. - */ - explicit SetDisplayNameJob(const QString& userId, const QString& displayname = {}); - }; - - /// Get the user's display name. - /// - /// Get the user's display name. This API may be used to fetch the user's - /// own displayname or to query the name of other users; either locally or - /// on remote homeservers. - class GetDisplayNameJob : public BaseJob - { - public: - /*! Get the user's display name. - * \param userId - * The user whose display name to get. - */ - explicit GetDisplayNameJob(const QString& userId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetDisplayNameJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - - ~GetDisplayNameJob() override; - - // Result properties - - /// The user's display name if they have set one, otherwise not present. - const QString& displayname() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Set the user's avatar URL. - /// - /// This API sets the given user's avatar URL. You must have permission to - /// set this user's avatar URL, e.g. you need to have their ``access_token``. - class SetAvatarUrlJob : public BaseJob - { - public: - /*! Set the user's avatar URL. - * \param userId - * The user whose avatar URL to set. - * \param avatarUrl - * The new avatar URL for this user. - */ - explicit SetAvatarUrlJob(const QString& userId, const QString& avatarUrl = {}); - }; - - /// Get the user's avatar URL. - /// - /// Get the user's avatar URL. This API may be used to fetch the user's - /// own avatar URL or to query the URL of other users; either locally or - /// on remote homeservers. - class GetAvatarUrlJob : public BaseJob +/*! \brief Set the user's display name. + * + * This API sets the given user's display name. You must have permission to + * set this user's display name, e.g. you need to have their ``access_token``. + */ +class SetDisplayNameJob : public BaseJob { +public: + /*! \brief Set the user's display name. + * + * \param userId + * The user whose display name to set. + * + * \param displayname + * The new display name for this user. + */ + explicit SetDisplayNameJob(const QString& userId, + const QString& displayname); +}; + +/*! \brief Get the user's display name. + * + * Get the user's display name. This API may be used to fetch the user's + * own displayname or to query the name of other users; either locally or + * on remote homeservers. + */ +class GetDisplayNameJob : public BaseJob { +public: + /*! \brief Get the user's display name. + * + * \param userId + * The user whose display name to get. + */ + explicit GetDisplayNameJob(const QString& userId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetDisplayNameJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); + + // Result properties + + /// The user's display name if they have set one, otherwise not present. + QString displayname() const { - public: - /*! Get the user's avatar URL. - * \param userId - * The user whose avatar URL to get. - */ - explicit GetAvatarUrlJob(const QString& userId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetAvatarUrlJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - - ~GetAvatarUrlJob() override; - - // Result properties - - /// The user's avatar URL if they have set one, otherwise not present. - const QString& avatarUrl() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Get this user's profile information. - /// - /// Get the combined profile information for this user. This API may be used - /// to fetch the user's own profile information or other users; either - /// locally or on remote homeservers. This API may return keys which are not - /// limited to ``displayname`` or ``avatar_url``. - class GetUserProfileJob : public BaseJob + return loadFromJson<QString>("displayname"_ls); + } +}; + +/*! \brief Set the user's avatar URL. + * + * This API sets the given user's avatar URL. You must have permission to + * set this user's avatar URL, e.g. you need to have their ``access_token``. + */ +class SetAvatarUrlJob : public BaseJob { +public: + /*! \brief Set the user's avatar URL. + * + * \param userId + * The user whose avatar URL to set. + * + * \param avatarUrl + * The new avatar URL for this user. + */ + explicit SetAvatarUrlJob(const QString& userId, const QString& avatarUrl); +}; + +/*! \brief Get the user's avatar URL. + * + * Get the user's avatar URL. This API may be used to fetch the user's + * own avatar URL or to query the URL of other users; either locally or + * on remote homeservers. + */ +class GetAvatarUrlJob : public BaseJob { +public: + /*! \brief Get the user's avatar URL. + * + * \param userId + * The user whose avatar URL to get. + */ + explicit GetAvatarUrlJob(const QString& userId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetAvatarUrlJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); + + // Result properties + + /// The user's avatar URL if they have set one, otherwise not present. + QString avatarUrl() const { return loadFromJson<QString>("avatar_url"_ls); } +}; + +/*! \brief Get this user's profile information. + * + * Get the combined profile information for this user. This API may be used + * to fetch the user's own profile information or other users; either + * locally or on remote homeservers. This API may return keys which are not + * limited to ``displayname`` or ``avatar_url``. + */ +class GetUserProfileJob : public BaseJob { +public: + /*! \brief Get this user's profile information. + * + * \param userId + * The user whose profile information to get. + */ + explicit GetUserProfileJob(const QString& userId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetUserProfileJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); + + // Result properties + + /// The user's avatar URL if they have set one, otherwise not present. + QString avatarUrl() const { return loadFromJson<QString>("avatar_url"_ls); } + + /// The user's display name if they have set one, otherwise not present. + QString displayname() const { - public: - /*! Get this user's profile information. - * \param userId - * The user whose profile information to get. - */ - explicit GetUserProfileJob(const QString& userId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetUserProfileJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - - ~GetUserProfileJob() override; - - // Result properties - - /// The user's avatar URL if they have set one, otherwise not present. - const QString& avatarUrl() const; - /// The user's display name if they have set one, otherwise not present. - const QString& displayname() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + return loadFromJson<QString>("displayname"_ls); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/pusher.cpp b/lib/csapi/pusher.cpp index 664959f4..028022c5 100644 --- a/lib/csapi/pusher.cpp +++ b/lib/csapi/pusher.cpp @@ -4,97 +4,29 @@ #include "pusher.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<GetPushersJob::PusherData> - { - static void fillFrom(const QJsonObject& jo, GetPushersJob::PusherData& result) - { - fromJson(jo.value("url"_ls), result.url); - fromJson(jo.value("format"_ls), result.format); - } - }; - - template <> struct JsonObjectConverter<GetPushersJob::Pusher> - { - static void fillFrom(const QJsonObject& jo, GetPushersJob::Pusher& result) - { - fromJson(jo.value("pushkey"_ls), result.pushkey); - fromJson(jo.value("kind"_ls), result.kind); - fromJson(jo.value("app_id"_ls), result.appId); - fromJson(jo.value("app_display_name"_ls), result.appDisplayName); - fromJson(jo.value("device_display_name"_ls), result.deviceDisplayName); - fromJson(jo.value("profile_tag"_ls), result.profileTag); - fromJson(jo.value("lang"_ls), result.lang); - fromJson(jo.value("data"_ls), result.data); - } - }; -} // namespace QMatrixClient - -class GetPushersJob::Private -{ - public: - QVector<Pusher> pushers; -}; +using namespace Quotient; QUrl GetPushersJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/pushers"); + QStringLiteral("/_matrix/client/r0") + % "/pushers"); } -static const auto GetPushersJobName = QStringLiteral("GetPushersJob"); - GetPushersJob::GetPushersJob() - : BaseJob(HttpVerb::Get, GetPushersJobName, - basePath % "/pushers") - , d(new Private) -{ -} - -GetPushersJob::~GetPushersJob() = default; - -const QVector<GetPushersJob::Pusher>& GetPushersJob::pushers() const -{ - return d->pushers; -} - -BaseJob::Status GetPushersJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("pushers"_ls), d->pushers); - return Success; -} - -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<PostPusherJob::PusherData> - { - static void dumpTo(QJsonObject& jo, const PostPusherJob::PusherData& pod) - { - addParam<IfNotEmpty>(jo, QStringLiteral("url"), pod.url); - addParam<IfNotEmpty>(jo, QStringLiteral("format"), pod.format); - } - }; -} // namespace QMatrixClient - -static const auto PostPusherJobName = QStringLiteral("PostPusherJob"); - -PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag, Omittable<bool> append) - : BaseJob(HttpVerb::Post, PostPusherJobName, - basePath % "/pushers/set") + : BaseJob(HttpVerb::Get, QStringLiteral("GetPushersJob"), + QStringLiteral("/_matrix/client/r0") % "/pushers") +{} + +PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, + const QString& appId, const QString& appDisplayName, + const QString& deviceDisplayName, + const QString& lang, const PusherData& data, + const QString& profileTag, Omittable<bool> append) + : BaseJob(HttpVerb::Post, QStringLiteral("PostPusherJob"), + QStringLiteral("/_matrix/client/r0") % "/pushers/set") { QJsonObject _data; addParam<>(_data, QStringLiteral("pushkey"), pushkey); @@ -106,6 +38,5 @@ PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, const addParam<>(_data, QStringLiteral("lang"), lang); addParam<>(_data, QStringLiteral("data"), data); addParam<IfNotEmpty>(_data, QStringLiteral("append"), append); - setRequestData(_data); + setRequestData(std::move(_data)); } - diff --git a/lib/csapi/pusher.h b/lib/csapi/pusher.h index da3303fe..ae0050d2 100644 --- a/lib/csapi/pusher.h +++ b/lib/csapi/pusher.h @@ -6,164 +6,199 @@ #include "jobs/basejob.h" -#include <QtCore/QVector> -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Gets the current pushers for the authenticated user + * + * Gets all currently active pushers for the authenticated user. + */ +class GetPushersJob : public BaseJob { +public: + // Inner data structures + + /// A dictionary of information for the pusher implementation + /// itself. + struct PusherData { + /// Required if ``kind`` is ``http``. The URL to use to send + /// notifications to. + QString url; + /// The format to use when sending notifications to the Push + /// Gateway. + QString format; + }; - /// Gets the current pushers for the authenticated user - /// /// Gets all currently active pushers for the authenticated user. - class GetPushersJob : public BaseJob - { - public: - // Inner data structures - - /// A dictionary of information for the pusher implementation - /// itself. - struct PusherData - { - /// Required if ``kind`` is ``http``. The URL to use to send - /// notifications to. - QString url; - /// The format to use when sending notifications to the Push - /// Gateway. - QString format; - }; - - /// Gets all currently active pushers for the authenticated user. - struct Pusher - { - /// This is a unique identifier for this pusher. See ``/set`` for - /// more detail. - /// Max length, 512 bytes. - QString pushkey; - /// The kind of pusher. ``"http"`` is a pusher that - /// sends HTTP pokes. - QString kind; - /// This is a reverse-DNS style identifier for the application. - /// Max length, 64 chars. - QString appId; - /// A string that will allow the user to identify what application - /// owns this pusher. - QString appDisplayName; - /// A string that will allow the user to identify what device owns - /// this pusher. - QString deviceDisplayName; - /// This string determines which set of device specific rules this - /// pusher executes. - QString profileTag; - /// The preferred language for receiving notifications (e.g. 'en' - /// or 'en-US') - QString lang; - /// A dictionary of information for the pusher implementation - /// itself. - PusherData data; - }; - - // Construction/destruction - - explicit GetPushersJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetPushersJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetPushersJob() override; - - // Result properties - - /// An array containing the current pushers for the user - const QVector<Pusher>& pushers() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; + struct Pusher { + /// This is a unique identifier for this pusher. See ``/set`` for + /// more detail. + /// Max length, 512 bytes. + QString pushkey; + /// The kind of pusher. ``"http"`` is a pusher that + /// sends HTTP pokes. + QString kind; + /// This is a reverse-DNS style identifier for the application. + /// Max length, 64 chars. + QString appId; + /// A string that will allow the user to identify what application + /// owns this pusher. + QString appDisplayName; + /// A string that will allow the user to identify what device owns + /// this pusher. + QString deviceDisplayName; + /// This string determines which set of device specific rules this + /// pusher executes. + QString profileTag; + /// The preferred language for receiving notifications (e.g. 'en' + /// or 'en-US') + QString lang; + /// A dictionary of information for the pusher implementation + /// itself. + PusherData data; }; - /// Modify a pusher for this user on the homeserver. - /// - /// This endpoint allows the creation, modification and deletion of `pushers`_ - /// for this user ID. The behaviour of this endpoint varies depending on the - /// values in the JSON body. - class PostPusherJob : public BaseJob + // Construction/destruction + + /// Gets the current pushers for the authenticated user + explicit GetPushersJob(); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetPushersJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + // Result properties + + /// An array containing the current pushers for the user + QVector<Pusher> pushers() const + { + return loadFromJson<QVector<Pusher>>("pushers"_ls); + } +}; + +template <> +struct JsonObjectConverter<GetPushersJob::PusherData> { + static void fillFrom(const QJsonObject& jo, + GetPushersJob::PusherData& result) { - public: - // Inner data structures - - /// A dictionary of information for the pusher implementation - /// itself. If ``kind`` is ``http``, this should contain ``url`` - /// which is the URL to use to send notifications to. - struct PusherData - { - /// Required if ``kind`` is ``http``. The URL to use to send - /// notifications to. MUST be an HTTPS URL with a path of - /// ``/_matrix/push/v1/notify``. - QString url; - /// The format to send notifications in to Push Gateways if the - /// ``kind`` is ``http``. The details about what fields the - /// homeserver should send to the push gateway are defined in the - /// `Push Gateway Specification`_. Currently the only format - /// available is 'event_id_only'. - QString format; - }; - - // Construction/destruction - - /*! Modify a pusher for this user on the homeserver. - * \param pushkey - * This is a unique identifier for this pusher. The value you - * should use for this is the routing or destination address - * information for the notification, for example, the APNS token - * for APNS or the Registration ID for GCM. If your notification - * client has no such concept, use any unique identifier. - * Max length, 512 bytes. - * - * If the ``kind`` is ``"email"``, this is the email address to - * send notifications to. - * \param kind - * The kind of pusher to configure. ``"http"`` makes a pusher that - * sends HTTP pokes. ``"email"`` makes a pusher that emails the - * user with unread notifications. ``null`` deletes the pusher. - * \param appId - * This is a reverse-DNS style identifier for the application. - * It is recommended that this end with the platform, such that - * different platform versions get different app identifiers. - * Max length, 64 chars. - * - * If the ``kind`` is ``"email"``, this is ``"m.email"``. - * \param appDisplayName - * A string that will allow the user to identify what application - * owns this pusher. - * \param deviceDisplayName - * A string that will allow the user to identify what device owns - * this pusher. - * \param lang - * The preferred language for receiving notifications (e.g. 'en' - * or 'en-US'). - * \param data - * A dictionary of information for the pusher implementation - * itself. If ``kind`` is ``http``, this should contain ``url`` - * which is the URL to use to send notifications to. - * \param profileTag - * This string determines which set of device specific rules this - * pusher executes. - * \param append - * If true, the homeserver should add another pusher with the - * given pushkey and App ID in addition to any others with - * different user IDs. Otherwise, the homeserver must remove any - * other pushers with the same App ID and pushkey for different - * users. The default is ``false``. - */ - explicit PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag = {}, Omittable<bool> append = none); + fromJson(jo.value("url"_ls), result.url); + fromJson(jo.value("format"_ls), result.format); + } +}; + +template <> +struct JsonObjectConverter<GetPushersJob::Pusher> { + static void fillFrom(const QJsonObject& jo, GetPushersJob::Pusher& result) + { + fromJson(jo.value("pushkey"_ls), result.pushkey); + fromJson(jo.value("kind"_ls), result.kind); + fromJson(jo.value("app_id"_ls), result.appId); + fromJson(jo.value("app_display_name"_ls), result.appDisplayName); + fromJson(jo.value("device_display_name"_ls), result.deviceDisplayName); + fromJson(jo.value("profile_tag"_ls), result.profileTag); + fromJson(jo.value("lang"_ls), result.lang); + fromJson(jo.value("data"_ls), result.data); + } +}; + +/*! \brief Modify a pusher for this user on the homeserver. + * + * This endpoint allows the creation, modification and deletion of `pushers`_ + * for this user ID. The behaviour of this endpoint varies depending on the + * values in the JSON body. + */ +class PostPusherJob : public BaseJob { +public: + // Inner data structures + + /// A dictionary of information for the pusher implementation + /// itself. If ``kind`` is ``http``, this should contain ``url`` + /// which is the URL to use to send notifications to. + struct PusherData { + /// Required if ``kind`` is ``http``. The URL to use to send + /// notifications to. MUST be an HTTPS URL with a path of + /// ``/_matrix/push/v1/notify``. + QString url; + /// The format to send notifications in to Push Gateways if the + /// ``kind`` is ``http``. The details about what fields the + /// homeserver should send to the push gateway are defined in the + /// `Push Gateway Specification`_. Currently the only format + /// available is 'event_id_only'. + QString format; }; -} // namespace QMatrixClient + + // Construction/destruction + + /*! \brief Modify a pusher for this user on the homeserver. + * + * \param pushkey + * This is a unique identifier for this pusher. The value you + * should use for this is the routing or destination address + * information for the notification, for example, the APNS token + * for APNS or the Registration ID for GCM. If your notification + * client has no such concept, use any unique identifier. + * Max length, 512 bytes. + * + * If the ``kind`` is ``"email"``, this is the email address to + * send notifications to. + * + * \param kind + * The kind of pusher to configure. ``"http"`` makes a pusher that + * sends HTTP pokes. ``"email"`` makes a pusher that emails the + * user with unread notifications. ``null`` deletes the pusher. + * + * \param appId + * This is a reverse-DNS style identifier for the application. + * It is recommended that this end with the platform, such that + * different platform versions get different app identifiers. + * Max length, 64 chars. + * + * If the ``kind`` is ``"email"``, this is ``"m.email"``. + * + * \param appDisplayName + * A string that will allow the user to identify what application + * owns this pusher. + * + * \param deviceDisplayName + * A string that will allow the user to identify what device owns + * this pusher. + * + * \param lang + * The preferred language for receiving notifications (e.g. 'en' + * or 'en-US'). + * + * \param data + * A dictionary of information for the pusher implementation + * itself. If ``kind`` is ``http``, this should contain ``url`` + * which is the URL to use to send notifications to. + * + * \param profileTag + * This string determines which set of device specific rules this + * pusher executes. + * + * \param append + * If true, the homeserver should add another pusher with the + * given pushkey and App ID in addition to any others with + * different user IDs. Otherwise, the homeserver must remove any + * other pushers with the same App ID and pushkey for different + * users. The default is ``false``. + */ + explicit PostPusherJob(const QString& pushkey, const QString& kind, + const QString& appId, const QString& appDisplayName, + const QString& deviceDisplayName, + const QString& lang, const PusherData& data, + const QString& profileTag = {}, + Omittable<bool> append = none); +}; + +template <> +struct JsonObjectConverter<PostPusherJob::PusherData> { + static void dumpTo(QJsonObject& jo, const PostPusherJob::PusherData& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("url"), pod.url); + addParam<IfNotEmpty>(jo, QStringLiteral("format"), pod.format); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/pushrules.cpp b/lib/csapi/pushrules.cpp index b91d18f7..86165744 100644 --- a/lib/csapi/pushrules.cpp +++ b/lib/csapi/pushrules.cpp @@ -4,101 +4,58 @@ #include "pushrules.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetPushRulesJob::Private -{ - public: - PushRuleset global; -}; +using namespace Quotient; QUrl GetPushRulesJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/pushrules"); + QStringLiteral("/_matrix/client/r0") + % "/pushrules"); } -static const auto GetPushRulesJobName = QStringLiteral("GetPushRulesJob"); - GetPushRulesJob::GetPushRulesJob() - : BaseJob(HttpVerb::Get, GetPushRulesJobName, - basePath % "/pushrules") - , d(new Private) + : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRulesJob"), + QStringLiteral("/_matrix/client/r0") % "/pushrules") { + addExpectedKey("global"); } -GetPushRulesJob::~GetPushRulesJob() = default; - -const PushRuleset& GetPushRulesJob::global() const -{ - return d->global; -} - -BaseJob::Status GetPushRulesJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("global"_ls)) - return { JsonParseError, - "The key 'global' not found in the response" }; - fromJson(json.value("global"_ls), d->global); - return Success; -} - -class GetPushRuleJob::Private -{ - public: - PushRule data; -}; - -QUrl GetPushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId) +QUrl GetPushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, + const QString& kind, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId); + QStringLiteral("/_matrix/client/r0") + % "/pushrules/" % scope % "/" % kind + % "/" % ruleId); } -static const auto GetPushRuleJobName = QStringLiteral("GetPushRuleJob"); +GetPushRuleJob::GetPushRuleJob(const QString& scope, const QString& kind, + const QString& ruleId) + : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleJob"), + QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" + % kind % "/" % ruleId) +{} -GetPushRuleJob::GetPushRuleJob(const QString& scope, const QString& kind, const QString& ruleId) - : BaseJob(HttpVerb::Get, GetPushRuleJobName, - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId) - , d(new Private) -{ -} - -GetPushRuleJob::~GetPushRuleJob() = default; - -const PushRule& GetPushRuleJob::data() const -{ - return d->data; -} - -BaseJob::Status GetPushRuleJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - -QUrl DeletePushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId) +QUrl DeletePushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, + const QString& kind, + const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId); + QStringLiteral("/_matrix/client/r0") + % "/pushrules/" % scope % "/" % kind + % "/" % ruleId); } -static const auto DeletePushRuleJobName = QStringLiteral("DeletePushRuleJob"); +DeletePushRuleJob::DeletePushRuleJob(const QString& scope, const QString& kind, + const QString& ruleId) + : BaseJob(HttpVerb::Delete, QStringLiteral("DeletePushRuleJob"), + QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" + % kind % "/" % ruleId) +{} -DeletePushRuleJob::DeletePushRuleJob(const QString& scope, const QString& kind, const QString& ruleId) - : BaseJob(HttpVerb::Delete, DeletePushRuleJobName, - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId) -{ -} - -BaseJob::Query queryToSetPushRule(const QString& before, const QString& after) +auto queryToSetPushRule(const QString& before, const QString& after) { BaseJob::Query _q; addParam<IfNotEmpty>(_q, QStringLiteral("before"), before); @@ -106,115 +63,85 @@ BaseJob::Query queryToSetPushRule(const QString& before, const QString& after) return _q; } -static const auto SetPushRuleJobName = QStringLiteral("SetPushRuleJob"); - -SetPushRuleJob::SetPushRuleJob(const QString& scope, const QString& kind, const QString& ruleId, const QStringList& actions, const QString& before, const QString& after, const QVector<PushCondition>& conditions, const QString& pattern) - : BaseJob(HttpVerb::Put, SetPushRuleJobName, - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId, - queryToSetPushRule(before, after)) +SetPushRuleJob::SetPushRuleJob(const QString& scope, const QString& kind, + const QString& ruleId, + const QVector<QVariant>& actions, + const QString& before, const QString& after, + const QVector<PushCondition>& conditions, + const QString& pattern) + : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleJob"), + QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" + % kind % "/" % ruleId, + queryToSetPushRule(before, after)) { QJsonObject _data; addParam<>(_data, QStringLiteral("actions"), actions); addParam<IfNotEmpty>(_data, QStringLiteral("conditions"), conditions); addParam<IfNotEmpty>(_data, QStringLiteral("pattern"), pattern); - setRequestData(_data); + setRequestData(std::move(_data)); } -class IsPushRuleEnabledJob::Private -{ - public: - bool enabled; -}; - -QUrl IsPushRuleEnabledJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId) +QUrl IsPushRuleEnabledJob::makeRequestUrl(QUrl baseUrl, const QString& scope, + const QString& kind, + const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/enabled"); + QStringLiteral("/_matrix/client/r0") + % "/pushrules/" % scope % "/" % kind + % "/" % ruleId % "/enabled"); } -static const auto IsPushRuleEnabledJobName = QStringLiteral("IsPushRuleEnabledJob"); - -IsPushRuleEnabledJob::IsPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId) - : BaseJob(HttpVerb::Get, IsPushRuleEnabledJobName, - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/enabled") - , d(new Private) +IsPushRuleEnabledJob::IsPushRuleEnabledJob(const QString& scope, + const QString& kind, + const QString& ruleId) + : BaseJob(HttpVerb::Get, QStringLiteral("IsPushRuleEnabledJob"), + QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" + % kind % "/" % ruleId % "/enabled") { + addExpectedKey("enabled"); } -IsPushRuleEnabledJob::~IsPushRuleEnabledJob() = default; - -bool IsPushRuleEnabledJob::enabled() const -{ - return d->enabled; -} - -BaseJob::Status IsPushRuleEnabledJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("enabled"_ls)) - return { JsonParseError, - "The key 'enabled' not found in the response" }; - fromJson(json.value("enabled"_ls), d->enabled); - return Success; -} - -static const auto SetPushRuleEnabledJobName = QStringLiteral("SetPushRuleEnabledJob"); - -SetPushRuleEnabledJob::SetPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId, bool enabled) - : BaseJob(HttpVerb::Put, SetPushRuleEnabledJobName, - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/enabled") +SetPushRuleEnabledJob::SetPushRuleEnabledJob(const QString& scope, + const QString& kind, + const QString& ruleId, bool enabled) + : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleEnabledJob"), + QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" + % kind % "/" % ruleId % "/enabled") { QJsonObject _data; addParam<>(_data, QStringLiteral("enabled"), enabled); - setRequestData(_data); + setRequestData(std::move(_data)); } -class GetPushRuleActionsJob::Private -{ - public: - QStringList actions; -}; - -QUrl GetPushRuleActionsJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId) +QUrl GetPushRuleActionsJob::makeRequestUrl(QUrl baseUrl, const QString& scope, + const QString& kind, + const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/actions"); -} - -static const auto GetPushRuleActionsJobName = QStringLiteral("GetPushRuleActionsJob"); - -GetPushRuleActionsJob::GetPushRuleActionsJob(const QString& scope, const QString& kind, const QString& ruleId) - : BaseJob(HttpVerb::Get, GetPushRuleActionsJobName, - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/actions") - , d(new Private) -{ + QStringLiteral("/_matrix/client/r0") + % "/pushrules/" % scope % "/" % kind + % "/" % ruleId % "/actions"); } -GetPushRuleActionsJob::~GetPushRuleActionsJob() = default; - -const QStringList& GetPushRuleActionsJob::actions() const -{ - return d->actions; -} - -BaseJob::Status GetPushRuleActionsJob::parseJson(const QJsonDocument& data) +GetPushRuleActionsJob::GetPushRuleActionsJob(const QString& scope, + const QString& kind, + const QString& ruleId) + : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleActionsJob"), + QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" + % kind % "/" % ruleId % "/actions") { - auto json = data.object(); - if (!json.contains("actions"_ls)) - return { JsonParseError, - "The key 'actions' not found in the response" }; - fromJson(json.value("actions"_ls), d->actions); - return Success; + addExpectedKey("actions"); } -static const auto SetPushRuleActionsJobName = QStringLiteral("SetPushRuleActionsJob"); - -SetPushRuleActionsJob::SetPushRuleActionsJob(const QString& scope, const QString& kind, const QString& ruleId, const QStringList& actions) - : BaseJob(HttpVerb::Put, SetPushRuleActionsJobName, - basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/actions") +SetPushRuleActionsJob::SetPushRuleActionsJob(const QString& scope, + const QString& kind, + const QString& ruleId, + const QVector<QVariant>& actions) + : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleActionsJob"), + QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" + % kind % "/" % ruleId % "/actions") { QJsonObject _data; addParam<>(_data, QStringLiteral("actions"), actions); - setRequestData(_data); + setRequestData(std::move(_data)); } - diff --git a/lib/csapi/pushrules.h b/lib/csapi/pushrules.h index c038401c..1c6d5c2d 100644 --- a/lib/csapi/pushrules.h +++ b/lib/csapi/pushrules.h @@ -4,270 +4,279 @@ #pragma once -#include "jobs/basejob.h" - -#include "csapi/definitions/push_ruleset.h" -#include "converters.h" -#include "csapi/definitions/push_rule.h" -#include <QtCore/QVector> #include "csapi/definitions/push_condition.h" +#include "csapi/definitions/push_rule.h" +#include "csapi/definitions/push_ruleset.h" -namespace QMatrixClient -{ - // Operations - - /// Retrieve all push rulesets. - /// - /// Retrieve all push rulesets for this user. Clients can "drill-down" on - /// the rulesets by suffixing a ``scope`` to this path e.g. - /// ``/pushrules/global/``. This will return a subset of this data under the - /// specified key e.g. the ``global`` key. - class GetPushRulesJob : public BaseJob - { - public: - explicit GetPushRulesJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetPushRulesJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetPushRulesJob() override; - - // Result properties - - /// The global ruleset. - const PushRuleset& global() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Retrieve a push rule. - /// - /// Retrieve a single specified push rule. - class GetPushRuleJob : public BaseJob - { - public: - /*! Retrieve a push rule. - * \param scope - * ``global`` to specify global rules. - * \param kind - * The kind of rule - * \param ruleId - * The identifier for the rule. - */ - explicit GetPushRuleJob(const QString& scope, const QString& kind, const QString& ruleId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetPushRuleJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId); - - ~GetPushRuleJob() override; - - // Result properties - - /// The push rule. - const PushRule& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Delete a push rule. - /// - /// This endpoint removes the push rule defined in the path. - class DeletePushRuleJob : public BaseJob - { - public: - /*! Delete a push rule. - * \param scope - * ``global`` to specify global rules. - * \param kind - * The kind of rule - * \param ruleId - * The identifier for the rule. - */ - explicit DeletePushRuleJob(const QString& scope, const QString& kind, const QString& ruleId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * DeletePushRuleJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId); - - }; - - /// Add or change a push rule. - /// - /// This endpoint allows the creation, modification and deletion of pushers - /// for this user ID. The behaviour of this endpoint varies depending on the - /// values in the JSON body. - /// - /// When creating push rules, they MUST be enabled by default. - class SetPushRuleJob : public BaseJob - { - public: - /*! Add or change a push rule. - * \param scope - * ``global`` to specify global rules. - * \param kind - * The kind of rule - * \param ruleId - * The identifier for the rule. - * \param actions - * The action(s) to perform when the conditions for this rule are met. - * \param before - * Use 'before' with a ``rule_id`` as its value to make the new rule the - * next-most important rule with respect to the given user defined rule. - * It is not possible to add a rule relative to a predefined server rule. - * \param after - * This makes the new rule the next-less important rule relative to the - * given user defined rule. It is not possible to add a rule relative - * to a predefined server rule. - * \param conditions - * The conditions that must hold true for an event in order for a - * rule to be applied to an event. A rule with no conditions - * always matches. Only applicable to ``underride`` and ``override`` rules. - * \param pattern - * Only applicable to ``content`` rules. The glob-style pattern to match against. - */ - explicit SetPushRuleJob(const QString& scope, const QString& kind, const QString& ruleId, const QStringList& actions, const QString& before = {}, const QString& after = {}, const QVector<PushCondition>& conditions = {}, const QString& pattern = {}); - }; - - /// Get whether a push rule is enabled - /// - /// This endpoint gets whether the specified push rule is enabled. - class IsPushRuleEnabledJob : public BaseJob - { - public: - /*! Get whether a push rule is enabled - * \param scope - * Either ``global`` or ``device/<profile_tag>`` to specify global - * rules or device rules for the given ``profile_tag``. - * \param kind - * The kind of rule - * \param ruleId - * The identifier for the rule. - */ - explicit IsPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * IsPushRuleEnabledJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId); - - ~IsPushRuleEnabledJob() override; - - // Result properties +#include "jobs/basejob.h" - /// Whether the push rule is enabled or not. - bool enabled() const; +namespace Quotient { - protected: - Status parseJson(const QJsonDocument& data) override; +/*! \brief Retrieve all push rulesets. + * + * Retrieve all push rulesets for this user. Clients can "drill-down" on + * the rulesets by suffixing a ``scope`` to this path e.g. + * ``/pushrules/global/``. This will return a subset of this data under the + * specified key e.g. the ``global`` key. + */ +class GetPushRulesJob : public BaseJob { +public: + /// Retrieve all push rulesets. + explicit GetPushRulesJob(); - private: - class Private; - QScopedPointer<Private> d; - }; + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetPushRulesJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); - /// Enable or disable a push rule. - /// - /// This endpoint allows clients to enable or disable the specified push rule. - class SetPushRuleEnabledJob : public BaseJob - { - public: - /*! Enable or disable a push rule. - * \param scope - * ``global`` to specify global rules. - * \param kind - * The kind of rule - * \param ruleId - * The identifier for the rule. - * \param enabled - * Whether the push rule is enabled or not. - */ - explicit SetPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId, bool enabled); - }; + // Result properties - /// The actions for a push rule - /// - /// This endpoint get the actions for the specified push rule. - class GetPushRuleActionsJob : public BaseJob + /// The global ruleset. + PushRuleset global() const { - public: - /*! The actions for a push rule - * \param scope - * Either ``global`` or ``device/<profile_tag>`` to specify global - * rules or device rules for the given ``profile_tag``. - * \param kind - * The kind of rule - * \param ruleId - * The identifier for the rule. - */ - explicit GetPushRuleActionsJob(const QString& scope, const QString& kind, const QString& ruleId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetPushRuleActionsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId); + return loadFromJson<PushRuleset>("global"_ls); + } +}; - ~GetPushRuleActionsJob() override; - - // Result properties - - /// The action(s) to perform for this rule. - const QStringList& actions() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Set the actions for a push rule. - /// - /// This endpoint allows clients to change the actions of a push rule. - /// This can be used to change the actions of builtin rules. - class SetPushRuleActionsJob : public BaseJob +/*! \brief Retrieve a push rule. + * + * Retrieve a single specified push rule. + */ +class GetPushRuleJob : public BaseJob { +public: + /*! \brief Retrieve a push rule. + * + * \param scope + * ``global`` to specify global rules. + * + * \param kind + * The kind of rule + * + * \param ruleId + * The identifier for the rule. + */ + explicit GetPushRuleJob(const QString& scope, const QString& kind, + const QString& ruleId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetPushRuleJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, + const QString& kind, const QString& ruleId); + + // Result properties + + /// The specific push rule. This will also include keys specific to the + /// rule itself such as the rule's ``actions`` and ``conditions`` if set. + PushRule pushRule() const { return fromJson<PushRule>(jsonData()); } +}; + +/*! \brief Delete a push rule. + * + * This endpoint removes the push rule defined in the path. + */ +class DeletePushRuleJob : public BaseJob { +public: + /*! \brief Delete a push rule. + * + * \param scope + * ``global`` to specify global rules. + * + * \param kind + * The kind of rule + * + * \param ruleId + * The identifier for the rule. + */ + explicit DeletePushRuleJob(const QString& scope, const QString& kind, + const QString& ruleId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for DeletePushRuleJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, + const QString& kind, const QString& ruleId); +}; + +/*! \brief Add or change a push rule. + * + * This endpoint allows the creation, modification and deletion of pushers + * for this user ID. The behaviour of this endpoint varies depending on the + * values in the JSON body. + * + * When creating push rules, they MUST be enabled by default. + */ +class SetPushRuleJob : public BaseJob { +public: + /*! \brief Add or change a push rule. + * + * \param scope + * ``global`` to specify global rules. + * + * \param kind + * The kind of rule + * + * \param ruleId + * The identifier for the rule. + * + * \param actions + * The action(s) to perform when the conditions for this rule are met. + * + * \param before + * Use 'before' with a ``rule_id`` as its value to make the new rule the + * next-most important rule with respect to the given user defined rule. + * It is not possible to add a rule relative to a predefined server rule. + * + * \param after + * This makes the new rule the next-less important rule relative to the + * given user defined rule. It is not possible to add a rule relative + * to a predefined server rule. + * + * \param conditions + * The conditions that must hold true for an event in order for a + * rule to be applied to an event. A rule with no conditions + * always matches. Only applicable to ``underride`` and ``override`` rules. + * + * \param pattern + * Only applicable to ``content`` rules. The glob-style pattern to match + * against. + */ + explicit SetPushRuleJob(const QString& scope, const QString& kind, + const QString& ruleId, + const QVector<QVariant>& actions, + const QString& before = {}, + const QString& after = {}, + const QVector<PushCondition>& conditions = {}, + const QString& pattern = {}); +}; + +/*! \brief Get whether a push rule is enabled + * + * This endpoint gets whether the specified push rule is enabled. + */ +class IsPushRuleEnabledJob : public BaseJob { +public: + /*! \brief Get whether a push rule is enabled + * + * \param scope + * Either ``global`` or ``device/<profile_tag>`` to specify global + * rules or device rules for the given ``profile_tag``. + * + * \param kind + * The kind of rule + * + * \param ruleId + * The identifier for the rule. + */ + explicit IsPushRuleEnabledJob(const QString& scope, const QString& kind, + const QString& ruleId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for IsPushRuleEnabledJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, + const QString& kind, const QString& ruleId); + + // Result properties + + /// Whether the push rule is enabled or not. + bool enabled() const { return loadFromJson<bool>("enabled"_ls); } +}; + +/*! \brief Enable or disable a push rule. + * + * This endpoint allows clients to enable or disable the specified push rule. + */ +class SetPushRuleEnabledJob : public BaseJob { +public: + /*! \brief Enable or disable a push rule. + * + * \param scope + * ``global`` to specify global rules. + * + * \param kind + * The kind of rule + * + * \param ruleId + * The identifier for the rule. + * + * \param enabled + * Whether the push rule is enabled or not. + */ + explicit SetPushRuleEnabledJob(const QString& scope, const QString& kind, + const QString& ruleId, bool enabled); +}; + +/*! \brief The actions for a push rule + * + * This endpoint get the actions for the specified push rule. + */ +class GetPushRuleActionsJob : public BaseJob { +public: + /*! \brief The actions for a push rule + * + * \param scope + * Either ``global`` or ``device/<profile_tag>`` to specify global + * rules or device rules for the given ``profile_tag``. + * + * \param kind + * The kind of rule + * + * \param ruleId + * The identifier for the rule. + */ + explicit GetPushRuleActionsJob(const QString& scope, const QString& kind, + const QString& ruleId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetPushRuleActionsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, + const QString& kind, const QString& ruleId); + + // Result properties + + /// The action(s) to perform for this rule. + QVector<QVariant> actions() const { - public: - /*! Set the actions for a push rule. - * \param scope - * ``global`` to specify global rules. - * \param kind - * The kind of rule - * \param ruleId - * The identifier for the rule. - * \param actions - * The action(s) to perform for this rule. - */ - explicit SetPushRuleActionsJob(const QString& scope, const QString& kind, const QString& ruleId, const QStringList& actions); - }; -} // namespace QMatrixClient + return loadFromJson<QVector<QVariant>>("actions"_ls); + } +}; + +/*! \brief Set the actions for a push rule. + * + * This endpoint allows clients to change the actions of a push rule. + * This can be used to change the actions of builtin rules. + */ +class SetPushRuleActionsJob : public BaseJob { +public: + /*! \brief Set the actions for a push rule. + * + * \param scope + * ``global`` to specify global rules. + * + * \param kind + * The kind of rule + * + * \param ruleId + * The identifier for the rule. + * + * \param actions + * The action(s) to perform for this rule. + */ + explicit SetPushRuleActionsJob(const QString& scope, const QString& kind, + const QString& ruleId, + const QVector<QVariant>& actions); +}; + +} // namespace Quotient diff --git a/lib/csapi/read_markers.cpp b/lib/csapi/read_markers.cpp index 1bc67ba0..39e4d148 100644 --- a/lib/csapi/read_markers.cpp +++ b/lib/csapi/read_markers.cpp @@ -4,23 +4,19 @@ #include "read_markers.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; -static const auto SetReadMarkerJobName = QStringLiteral("SetReadMarkerJob"); - -SetReadMarkerJob::SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, const QString& mRead) - : BaseJob(HttpVerb::Post, SetReadMarkerJobName, - basePath % "/rooms/" % roomId % "/read_markers") +SetReadMarkerJob::SetReadMarkerJob(const QString& roomId, + const QString& mFullyRead, + const QString& mRead) + : BaseJob(HttpVerb::Post, QStringLiteral("SetReadMarkerJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/read_markers") { QJsonObject _data; addParam<>(_data, QStringLiteral("m.fully_read"), mFullyRead); addParam<IfNotEmpty>(_data, QStringLiteral("m.read"), mRead); - setRequestData(_data); + setRequestData(std::move(_data)); } - diff --git a/lib/csapi/read_markers.h b/lib/csapi/read_markers.h index d982b477..0e122c63 100644 --- a/lib/csapi/read_markers.h +++ b/lib/csapi/read_markers.h @@ -6,29 +6,31 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Set the position of the read marker for a room. + * + * Sets the position of the read marker for a given room, and optionally + * the read receipt's location. + */ +class SetReadMarkerJob : public BaseJob { +public: + /*! \brief Set the position of the read marker for a room. + * + * \param roomId + * The room ID to set the read marker in for the user. + * + * \param mFullyRead + * The event ID the read marker should be located at. The + * event MUST belong to the room. + * + * \param mRead + * The event ID to set the read receipt location at. This is + * equivalent to calling ``/receipt/m.read/$elsewhere:example.org`` + * and is provided here to save that extra call. + */ + explicit SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, + const QString& mRead = {}); +}; - /// Set the position of the read marker for a room. - /// - /// Sets the position of the read marker for a given room, and optionally - /// the read receipt's location. - class SetReadMarkerJob : public BaseJob - { - public: - /*! Set the position of the read marker for a room. - * \param roomId - * The room ID to set the read marker in for the user. - * \param mFullyRead - * The event ID the read marker should be located at. The - * event MUST belong to the room. - * \param mRead - * The event ID to set the read receipt location at. This is - * equivalent to calling ``/receipt/m.read/$elsewhere:example.org`` - * and is provided here to save that extra call. - */ - explicit SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, const QString& mRead = {}); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/receipts.cpp b/lib/csapi/receipts.cpp index b78ba533..00d1c28a 100644 --- a/lib/csapi/receipts.cpp +++ b/lib/csapi/receipts.cpp @@ -4,20 +4,16 @@ #include "receipts.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; -static const auto PostReceiptJobName = QStringLiteral("PostReceiptJob"); - -PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& receiptType, const QString& eventId, const QJsonObject& receipt) - : BaseJob(HttpVerb::Post, PostReceiptJobName, - basePath % "/rooms/" % roomId % "/receipt/" % receiptType % "/" % eventId) +PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& receiptType, + const QString& eventId, + const QJsonObject& receipt) + : BaseJob(HttpVerb::Post, QStringLiteral("PostReceiptJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/receipt/" % receiptType % "/" % eventId) { setRequestData(Data(toJson(receipt))); } - diff --git a/lib/csapi/receipts.h b/lib/csapi/receipts.h index 47e2f3c7..1fac0acf 100644 --- a/lib/csapi/receipts.h +++ b/lib/csapi/receipts.h @@ -6,30 +6,33 @@ #include "jobs/basejob.h" -#include <QtCore/QJsonObject> +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Send a receipt for the given event ID. + * + * This API updates the marker for the given receipt type to the event ID + * specified. + */ +class PostReceiptJob : public BaseJob { +public: + /*! \brief Send a receipt for the given event ID. + * + * \param roomId + * The room in which to send the event. + * + * \param receiptType + * The type of receipt to send. + * + * \param eventId + * The event ID to acknowledge up to. + * + * \param receipt + * Extra receipt information to attach to ``content`` if any. The + * server will automatically set the ``ts`` field. + */ + explicit PostReceiptJob(const QString& roomId, const QString& receiptType, + const QString& eventId, + const QJsonObject& receipt = {}); +}; - /// Send a receipt for the given event ID. - /// - /// This API updates the marker for the given receipt type to the event ID - /// specified. - class PostReceiptJob : public BaseJob - { - public: - /*! Send a receipt for the given event ID. - * \param roomId - * The room in which to send the event. - * \param receiptType - * The type of receipt to send. - * \param eventId - * The event ID to acknowledge up to. - * \param receipt - * Extra receipt information to attach to ``content`` if any. The - * server will automatically set the ``ts`` field. - */ - explicit PostReceiptJob(const QString& roomId, const QString& receiptType, const QString& eventId, const QJsonObject& receipt = {}); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/redaction.cpp b/lib/csapi/redaction.cpp index 1d54e36d..91497064 100644 --- a/lib/csapi/redaction.cpp +++ b/lib/csapi/redaction.cpp @@ -4,43 +4,17 @@ #include "redaction.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; -class RedactEventJob::Private -{ - public: - QString eventId; -}; - -static const auto RedactEventJobName = QStringLiteral("RedactEventJob"); - -RedactEventJob::RedactEventJob(const QString& roomId, const QString& eventId, const QString& txnId, const QString& reason) - : BaseJob(HttpVerb::Put, RedactEventJobName, - basePath % "/rooms/" % roomId % "/redact/" % eventId % "/" % txnId) - , d(new Private) +RedactEventJob::RedactEventJob(const QString& roomId, const QString& eventId, + const QString& txnId, const QString& reason) + : BaseJob(HttpVerb::Put, QStringLiteral("RedactEventJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/redact/" % eventId % "/" % txnId) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); - setRequestData(_data); + setRequestData(std::move(_data)); } - -RedactEventJob::~RedactEventJob() = default; - -const QString& RedactEventJob::eventId() const -{ - return d->eventId; -} - -BaseJob::Status RedactEventJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("event_id"_ls), d->eventId); - return Success; -} - diff --git a/lib/csapi/redaction.h b/lib/csapi/redaction.h index d02abfd0..541e433a 100644 --- a/lib/csapi/redaction.h +++ b/lib/csapi/redaction.h @@ -6,48 +6,44 @@ #include "jobs/basejob.h" - -namespace QMatrixClient -{ - // Operations - - /// Strips all non-integrity-critical information out of an event. - /// - /// Strips all information out of an event which isn't critical to the - /// integrity of the server-side representation of the room. - /// - /// This cannot be undone. - /// - /// Users may redact their own events, and any user with a power level - /// greater than or equal to the `redact` power level of the room may - /// redact events there. - class RedactEventJob : public BaseJob - { - public: - /*! Strips all non-integrity-critical information out of an event. - * \param roomId - * The room from which to redact the event. - * \param eventId - * The ID of the event to redact - * \param txnId - * The transaction ID for this event. Clients should generate a - * unique ID; it will be used by the server to ensure idempotency of requests. - * \param reason - * The reason for the event being redacted. - */ - explicit RedactEventJob(const QString& roomId, const QString& eventId, const QString& txnId, const QString& reason = {}); - ~RedactEventJob() override; - - // Result properties - - /// A unique identifier for the event. - const QString& eventId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +namespace Quotient { + +/*! \brief Strips all non-integrity-critical information out of an event. + * + * Strips all information out of an event which isn't critical to the + * integrity of the server-side representation of the room. + * + * This cannot be undone. + * + * Users may redact their own events, and any user with a power level + * greater than or equal to the ``redact`` power level of the room may + * redact events there. + */ +class RedactEventJob : public BaseJob { +public: + /*! \brief Strips all non-integrity-critical information out of an event. + * + * \param roomId + * The room from which to redact the event. + * + * \param eventId + * The ID of the event to redact + * + * \param txnId + * The transaction ID for this event. Clients should generate a + * unique ID; it will be used by the server to ensure idempotency of + * requests. + * + * \param reason + * The reason for the event being redacted. + */ + explicit RedactEventJob(const QString& roomId, const QString& eventId, + const QString& txnId, const QString& reason = {}); + + // Result properties + + /// A unique identifier for the event. + QString eventId() const { return loadFromJson<QString>("event_id"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp index 5dc9c1e5..b80abc84 100644 --- a/lib/csapi/registration.cpp +++ b/lib/csapi/registration.cpp @@ -4,292 +4,124 @@ #include "registration.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; -class RegisterJob::Private -{ - public: - QString userId; - QString accessToken; - QString homeServer; - QString deviceId; -}; - -BaseJob::Query queryToRegister(const QString& kind) +auto queryToRegister(const QString& kind) { BaseJob::Query _q; addParam<IfNotEmpty>(_q, QStringLiteral("kind"), kind); return _q; } -static const auto RegisterJobName = QStringLiteral("RegisterJob"); - -RegisterJob::RegisterJob(const QString& kind, const Omittable<AuthenticationData>& auth, Omittable<bool> bindEmail, const QString& username, const QString& password, const QString& deviceId, const QString& initialDeviceDisplayName, Omittable<bool> inhibitLogin) - : BaseJob(HttpVerb::Post, RegisterJobName, - basePath % "/register", - queryToRegister(kind), - {}, false) - , d(new Private) +RegisterJob::RegisterJob(const QString& kind, + const Omittable<AuthenticationData>& auth, + const QString& username, const QString& password, + const QString& deviceId, + const QString& initialDeviceDisplayName, + Omittable<bool> inhibitLogin) + : BaseJob(HttpVerb::Post, QStringLiteral("RegisterJob"), + QStringLiteral("/_matrix/client/r0") % "/register", + queryToRegister(kind), {}, false) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); - addParam<IfNotEmpty>(_data, QStringLiteral("bind_email"), bindEmail); addParam<IfNotEmpty>(_data, QStringLiteral("username"), username); addParam<IfNotEmpty>(_data, QStringLiteral("password"), password); addParam<IfNotEmpty>(_data, QStringLiteral("device_id"), deviceId); - addParam<IfNotEmpty>(_data, QStringLiteral("initial_device_display_name"), initialDeviceDisplayName); + addParam<IfNotEmpty>(_data, QStringLiteral("initial_device_display_name"), + initialDeviceDisplayName); addParam<IfNotEmpty>(_data, QStringLiteral("inhibit_login"), inhibitLogin); - setRequestData(_data); -} - -RegisterJob::~RegisterJob() = default; - -const QString& RegisterJob::userId() const -{ - return d->userId; -} - -const QString& RegisterJob::accessToken() const -{ - return d->accessToken; -} - -const QString& RegisterJob::homeServer() const -{ - return d->homeServer; -} - -const QString& RegisterJob::deviceId() const -{ - return d->deviceId; -} - -BaseJob::Status RegisterJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("user_id"_ls)) - return { JsonParseError, - "The key 'user_id' not found in the response" }; - fromJson(json.value("user_id"_ls), d->userId); - fromJson(json.value("access_token"_ls), d->accessToken); - fromJson(json.value("home_server"_ls), d->homeServer); - fromJson(json.value("device_id"_ls), d->deviceId); - return Success; -} - -class RequestTokenToRegisterEmailJob::Private -{ - public: - Sid data; -}; - -static const auto RequestTokenToRegisterEmailJobName = QStringLiteral("RequestTokenToRegisterEmailJob"); - -RequestTokenToRegisterEmailJob::RequestTokenToRegisterEmailJob(const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink) - : BaseJob(HttpVerb::Post, RequestTokenToRegisterEmailJobName, - basePath % "/register/email/requestToken", false) - , d(new Private) -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("client_secret"), clientSecret); - addParam<>(_data, QStringLiteral("email"), email); - addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); - addParam<IfNotEmpty>(_data, QStringLiteral("next_link"), nextLink); - addParam<>(_data, QStringLiteral("id_server"), idServer); - setRequestData(_data); -} - -RequestTokenToRegisterEmailJob::~RequestTokenToRegisterEmailJob() = default; - -const Sid& RequestTokenToRegisterEmailJob::data() const -{ - return d->data; -} - -BaseJob::Status RequestTokenToRegisterEmailJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - -class RequestTokenToRegisterMSISDNJob::Private -{ - public: - Sid data; -}; - -static const auto RequestTokenToRegisterMSISDNJobName = QStringLiteral("RequestTokenToRegisterMSISDNJob"); - -RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob(const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink) - : BaseJob(HttpVerb::Post, RequestTokenToRegisterMSISDNJobName, - basePath % "/register/msisdn/requestToken", false) - , d(new Private) -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("client_secret"), clientSecret); - addParam<>(_data, QStringLiteral("country"), country); - addParam<>(_data, QStringLiteral("phone_number"), phoneNumber); - addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); - addParam<IfNotEmpty>(_data, QStringLiteral("next_link"), nextLink); - addParam<>(_data, QStringLiteral("id_server"), idServer); - setRequestData(_data); + setRequestData(std::move(_data)); + addExpectedKey("user_id"); } -RequestTokenToRegisterMSISDNJob::~RequestTokenToRegisterMSISDNJob() = default; - -const Sid& RequestTokenToRegisterMSISDNJob::data() const +RequestTokenToRegisterEmailJob::RequestTokenToRegisterEmailJob( + const EmailValidationData& body) + : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterEmailJob"), + QStringLiteral("/_matrix/client/r0") + % "/register/email/requestToken", + false) { - return d->data; + setRequestData(Data(toJson(body))); } -BaseJob::Status RequestTokenToRegisterMSISDNJob::parseJson(const QJsonDocument& data) +RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob( + const MsisdnValidationData& body) + : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterMSISDNJob"), + QStringLiteral("/_matrix/client/r0") + % "/register/msisdn/requestToken", + false) { - fromJson(data, d->data); - return Success; + setRequestData(Data(toJson(body))); } -static const auto ChangePasswordJobName = QStringLiteral("ChangePasswordJob"); - -ChangePasswordJob::ChangePasswordJob(const QString& newPassword, const Omittable<AuthenticationData>& auth) - : BaseJob(HttpVerb::Post, ChangePasswordJobName, - basePath % "/account/password") +ChangePasswordJob::ChangePasswordJob(const QString& newPassword, + Omittable<bool> logoutDevices, + const Omittable<AuthenticationData>& auth) + : BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), + QStringLiteral("/_matrix/client/r0") % "/account/password") { QJsonObject _data; addParam<>(_data, QStringLiteral("new_password"), newPassword); + addParam<IfNotEmpty>(_data, QStringLiteral("logout_devices"), logoutDevices); addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); - setRequestData(_data); -} - -class RequestTokenToResetPasswordEmailJob::Private -{ - public: - Sid data; -}; - -static const auto RequestTokenToResetPasswordEmailJobName = QStringLiteral("RequestTokenToResetPasswordEmailJob"); - -RequestTokenToResetPasswordEmailJob::RequestTokenToResetPasswordEmailJob(const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink) - : BaseJob(HttpVerb::Post, RequestTokenToResetPasswordEmailJobName, - basePath % "/account/password/email/requestToken", false) - , d(new Private) -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("client_secret"), clientSecret); - addParam<>(_data, QStringLiteral("email"), email); - addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); - addParam<IfNotEmpty>(_data, QStringLiteral("next_link"), nextLink); - addParam<>(_data, QStringLiteral("id_server"), idServer); - setRequestData(_data); -} - -RequestTokenToResetPasswordEmailJob::~RequestTokenToResetPasswordEmailJob() = default; - -const Sid& RequestTokenToResetPasswordEmailJob::data() const -{ - return d->data; -} - -BaseJob::Status RequestTokenToResetPasswordEmailJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - -class RequestTokenToResetPasswordMSISDNJob::Private -{ - public: - Sid data; -}; - -static const auto RequestTokenToResetPasswordMSISDNJobName = QStringLiteral("RequestTokenToResetPasswordMSISDNJob"); - -RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob(const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink) - : BaseJob(HttpVerb::Post, RequestTokenToResetPasswordMSISDNJobName, - basePath % "/account/password/msisdn/requestToken", false) - , d(new Private) -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("client_secret"), clientSecret); - addParam<>(_data, QStringLiteral("country"), country); - addParam<>(_data, QStringLiteral("phone_number"), phoneNumber); - addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); - addParam<IfNotEmpty>(_data, QStringLiteral("next_link"), nextLink); - addParam<>(_data, QStringLiteral("id_server"), idServer); - setRequestData(_data); + setRequestData(std::move(_data)); } -RequestTokenToResetPasswordMSISDNJob::~RequestTokenToResetPasswordMSISDNJob() = default; - -const Sid& RequestTokenToResetPasswordMSISDNJob::data() const +RequestTokenToResetPasswordEmailJob::RequestTokenToResetPasswordEmailJob( + const EmailValidationData& body) + : BaseJob(HttpVerb::Post, + QStringLiteral("RequestTokenToResetPasswordEmailJob"), + QStringLiteral("/_matrix/client/r0") + % "/account/password/email/requestToken", + false) { - return d->data; + setRequestData(Data(toJson(body))); } -BaseJob::Status RequestTokenToResetPasswordMSISDNJob::parseJson(const QJsonDocument& data) +RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob( + const MsisdnValidationData& body) + : BaseJob(HttpVerb::Post, + QStringLiteral("RequestTokenToResetPasswordMSISDNJob"), + QStringLiteral("/_matrix/client/r0") + % "/account/password/msisdn/requestToken", + false) { - fromJson(data, d->data); - return Success; + setRequestData(Data(toJson(body))); } -static const auto DeactivateAccountJobName = QStringLiteral("DeactivateAccountJob"); - -DeactivateAccountJob::DeactivateAccountJob(const Omittable<AuthenticationData>& auth) - : BaseJob(HttpVerb::Post, DeactivateAccountJobName, - basePath % "/account/deactivate") +DeactivateAccountJob::DeactivateAccountJob( + const Omittable<AuthenticationData>& auth, const QString& idServer) + : BaseJob(HttpVerb::Post, QStringLiteral("DeactivateAccountJob"), + QStringLiteral("/_matrix/client/r0") % "/account/deactivate") { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); - setRequestData(_data); + addParam<IfNotEmpty>(_data, QStringLiteral("id_server"), idServer); + setRequestData(std::move(_data)); + addExpectedKey("id_server_unbind_result"); } -class CheckUsernameAvailabilityJob::Private -{ - public: - Omittable<bool> available; -}; - -BaseJob::Query queryToCheckUsernameAvailability(const QString& username) +auto queryToCheckUsernameAvailability(const QString& username) { BaseJob::Query _q; addParam<>(_q, QStringLiteral("username"), username); return _q; } -QUrl CheckUsernameAvailabilityJob::makeRequestUrl(QUrl baseUrl, const QString& username) +QUrl CheckUsernameAvailabilityJob::makeRequestUrl(QUrl baseUrl, + const QString& username) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/register/available", - queryToCheckUsernameAvailability(username)); + QStringLiteral("/_matrix/client/r0") + % "/register/available", + queryToCheckUsernameAvailability(username)); } -static const auto CheckUsernameAvailabilityJobName = QStringLiteral("CheckUsernameAvailabilityJob"); - CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(const QString& username) - : BaseJob(HttpVerb::Get, CheckUsernameAvailabilityJobName, - basePath % "/register/available", - queryToCheckUsernameAvailability(username), - {}, false) - , d(new Private) -{ -} - -CheckUsernameAvailabilityJob::~CheckUsernameAvailabilityJob() = default; - -Omittable<bool> CheckUsernameAvailabilityJob::available() const -{ - return d->available; -} - -BaseJob::Status CheckUsernameAvailabilityJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("available"_ls), d->available); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("CheckUsernameAvailabilityJob"), + QStringLiteral("/_matrix/client/r0") % "/register/available", + queryToCheckUsernameAvailability(username), {}, false) +{} diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h index ca1a1c21..62bc35f1 100644 --- a/lib/csapi/registration.h +++ b/lib/csapi/registration.h @@ -4,427 +4,454 @@ #pragma once -#include "jobs/basejob.h" - -#include "csapi/../identity/definitions/sid.h" -#include "converters.h" #include "csapi/definitions/auth_data.h" +#include "csapi/definitions/request_email_validation.h" +#include "csapi/definitions/request_msisdn_validation.h" +#include "csapi/definitions/request_token_response.h" -namespace QMatrixClient -{ - // Operations +#include "jobs/basejob.h" - /// Register for an account on this homeserver. +namespace Quotient { + +/*! \brief Register for an account on this homeserver. + * + * This API endpoint uses the `User-Interactive Authentication API`_, except in + * the cases where a guest account is being registered. + * + * Register for an account on this homeserver. + * + * There are two kinds of user account: + * + * - `user` accounts. These accounts may use the full API described in this + * specification. + * + * - `guest` accounts. These accounts may have limited permissions and may not + * be supported by all servers. + * + * If registration is successful, this endpoint will issue an access token + * the client can use to authorize itself in subsequent requests. + * + * If the client does not supply a ``device_id``, the server must + * auto-generate one. + * + * The server SHOULD register an account with a User ID based on the + * ``username`` provided, if any. Note that the grammar of Matrix User ID + * localparts is restricted, so the server MUST either map the provided + * ``username`` onto a ``user_id`` in a logical manner, or reject + * ``username``\s which do not comply to the grammar, with + * ``M_INVALID_USERNAME``. + * + * Matrix clients MUST NOT assume that localpart of the registered + * ``user_id`` matches the provided ``username``. + * + * The returned access token must be associated with the ``device_id`` + * supplied by the client or generated by the server. The server may + * invalidate any access token previously associated with that device. See + * `Relationship between access tokens and devices`_. + * + * When registering a guest account, all parameters in the request body + * with the exception of ``initial_device_display_name`` MUST BE ignored + * by the server. The server MUST pick a ``device_id`` for the account + * regardless of input. + * + * Any user ID returned by this API must conform to the grammar given in the + * `Matrix specification <../appendices.html#user-identifiers>`_. + */ +class RegisterJob : public BaseJob { +public: + /*! \brief Register for an account on this homeserver. + * + * \param kind + * The kind of account to register. Defaults to ``user``. + * + * \param auth + * Additional authentication information for the + * user-interactive authentication API. Note that this + * information is *not* used to define how the registered user + * should be authenticated, but is instead used to + * authenticate the ``register`` call itself. + * + * \param username + * The basis for the localpart of the desired Matrix ID. If omitted, + * the homeserver MUST generate a Matrix ID local part. + * + * \param password + * The desired password for the account. + * + * \param deviceId + * ID of the client device. If this does not correspond to a + * known client device, a new device will be created. The server + * will auto-generate a device_id if this is not specified. + * + * \param initialDeviceDisplayName + * A display name to assign to the newly-created device. Ignored + * if ``device_id`` corresponds to a known device. + * + * \param inhibitLogin + * If true, an ``access_token`` and ``device_id`` should not be + * returned from this call, therefore preventing an automatic + * login. Defaults to false. + */ + explicit RegisterJob(const QString& kind = QStringLiteral("user"), + const Omittable<AuthenticationData>& auth = none, + const QString& username = {}, + const QString& password = {}, + const QString& deviceId = {}, + const QString& initialDeviceDisplayName = {}, + Omittable<bool> inhibitLogin = none); + + // Result properties + + /// The fully-qualified Matrix user ID (MXID) that has been registered. /// - /// This API endpoint uses the `User-Interactive Authentication API`_. - /// - /// Register for an account on this homeserver. - /// - /// There are two kinds of user account: - /// - /// - `user` accounts. These accounts may use the full API described in this specification. - /// - /// - `guest` accounts. These accounts may have limited permissions and may not be supported by all servers. - /// - /// If registration is successful, this endpoint will issue an access token - /// the client can use to authorize itself in subsequent requests. - /// - /// If the client does not supply a ``device_id``, the server must - /// auto-generate one. - /// - /// The server SHOULD register an account with a User ID based on the - /// ``username`` provided, if any. Note that the grammar of Matrix User ID - /// localparts is restricted, so the server MUST either map the provided - /// ``username`` onto a ``user_id`` in a logical manner, or reject - /// ``username``\s which do not comply to the grammar, with - /// ``M_INVALID_USERNAME``. - /// - /// Matrix clients MUST NOT assume that localpart of the registered - /// ``user_id`` matches the provided ``username``. - /// - /// The returned access token must be associated with the ``device_id`` - /// supplied by the client or generated by the server. The server may - /// invalidate any access token previously associated with that device. See - /// `Relationship between access tokens and devices`_. - class RegisterJob : public BaseJob + /// Any user ID returned by this API must conform to the grammar given in + /// the `Matrix specification <../appendices.html#user-identifiers>`_. + QString userId() const { return loadFromJson<QString>("user_id"_ls); } + + /// An access token for the account. + /// This access token can then be used to authorize other requests. + /// Required if the ``inhibit_login`` option is false. + QString accessToken() const { - public: - /*! Register for an account on this homeserver. - * \param kind - * The kind of account to register. Defaults to `user`. - * \param auth - * Additional authentication information for the - * user-interactive authentication API. Note that this - * information is *not* used to define how the registered user - * should be authenticated, but is instead used to - * authenticate the ``register`` call itself. It should be - * left empty, or omitted, unless an earlier call returned an - * response with status code 401. - * \param bindEmail - * If true, the server binds the email used for authentication to - * the Matrix ID with the identity server. - * \param username - * The basis for the localpart of the desired Matrix ID. If omitted, - * the homeserver MUST generate a Matrix ID local part. - * \param password - * The desired password for the account. - * \param deviceId - * ID of the client device. If this does not correspond to a - * known client device, a new device will be created. The server - * will auto-generate a device_id if this is not specified. - * \param initialDeviceDisplayName - * A display name to assign to the newly-created device. Ignored - * if ``device_id`` corresponds to a known device. - * \param inhibitLogin - * If true, an ``access_token`` and ``device_id`` should not be - * returned from this call, therefore preventing an automatic - * login. Defaults to false. - */ - explicit RegisterJob(const QString& kind = QStringLiteral("user"), const Omittable<AuthenticationData>& auth = none, Omittable<bool> bindEmail = none, const QString& username = {}, const QString& password = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}, Omittable<bool> inhibitLogin = none); - ~RegisterJob() override; - - // Result properties - - /// The fully-qualified Matrix user ID (MXID) that has been registered. - /// - /// Any user ID returned by this API must conform to the grammar given in the - /// `Matrix specification <https://matrix.org/docs/spec/appendices.html#user-identifiers>`_. - const QString& userId() const; - /// An access token for the account. - /// This access token can then be used to authorize other requests. - /// Required if the ``inhibit_login`` option is false. - const QString& accessToken() const; - /// The server_name of the homeserver on which the account has - /// been registered. - /// - /// **Deprecated**. Clients should extract the server_name from - /// ``user_id`` (by splitting at the first colon) if they require - /// it. Note also that ``homeserver`` is not spelt this way. - const QString& homeServer() const; - /// ID of the registered device. Will be the same as the - /// corresponding parameter in the request, if one was specified. - /// Required if the ``inhibit_login`` option is false. - const QString& deviceId() const; + return loadFromJson<QString>("access_token"_ls); + } - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Begins the validation process for an email to be used during registration. + /// The server_name of the homeserver on which the account has + /// been registered. /// - /// Proxies the Identity Service API ``validate/email/requestToken``, but - /// first checks that the given email address is not already associated - /// with an account on this homeserver. See the Identity Service API for - /// further information. - class RequestTokenToRegisterEmailJob : public BaseJob + /// **Deprecated**. Clients should extract the server_name from + /// ``user_id`` (by splitting at the first colon) if they require + /// it. Note also that ``homeserver`` is not spelt this way. + QString homeServer() const { - public: - /*! Begins the validation process for an email to be used during registration. - * \param clientSecret - * A unique string generated by the client, and used to identify the - * validation attempt. It must be a string consisting of the characters - * ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it - * must not be empty. - * \param email - * The email address to validate. - * \param sendAttempt - * The server will only send an email if the ``send_attempt`` - * is a number greater than the most recent one which it has seen, - * scoped to that ``email`` + ``client_secret`` pair. This is to - * avoid repeatedly sending the same email in the case of request - * retries between the POSTing user and the identity server. - * The client should increment this value if they desire a new - * email (e.g. a reminder) to be sent. - * \param idServer - * The hostname of the identity server to communicate with. May - * optionally include a port. - * \param nextLink - * Optional. When the validation is completed, the identity - * server will redirect the user to this URL. - */ - explicit RequestTokenToRegisterEmailJob(const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink = {}); - ~RequestTokenToRegisterEmailJob() override; - - // Result properties - - /// An email has been sent to the specified address. - /// Note that this may be an email containing the validation token or it may be informing - /// the user of an error. - const Sid& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Requests a validation token be sent to the given phone number for the purpose of registering an account - /// - /// Proxies the Identity Service API ``validate/msisdn/requestToken``, but - /// first checks that the given phone number is not already associated - /// with an account on this homeserver. See the Identity Service API for - /// further information. - class RequestTokenToRegisterMSISDNJob : public BaseJob + return loadFromJson<QString>("home_server"_ls); + } + + /// ID of the registered device. Will be the same as the + /// corresponding parameter in the request, if one was specified. + /// Required if the ``inhibit_login`` option is false. + QString deviceId() const { return loadFromJson<QString>("device_id"_ls); } +}; + +/*! \brief Begins the validation process for an email to be used during + * registration. + * + * The homeserver must check that the given email address is **not** + * already associated with an account on this homeserver. The homeserver + * should validate the email itself, either by sending a validation email + * itself or by using a service it has control over. + */ +class RequestTokenToRegisterEmailJob : public BaseJob { +public: + /*! \brief Begins the validation process for an email to be used during + * registration. + * + * \param body + * The homeserver must check that the given email address is **not** + * already associated with an account on this homeserver. The homeserver + * should validate the email itself, either by sending a validation email + * itself or by using a service it has control over. + */ + explicit RequestTokenToRegisterEmailJob(const EmailValidationData& body); + + // Result properties + + /// An email has been sent to the specified address. Note that this + /// may be an email containing the validation token or it may be + /// informing the user of an error. + RequestTokenResponse response() const { - public: - /*! Requests a validation token be sent to the given phone number for the purpose of registering an account - * \param clientSecret - * A unique string generated by the client, and used to identify the - * validation attempt. It must be a string consisting of the characters - * ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it - * must not be empty. - * \param country - * The two-letter uppercase ISO country code that the number in - * ``phone_number`` should be parsed as if it were dialled from. - * \param phoneNumber - * The phone number to validate. - * \param sendAttempt - * The server will only send an SMS if the ``send_attempt`` is a - * number greater than the most recent one which it has seen, - * scoped to that ``country`` + ``phone_number`` + ``client_secret`` - * triple. This is to avoid repeatedly sending the same SMS in - * the case of request retries between the POSTing user and the - * identity server. The client should increment this value if - * they desire a new SMS (e.g. a reminder) to be sent. - * \param idServer - * The hostname of the identity server to communicate with. May - * optionally include a port. - * \param nextLink - * Optional. When the validation is completed, the identity - * server will redirect the user to this URL. - */ - explicit RequestTokenToRegisterMSISDNJob(const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink = {}); - ~RequestTokenToRegisterMSISDNJob() override; - - // Result properties - - /// An SMS message has been sent to the specified phone number. - /// Note that this may be an SMS message containing the validation token or it may be informing - /// the user of an error. - const Sid& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Changes a user's password. - /// - /// Changes the password for an account on this homeserver. - /// - /// This API endpoint uses the `User-Interactive Authentication API`_. - /// - /// An access token should be submitted to this endpoint if the client has - /// an active session. - /// - /// The homeserver may change the flows available depending on whether a - /// valid access token is provided. - class ChangePasswordJob : public BaseJob + return fromJson<RequestTokenResponse>(jsonData()); + } +}; + +/*! \brief Requests a validation token be sent to the given phone number for the + * purpose of registering an account + * + * The homeserver must check that the given phone number is **not** + * already associated with an account on this homeserver. The homeserver + * should validate the phone number itself, either by sending a validation + * message itself or by using a service it has control over. + */ +class RequestTokenToRegisterMSISDNJob : public BaseJob { +public: + /*! \brief Requests a validation token be sent to the given phone number for + * the purpose of registering an account + * + * \param body + * The homeserver must check that the given phone number is **not** + * already associated with an account on this homeserver. The homeserver + * should validate the phone number itself, either by sending a validation + * message itself or by using a service it has control over. + */ + explicit RequestTokenToRegisterMSISDNJob(const MsisdnValidationData& body); + + // Result properties + + /// An SMS message has been sent to the specified phone number. Note + /// that this may be an SMS message containing the validation token or + /// it may be informing the user of an error. + RequestTokenResponse response() const { - public: - /*! Changes a user's password. - * \param newPassword - * The new password for the account. - * \param auth - * Additional authentication information for the user-interactive authentication API. - */ - explicit ChangePasswordJob(const QString& newPassword, const Omittable<AuthenticationData>& auth = none); - }; - - /// Requests a validation token be sent to the given email address for the purpose of resetting a user's password - /// - /// Proxies the Identity Service API ``validate/email/requestToken``, but - /// first checks that the given email address **is** associated with an account - /// on this homeserver. This API should be used to request - /// validation tokens when authenticating for the - /// `account/password` endpoint. This API's parameters and response are - /// identical to that of the HS API |/register/email/requestToken|_ except that - /// `M_THREEPID_NOT_FOUND` may be returned if no account matching the - /// given email address could be found. The server may instead send an - /// email to the given address prompting the user to create an account. - /// `M_THREEPID_IN_USE` may not be returned. - /// - /// .. |/register/email/requestToken| replace:: ``/register/email/requestToken`` - /// - /// .. _/register/email/requestToken: #post-matrix-client-r0-register-email-requesttoken - class RequestTokenToResetPasswordEmailJob : public BaseJob + return fromJson<RequestTokenResponse>(jsonData()); + } +}; + +/*! \brief Changes a user's password. + * + * Changes the password for an account on this homeserver. + * + * This API endpoint uses the `User-Interactive Authentication API`_ to + * ensure the user changing the password is actually the owner of the + * account. + * + * An access token should be submitted to this endpoint if the client has + * an active session. + * + * The homeserver may change the flows available depending on whether a + * valid access token is provided. The homeserver SHOULD NOT revoke the + * access token provided in the request. Whether other access tokens for + * the user are revoked depends on the request parameters. + */ +class ChangePasswordJob : public BaseJob { +public: + /*! \brief Changes a user's password. + * + * \param newPassword + * The new password for the account. + * + * \param logoutDevices + * Whether the user's other access tokens, and their associated devices, + * should be revoked if the request succeeds. Defaults to true. + * + * When ``false``, the server can still take advantage of `the soft logout + * method <#soft-logout>`_ for the user's remaining devices. + * + * \param auth + * Additional authentication information for the user-interactive + * authentication API. + */ + explicit ChangePasswordJob(const QString& newPassword, + Omittable<bool> logoutDevices = none, + const Omittable<AuthenticationData>& auth = none); +}; + +/*! \brief Requests a validation token be sent to the given email address for + * the purpose of resetting a user's password + * + * The homeserver must check that the given email address **is + * associated** with an account on this homeserver. This API should be + * used to request validation tokens when authenticating for the + * ``/account/password`` endpoint. + * + * This API's parameters and response are identical to that of the + * |/register/email/requestToken|_ endpoint, except that + * ``M_THREEPID_NOT_FOUND`` may be returned if no account matching the + * given email address could be found. The server may instead send an + * email to the given address prompting the user to create an account. + * ``M_THREEPID_IN_USE`` may not be returned. + * + * The homeserver should validate the email itself, either by sending a + * validation email itself or by using a service it has control over. + * + * + * .. |/register/email/requestToken| replace:: ``/register/email/requestToken`` + * + * .. _/register/email/requestToken: + * #post-matrix-client-r0-register-email-requesttoken + */ +class RequestTokenToResetPasswordEmailJob : public BaseJob { +public: + /*! \brief Requests a validation token be sent to the given email address + * for the purpose of resetting a user's password + * + * \param body + * The homeserver must check that the given email address **is + * associated** with an account on this homeserver. This API should be + * used to request validation tokens when authenticating for the + * ``/account/password`` endpoint. + * + * This API's parameters and response are identical to that of the + * |/register/email/requestToken|_ endpoint, except that + * ``M_THREEPID_NOT_FOUND`` may be returned if no account matching the + * given email address could be found. The server may instead send an + * email to the given address prompting the user to create an account. + * ``M_THREEPID_IN_USE`` may not be returned. + * + * The homeserver should validate the email itself, either by sending a + * validation email itself or by using a service it has control over. + * + * + * .. |/register/email/requestToken| replace:: + * ``/register/email/requestToken`` + * + * .. _/register/email/requestToken: + * #post-matrix-client-r0-register-email-requesttoken + */ + explicit RequestTokenToResetPasswordEmailJob(const EmailValidationData& body); + + // Result properties + + /// An email was sent to the given address. + RequestTokenResponse response() const { - public: - /*! Requests a validation token be sent to the given email address for the purpose of resetting a user's password - * \param clientSecret - * A unique string generated by the client, and used to identify the - * validation attempt. It must be a string consisting of the characters - * ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it - * must not be empty. - * \param email - * The email address to validate. - * \param sendAttempt - * The server will only send an email if the ``send_attempt`` - * is a number greater than the most recent one which it has seen, - * scoped to that ``email`` + ``client_secret`` pair. This is to - * avoid repeatedly sending the same email in the case of request - * retries between the POSTing user and the identity server. - * The client should increment this value if they desire a new - * email (e.g. a reminder) to be sent. - * \param idServer - * The hostname of the identity server to communicate with. May - * optionally include a port. - * \param nextLink - * Optional. When the validation is completed, the identity - * server will redirect the user to this URL. - */ - explicit RequestTokenToResetPasswordEmailJob(const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink = {}); - ~RequestTokenToResetPasswordEmailJob() override; - - // Result properties - - /// An email was sent to the given address. - const Sid& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Requests a validation token be sent to the given phone number for the purpose of resetting a user's password. - /// - /// Proxies the Identity Service API ``validate/msisdn/requestToken``, but - /// first checks that the given phone number **is** associated with an account - /// on this homeserver. This API should be used to request - /// validation tokens when authenticating for the - /// `account/password` endpoint. This API's parameters and response are - /// identical to that of the HS API |/register/msisdn/requestToken|_ except that - /// `M_THREEPID_NOT_FOUND` may be returned if no account matching the - /// given phone number could be found. The server may instead send an - /// SMS message to the given address prompting the user to create an account. - /// `M_THREEPID_IN_USE` may not be returned. - /// - /// .. |/register/msisdn/requestToken| replace:: ``/register/msisdn/requestToken`` - /// - /// .. _/register/msisdn/requestToken: #post-matrix-client-r0-register-email-requesttoken - class RequestTokenToResetPasswordMSISDNJob : public BaseJob + return fromJson<RequestTokenResponse>(jsonData()); + } +}; + +/*! \brief Requests a validation token be sent to the given phone number for the + * purpose of resetting a user's password. + * + * The homeserver must check that the given phone number **is + * associated** with an account on this homeserver. This API should be + * used to request validation tokens when authenticating for the + * ``/account/password`` endpoint. + * + * This API's parameters and response are identical to that of the + * |/register/msisdn/requestToken|_ endpoint, except that + * ``M_THREEPID_NOT_FOUND`` may be returned if no account matching the + * given phone number could be found. The server may instead send the SMS + * to the given phone number prompting the user to create an account. + * ``M_THREEPID_IN_USE`` may not be returned. + * + * The homeserver should validate the phone number itself, either by sending a + * validation message itself or by using a service it has control over. + * + * .. |/register/msisdn/requestToken| replace:: ``/register/msisdn/requestToken`` + * + * .. _/register/msisdn/requestToken: + * #post-matrix-client-r0-register-email-requesttoken + */ +class RequestTokenToResetPasswordMSISDNJob : public BaseJob { +public: + /*! \brief Requests a validation token be sent to the given phone number for + * the purpose of resetting a user's password. + * + * \param body + * The homeserver must check that the given phone number **is + * associated** with an account on this homeserver. This API should be + * used to request validation tokens when authenticating for the + * ``/account/password`` endpoint. + * + * This API's parameters and response are identical to that of the + * |/register/msisdn/requestToken|_ endpoint, except that + * ``M_THREEPID_NOT_FOUND`` may be returned if no account matching the + * given phone number could be found. The server may instead send the SMS + * to the given phone number prompting the user to create an account. + * ``M_THREEPID_IN_USE`` may not be returned. + * + * The homeserver should validate the phone number itself, either by sending + * a validation message itself or by using a service it has control over. + * + * .. |/register/msisdn/requestToken| replace:: + * ``/register/msisdn/requestToken`` + * + * .. _/register/msisdn/requestToken: + * #post-matrix-client-r0-register-email-requesttoken + */ + explicit RequestTokenToResetPasswordMSISDNJob( + const MsisdnValidationData& body); + + // Result properties + + /// An SMS message was sent to the given phone number. + RequestTokenResponse response() const { - public: - /*! Requests a validation token be sent to the given phone number for the purpose of resetting a user's password. - * \param clientSecret - * A unique string generated by the client, and used to identify the - * validation attempt. It must be a string consisting of the characters - * ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it - * must not be empty. - * \param country - * The two-letter uppercase ISO country code that the number in - * ``phone_number`` should be parsed as if it were dialled from. - * \param phoneNumber - * The phone number to validate. - * \param sendAttempt - * The server will only send an SMS if the ``send_attempt`` is a - * number greater than the most recent one which it has seen, - * scoped to that ``country`` + ``phone_number`` + ``client_secret`` - * triple. This is to avoid repeatedly sending the same SMS in - * the case of request retries between the POSTing user and the - * identity server. The client should increment this value if - * they desire a new SMS (e.g. a reminder) to be sent. - * \param idServer - * The hostname of the identity server to communicate with. May - * optionally include a port. - * \param nextLink - * Optional. When the validation is completed, the identity - * server will redirect the user to this URL. - */ - explicit RequestTokenToResetPasswordMSISDNJob(const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink = {}); - ~RequestTokenToResetPasswordMSISDNJob() override; - - // Result properties - - /// An SMS message was sent to the given phone number. - const Sid& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Deactivate a user's account. - /// - /// Deactivate the user's account, removing all ability for the user to - /// login again. - /// - /// This API endpoint uses the `User-Interactive Authentication API`_. - /// - /// An access token should be submitted to this endpoint if the client has - /// an active session. - /// - /// The homeserver may change the flows available depending on whether a - /// valid access token is provided. - class DeactivateAccountJob : public BaseJob + return fromJson<RequestTokenResponse>(jsonData()); + } +}; + +/*! \brief Deactivate a user's account. + * + * Deactivate the user's account, removing all ability for the user to + * login again. + * + * This API endpoint uses the `User-Interactive Authentication API`_. + * + * An access token should be submitted to this endpoint if the client has + * an active session. + * + * The homeserver may change the flows available depending on whether a + * valid access token is provided. + * + * Unlike other endpoints, this endpoint does not take an ``id_access_token`` + * parameter because the homeserver is expected to sign the request to the + * identity server instead. + */ +class DeactivateAccountJob : public BaseJob { +public: + /*! \brief Deactivate a user's account. + * + * \param auth + * Additional authentication information for the user-interactive + * authentication API. + * + * \param idServer + * The identity server to unbind all of the user's 3PIDs from. + * If not provided, the homeserver MUST use the ``id_server`` + * that was originally use to bind each identifier. If the + * homeserver does not know which ``id_server`` that was, + * it must return an ``id_server_unbind_result`` of + * ``no-support``. + */ + explicit DeactivateAccountJob(const Omittable<AuthenticationData>& auth = none, + const QString& idServer = {}); + + // Result properties + + /// An indicator as to whether or not the homeserver was able to unbind + /// the user's 3PIDs from the identity server(s). ``success`` indicates + /// that all identifiers have been unbound from the identity server while + /// ``no-support`` indicates that one or more identifiers failed to unbind + /// due to the identity server refusing the request or the homeserver + /// being unable to determine an identity server to unbind from. This + /// must be ``success`` if the homeserver has no identifiers to unbind + /// for the user. + QString idServerUnbindResult() const { - public: - /*! Deactivate a user's account. - * \param auth - * Additional authentication information for the user-interactive authentication API. - */ - explicit DeactivateAccountJob(const Omittable<AuthenticationData>& auth = none); - }; - - /// Checks to see if a username is available on the server. - /// - /// Checks to see if a username is available, and valid, for the server. - /// - /// The server should check to ensure that, at the time of the request, the - /// username requested is available for use. This includes verifying that an - /// application service has not claimed the username and that the username - /// fits the server's desired requirements (for example, a server could dictate - /// that it does not permit usernames with underscores). - /// - /// Matrix clients may wish to use this API prior to attempting registration, - /// however the clients must also be aware that using this API does not normally - /// reserve the username. This can mean that the username becomes unavailable - /// between checking its availability and attempting to register it. - class CheckUsernameAvailabilityJob : public BaseJob + return loadFromJson<QString>("id_server_unbind_result"_ls); + } +}; + +/*! \brief Checks to see if a username is available on the server. + * + * Checks to see if a username is available, and valid, for the server. + * + * The server should check to ensure that, at the time of the request, the + * username requested is available for use. This includes verifying that an + * application service has not claimed the username and that the username + * fits the server's desired requirements (for example, a server could dictate + * that it does not permit usernames with underscores). + * + * Matrix clients may wish to use this API prior to attempting registration, + * however the clients must also be aware that using this API does not normally + * reserve the username. This can mean that the username becomes unavailable + * between checking its availability and attempting to register it. + */ +class CheckUsernameAvailabilityJob : public BaseJob { +public: + /*! \brief Checks to see if a username is available on the server. + * + * \param username + * The username to check the availability of. + */ + explicit CheckUsernameAvailabilityJob(const QString& username); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for CheckUsernameAvailabilityJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& username); + + // Result properties + + /// A flag to indicate that the username is available. This should always + /// be ``true`` when the server replies with 200 OK. + Omittable<bool> available() const { - public: - /*! Checks to see if a username is available on the server. - * \param username - * The username to check the availability of. - */ - explicit CheckUsernameAvailabilityJob(const QString& username); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * CheckUsernameAvailabilityJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& username); - - ~CheckUsernameAvailabilityJob() override; - - // Result properties - - /// A flag to indicate that the username is available. This should always - /// be ``true`` when the server replies with 200 OK. - Omittable<bool> available() const; - - protected: - Status parseJson(const QJsonDocument& data) override; + return loadFromJson<Omittable<bool>>("available"_ls); + } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/report_content.cpp b/lib/csapi/report_content.cpp index a79d4dad..0a41625f 100644 --- a/lib/csapi/report_content.cpp +++ b/lib/csapi/report_content.cpp @@ -4,23 +4,18 @@ #include "report_content.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; -static const auto ReportContentJobName = QStringLiteral("ReportContentJob"); - -ReportContentJob::ReportContentJob(const QString& roomId, const QString& eventId, int score, const QString& reason) - : BaseJob(HttpVerb::Post, ReportContentJobName, - basePath % "/rooms/" % roomId % "/report/" % eventId) +ReportContentJob::ReportContentJob(const QString& roomId, const QString& eventId, + int score, const QString& reason) + : BaseJob(HttpVerb::Post, QStringLiteral("ReportContentJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/report/" % eventId) { QJsonObject _data; addParam<>(_data, QStringLiteral("score"), score); addParam<>(_data, QStringLiteral("reason"), reason); - setRequestData(_data); + setRequestData(std::move(_data)); } - diff --git a/lib/csapi/report_content.h b/lib/csapi/report_content.h index a20c6838..375e1829 100644 --- a/lib/csapi/report_content.h +++ b/lib/csapi/report_content.h @@ -6,30 +6,32 @@ #include "jobs/basejob.h" -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Reports an event as inappropriate. + * + * Reports an event as inappropriate to the server, which may then notify + * the appropriate people. + */ +class ReportContentJob : public BaseJob { +public: + /*! \brief Reports an event as inappropriate. + * + * \param roomId + * The room in which the event being reported is located. + * + * \param eventId + * The event to report. + * + * \param score + * The score to rate this content as where -100 is most offensive + * and 0 is inoffensive. + * + * \param reason + * The reason the content is being reported. May be blank. + */ + explicit ReportContentJob(const QString& roomId, const QString& eventId, + int score, const QString& reason); +}; - /// Reports an event as inappropriate. - /// - /// Reports an event as inappropriate to the server, which may then notify - /// the appropriate people. - class ReportContentJob : public BaseJob - { - public: - /*! Reports an event as inappropriate. - * \param roomId - * The room in which the event being reported is located. - * \param eventId - * The event to report. - * \param score - * The score to rate this content as where -100 is most offensive - * and 0 is inoffensive. - * \param reason - * The reason the content is being reported. May be blank. - */ - explicit ReportContentJob(const QString& roomId, const QString& eventId, int score, const QString& reason); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/room_send.cpp b/lib/csapi/room_send.cpp index 0d25eb69..63986c56 100644 --- a/lib/csapi/room_send.cpp +++ b/lib/csapi/room_send.cpp @@ -4,41 +4,16 @@ #include "room_send.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; -class SendMessageJob::Private -{ - public: - QString eventId; -}; - -static const auto SendMessageJobName = QStringLiteral("SendMessageJob"); - -SendMessageJob::SendMessageJob(const QString& roomId, const QString& eventType, const QString& txnId, const QJsonObject& body) - : BaseJob(HttpVerb::Put, SendMessageJobName, - basePath % "/rooms/" % roomId % "/send/" % eventType % "/" % txnId) - , d(new Private) +SendMessageJob::SendMessageJob(const QString& roomId, const QString& eventType, + const QString& txnId, const QJsonObject& body) + : BaseJob(HttpVerb::Put, QStringLiteral("SendMessageJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/send/" % eventType % "/" % txnId) { setRequestData(Data(toJson(body))); + addExpectedKey("event_id"); } - -SendMessageJob::~SendMessageJob() = default; - -const QString& SendMessageJob::eventId() const -{ - return d->eventId; -} - -BaseJob::Status SendMessageJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("event_id"_ls), d->eventId); - return Success; -} - diff --git a/lib/csapi/room_send.h b/lib/csapi/room_send.h index 85c298e0..39460aca 100644 --- a/lib/csapi/room_send.h +++ b/lib/csapi/room_send.h @@ -6,55 +6,49 @@ #include "jobs/basejob.h" -#include <QtCore/QJsonObject> - -namespace QMatrixClient -{ - // Operations - - /// Send a message event to the given room. - /// - /// This endpoint is used to send a message event to a room. Message events - /// allow access to historical events and pagination, making them suited - /// for "once-off" activity in a room. - /// - /// The body of the request should be the content object of the event; the - /// fields in this object will vary depending on the type of event. See - /// `Room Events`_ for the m. event specification. - class SendMessageJob : public BaseJob - { - public: - /*! Send a message event to the given room. - * \param roomId - * The room to send the event to. - * \param eventType - * The type of event to send. - * \param txnId - * The transaction ID for this event. Clients should generate an - * ID unique across requests with the same access token; it will be - * used by the server to ensure idempotency of requests. - * \param body - * This endpoint is used to send a message event to a room. Message events - * allow access to historical events and pagination, making them suited - * for "once-off" activity in a room. - * - * The body of the request should be the content object of the event; the - * fields in this object will vary depending on the type of event. See - * `Room Events`_ for the m. event specification. - */ - explicit SendMessageJob(const QString& roomId, const QString& eventType, const QString& txnId, const QJsonObject& body = {}); - ~SendMessageJob() override; - - // Result properties - - /// A unique identifier for the event. - const QString& eventId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +namespace Quotient { + +/*! \brief Send a message event to the given room. + * + * This endpoint is used to send a message event to a room. Message events + * allow access to historical events and pagination, making them suited + * for "once-off" activity in a room. + * + * The body of the request should be the content object of the event; the + * fields in this object will vary depending on the type of event. See + * `Room Events`_ for the m. event specification. + */ +class SendMessageJob : public BaseJob { +public: + /*! \brief Send a message event to the given room. + * + * \param roomId + * The room to send the event to. + * + * \param eventType + * The type of event to send. + * + * \param txnId + * The transaction ID for this event. Clients should generate an + * ID unique across requests with the same access token; it will be + * used by the server to ensure idempotency of requests. + * + * \param body + * This endpoint is used to send a message event to a room. Message events + * allow access to historical events and pagination, making them suited + * for "once-off" activity in a room. + * + * The body of the request should be the content object of the event; the + * fields in this object will vary depending on the type of event. See + * `Room Events`_ for the m. event specification. + */ + explicit SendMessageJob(const QString& roomId, const QString& eventType, + const QString& txnId, const QJsonObject& body = {}); + + // Result properties + + /// A unique identifier for the event. + QString eventId() const { return loadFromJson<QString>("event_id"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp index 3aa7d736..e18108ac 100644 --- a/lib/csapi/room_state.cpp +++ b/lib/csapi/room_state.cpp @@ -4,71 +4,18 @@ #include "room_state.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; -class SetRoomStateWithKeyJob::Private -{ - public: - QString eventId; -}; - -static const auto SetRoomStateWithKeyJobName = QStringLiteral("SetRoomStateWithKeyJob"); - -SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey, const QJsonObject& body) - : BaseJob(HttpVerb::Put, SetRoomStateWithKeyJobName, - basePath % "/rooms/" % roomId % "/state/" % eventType % "/" % stateKey) - , d(new Private) +SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId, + const QString& eventType, + const QString& stateKey, + const QJsonObject& body) + : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomStateWithKeyJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/state/" % eventType % "/" % stateKey) { setRequestData(Data(toJson(body))); + addExpectedKey("event_id"); } - -SetRoomStateWithKeyJob::~SetRoomStateWithKeyJob() = default; - -const QString& SetRoomStateWithKeyJob::eventId() const -{ - return d->eventId; -} - -BaseJob::Status SetRoomStateWithKeyJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("event_id"_ls), d->eventId); - return Success; -} - -class SetRoomStateJob::Private -{ - public: - QString eventId; -}; - -static const auto SetRoomStateJobName = QStringLiteral("SetRoomStateJob"); - -SetRoomStateJob::SetRoomStateJob(const QString& roomId, const QString& eventType, const QJsonObject& body) - : BaseJob(HttpVerb::Put, SetRoomStateJobName, - basePath % "/rooms/" % roomId % "/state/" % eventType) - , d(new Private) -{ - setRequestData(Data(toJson(body))); -} - -SetRoomStateJob::~SetRoomStateJob() = default; - -const QString& SetRoomStateJob::eventId() const -{ - return d->eventId; -} - -BaseJob::Status SetRoomStateJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("event_id"_ls), d->eventId); - return Success; -} - diff --git a/lib/csapi/room_state.h b/lib/csapi/room_state.h index 67420545..447605ff 100644 --- a/lib/csapi/room_state.h +++ b/lib/csapi/room_state.h @@ -6,113 +6,76 @@ #include "jobs/basejob.h" -#include <QtCore/QJsonObject> +namespace Quotient { -namespace QMatrixClient -{ - // Operations - - /// Send a state event to the given room. - /// - /// State events can be sent using this endpoint. These events will be - /// overwritten if ``<room id>``, ``<event type>`` and ``<state key>`` all - /// match. - /// - /// Requests to this endpoint **cannot use transaction IDs** - /// like other ``PUT`` paths because they cannot be differentiated from the - /// ``state_key``. Furthermore, ``POST`` is unsupported on state paths. - /// - /// The body of the request should be the content object of the event; the - /// fields in this object will vary depending on the type of event. See - /// `Room Events`_ for the ``m.`` event specification. - class SetRoomStateWithKeyJob : public BaseJob - { - public: - /*! Send a state event to the given room. - * \param roomId - * The room to set the state in - * \param eventType - * The type of event to send. - * \param stateKey - * The state_key for the state to send. Defaults to the empty string. - * \param body - * State events can be sent using this endpoint. These events will be - * overwritten if ``<room id>``, ``<event type>`` and ``<state key>`` all - * match. - * - * Requests to this endpoint **cannot use transaction IDs** - * like other ``PUT`` paths because they cannot be differentiated from the - * ``state_key``. Furthermore, ``POST`` is unsupported on state paths. - * - * The body of the request should be the content object of the event; the - * fields in this object will vary depending on the type of event. See - * `Room Events`_ for the ``m.`` event specification. - */ - explicit SetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey, const QJsonObject& body = {}); - ~SetRoomStateWithKeyJob() override; - - // Result properties - - /// A unique identifier for the event. - const QString& eventId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Send a state event to the given room. - /// - /// State events can be sent using this endpoint. This endpoint is - /// equivalent to calling `/rooms/{roomId}/state/{eventType}/{stateKey}` - /// with an empty `stateKey`. Previous state events with matching - /// `<roomId>` and `<eventType>`, and empty `<stateKey>`, will be overwritten. - /// - /// Requests to this endpoint **cannot use transaction IDs** - /// like other ``PUT`` paths because they cannot be differentiated from the - /// ``state_key``. Furthermore, ``POST`` is unsupported on state paths. - /// - /// The body of the request should be the content object of the event; the - /// fields in this object will vary depending on the type of event. See - /// `Room Events`_ for the ``m.`` event specification. - class SetRoomStateJob : public BaseJob - { - public: - /*! Send a state event to the given room. - * \param roomId - * The room to set the state in - * \param eventType - * The type of event to send. - * \param body - * State events can be sent using this endpoint. This endpoint is - * equivalent to calling `/rooms/{roomId}/state/{eventType}/{stateKey}` - * with an empty `stateKey`. Previous state events with matching - * `<roomId>` and `<eventType>`, and empty `<stateKey>`, will be overwritten. - * - * Requests to this endpoint **cannot use transaction IDs** - * like other ``PUT`` paths because they cannot be differentiated from the - * ``state_key``. Furthermore, ``POST`` is unsupported on state paths. - * - * The body of the request should be the content object of the event; the - * fields in this object will vary depending on the type of event. See - * `Room Events`_ for the ``m.`` event specification. - */ - explicit SetRoomStateJob(const QString& roomId, const QString& eventType, const QJsonObject& body = {}); - ~SetRoomStateJob() override; - - // Result properties +/*! \brief Send a state event to the given room. + * + * .. For backwards compatibility with older links... + * .. _`put-matrix-client-r0-rooms-roomid-state-eventtype`: + * + * State events can be sent using this endpoint. These events will be + * overwritten if ``<room id>``, ``<event type>`` and ``<state key>`` all + * match. + * + * Requests to this endpoint **cannot use transaction IDs** + * like other ``PUT`` paths because they cannot be differentiated from the + * ``state_key``. Furthermore, ``POST`` is unsupported on state paths. + * + * The body of the request should be the content object of the event; the + * fields in this object will vary depending on the type of event. See + * `Room Events`_ for the ``m.`` event specification. + * + * If the event type being sent is ``m.room.canonical_alias`` servers + * SHOULD ensure that any new aliases being listed in the event are valid + * per their grammar/syntax and that they point to the room ID where the + * state event is to be sent. Servers do not validate aliases which are + * being removed or are already present in the state event. + */ +class SetRoomStateWithKeyJob : public BaseJob { +public: + /*! \brief Send a state event to the given room. + * + * \param roomId + * The room to set the state in + * + * \param eventType + * The type of event to send. + * + * \param stateKey + * The state_key for the state to send. Defaults to the empty string. When + * an empty string, the trailing slash on this endpoint is optional. + * + * \param body + * .. For backwards compatibility with older links... + * .. _`put-matrix-client-r0-rooms-roomid-state-eventtype`: + * + * State events can be sent using this endpoint. These events will be + * overwritten if ``<room id>``, ``<event type>`` and ``<state key>`` all + * match. + * + * Requests to this endpoint **cannot use transaction IDs** + * like other ``PUT`` paths because they cannot be differentiated from the + * ``state_key``. Furthermore, ``POST`` is unsupported on state paths. + * + * The body of the request should be the content object of the event; the + * fields in this object will vary depending on the type of event. See + * `Room Events`_ for the ``m.`` event specification. + * + * If the event type being sent is ``m.room.canonical_alias`` servers + * SHOULD ensure that any new aliases being listed in the event are valid + * per their grammar/syntax and that they point to the room ID where the + * state event is to be sent. Servers do not validate aliases which are + * being removed or are already present in the state event. + */ + explicit SetRoomStateWithKeyJob(const QString& roomId, + const QString& eventType, + const QString& stateKey, + const QJsonObject& body = {}); - /// A unique identifier for the event. - const QString& eventId() const; + // Result properties - protected: - Status parseJson(const QJsonDocument& data) override; + /// A unique identifier for the event. + QString eventId() const { return loadFromJson<QString>("event_id"_ls); } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp index f58fd675..e3791b08 100644 --- a/lib/csapi/room_upgrades.cpp +++ b/lib/csapi/room_upgrades.cpp @@ -4,46 +4,17 @@ #include "room_upgrades.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class UpgradeRoomJob::Private -{ - public: - QString replacementRoom; -}; - -static const auto UpgradeRoomJobName = QStringLiteral("UpgradeRoomJob"); +using namespace Quotient; UpgradeRoomJob::UpgradeRoomJob(const QString& roomId, const QString& newVersion) - : BaseJob(HttpVerb::Post, UpgradeRoomJobName, - basePath % "/rooms/" % roomId % "/upgrade") - , d(new Private) + : BaseJob(HttpVerb::Post, QStringLiteral("UpgradeRoomJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/upgrade") { QJsonObject _data; addParam<>(_data, QStringLiteral("new_version"), newVersion); - setRequestData(_data); + setRequestData(std::move(_data)); + addExpectedKey("replacement_room"); } - -UpgradeRoomJob::~UpgradeRoomJob() = default; - -const QString& UpgradeRoomJob::replacementRoom() const -{ - return d->replacementRoom; -} - -BaseJob::Status UpgradeRoomJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("replacement_room"_ls)) - return { JsonParseError, - "The key 'replacement_room' not found in the response" }; - fromJson(json.value("replacement_room"_ls), d->replacementRoom); - return Success; -} - diff --git a/lib/csapi/room_upgrades.h b/lib/csapi/room_upgrades.h index 4da5941a..58327587 100644 --- a/lib/csapi/room_upgrades.h +++ b/lib/csapi/room_upgrades.h @@ -6,36 +6,31 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations - - /// Upgrades a room to a new room version. - /// - /// Upgrades the given room to a particular room version. - class UpgradeRoomJob : public BaseJob +/*! \brief Upgrades a room to a new room version. + * + * Upgrades the given room to a particular room version. + */ +class UpgradeRoomJob : public BaseJob { +public: + /*! \brief Upgrades a room to a new room version. + * + * \param roomId + * The ID of the room to upgrade. + * + * \param newVersion + * The new version for the room. + */ + explicit UpgradeRoomJob(const QString& roomId, const QString& newVersion); + + // Result properties + + /// The ID of the new room. + QString replacementRoom() const { - public: - /*! Upgrades a room to a new room version. - * \param roomId - * The ID of the room to upgrade. - * \param newVersion - * The new version for the room. - */ - explicit UpgradeRoomJob(const QString& roomId, const QString& newVersion); - ~UpgradeRoomJob() override; - - // Result properties - - /// The ID of the new room. - const QString& replacementRoom() const; - - protected: - Status parseJson(const QJsonDocument& data) override; + return loadFromJson<QString>("replacement_room"_ls); + } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/rooms.cpp b/lib/csapi/rooms.cpp index 0b08ccec..724d941f 100644 --- a/lib/csapi/rooms.cpp +++ b/lib/csapi/rooms.cpp @@ -4,117 +4,59 @@ #include "rooms.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; -class GetOneRoomEventJob::Private -{ - public: - EventPtr data; -}; - -QUrl GetOneRoomEventJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId) +QUrl GetOneRoomEventJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/event/" % eventId); + QStringLiteral("/_matrix/client/r0") + % "/rooms/" % roomId % "/event/" + % eventId); } -static const auto GetOneRoomEventJobName = QStringLiteral("GetOneRoomEventJob"); +GetOneRoomEventJob::GetOneRoomEventJob(const QString& roomId, + const QString& eventId) + : BaseJob(HttpVerb::Get, QStringLiteral("GetOneRoomEventJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/event/" % eventId) +{} -GetOneRoomEventJob::GetOneRoomEventJob(const QString& roomId, const QString& eventId) - : BaseJob(HttpVerb::Get, GetOneRoomEventJobName, - basePath % "/rooms/" % roomId % "/event/" % eventId) - , d(new Private) -{ -} - -GetOneRoomEventJob::~GetOneRoomEventJob() = default; - -EventPtr&& GetOneRoomEventJob::data() -{ - return std::move(d->data); -} - -BaseJob::Status GetOneRoomEventJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - -QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType, const QString& stateKey) +QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventType, + const QString& stateKey) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/state/" % eventType % "/" % stateKey); + QStringLiteral("/_matrix/client/r0") + % "/rooms/" % roomId % "/state/" + % eventType % "/" % stateKey); } -static const auto GetRoomStateWithKeyJobName = QStringLiteral("GetRoomStateWithKeyJob"); - -GetRoomStateWithKeyJob::GetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey) - : BaseJob(HttpVerb::Get, GetRoomStateWithKeyJobName, - basePath % "/rooms/" % roomId % "/state/" % eventType % "/" % stateKey) -{ -} - -QUrl GetRoomStateByTypeJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType) -{ - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/state/" % eventType); -} - -static const auto GetRoomStateByTypeJobName = QStringLiteral("GetRoomStateByTypeJob"); - -GetRoomStateByTypeJob::GetRoomStateByTypeJob(const QString& roomId, const QString& eventType) - : BaseJob(HttpVerb::Get, GetRoomStateByTypeJobName, - basePath % "/rooms/" % roomId % "/state/" % eventType) -{ -} - -class GetRoomStateJob::Private -{ - public: - StateEvents data; -}; +GetRoomStateWithKeyJob::GetRoomStateWithKeyJob(const QString& roomId, + const QString& eventType, + const QString& stateKey) + : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateWithKeyJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/state/" % eventType % "/" % stateKey) +{} QUrl GetRoomStateJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/state"); + QStringLiteral("/_matrix/client/r0") + % "/rooms/" % roomId % "/state"); } -static const auto GetRoomStateJobName = QStringLiteral("GetRoomStateJob"); - GetRoomStateJob::GetRoomStateJob(const QString& roomId) - : BaseJob(HttpVerb::Get, GetRoomStateJobName, - basePath % "/rooms/" % roomId % "/state") - , d(new Private) -{ -} - -GetRoomStateJob::~GetRoomStateJob() = default; - -StateEvents&& GetRoomStateJob::data() -{ - return std::move(d->data); -} + : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/state") +{} -BaseJob::Status GetRoomStateJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - -class GetMembersByRoomJob::Private -{ - public: - EventsArray<RoomMemberEvent> chunk; -}; - -BaseJob::Query queryToGetMembersByRoom(const QString& at, const QString& membership, const QString& notMembership) +auto queryToGetMembersByRoom(const QString& at, const QString& membership, + const QString& notMembership) { BaseJob::Query _q; addParam<IfNotEmpty>(_q, QStringLiteral("at"), at); @@ -123,83 +65,37 @@ BaseJob::Query queryToGetMembersByRoom(const QString& at, const QString& members return _q; } -QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& at, const QString& membership, const QString& notMembership) +QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& at, + const QString& membership, + const QString& notMembership) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/members", - queryToGetMembersByRoom(at, membership, notMembership)); + return BaseJob::makeRequestUrl( + std::move(baseUrl), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/members", + queryToGetMembersByRoom(at, membership, notMembership)); } -static const auto GetMembersByRoomJobName = QStringLiteral("GetMembersByRoomJob"); - -GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId, const QString& at, const QString& membership, const QString& notMembership) - : BaseJob(HttpVerb::Get, GetMembersByRoomJobName, - basePath % "/rooms/" % roomId % "/members", - queryToGetMembersByRoom(at, membership, notMembership)) - , d(new Private) -{ -} - -GetMembersByRoomJob::~GetMembersByRoomJob() = default; - -EventsArray<RoomMemberEvent>&& GetMembersByRoomJob::chunk() -{ - return std::move(d->chunk); -} - -BaseJob::Status GetMembersByRoomJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("chunk"_ls), d->chunk); - return Success; -} +GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId, + const QString& at, + const QString& membership, + const QString& notMembership) + : BaseJob(HttpVerb::Get, QStringLiteral("GetMembersByRoomJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/members", + queryToGetMembersByRoom(at, membership, notMembership)) +{} -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<GetJoinedMembersByRoomJob::RoomMember> - { - static void fillFrom(const QJsonObject& jo, GetJoinedMembersByRoomJob::RoomMember& result) - { - fromJson(jo.value("display_name"_ls), result.displayName); - fromJson(jo.value("avatar_url"_ls), result.avatarUrl); - } - }; -} // namespace QMatrixClient - -class GetJoinedMembersByRoomJob::Private -{ - public: - QHash<QString, RoomMember> joined; -}; - -QUrl GetJoinedMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) +QUrl GetJoinedMembersByRoomJob::makeRequestUrl(QUrl baseUrl, + const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/joined_members"); + QStringLiteral("/_matrix/client/r0") + % "/rooms/" % roomId % "/joined_members"); } -static const auto GetJoinedMembersByRoomJobName = QStringLiteral("GetJoinedMembersByRoomJob"); - GetJoinedMembersByRoomJob::GetJoinedMembersByRoomJob(const QString& roomId) - : BaseJob(HttpVerb::Get, GetJoinedMembersByRoomJobName, - basePath % "/rooms/" % roomId % "/joined_members") - , d(new Private) -{ -} - -GetJoinedMembersByRoomJob::~GetJoinedMembersByRoomJob() = default; - -const QHash<QString, GetJoinedMembersByRoomJob::RoomMember>& GetJoinedMembersByRoomJob::joined() const -{ - return d->joined; -} - -BaseJob::Status GetJoinedMembersByRoomJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("joined"_ls), d->joined); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedMembersByRoomJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/joined_members") +{} diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index b4d3d9b6..f0bfa349 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -4,240 +4,217 @@ #pragma once +#include "events/eventloader.h" +#include "events/roommemberevent.h" #include "jobs/basejob.h" -#include "events/roommemberevent.h" -#include "events/eventloader.h" -#include <QtCore/QHash> -#include "converters.h" - -namespace QMatrixClient -{ - // Operations - - /// Get a single event by event ID. - /// - /// Get a single event based on ``roomId/eventId``. You must have permission to - /// retrieve this event e.g. by being a member in the room for this event. - class GetOneRoomEventJob : public BaseJob +namespace Quotient { + +/*! \brief Get a single event by event ID. + * + * Get a single event based on ``roomId/eventId``. You must have permission to + * retrieve this event e.g. by being a member in the room for this event. + */ +class GetOneRoomEventJob : public BaseJob { +public: + /*! \brief Get a single event by event ID. + * + * \param roomId + * The ID of the room the event is in. + * + * \param eventId + * The event ID to get. + */ + explicit GetOneRoomEventJob(const QString& roomId, const QString& eventId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetOneRoomEventJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId); + + // Result properties + + /// The full event. + EventPtr event() { return fromJson<EventPtr>(jsonData()); } +}; + +/*! \brief Get the state identified by the type and key. + * + * .. For backwards compatibility with older links... + * .. _`get-matrix-client-r0-rooms-roomid-state-eventtype`: + * + * Looks up the contents of a state event in a room. If the user is + * joined to the room then the state is taken from the current + * state of the room. If the user has left the room then the state is + * taken from the state of the room when they left. + */ +class GetRoomStateWithKeyJob : public BaseJob { +public: + /*! \brief Get the state identified by the type and key. + * + * \param roomId + * The room to look up the state in. + * + * \param eventType + * The type of state to look up. + * + * \param stateKey + * The key of the state to look up. Defaults to an empty string. When + * an empty string, the trailing slash on this endpoint is optional. + */ + explicit GetRoomStateWithKeyJob(const QString& roomId, + const QString& eventType, + const QString& stateKey); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRoomStateWithKeyJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventType, + const QString& stateKey); +}; + +/*! \brief Get all state events in the current state of a room. + * + * Get the state events for the current state of a room. + */ +class GetRoomStateJob : public BaseJob { +public: + /*! \brief Get all state events in the current state of a room. + * + * \param roomId + * The room to look up the state for. + */ + explicit GetRoomStateJob(const QString& roomId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRoomStateJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); + + // Result properties + + /// The current state of the room + StateEvents events() { return fromJson<StateEvents>(jsonData()); } +}; + +/*! \brief Get the m.room.member events for the room. + * + * Get the list of members for this room. + */ +class GetMembersByRoomJob : public BaseJob { +public: + /*! \brief Get the m.room.member events for the room. + * + * \param roomId + * The room to get the member events for. + * + * \param at + * The point in time (pagination token) to return members for in the room. + * This token can be obtained from a ``prev_batch`` token returned for + * each room by the sync API. Defaults to the current state of the room, + * as determined by the server. + * + * \param membership + * The kind of membership to filter for. Defaults to no filtering if + * unspecified. When specified alongside ``not_membership``, the two + * parameters create an 'or' condition: either the membership *is* + * the same as ``membership`` **or** *is not* the same as + * ``not_membership``. + * + * \param notMembership + * The kind of membership to exclude from the results. Defaults to no + * filtering if unspecified. + */ + explicit GetMembersByRoomJob(const QString& roomId, const QString& at = {}, + const QString& membership = {}, + const QString& notMembership = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetMembersByRoomJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& at = {}, + const QString& membership = {}, + const QString& notMembership = {}); + + // Result properties + + /// Get the list of members for this room. + EventsArray<RoomMemberEvent> chunk() { - public: - /*! Get a single event by event ID. - * \param roomId - * The ID of the room the event is in. - * \param eventId - * The event ID to get. - */ - explicit GetOneRoomEventJob(const QString& roomId, const QString& eventId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetOneRoomEventJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId); - - ~GetOneRoomEventJob() override; - - // Result properties - - /// The full event. - EventPtr&& data(); - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; + return takeFromJson<EventsArray<RoomMemberEvent>>("chunk"_ls); + } +}; + +/*! \brief Gets the list of currently joined users and their profile data. + * + * This API returns a map of MXIDs to member info objects for members of the + * room. The current user must be in the room for it to work, unless it is an + * Application Service in which case any of the AS's users must be in the room. + * This API is primarily for Application Services and should be faster to + * respond than ``/members`` as it can be implemented more efficiently on the + * server. + */ +class GetJoinedMembersByRoomJob : public BaseJob { +public: + // Inner data structures + + /// This API returns a map of MXIDs to member info objects for members of + /// the room. The current user must be in the room for it to work, unless it + /// is an Application Service in which case any of the AS's users must be in + /// the room. This API is primarily for Application Services and should be + /// faster to respond than ``/members`` as it can be implemented more + /// efficiently on the server. + struct RoomMember { + /// The display name of the user this object is representing. + QString displayName; + /// The mxc avatar url of the user this object is representing. + QString avatarUrl; }; - /// Get the state identified by the type and key. - /// - /// Looks up the contents of a state event in a room. If the user is - /// joined to the room then the state is taken from the current - /// state of the room. If the user has left the room then the state is - /// taken from the state of the room when they left. - class GetRoomStateWithKeyJob : public BaseJob - { - public: - /*! Get the state identified by the type and key. - * \param roomId - * The room to look up the state in. - * \param eventType - * The type of state to look up. - * \param stateKey - * The key of the state to look up. - */ - explicit GetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetRoomStateWithKeyJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType, const QString& stateKey); + // Construction/destruction - }; + /*! \brief Gets the list of currently joined users and their profile data. + * + * \param roomId + * The room to get the members of. + */ + explicit GetJoinedMembersByRoomJob(const QString& roomId); - /// Get the state identified by the type, with the empty state key. - /// - /// Looks up the contents of a state event in a room. If the user is - /// joined to the room then the state is taken from the current - /// state of the room. If the user has left the room then the state is - /// taken from the state of the room when they left. - /// - /// This looks up the state event with the empty state key. - class GetRoomStateByTypeJob : public BaseJob - { - public: - /*! Get the state identified by the type, with the empty state key. - * \param roomId - * The room to look up the state in. - * \param eventType - * The type of state to look up. - */ - explicit GetRoomStateByTypeJob(const QString& roomId, const QString& eventType); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetRoomStateByTypeJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetJoinedMembersByRoomJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); - }; + // Result properties - /// Get all state events in the current state of a room. - /// - /// Get the state events for the current state of a room. - class GetRoomStateJob : public BaseJob + /// A map from user ID to a RoomMember object. + QHash<QString, RoomMember> joined() const { - public: - /*! Get all state events in the current state of a room. - * \param roomId - * The room to look up the state for. - */ - explicit GetRoomStateJob(const QString& roomId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetRoomStateJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); - - ~GetRoomStateJob() override; - - // Result properties - - /// If the user is a member of the room this will be the - /// current state of the room as a list of events. If the user - /// has left the room then this will be the state of the room - /// when they left as a list of events. - StateEvents&& data(); - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Get the m.room.member events for the room. - /// - /// Get the list of members for this room. - class GetMembersByRoomJob : public BaseJob + return loadFromJson<QHash<QString, RoomMember>>("joined"_ls); + } +}; + +template <> +struct JsonObjectConverter<GetJoinedMembersByRoomJob::RoomMember> { + static void fillFrom(const QJsonObject& jo, + GetJoinedMembersByRoomJob::RoomMember& result) { - public: - /*! Get the m.room.member events for the room. - * \param roomId - * The room to get the member events for. - * \param at - * The token defining the timeline position as-of which to return - * the list of members. This token can be obtained from a batch token - * returned for each room by the sync API, or from - * a ``start``/``end`` token returned by a ``/messages`` request. - * \param membership - * Only return users with the specified membership - * \param notMembership - * Only return users with membership state other than specified - */ - explicit GetMembersByRoomJob(const QString& roomId, const QString& at = {}, const QString& membership = {}, const QString& notMembership = {}); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetMembersByRoomJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& at = {}, const QString& membership = {}, const QString& notMembership = {}); - - ~GetMembersByRoomJob() override; - - // Result properties - - /// Get the list of members for this room. - EventsArray<RoomMemberEvent>&& chunk(); - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; + fromJson(jo.value("display_name"_ls), result.displayName); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); + } +}; - /// Gets the list of currently joined users and their profile data. - /// - /// This API returns a map of MXIDs to member info objects for members of the room. The current user must be in the room for it to work, unless it is an Application Service in which case any of the AS's users must be in the room. This API is primarily for Application Services and should be faster to respond than ``/members`` as it can be implemented more efficiently on the server. - class GetJoinedMembersByRoomJob : public BaseJob - { - public: - // Inner data structures - - /// This API returns a map of MXIDs to member info objects for members of the room. The current user must be in the room for it to work, unless it is an Application Service in which case any of the AS's users must be in the room. This API is primarily for Application Services and should be faster to respond than ``/members`` as it can be implemented more efficiently on the server. - struct RoomMember - { - /// The display name of the user this object is representing. - QString displayName; - /// The mxc avatar url of the user this object is representing. - QString avatarUrl; - }; - - // Construction/destruction - - /*! Gets the list of currently joined users and their profile data. - * \param roomId - * The room to get the members of. - */ - explicit GetJoinedMembersByRoomJob(const QString& roomId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetJoinedMembersByRoomJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); - - ~GetJoinedMembersByRoomJob() override; - - // Result properties - - /// A map from user ID to a RoomMember object. - const QHash<QString, RoomMember>& joined() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp deleted file mode 100644 index a5f83c79..00000000 --- a/lib/csapi/search.cpp +++ /dev/null @@ -1,172 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "search.h" - -#include "converters.h" - -#include <QtCore/QStringBuilder> - -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<SearchJob::IncludeEventContext> - { - static void dumpTo(QJsonObject& jo, const SearchJob::IncludeEventContext& pod) - { - addParam<IfNotEmpty>(jo, QStringLiteral("before_limit"), pod.beforeLimit); - addParam<IfNotEmpty>(jo, QStringLiteral("after_limit"), pod.afterLimit); - addParam<IfNotEmpty>(jo, QStringLiteral("include_profile"), pod.includeProfile); - } - }; - - template <> struct JsonObjectConverter<SearchJob::Group> - { - static void dumpTo(QJsonObject& jo, const SearchJob::Group& pod) - { - addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key); - } - }; - - template <> struct JsonObjectConverter<SearchJob::Groupings> - { - static void dumpTo(QJsonObject& jo, const SearchJob::Groupings& pod) - { - addParam<IfNotEmpty>(jo, QStringLiteral("group_by"), pod.groupBy); - } - }; - - template <> struct JsonObjectConverter<SearchJob::RoomEventsCriteria> - { - static void dumpTo(QJsonObject& jo, const SearchJob::RoomEventsCriteria& pod) - { - addParam<>(jo, QStringLiteral("search_term"), pod.searchTerm); - addParam<IfNotEmpty>(jo, QStringLiteral("keys"), pod.keys); - addParam<IfNotEmpty>(jo, QStringLiteral("filter"), pod.filter); - addParam<IfNotEmpty>(jo, QStringLiteral("order_by"), pod.orderBy); - addParam<IfNotEmpty>(jo, QStringLiteral("event_context"), pod.eventContext); - addParam<IfNotEmpty>(jo, QStringLiteral("include_state"), pod.includeState); - addParam<IfNotEmpty>(jo, QStringLiteral("groupings"), pod.groupings); - } - }; - - template <> struct JsonObjectConverter<SearchJob::Categories> - { - static void dumpTo(QJsonObject& jo, const SearchJob::Categories& pod) - { - addParam<IfNotEmpty>(jo, QStringLiteral("room_events"), pod.roomEvents); - } - }; - - template <> struct JsonObjectConverter<SearchJob::UserProfile> - { - static void fillFrom(const QJsonObject& jo, SearchJob::UserProfile& result) - { - fromJson(jo.value("displayname"_ls), result.displayname); - fromJson(jo.value("avatar_url"_ls), result.avatarUrl); - } - }; - - template <> struct JsonObjectConverter<SearchJob::EventContext> - { - static void fillFrom(const QJsonObject& jo, SearchJob::EventContext& result) - { - fromJson(jo.value("start"_ls), result.begin); - fromJson(jo.value("end"_ls), result.end); - fromJson(jo.value("profile_info"_ls), result.profileInfo); - fromJson(jo.value("events_before"_ls), result.eventsBefore); - fromJson(jo.value("events_after"_ls), result.eventsAfter); - } - }; - - template <> struct JsonObjectConverter<SearchJob::Result> - { - static void fillFrom(const QJsonObject& jo, SearchJob::Result& result) - { - fromJson(jo.value("rank"_ls), result.rank); - fromJson(jo.value("result"_ls), result.result); - fromJson(jo.value("context"_ls), result.context); - } - }; - - template <> struct JsonObjectConverter<SearchJob::GroupValue> - { - static void fillFrom(const QJsonObject& jo, SearchJob::GroupValue& result) - { - fromJson(jo.value("next_batch"_ls), result.nextBatch); - fromJson(jo.value("order"_ls), result.order); - fromJson(jo.value("results"_ls), result.results); - } - }; - - template <> struct JsonObjectConverter<SearchJob::ResultRoomEvents> - { - static void fillFrom(const QJsonObject& jo, SearchJob::ResultRoomEvents& result) - { - fromJson(jo.value("count"_ls), result.count); - fromJson(jo.value("highlights"_ls), result.highlights); - fromJson(jo.value("results"_ls), result.results); - fromJson(jo.value("state"_ls), result.state); - fromJson(jo.value("groups"_ls), result.groups); - fromJson(jo.value("next_batch"_ls), result.nextBatch); - } - }; - - template <> struct JsonObjectConverter<SearchJob::ResultCategories> - { - static void fillFrom(const QJsonObject& jo, SearchJob::ResultCategories& result) - { - fromJson(jo.value("room_events"_ls), result.roomEvents); - } - }; -} // namespace QMatrixClient - -class SearchJob::Private -{ - public: - ResultCategories searchCategories; -}; - -BaseJob::Query queryToSearch(const QString& nextBatch) -{ - BaseJob::Query _q; - addParam<IfNotEmpty>(_q, QStringLiteral("next_batch"), nextBatch); - return _q; -} - -static const auto SearchJobName = QStringLiteral("SearchJob"); - -SearchJob::SearchJob(const Categories& searchCategories, const QString& nextBatch) - : BaseJob(HttpVerb::Post, SearchJobName, - basePath % "/search", - queryToSearch(nextBatch)) - , d(new Private) -{ - QJsonObject _data; - addParam<>(_data, QStringLiteral("search_categories"), searchCategories); - setRequestData(_data); -} - -SearchJob::~SearchJob() = default; - -const SearchJob::ResultCategories& SearchJob::searchCategories() const -{ - return d->searchCategories; -} - -BaseJob::Status SearchJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("search_categories"_ls)) - return { JsonParseError, - "The key 'search_categories' not found in the response" }; - fromJson(json.value("search_categories"_ls), d->searchCategories); - return Success; -} - diff --git a/lib/csapi/search.h b/lib/csapi/search.h deleted file mode 100644 index 86a0ee92..00000000 --- a/lib/csapi/search.h +++ /dev/null @@ -1,205 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "jobs/basejob.h" - -#include "csapi/definitions/room_event_filter.h" -#include "converters.h" -#include <QtCore/QVector> -#include "events/eventloader.h" -#include <unordered_map> -#include <QtCore/QHash> - -namespace QMatrixClient -{ - // Operations - - /// Perform a server-side search. - /// - /// Performs a full text search across different categories. - class SearchJob : public BaseJob - { - public: - // Inner data structures - - /// Configures whether any context for the events - /// returned are included in the response. - struct IncludeEventContext - { - /// How many events before the result are - /// returned. By default, this is ``5``. - Omittable<int> beforeLimit; - /// How many events after the result are - /// returned. By default, this is ``5``. - Omittable<int> afterLimit; - /// Requests that the server returns the - /// historic profile information for the users - /// that sent the events that were returned. - /// By default, this is ``false``. - Omittable<bool> includeProfile; - }; - - /// Configuration for group. - struct Group - { - /// Key that defines the group. - QString key; - }; - - /// Requests that the server partitions the result set - /// based on the provided list of keys. - struct Groupings - { - /// List of groups to request. - QVector<Group> groupBy; - }; - - /// Mapping of category name to search criteria. - struct RoomEventsCriteria - { - /// The string to search events for - QString searchTerm; - /// The keys to search. Defaults to all. - QStringList keys; - /// This takes a `filter`_. - Omittable<RoomEventFilter> filter; - /// The order in which to search for results. - /// By default, this is ``"rank"``. - QString orderBy; - /// Configures whether any context for the events - /// returned are included in the response. - Omittable<IncludeEventContext> eventContext; - /// Requests the server return the current state for - /// each room returned. - Omittable<bool> includeState; - /// Requests that the server partitions the result set - /// based on the provided list of keys. - Omittable<Groupings> groupings; - }; - - /// Describes which categories to search in and their criteria. - struct Categories - { - /// Mapping of category name to search criteria. - Omittable<RoomEventsCriteria> roomEvents; - }; - - /// Performs a full text search across different categories. - struct UserProfile - { - /// Performs a full text search across different categories. - QString displayname; - /// Performs a full text search across different categories. - QString avatarUrl; - }; - - /// Context for result, if requested. - struct EventContext - { - /// Pagination token for the start of the chunk - QString begin; - /// Pagination token for the end of the chunk - QString end; - /// The historic profile information of the - /// users that sent the events returned. - /// - /// The ``string`` key is the user ID for which - /// the profile belongs to. - QHash<QString, UserProfile> profileInfo; - /// Events just before the result. - RoomEvents eventsBefore; - /// Events just after the result. - RoomEvents eventsAfter; - }; - - /// The result object. - struct Result - { - /// A number that describes how closely this result matches the search. Higher is closer. - Omittable<double> rank; - /// The event that matched. - RoomEventPtr result; - /// Context for result, if requested. - Omittable<EventContext> context; - }; - - /// The results for a particular group value. - struct GroupValue - { - /// Token that can be used to get the next batch - /// of results in the group, by passing as the - /// `next_batch` parameter to the next call. If - /// this field is absent, there are no more - /// results in this group. - QString nextBatch; - /// Key that can be used to order different - /// groups. - Omittable<int> order; - /// Which results are in this group. - QStringList results; - }; - - /// Mapping of category name to search criteria. - struct ResultRoomEvents - { - /// An approximate count of the total number of results found. - Omittable<int> count; - /// List of words which should be highlighted, useful for stemming which may change the query terms. - QStringList highlights; - /// List of results in the requested order. - std::vector<Result> results; - /// The current state for every room in the results. - /// This is included if the request had the - /// ``include_state`` key set with a value of ``true``. - /// - /// The ``string`` key is the room ID for which the ``State - /// Event`` array belongs to. - std::unordered_map<QString, StateEvents> state; - /// Any groups that were requested. - /// - /// The outer ``string`` key is the group key requested (eg: ``room_id`` - /// or ``sender``). The inner ``string`` key is the grouped value (eg: - /// a room's ID or a user's ID). - QHash<QString, QHash<QString, GroupValue>> groups; - /// Token that can be used to get the next batch of - /// results, by passing as the `next_batch` parameter to - /// the next call. If this field is absent, there are no - /// more results. - QString nextBatch; - }; - - /// Describes which categories to search in and their criteria. - struct ResultCategories - { - /// Mapping of category name to search criteria. - Omittable<ResultRoomEvents> roomEvents; - }; - - // Construction/destruction - - /*! Perform a server-side search. - * \param searchCategories - * Describes which categories to search in and their criteria. - * \param nextBatch - * The point to return events from. If given, this should be a - * ``next_batch`` result from a previous call to this endpoint. - */ - explicit SearchJob(const Categories& searchCategories, const QString& nextBatch = {}); - ~SearchJob() override; - - // Result properties - - /// Describes which categories to search in and their criteria. - const ResultCategories& searchCategories() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient diff --git a/lib/csapi/sso_login_redirect.cpp b/lib/csapi/sso_login_redirect.cpp index 7323951c..85a18560 100644 --- a/lib/csapi/sso_login_redirect.cpp +++ b/lib/csapi/sso_login_redirect.cpp @@ -4,15 +4,11 @@ #include "sso_login_redirect.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; -BaseJob::Query queryToRedirectToSSO(const QString& redirectUrl) +auto queryToRedirectToSSO(const QString& redirectUrl) { BaseJob::Query _q; addParam<>(_q, QStringLiteral("redirectUrl"), redirectUrl); @@ -22,17 +18,13 @@ BaseJob::Query queryToRedirectToSSO(const QString& redirectUrl) QUrl RedirectToSSOJob::makeRequestUrl(QUrl baseUrl, const QString& redirectUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/login/sso/redirect", - queryToRedirectToSSO(redirectUrl)); + QStringLiteral("/_matrix/client/r0") + % "/login/sso/redirect", + queryToRedirectToSSO(redirectUrl)); } -static const auto RedirectToSSOJobName = QStringLiteral("RedirectToSSOJob"); - RedirectToSSOJob::RedirectToSSOJob(const QString& redirectUrl) - : BaseJob(HttpVerb::Get, RedirectToSSOJobName, - basePath % "/login/sso/redirect", - queryToRedirectToSSO(redirectUrl), - {}, false) -{ -} - + : BaseJob(HttpVerb::Get, QStringLiteral("RedirectToSSOJob"), + QStringLiteral("/_matrix/client/r0") % "/login/sso/redirect", + queryToRedirectToSSO(redirectUrl), {}, false) +{} diff --git a/lib/csapi/sso_login_redirect.h b/lib/csapi/sso_login_redirect.h index c09365b0..d6330e38 100644 --- a/lib/csapi/sso_login_redirect.h +++ b/lib/csapi/sso_login_redirect.h @@ -6,34 +6,31 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations - - /// Redirect the user's browser to the SSO interface. - /// - /// A web-based Matrix client should instruct the user's browser to - /// navigate to this endpoint in order to log in via SSO. - /// - /// The server MUST respond with an HTTP redirect to the SSO interface. - class RedirectToSSOJob : public BaseJob - { - public: - /*! Redirect the user's browser to the SSO interface. - * \param redirectUrl - * URI to which the user will be redirected after the homeserver has - * authenticated the user with SSO. - */ - explicit RedirectToSSOJob(const QString& redirectUrl); +/*! \brief Redirect the user's browser to the SSO interface. + * + * A web-based Matrix client should instruct the user's browser to + * navigate to this endpoint in order to log in via SSO. + * + * The server MUST respond with an HTTP redirect to the SSO interface. + */ +class RedirectToSSOJob : public BaseJob { +public: + /*! \brief Redirect the user's browser to the SSO interface. + * + * \param redirectUrl + * URI to which the user will be redirected after the homeserver has + * authenticated the user with SSO. + */ + explicit RedirectToSSOJob(const QString& redirectUrl); - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * RedirectToSSOJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& redirectUrl); + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for RedirectToSSOJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& redirectUrl); +}; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/tags.cpp b/lib/csapi/tags.cpp index 94026bb9..dc22dc18 100644 --- a/lib/csapi/tags.cpp +++ b/lib/csapi/tags.cpp @@ -4,85 +4,49 @@ #include "tags.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<GetRoomTagsJob::Tag> - { - static void fillFrom(QJsonObject jo, GetRoomTagsJob::Tag& result) - { - fromJson(jo.take("order"_ls), result.order); - fromJson(jo, result.additionalProperties); - } - }; -} // namespace QMatrixClient - -class GetRoomTagsJob::Private -{ - public: - QHash<QString, Tag> tags; -}; +using namespace Quotient; -QUrl GetRoomTagsJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId) +QUrl GetRoomTagsJob::makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/user/" % userId % "/rooms/" % roomId % "/tags"); + QStringLiteral("/_matrix/client/r0") % "/user/" + % userId % "/rooms/" % roomId % "/tags"); } -static const auto GetRoomTagsJobName = QStringLiteral("GetRoomTagsJob"); - GetRoomTagsJob::GetRoomTagsJob(const QString& userId, const QString& roomId) - : BaseJob(HttpVerb::Get, GetRoomTagsJobName, - basePath % "/user/" % userId % "/rooms/" % roomId % "/tags") - , d(new Private) -{ -} - -GetRoomTagsJob::~GetRoomTagsJob() = default; - -const QHash<QString, GetRoomTagsJob::Tag>& GetRoomTagsJob::tags() const -{ - return d->tags; -} - -BaseJob::Status GetRoomTagsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - fromJson(json.value("tags"_ls), d->tags); - return Success; -} - -static const auto SetRoomTagJobName = QStringLiteral("SetRoomTagJob"); - -SetRoomTagJob::SetRoomTagJob(const QString& userId, const QString& roomId, const QString& tag, Omittable<float> order) - : BaseJob(HttpVerb::Put, SetRoomTagJobName, - basePath % "/user/" % userId % "/rooms/" % roomId % "/tags/" % tag) + : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomTagsJob"), + QStringLiteral("/_matrix/client/r0") % "/user/" % userId + % "/rooms/" % roomId % "/tags") +{} + +SetRoomTagJob::SetRoomTagJob(const QString& userId, const QString& roomId, + const QString& tag, Omittable<float> order, + const QVariantHash& additionalProperties) + : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomTagJob"), + QStringLiteral("/_matrix/client/r0") % "/user/" % userId + % "/rooms/" % roomId % "/tags/" % tag) { QJsonObject _data; + fillJson(_data, additionalProperties); addParam<IfNotEmpty>(_data, QStringLiteral("order"), order); - setRequestData(_data); + setRequestData(std::move(_data)); } -QUrl DeleteRoomTagJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& tag) +QUrl DeleteRoomTagJob::makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& roomId, const QString& tag) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/user/" % userId % "/rooms/" % roomId % "/tags/" % tag); -} - -static const auto DeleteRoomTagJobName = QStringLiteral("DeleteRoomTagJob"); - -DeleteRoomTagJob::DeleteRoomTagJob(const QString& userId, const QString& roomId, const QString& tag) - : BaseJob(HttpVerb::Delete, DeleteRoomTagJobName, - basePath % "/user/" % userId % "/rooms/" % roomId % "/tags/" % tag) -{ + QStringLiteral("/_matrix/client/r0") + % "/user/" % userId % "/rooms/" % roomId + % "/tags/" % tag); } +DeleteRoomTagJob::DeleteRoomTagJob(const QString& userId, const QString& roomId, + const QString& tag) + : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomTagJob"), + QStringLiteral("/_matrix/client/r0") % "/user/" % userId + % "/rooms/" % roomId % "/tags/" % tag) +{} diff --git a/lib/csapi/tags.h b/lib/csapi/tags.h index 2c20c2a2..a815d9b3 100644 --- a/lib/csapi/tags.h +++ b/lib/csapi/tags.h @@ -6,111 +6,122 @@ #include "jobs/basejob.h" -#include <QtCore/QVariant> -#include <QtCore/QHash> -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief List the tags for a room. + * + * List the tags set by a user on a room. + */ +class GetRoomTagsJob : public BaseJob { +public: + // Inner data structures - /// List the tags for a room. - /// /// List the tags set by a user on a room. - class GetRoomTagsJob : public BaseJob - { - public: - // Inner data structures - - /// List the tags set by a user on a room. - struct Tag - { - /// A number in a range ``[0,1]`` describing a relative - /// position of the room under the given tag. - Omittable<float> order; - /// List the tags set by a user on a room. - QVariantHash additionalProperties; - }; - - // Construction/destruction - - /*! List the tags for a room. - * \param userId - * The id of the user to get tags for. The access token must be - * authorized to make requests for this user ID. - * \param roomId - * The ID of the room to get tags for. - */ - explicit GetRoomTagsJob(const QString& userId, const QString& roomId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetRoomTagsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId); - - ~GetRoomTagsJob() override; - - // Result properties - - /// List the tags set by a user on a room. - const QHash<QString, Tag>& tags() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; + struct Tag { + /// A number in a range ``[0,1]`` describing a relative + /// position of the room under the given tag. + Omittable<float> order; + /// List the tags set by a user on a room. + QVariantHash additionalProperties; }; - /// Add a tag to a room. - /// - /// Add a tag to the room. - class SetRoomTagJob : public BaseJob - { - public: - /*! Add a tag to a room. - * \param userId - * The id of the user to add a tag for. The access token must be - * authorized to make requests for this user ID. - * \param roomId - * The ID of the room to add a tag to. - * \param tag - * The tag to add. - * \param order - * A number in a range ``[0,1]`` describing a relative - * position of the room under the given tag. - */ - explicit SetRoomTagJob(const QString& userId, const QString& roomId, const QString& tag, Omittable<float> order = none); - }; + // Construction/destruction + + /*! \brief List the tags for a room. + * + * \param userId + * The id of the user to get tags for. The access token must be + * authorized to make requests for this user ID. + * + * \param roomId + * The ID of the room to get tags for. + */ + explicit GetRoomTagsJob(const QString& userId, const QString& roomId); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRoomTagsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& roomId); + + // Result properties - /// Remove a tag from the room. - /// - /// Remove a tag from the room. - class DeleteRoomTagJob : public BaseJob + /// List the tags set by a user on a room. + QHash<QString, Tag> tags() const { - public: - /*! Remove a tag from the room. - * \param userId - * The id of the user to remove a tag for. The access token must be - * authorized to make requests for this user ID. - * \param roomId - * The ID of the room to remove a tag from. - * \param tag - * The tag to remove. - */ - explicit DeleteRoomTagJob(const QString& userId, const QString& roomId, const QString& tag); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * DeleteRoomTagJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& tag); + return loadFromJson<QHash<QString, Tag>>("tags"_ls); + } +}; - }; -} // namespace QMatrixClient +template <> +struct JsonObjectConverter<GetRoomTagsJob::Tag> { + static void fillFrom(QJsonObject jo, GetRoomTagsJob::Tag& result) + { + fromJson(jo.take("order"_ls), result.order); + fromJson(jo, result.additionalProperties); + } +}; + +/*! \brief Add a tag to a room. + * + * Add a tag to the room. + */ +class SetRoomTagJob : public BaseJob { +public: + /*! \brief Add a tag to a room. + * + * \param userId + * The id of the user to add a tag for. The access token must be + * authorized to make requests for this user ID. + * + * \param roomId + * The ID of the room to add a tag to. + * + * \param tag + * The tag to add. + * + * \param order + * A number in a range ``[0,1]`` describing a relative + * position of the room under the given tag. + * + * \param additionalProperties + * Add a tag to the room. + */ + explicit SetRoomTagJob(const QString& userId, const QString& roomId, + const QString& tag, Omittable<float> order = none, + const QVariantHash& additionalProperties = {}); +}; + +/*! \brief Remove a tag from the room. + * + * Remove a tag from the room. + */ +class DeleteRoomTagJob : public BaseJob { +public: + /*! \brief Remove a tag from the room. + * + * \param userId + * The id of the user to remove a tag for. The access token must be + * authorized to make requests for this user ID. + * + * \param roomId + * The ID of the room to remove a tag from. + * + * \param tag + * The tag to remove. + */ + explicit DeleteRoomTagJob(const QString& userId, const QString& roomId, + const QString& tag); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for DeleteRoomTagJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, + const QString& roomId, const QString& tag); +}; + +} // namespace Quotient diff --git a/lib/csapi/third_party_lookup.cpp b/lib/csapi/third_party_lookup.cpp index 12cb7c59..baf1fab5 100644 --- a/lib/csapi/third_party_lookup.cpp +++ b/lib/csapi/third_party_lookup.cpp @@ -4,175 +4,87 @@ #include "third_party_lookup.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetProtocolsJob::Private -{ - public: - QHash<QString, ThirdPartyProtocol> data; -}; +using namespace Quotient; QUrl GetProtocolsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/thirdparty/protocols"); + QStringLiteral("/_matrix/client/r0") + % "/thirdparty/protocols"); } -static const auto GetProtocolsJobName = QStringLiteral("GetProtocolsJob"); - GetProtocolsJob::GetProtocolsJob() - : BaseJob(HttpVerb::Get, GetProtocolsJobName, - basePath % "/thirdparty/protocols") - , d(new Private) -{ -} - -GetProtocolsJob::~GetProtocolsJob() = default; + : BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolsJob"), + QStringLiteral("/_matrix/client/r0") % "/thirdparty/protocols") +{} -const QHash<QString, ThirdPartyProtocol>& GetProtocolsJob::data() const -{ - return d->data; -} - -BaseJob::Status GetProtocolsJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - -class GetProtocolMetadataJob::Private -{ - public: - ThirdPartyProtocol data; -}; - -QUrl GetProtocolMetadataJob::makeRequestUrl(QUrl baseUrl, const QString& protocol) +QUrl GetProtocolMetadataJob::makeRequestUrl(QUrl baseUrl, + const QString& protocol) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/thirdparty/protocol/" % protocol); + QStringLiteral("/_matrix/client/r0") + % "/thirdparty/protocol/" % protocol); } -static const auto GetProtocolMetadataJobName = QStringLiteral("GetProtocolMetadataJob"); - GetProtocolMetadataJob::GetProtocolMetadataJob(const QString& protocol) - : BaseJob(HttpVerb::Get, GetProtocolMetadataJobName, - basePath % "/thirdparty/protocol/" % protocol) - , d(new Private) -{ -} + : BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolMetadataJob"), + QStringLiteral("/_matrix/client/r0") % "/thirdparty/protocol/" + % protocol) +{} -GetProtocolMetadataJob::~GetProtocolMetadataJob() = default; - -const ThirdPartyProtocol& GetProtocolMetadataJob::data() const -{ - return d->data; -} - -BaseJob::Status GetProtocolMetadataJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - -class QueryLocationByProtocolJob::Private -{ - public: - QVector<ThirdPartyLocation> data; -}; - -BaseJob::Query queryToQueryLocationByProtocol(const QString& searchFields) +auto queryToQueryLocationByProtocol(const QString& searchFields) { BaseJob::Query _q; addParam<IfNotEmpty>(_q, QStringLiteral("searchFields"), searchFields); return _q; } -QUrl QueryLocationByProtocolJob::makeRequestUrl(QUrl baseUrl, const QString& protocol, const QString& searchFields) +QUrl QueryLocationByProtocolJob::makeRequestUrl(QUrl baseUrl, + const QString& protocol, + const QString& searchFields) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/thirdparty/location/" % protocol, - queryToQueryLocationByProtocol(searchFields)); + QStringLiteral("/_matrix/client/r0") + % "/thirdparty/location/" % protocol, + queryToQueryLocationByProtocol(searchFields)); } -static const auto QueryLocationByProtocolJobName = QStringLiteral("QueryLocationByProtocolJob"); - -QueryLocationByProtocolJob::QueryLocationByProtocolJob(const QString& protocol, const QString& searchFields) - : BaseJob(HttpVerb::Get, QueryLocationByProtocolJobName, - basePath % "/thirdparty/location/" % protocol, - queryToQueryLocationByProtocol(searchFields)) - , d(new Private) -{ -} - -QueryLocationByProtocolJob::~QueryLocationByProtocolJob() = default; - -const QVector<ThirdPartyLocation>& QueryLocationByProtocolJob::data() const -{ - return d->data; -} - -BaseJob::Status QueryLocationByProtocolJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} +QueryLocationByProtocolJob::QueryLocationByProtocolJob( + const QString& protocol, const QString& searchFields) + : BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByProtocolJob"), + QStringLiteral("/_matrix/client/r0") % "/thirdparty/location/" + % protocol, + queryToQueryLocationByProtocol(searchFields)) +{} -class QueryUserByProtocolJob::Private -{ - public: - QVector<ThirdPartyUser> data; -}; - -BaseJob::Query queryToQueryUserByProtocol(const QString& fields) +auto queryToQueryUserByProtocol(const QString& fields) { BaseJob::Query _q; addParam<IfNotEmpty>(_q, QStringLiteral("fields..."), fields); return _q; } -QUrl QueryUserByProtocolJob::makeRequestUrl(QUrl baseUrl, const QString& protocol, const QString& fields) +QUrl QueryUserByProtocolJob::makeRequestUrl(QUrl baseUrl, + const QString& protocol, + const QString& fields) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/thirdparty/user/" % protocol, - queryToQueryUserByProtocol(fields)); -} - -static const auto QueryUserByProtocolJobName = QStringLiteral("QueryUserByProtocolJob"); - -QueryUserByProtocolJob::QueryUserByProtocolJob(const QString& protocol, const QString& fields) - : BaseJob(HttpVerb::Get, QueryUserByProtocolJobName, - basePath % "/thirdparty/user/" % protocol, - queryToQueryUserByProtocol(fields)) - , d(new Private) -{ + QStringLiteral("/_matrix/client/r0") + % "/thirdparty/user/" % protocol, + queryToQueryUserByProtocol(fields)); } -QueryUserByProtocolJob::~QueryUserByProtocolJob() = default; +QueryUserByProtocolJob::QueryUserByProtocolJob(const QString& protocol, + const QString& fields) + : BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByProtocolJob"), + QStringLiteral("/_matrix/client/r0") % "/thirdparty/user/" + % protocol, + queryToQueryUserByProtocol(fields)) +{} -const QVector<ThirdPartyUser>& QueryUserByProtocolJob::data() const -{ - return d->data; -} - -BaseJob::Status QueryUserByProtocolJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - -class QueryLocationByAliasJob::Private -{ - public: - QVector<ThirdPartyLocation> data; -}; - -BaseJob::Query queryToQueryLocationByAlias(const QString& alias) +auto queryToQueryLocationByAlias(const QString& alias) { BaseJob::Query _q; addParam<>(_q, QStringLiteral("alias"), alias); @@ -182,40 +94,18 @@ BaseJob::Query queryToQueryLocationByAlias(const QString& alias) QUrl QueryLocationByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& alias) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/thirdparty/location", - queryToQueryLocationByAlias(alias)); + QStringLiteral("/_matrix/client/r0") + % "/thirdparty/location", + queryToQueryLocationByAlias(alias)); } -static const auto QueryLocationByAliasJobName = QStringLiteral("QueryLocationByAliasJob"); - QueryLocationByAliasJob::QueryLocationByAliasJob(const QString& alias) - : BaseJob(HttpVerb::Get, QueryLocationByAliasJobName, - basePath % "/thirdparty/location", - queryToQueryLocationByAlias(alias)) - , d(new Private) -{ -} + : BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByAliasJob"), + QStringLiteral("/_matrix/client/r0") % "/thirdparty/location", + queryToQueryLocationByAlias(alias)) +{} -QueryLocationByAliasJob::~QueryLocationByAliasJob() = default; - -const QVector<ThirdPartyLocation>& QueryLocationByAliasJob::data() const -{ - return d->data; -} - -BaseJob::Status QueryLocationByAliasJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - -class QueryUserByIDJob::Private -{ - public: - QVector<ThirdPartyUser> data; -}; - -BaseJob::Query queryToQueryUserByID(const QString& userid) +auto queryToQueryUserByID(const QString& userid) { BaseJob::Query _q; addParam<>(_q, QStringLiteral("userid"), userid); @@ -225,30 +115,13 @@ BaseJob::Query queryToQueryUserByID(const QString& userid) QUrl QueryUserByIDJob::makeRequestUrl(QUrl baseUrl, const QString& userid) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/thirdparty/user", - queryToQueryUserByID(userid)); + QStringLiteral("/_matrix/client/r0") + % "/thirdparty/user", + queryToQueryUserByID(userid)); } -static const auto QueryUserByIDJobName = QStringLiteral("QueryUserByIDJob"); - QueryUserByIDJob::QueryUserByIDJob(const QString& userid) - : BaseJob(HttpVerb::Get, QueryUserByIDJobName, - basePath % "/thirdparty/user", - queryToQueryUserByID(userid)) - , d(new Private) -{ -} - -QueryUserByIDJob::~QueryUserByIDJob() = default; - -const QVector<ThirdPartyUser>& QueryUserByIDJob::data() const -{ - return d->data; -} - -BaseJob::Status QueryUserByIDJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByIDJob"), + QStringLiteral("/_matrix/client/r0") % "/thirdparty/user", + queryToQueryUserByID(userid)) +{} diff --git a/lib/csapi/third_party_lookup.h b/lib/csapi/third_party_lookup.h index 3a60432b..969e767c 100644 --- a/lib/csapi/third_party_lookup.h +++ b/lib/csapi/third_party_lookup.h @@ -4,238 +4,209 @@ #pragma once -#include "jobs/basejob.h" - -#include "csapi/../application-service/definitions/user.h" #include "csapi/../application-service/definitions/location.h" -#include <QtCore/QHash> -#include <QtCore/QVector> -#include "converters.h" #include "csapi/../application-service/definitions/protocol.h" +#include "csapi/../application-service/definitions/user.h" -namespace QMatrixClient -{ - // Operations - - /// Retrieve metadata about all protocols that a homeserver supports. - /// - /// Fetches the overall metadata about protocols supported by the - /// homeserver. Includes both the available protocols and all fields - /// required for queries against each protocol. - class GetProtocolsJob : public BaseJob - { - public: - explicit GetProtocolsJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetProtocolsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetProtocolsJob() override; +#include "jobs/basejob.h" - // Result properties +namespace Quotient { - /// The protocols supported by the homeserver. - const QHash<QString, ThirdPartyProtocol>& data() const; +/*! \brief Retrieve metadata about all protocols that a homeserver supports. + * + * Fetches the overall metadata about protocols supported by the + * homeserver. Includes both the available protocols and all fields + * required for queries against each protocol. + */ +class GetProtocolsJob : public BaseJob { +public: + /// Retrieve metadata about all protocols that a homeserver supports. + explicit GetProtocolsJob(); - protected: - Status parseJson(const QJsonDocument& data) override; + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetProtocolsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); - private: - class Private; - QScopedPointer<Private> d; - }; + // Result properties - /// Retrieve metadata about a specific protocol that the homeserver supports. - /// - /// Fetches the metadata from the homeserver about a particular third party protocol. - class GetProtocolMetadataJob : public BaseJob + /// The protocols supported by the homeserver. + QHash<QString, ThirdPartyProtocol> protocols() const { - public: - /*! Retrieve metadata about a specific protocol that the homeserver supports. - * \param protocol - * The name of the protocol. - */ - explicit GetProtocolMetadataJob(const QString& protocol); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetProtocolMetadataJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& protocol); - - ~GetProtocolMetadataJob() override; - - // Result properties - - /// The protocol was found and metadata returned. - const ThirdPartyProtocol& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Retrieve Matrix-side portals rooms leading to a third party location. - /// - /// Requesting this endpoint with a valid protocol name results in a list - /// of successful mapping results in a JSON array. Each result contains - /// objects to represent the Matrix room or rooms that represent a portal - /// to this third party network. Each has the Matrix room alias string, - /// an identifier for the particular third party network protocol, and an - /// object containing the network-specific fields that comprise this - /// identifier. It should attempt to canonicalise the identifier as much - /// as reasonably possible given the network type. - class QueryLocationByProtocolJob : public BaseJob + return fromJson<QHash<QString, ThirdPartyProtocol>>(jsonData()); + } +}; + +/*! \brief Retrieve metadata about a specific protocol that the homeserver + * supports. + * + * Fetches the metadata from the homeserver about a particular third party + * protocol. + */ +class GetProtocolMetadataJob : public BaseJob { +public: + /*! \brief Retrieve metadata about a specific protocol that the homeserver + * supports. + * + * \param protocol + * The name of the protocol. + */ + explicit GetProtocolMetadataJob(const QString& protocol); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetProtocolMetadataJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& protocol); + + // Result properties + + /// The protocol was found and metadata returned. + ThirdPartyProtocol data() const + { + return fromJson<ThirdPartyProtocol>(jsonData()); + } +}; + +/*! \brief Retrieve Matrix-side portals rooms leading to a third party location. + * + * Requesting this endpoint with a valid protocol name results in a list + * of successful mapping results in a JSON array. Each result contains + * objects to represent the Matrix room or rooms that represent a portal + * to this third party network. Each has the Matrix room alias string, + * an identifier for the particular third party network protocol, and an + * object containing the network-specific fields that comprise this + * identifier. It should attempt to canonicalise the identifier as much + * as reasonably possible given the network type. + */ +class QueryLocationByProtocolJob : public BaseJob { +public: + /*! \brief Retrieve Matrix-side portals rooms leading to a third party + * location. + * + * \param protocol + * The protocol used to communicate to the third party network. + * + * \param searchFields + * One or more custom fields to help identify the third party + * location. + */ + explicit QueryLocationByProtocolJob(const QString& protocol, + const QString& searchFields = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for QueryLocationByProtocolJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& protocol, + const QString& searchFields = {}); + + // Result properties + + /// At least one portal room was found. + QVector<ThirdPartyLocation> data() const { - public: - /*! Retrieve Matrix-side portals rooms leading to a third party location. - * \param protocol - * The protocol used to communicate to the third party network. - * \param searchFields - * One or more custom fields to help identify the third party - * location. - */ - explicit QueryLocationByProtocolJob(const QString& protocol, const QString& searchFields = {}); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * QueryLocationByProtocolJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& protocol, const QString& searchFields = {}); - - ~QueryLocationByProtocolJob() override; - - // Result properties - - /// At least one portal room was found. - const QVector<ThirdPartyLocation>& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Retrieve the Matrix User ID of a corresponding third party user. - /// - /// Retrieve a Matrix User ID linked to a user on the third party service, given - /// a set of user parameters. - class QueryUserByProtocolJob : public BaseJob + return fromJson<QVector<ThirdPartyLocation>>(jsonData()); + } +}; + +/*! \brief Retrieve the Matrix User ID of a corresponding third party user. + * + * Retrieve a Matrix User ID linked to a user on the third party service, given + * a set of user parameters. + */ +class QueryUserByProtocolJob : public BaseJob { +public: + /*! \brief Retrieve the Matrix User ID of a corresponding third party user. + * + * \param protocol + * The name of the protocol. + * + * \param fields + * One or more custom fields that are passed to the AS to help identify + * the user. + */ + explicit QueryUserByProtocolJob(const QString& protocol, + const QString& fields = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for QueryUserByProtocolJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& protocol, + const QString& fields = {}); + + // Result properties + + /// The Matrix User IDs found with the given parameters. + QVector<ThirdPartyUser> data() const { - public: - /*! Retrieve the Matrix User ID of a corresponding third party user. - * \param protocol - * The name of the protocol. - * \param fields - * One or more custom fields that are passed to the AS to help identify the user. - */ - explicit QueryUserByProtocolJob(const QString& protocol, const QString& fields = {}); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * QueryUserByProtocolJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& protocol, const QString& fields = {}); - - ~QueryUserByProtocolJob() override; - - // Result properties - - /// The Matrix User IDs found with the given parameters. - const QVector<ThirdPartyUser>& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Reverse-lookup third party locations given a Matrix room alias. - /// - /// Retrieve an array of third party network locations from a Matrix room - /// alias. - class QueryLocationByAliasJob : public BaseJob + return fromJson<QVector<ThirdPartyUser>>(jsonData()); + } +}; + +/*! \brief Reverse-lookup third party locations given a Matrix room alias. + * + * Retrieve an array of third party network locations from a Matrix room + * alias. + */ +class QueryLocationByAliasJob : public BaseJob { +public: + /*! \brief Reverse-lookup third party locations given a Matrix room alias. + * + * \param alias + * The Matrix room alias to look up. + */ + explicit QueryLocationByAliasJob(const QString& alias); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for QueryLocationByAliasJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& alias); + + // Result properties + + /// All found third party locations. + QVector<ThirdPartyLocation> data() const { - public: - /*! Reverse-lookup third party locations given a Matrix room alias. - * \param alias - * The Matrix room alias to look up. - */ - explicit QueryLocationByAliasJob(const QString& alias); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * QueryLocationByAliasJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& alias); - - ~QueryLocationByAliasJob() override; - - // Result properties - - /// All found third party locations. - const QVector<ThirdPartyLocation>& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Reverse-lookup third party users given a Matrix User ID. - /// - /// Retrieve an array of third party users from a Matrix User ID. - class QueryUserByIDJob : public BaseJob + return fromJson<QVector<ThirdPartyLocation>>(jsonData()); + } +}; + +/*! \brief Reverse-lookup third party users given a Matrix User ID. + * + * Retrieve an array of third party users from a Matrix User ID. + */ +class QueryUserByIDJob : public BaseJob { +public: + /*! \brief Reverse-lookup third party users given a Matrix User ID. + * + * \param userid + * The Matrix User ID to look up. + */ + explicit QueryUserByIDJob(const QString& userid); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for QueryUserByIDJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userid); + + // Result properties + + /// An array of third party users. + QVector<ThirdPartyUser> data() const { - public: - /*! Reverse-lookup third party users given a Matrix User ID. - * \param userid - * The Matrix User ID to look up. - */ - explicit QueryUserByIDJob(const QString& userid); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * QueryUserByIDJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userid); - - ~QueryUserByIDJob() override; - - // Result properties - - /// An array of third party users. - const QVector<ThirdPartyUser>& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + return fromJson<QVector<ThirdPartyUser>>(jsonData()); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/third_party_membership.cpp b/lib/csapi/third_party_membership.cpp index c1683338..fda772d2 100644 --- a/lib/csapi/third_party_membership.cpp +++ b/lib/csapi/third_party_membership.cpp @@ -4,24 +4,21 @@ #include "third_party_membership.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; -static const auto InviteBy3PIDJobName = QStringLiteral("InviteBy3PIDJob"); - -InviteBy3PIDJob::InviteBy3PIDJob(const QString& roomId, const QString& idServer, const QString& medium, const QString& address) - : BaseJob(HttpVerb::Post, InviteBy3PIDJobName, - basePath % "/rooms/" % roomId % "/invite") +InviteBy3PIDJob::InviteBy3PIDJob(const QString& roomId, const QString& idServer, + const QString& idAccessToken, + const QString& medium, const QString& address) + : BaseJob(HttpVerb::Post, QStringLiteral("InviteBy3PIDJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/invite") { QJsonObject _data; addParam<>(_data, QStringLiteral("id_server"), idServer); + addParam<>(_data, QStringLiteral("id_access_token"), idAccessToken); addParam<>(_data, QStringLiteral("medium"), medium); addParam<>(_data, QStringLiteral("address"), address); - setRequestData(_data); + setRequestData(std::move(_data)); } - diff --git a/lib/csapi/third_party_membership.h b/lib/csapi/third_party_membership.h index d18fe554..55cab370 100644 --- a/lib/csapi/third_party_membership.h +++ b/lib/csapi/third_party_membership.h @@ -6,70 +6,81 @@ #include "jobs/basejob.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Invite a user to participate in a particular room. + * + * .. _invite-by-third-party-id-endpoint: + * + * *Note that there are two forms of this API, which are documented separately. + * This version of the API does not require that the inviter know the Matrix + * identifier of the invitee, and instead relies on third party identifiers. + * The homeserver uses an identity server to perform the mapping from + * third party identifier to a Matrix identifier. The other is documented in + * the* `joining rooms section`_. + * + * This API invites a user to participate in a particular room. + * They do not start participating in the room until they actually join the + * room. + * + * Only users currently in a particular room can invite other users to + * join that room. + * + * If the identity server did know the Matrix user identifier for the + * third party identifier, the homeserver will append a ``m.room.member`` + * event to the room. + * + * If the identity server does not know a Matrix user identifier for the + * passed third party identifier, the homeserver will issue an invitation + * which can be accepted upon providing proof of ownership of the third + * party identifier. This is achieved by the identity server generating a + * token, which it gives to the inviting homeserver. The homeserver will + * add an ``m.room.third_party_invite`` event into the graph for the room, + * containing that token. + * + * When the invitee binds the invited third party identifier to a Matrix + * user ID, the identity server will give the user a list of pending + * invitations, each containing: + * + * - The room ID to which they were invited + * + * - The token given to the homeserver + * + * - A signature of the token, signed with the identity server's private key + * + * - The matrix user ID who invited them to the room + * + * If a token is requested from the identity server, the homeserver will + * append a ``m.room.third_party_invite`` event to the room. + * + * .. _joining rooms section: `invite-by-user-id-endpoint`_ + */ +class InviteBy3PIDJob : public BaseJob { +public: + /*! \brief Invite a user to participate in a particular room. + * + * \param roomId + * The room identifier (not alias) to which to invite the user. + * + * \param idServer + * The hostname+port of the identity server which should be used for third + * party identifier lookups. + * + * \param idAccessToken + * An access token previously registered with the identity server. Servers + * can treat this as optional to distinguish between r0.5-compatible + * clients and this specification version. + * + * \param medium + * The kind of address being passed in the address field, for example + * ``email``. + * + * \param address + * The invitee's third party identifier. + */ + explicit InviteBy3PIDJob(const QString& roomId, const QString& idServer, + const QString& idAccessToken, + const QString& medium, const QString& address); +}; - /// Invite a user to participate in a particular room. - /// - /// .. _invite-by-third-party-id-endpoint: - /// - /// *Note that there are two forms of this API, which are documented separately. - /// This version of the API does not require that the inviter know the Matrix - /// identifier of the invitee, and instead relies on third party identifiers. - /// The homeserver uses an identity server to perform the mapping from - /// third party identifier to a Matrix identifier. The other is documented in the* - /// `joining rooms section`_. - /// - /// This API invites a user to participate in a particular room. - /// They do not start participating in the room until they actually join the - /// room. - /// - /// Only users currently in a particular room can invite other users to - /// join that room. - /// - /// If the identity server did know the Matrix user identifier for the - /// third party identifier, the homeserver will append a ``m.room.member`` - /// event to the room. - /// - /// If the identity server does not know a Matrix user identifier for the - /// passed third party identifier, the homeserver will issue an invitation - /// which can be accepted upon providing proof of ownership of the third - /// party identifier. This is achieved by the identity server generating a - /// token, which it gives to the inviting homeserver. The homeserver will - /// add an ``m.room.third_party_invite`` event into the graph for the room, - /// containing that token. - /// - /// When the invitee binds the invited third party identifier to a Matrix - /// user ID, the identity server will give the user a list of pending - /// invitations, each containing: - /// - /// - The room ID to which they were invited - /// - /// - The token given to the homeserver - /// - /// - A signature of the token, signed with the identity server's private key - /// - /// - The matrix user ID who invited them to the room - /// - /// If a token is requested from the identity server, the homeserver will - /// append a ``m.room.third_party_invite`` event to the room. - /// - /// .. _joining rooms section: `invite-by-user-id-endpoint`_ - class InviteBy3PIDJob : public BaseJob - { - public: - /*! Invite a user to participate in a particular room. - * \param roomId - * The room identifier (not alias) to which to invite the user. - * \param idServer - * The hostname+port of the identity server which should be used for third party identifier lookups. - * \param medium - * The kind of address being passed in the address field, for example ``email``. - * \param address - * The invitee's third party identifier. - */ - explicit InviteBy3PIDJob(const QString& roomId, const QString& idServer, const QString& medium, const QString& address); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/to_device.cpp b/lib/csapi/to_device.cpp index 7c7f495a..28c4115a 100644 --- a/lib/csapi/to_device.cpp +++ b/lib/csapi/to_device.cpp @@ -4,22 +4,18 @@ #include "to_device.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; -static const auto SendToDeviceJobName = QStringLiteral("SendToDeviceJob"); - -SendToDeviceJob::SendToDeviceJob(const QString& eventType, const QString& txnId, const QHash<QString, QHash<QString, QJsonObject>>& messages) - : BaseJob(HttpVerb::Put, SendToDeviceJobName, - basePath % "/sendToDevice/" % eventType % "/" % txnId) +SendToDeviceJob::SendToDeviceJob( + const QString& eventType, const QString& txnId, + const QHash<QString, QHash<QString, QJsonObject>>& messages) + : BaseJob(HttpVerb::Put, QStringLiteral("SendToDeviceJob"), + QStringLiteral("/_matrix/client/r0") % "/sendToDevice/" + % eventType % "/" % txnId) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("messages"), messages); - setRequestData(_data); + setRequestData(std::move(_data)); } - diff --git a/lib/csapi/to_device.h b/lib/csapi/to_device.h index 10f6b971..f5d69d65 100644 --- a/lib/csapi/to_device.h +++ b/lib/csapi/to_device.h @@ -6,32 +6,33 @@ #include "jobs/basejob.h" -#include <QtCore/QJsonObject> -#include <QtCore/QHash> +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Send an event to a given set of devices. + * + * This endpoint is used to send send-to-device events to a set of + * client devices. + */ +class SendToDeviceJob : public BaseJob { +public: + /*! \brief Send an event to a given set of devices. + * + * \param eventType + * The type of event to send. + * + * \param txnId + * The transaction ID for this event. Clients should generate an + * ID unique across requests with the same access token; it will be + * used by the server to ensure idempotency of requests. + * + * \param messages + * The messages to send. A map from user ID, to a map from + * device ID to message body. The device ID may also be `*`, + * meaning all known devices for the user. + */ + explicit SendToDeviceJob( + const QString& eventType, const QString& txnId, + const QHash<QString, QHash<QString, QJsonObject>>& messages = {}); +}; - /// Send an event to a given set of devices. - /// - /// This endpoint is used to send send-to-device events to a set of - /// client devices. - class SendToDeviceJob : public BaseJob - { - public: - /*! Send an event to a given set of devices. - * \param eventType - * The type of event to send. - * \param txnId - * The transaction ID for this event. Clients should generate an - * ID unique across requests with the same access token; it will be - * used by the server to ensure idempotency of requests. - * \param messages - * The messages to send. A map from user ID, to a map from - * device ID to message body. The device ID may also be `*`, - * meaning all known devices for the user. - */ - explicit SendToDeviceJob(const QString& eventType, const QString& txnId, const QHash<QString, QHash<QString, QJsonObject>>& messages = {}); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/typing.cpp b/lib/csapi/typing.cpp index bf10912b..8e214053 100644 --- a/lib/csapi/typing.cpp +++ b/lib/csapi/typing.cpp @@ -4,23 +4,18 @@ #include "typing.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); +using namespace Quotient; -static const auto SetTypingJobName = QStringLiteral("SetTypingJob"); - -SetTypingJob::SetTypingJob(const QString& userId, const QString& roomId, bool typing, Omittable<int> timeout) - : BaseJob(HttpVerb::Put, SetTypingJobName, - basePath % "/rooms/" % roomId % "/typing/" % userId) +SetTypingJob::SetTypingJob(const QString& userId, const QString& roomId, + bool typing, Omittable<int> timeout) + : BaseJob(HttpVerb::Put, QStringLiteral("SetTypingJob"), + QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId + % "/typing/" % userId) { QJsonObject _data; addParam<>(_data, QStringLiteral("typing"), typing); addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout); - setRequestData(_data); + setRequestData(std::move(_data)); } - diff --git a/lib/csapi/typing.h b/lib/csapi/typing.h index c6201440..2c953949 100644 --- a/lib/csapi/typing.h +++ b/lib/csapi/typing.h @@ -6,32 +6,34 @@ #include "jobs/basejob.h" -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Informs the server that the user has started or stopped typing. + * + * This tells the server that the user is typing for the next N + * milliseconds where N is the value specified in the ``timeout`` key. + * Alternatively, if ``typing`` is ``false``, it tells the server that the + * user has stopped typing. + */ +class SetTypingJob : public BaseJob { +public: + /*! \brief Informs the server that the user has started or stopped typing. + * + * \param userId + * The user who has started to type. + * + * \param roomId + * The room in which the user is typing. + * + * \param typing + * Whether the user is typing or not. If ``false``, the ``timeout`` + * key can be omitted. + * + * \param timeout + * The length of time in milliseconds to mark this user as typing. + */ + explicit SetTypingJob(const QString& userId, const QString& roomId, + bool typing, Omittable<int> timeout = none); +}; - /// Informs the server that the user has started or stopped typing. - /// - /// This tells the server that the user is typing for the next N - /// milliseconds where N is the value specified in the ``timeout`` key. - /// Alternatively, if ``typing`` is ``false``, it tells the server that the - /// user has stopped typing. - class SetTypingJob : public BaseJob - { - public: - /*! Informs the server that the user has started or stopped typing. - * \param userId - * The user who has started to type. - * \param roomId - * The room in which the user is typing. - * \param typing - * Whether the user is typing or not. If ``false``, the ``timeout`` - * key can be omitted. - * \param timeout - * The length of time in milliseconds to mark this user as typing. - */ - explicit SetTypingJob(const QString& userId, const QString& roomId, bool typing, Omittable<int> timeout = none); - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/users.cpp b/lib/csapi/users.cpp index 97d8962d..a0279d7e 100644 --- a/lib/csapi/users.cpp +++ b/lib/csapi/users.cpp @@ -4,72 +4,19 @@ #include "users.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -namespace QMatrixClient -{ - // Converters - - template <> struct JsonObjectConverter<SearchUserDirectoryJob::User> - { - static void fillFrom(const QJsonObject& jo, SearchUserDirectoryJob::User& result) - { - fromJson(jo.value("user_id"_ls), result.userId); - fromJson(jo.value("display_name"_ls), result.displayName); - fromJson(jo.value("avatar_url"_ls), result.avatarUrl); - } - }; -} // namespace QMatrixClient +using namespace Quotient; -class SearchUserDirectoryJob::Private -{ - public: - QVector<User> results; - bool limited; -}; - -static const auto SearchUserDirectoryJobName = QStringLiteral("SearchUserDirectoryJob"); - -SearchUserDirectoryJob::SearchUserDirectoryJob(const QString& searchTerm, Omittable<int> limit) - : BaseJob(HttpVerb::Post, SearchUserDirectoryJobName, - basePath % "/user_directory/search") - , d(new Private) +SearchUserDirectoryJob::SearchUserDirectoryJob(const QString& searchTerm, + Omittable<int> limit) + : BaseJob(HttpVerb::Post, QStringLiteral("SearchUserDirectoryJob"), + QStringLiteral("/_matrix/client/r0") % "/user_directory/search") { QJsonObject _data; addParam<>(_data, QStringLiteral("search_term"), searchTerm); addParam<IfNotEmpty>(_data, QStringLiteral("limit"), limit); - setRequestData(_data); + setRequestData(std::move(_data)); + addExpectedKey("results"); + addExpectedKey("limited"); } - -SearchUserDirectoryJob::~SearchUserDirectoryJob() = default; - -const QVector<SearchUserDirectoryJob::User>& SearchUserDirectoryJob::results() const -{ - return d->results; -} - -bool SearchUserDirectoryJob::limited() const -{ - return d->limited; -} - -BaseJob::Status SearchUserDirectoryJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("results"_ls)) - return { JsonParseError, - "The key 'results' not found in the response" }; - fromJson(json.value("results"_ls), d->results); - if (!json.contains("limited"_ls)) - return { JsonParseError, - "The key 'limited' not found in the response" }; - fromJson(json.value("limited"_ls), d->limited); - return Success; -} - diff --git a/lib/csapi/users.h b/lib/csapi/users.h index 1e355b8f..6fc26f57 100644 --- a/lib/csapi/users.h +++ b/lib/csapi/users.h @@ -6,73 +6,78 @@ #include "jobs/basejob.h" -#include <QtCore/QVector> -#include "converters.h" +namespace Quotient { -namespace QMatrixClient -{ - // Operations +/*! \brief Searches the user directory. + * + * Performs a search for users. The homeserver may + * determine which subset of users are searched, however the homeserver + * MUST at a minimum consider the users the requesting user shares a + * room with and those who reside in public rooms (known to the homeserver). + * The search MUST consider local users to the homeserver, and SHOULD + * query remote users as part of the search. + * + * The search is performed case-insensitively on user IDs and display + * names preferably using a collation determined based upon the + * ``Accept-Language`` header provided in the request, if present. + */ +class SearchUserDirectoryJob : public BaseJob { +public: + // Inner data structures - /// Searches the user directory. - /// - /// Performs a search for users on the homeserver. The homeserver may + /// Performs a search for users. The homeserver may /// determine which subset of users are searched, however the homeserver /// MUST at a minimum consider the users the requesting user shares a - /// room with and those who reside in public rooms (known to the homeserver). - /// The search MUST consider local users to the homeserver, and SHOULD - /// query remote users as part of the search. - /// + /// room with and those who reside in public rooms (known to the + /// homeserver). The search MUST consider local users to the homeserver, and + /// SHOULD query remote users as part of the search. + /// /// The search is performed case-insensitively on user IDs and display - /// names preferably using a collation determined based upon the + /// names preferably using a collation determined based upon the /// ``Accept-Language`` header provided in the request, if present. - class SearchUserDirectoryJob : public BaseJob - { - public: - // Inner data structures + struct User { + /// The user's matrix user ID. + QString userId; + /// The display name of the user, if one exists. + QString displayName; + /// The avatar url, as an MXC, if one exists. + QString avatarUrl; + }; - /// Performs a search for users on the homeserver. The homeserver may - /// determine which subset of users are searched, however the homeserver - /// MUST at a minimum consider the users the requesting user shares a - /// room with and those who reside in public rooms (known to the homeserver). - /// The search MUST consider local users to the homeserver, and SHOULD - /// query remote users as part of the search. - /// - /// The search is performed case-insensitively on user IDs and display - /// names preferably using a collation determined based upon the - /// ``Accept-Language`` header provided in the request, if present. - struct User - { - /// The user's matrix user ID. - QString userId; - /// The display name of the user, if one exists. - QString displayName; - /// The avatar url, as an MXC, if one exists. - QString avatarUrl; - }; + // Construction/destruction - // Construction/destruction + /*! \brief Searches the user directory. + * + * \param searchTerm + * The term to search for + * + * \param limit + * The maximum number of results to return. Defaults to 10. + */ + explicit SearchUserDirectoryJob(const QString& searchTerm, + Omittable<int> limit = none); - /*! Searches the user directory. - * \param searchTerm - * The term to search for - * \param limit - * The maximum number of results to return. Defaults to 10. - */ - explicit SearchUserDirectoryJob(const QString& searchTerm, Omittable<int> limit = none); - ~SearchUserDirectoryJob() override; + // Result properties - // Result properties + /// Ordered by rank and then whether or not profile info is available. + QVector<User> results() const + { + return loadFromJson<QVector<User>>("results"_ls); + } - /// Ordered by rank and then whether or not profile info is available. - const QVector<User>& results() const; - /// Indicates if the result list has been truncated by the limit. - bool limited() const; + /// Indicates if the result list has been truncated by the limit. + bool limited() const { return loadFromJson<bool>("limited"_ls); } +}; - protected: - Status parseJson(const QJsonDocument& data) override; +template <> +struct JsonObjectConverter<SearchUserDirectoryJob::User> { + static void fillFrom(const QJsonObject& jo, + SearchUserDirectoryJob::User& result) + { + fromJson(jo.value("user_id"_ls), result.userId); + fromJson(jo.value("display_name"_ls), result.displayName); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); + } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/versions.cpp b/lib/csapi/versions.cpp index 6ee6725d..9003e27f 100644 --- a/lib/csapi/versions.cpp +++ b/lib/csapi/versions.cpp @@ -4,56 +4,20 @@ #include "versions.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client"); - -class GetVersionsJob::Private -{ - public: - QStringList versions; - QHash<QString, bool> unstableFeatures; -}; +using namespace Quotient; QUrl GetVersionsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/versions"); + QStringLiteral("/_matrix/client") + % "/versions"); } -static const auto GetVersionsJobName = QStringLiteral("GetVersionsJob"); - GetVersionsJob::GetVersionsJob() - : BaseJob(HttpVerb::Get, GetVersionsJobName, - basePath % "/versions", false) - , d(new Private) -{ -} - -GetVersionsJob::~GetVersionsJob() = default; - -const QStringList& GetVersionsJob::versions() const -{ - return d->versions; -} - -const QHash<QString, bool>& GetVersionsJob::unstableFeatures() const + : BaseJob(HttpVerb::Get, QStringLiteral("GetVersionsJob"), + QStringLiteral("/_matrix/client") % "/versions", false) { - return d->unstableFeatures; + addExpectedKey("versions"); } - -BaseJob::Status GetVersionsJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("versions"_ls)) - return { JsonParseError, - "The key 'versions' not found in the response" }; - fromJson(json.value("versions"_ls), d->versions); - fromJson(json.value("unstable_features"_ls), d->unstableFeatures); - return Success; -} - diff --git a/lib/csapi/versions.h b/lib/csapi/versions.h index b56f293f..828a7eb9 100644 --- a/lib/csapi/versions.h +++ b/lib/csapi/versions.h @@ -6,63 +6,58 @@ #include "jobs/basejob.h" -#include <QtCore/QHash> -#include "converters.h" - -namespace QMatrixClient -{ - // Operations - - /// Gets the versions of the specification supported by the server. - /// +namespace Quotient { + +/*! \brief Gets the versions of the specification supported by the server. + * + * Gets the versions of the specification supported by the server. + * + * Values will take the form ``rX.Y.Z``. + * + * Only the latest ``Z`` value will be reported for each supported ``X.Y`` + * value. i.e. if the server implements ``r0.0.0``, ``r0.0.1``, and ``r1.2.0``, + * it will report ``r0.0.1`` and ``r1.2.0``. + * + * The server may additionally advertise experimental features it supports + * through ``unstable_features``. These features should be namespaced and + * may optionally include version information within their name if desired. + * Features listed here are not for optionally toggling parts of the Matrix + * specification and should only be used to advertise support for a feature + * which has not yet landed in the spec. For example, a feature currently + * undergoing the proposal process may appear here and eventually be taken + * off this list once the feature lands in the spec and the server deems it + * reasonable to do so. Servers may wish to keep advertising features here + * after they've been released into the spec to give clients a chance to + * upgrade appropriately. Additionally, clients should avoid using unstable + * features in their stable releases. + */ +class GetVersionsJob : public BaseJob { +public: /// Gets the versions of the specification supported by the server. - /// - /// Values will take the form ``rX.Y.Z``. - /// - /// Only the latest ``Z`` value will be reported for each supported ``X.Y`` value. - /// i.e. if the server implements ``r0.0.0``, ``r0.0.1``, and ``r1.2.0``, it will report ``r0.0.1`` and ``r1.2.0``. - /// - /// The server may additionally advertise experimental features it supports - /// through ``unstable_features``. These features should be namespaced and - /// may optionally include version information within their name if desired. - /// Features listed here are not for optionally toggling parts of the Matrix - /// specification and should only be used to advertise support for a feature - /// which has not yet landed in the spec. For example, a feature currently - /// undergoing the proposal process may appear here and eventually be taken - /// off this list once the feature lands in the spec and the server deems it - /// reasonable to do so. Servers may wish to keep advertising features here - /// after they've been released into the spec to give clients a chance to - /// upgrade appropriately. Additionally, clients should avoid using unstable - /// features in their stable releases. - class GetVersionsJob : public BaseJob - { - public: - explicit GetVersionsJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetVersionsJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); + explicit GetVersionsJob(); - ~GetVersionsJob() override; + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetVersionsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); - // Result properties + // Result properties - /// The supported versions. - const QStringList& versions() const; - /// Experimental features the server supports. Features not listed here, - /// or the lack of this property all together, indicate that a feature is - /// not supported. - const QHash<QString, bool>& unstableFeatures() const; + /// The supported versions. + QStringList versions() const + { + return loadFromJson<QStringList>("versions"_ls); + } - protected: - Status parseJson(const QJsonDocument& data) override; + /// Experimental features the server supports. Features not listed here, + /// or the lack of this property all together, indicate that a feature is + /// not supported. + QHash<QString, bool> unstableFeatures() const + { + return loadFromJson<QHash<QString, bool>>("unstable_features"_ls); + } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/voip.cpp b/lib/csapi/voip.cpp index e8158723..43170057 100644 --- a/lib/csapi/voip.cpp +++ b/lib/csapi/voip.cpp @@ -4,45 +4,18 @@ #include "voip.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetTurnServerJob::Private -{ - public: - QJsonObject data; -}; +using namespace Quotient; QUrl GetTurnServerJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/voip/turnServer"); + QStringLiteral("/_matrix/client/r0") + % "/voip/turnServer"); } -static const auto GetTurnServerJobName = QStringLiteral("GetTurnServerJob"); - GetTurnServerJob::GetTurnServerJob() - : BaseJob(HttpVerb::Get, GetTurnServerJobName, - basePath % "/voip/turnServer") - , d(new Private) -{ -} - -GetTurnServerJob::~GetTurnServerJob() = default; - -const QJsonObject& GetTurnServerJob::data() const -{ - return d->data; -} - -BaseJob::Status GetTurnServerJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("GetTurnServerJob"), + QStringLiteral("/_matrix/client/r0") % "/voip/turnServer") +{} diff --git a/lib/csapi/voip.h b/lib/csapi/voip.h index bb858499..087ebbbd 100644 --- a/lib/csapi/voip.h +++ b/lib/csapi/voip.h @@ -6,41 +6,29 @@ #include "jobs/basejob.h" -#include <QtCore/QJsonObject> - -namespace QMatrixClient -{ - // Operations +namespace Quotient { +/*! \brief Obtain TURN server credentials. + * + * This API provides credentials for the client to use when initiating + * calls. + */ +class GetTurnServerJob : public BaseJob { +public: /// Obtain TURN server credentials. - /// - /// This API provides credentials for the client to use when initiating - /// calls. - class GetTurnServerJob : public BaseJob - { - public: - explicit GetTurnServerJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetTurnServerJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetTurnServerJob() override; - - // Result properties - - /// The TURN server credentials. - const QJsonObject& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + explicit GetTurnServerJob(); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetTurnServerJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + // Result properties + + /// The TURN server credentials. + QJsonObject data() const { return fromJson<QJsonObject>(jsonData()); } +}; + +} // namespace Quotient diff --git a/lib/csapi/wellknown.cpp b/lib/csapi/wellknown.cpp index a6107f86..1aa0a90b 100644 --- a/lib/csapi/wellknown.cpp +++ b/lib/csapi/wellknown.cpp @@ -4,45 +4,18 @@ #include "wellknown.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/.well-known"); - -class GetWellknownJob::Private -{ - public: - DiscoveryInformation data; -}; +using namespace Quotient; QUrl GetWellknownJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/matrix/client"); + QStringLiteral("/.well-known") + % "/matrix/client"); } -static const auto GetWellknownJobName = QStringLiteral("GetWellknownJob"); - GetWellknownJob::GetWellknownJob() - : BaseJob(HttpVerb::Get, GetWellknownJobName, - basePath % "/matrix/client", false) - , d(new Private) -{ -} - -GetWellknownJob::~GetWellknownJob() = default; - -const DiscoveryInformation& GetWellknownJob::data() const -{ - return d->data; -} - -BaseJob::Status GetWellknownJob::parseJson(const QJsonDocument& data) -{ - fromJson(data, d->data); - return Success; -} - + : BaseJob(HttpVerb::Get, QStringLiteral("GetWellknownJob"), + QStringLiteral("/.well-known") % "/matrix/client", false) +{} diff --git a/lib/csapi/wellknown.h b/lib/csapi/wellknown.h index 8da9ce9f..b21d9fc7 100644 --- a/lib/csapi/wellknown.h +++ b/lib/csapi/wellknown.h @@ -4,50 +4,42 @@ #pragma once -#include "jobs/basejob.h" - #include "csapi/definitions/wellknown/full.h" -#include "converters.h" -namespace QMatrixClient -{ - // Operations +#include "jobs/basejob.h" +namespace Quotient { + +/*! \brief Gets Matrix server discovery information about the domain. + * + * Gets discovery information about the domain. The file may include + * additional keys, which MUST follow the Java package naming convention, + * e.g. ``com.example.myapp.property``. This ensures property names are + * suitably namespaced for each application and reduces the risk of + * clashes. + * + * Note that this endpoint is not necessarily handled by the homeserver, + * but by another webserver, to be used for discovering the homeserver URL. + */ +class GetWellknownJob : public BaseJob { +public: /// Gets Matrix server discovery information about the domain. - /// - /// Gets discovery information about the domain. The file may include - /// additional keys, which MUST follow the Java package naming convention, - /// e.g. ``com.example.myapp.property``. This ensures property names are - /// suitably namespaced for each application and reduces the risk of - /// clashes. - /// - /// Note that this endpoint is not necessarily handled by the homeserver, - /// but by another webserver, to be used for discovering the homeserver URL. - class GetWellknownJob : public BaseJob - { - public: - explicit GetWellknownJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetWellknownJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); + explicit GetWellknownJob(); - ~GetWellknownJob() override; + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetWellknownJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); - // Result properties + // Result properties - /// Server discovery information. - const DiscoveryInformation& data() const; - - protected: - Status parseJson(const QJsonDocument& data) override; + /// Server discovery information. + DiscoveryInformation data() const + { + return fromJson<DiscoveryInformation>(jsonData()); + } +}; - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/csapi/whoami.cpp b/lib/csapi/whoami.cpp index aebdf5d3..73f0298e 100644 --- a/lib/csapi/whoami.cpp +++ b/lib/csapi/whoami.cpp @@ -4,49 +4,20 @@ #include "whoami.h" -#include "converters.h" - #include <QtCore/QStringBuilder> -using namespace QMatrixClient; - -static const auto basePath = QStringLiteral("/_matrix/client/r0"); - -class GetTokenOwnerJob::Private -{ - public: - QString userId; -}; +using namespace Quotient; QUrl GetTokenOwnerJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/account/whoami"); + QStringLiteral("/_matrix/client/r0") + % "/account/whoami"); } -static const auto GetTokenOwnerJobName = QStringLiteral("GetTokenOwnerJob"); - GetTokenOwnerJob::GetTokenOwnerJob() - : BaseJob(HttpVerb::Get, GetTokenOwnerJobName, - basePath % "/account/whoami") - , d(new Private) + : BaseJob(HttpVerb::Get, QStringLiteral("GetTokenOwnerJob"), + QStringLiteral("/_matrix/client/r0") % "/account/whoami") { + addExpectedKey("user_id"); } - -GetTokenOwnerJob::~GetTokenOwnerJob() = default; - -const QString& GetTokenOwnerJob::userId() const -{ - return d->userId; -} - -BaseJob::Status GetTokenOwnerJob::parseJson(const QJsonDocument& data) -{ - auto json = data.object(); - if (!json.contains("user_id"_ls)) - return { JsonParseError, - "The key 'user_id' not found in the response" }; - fromJson(json.value("user_id"_ls), d->userId); - return Success; -} - diff --git a/lib/csapi/whoami.h b/lib/csapi/whoami.h index 71e9d532..af8f1e8a 100644 --- a/lib/csapi/whoami.h +++ b/lib/csapi/whoami.h @@ -6,46 +6,35 @@ #include "jobs/basejob.h" +namespace Quotient { + +/*! \brief Gets information about the owner of an access token. + * + * Gets information about the owner of a given access token. + * + * Note that, as with the rest of the Client-Server API, + * Application Services may masquerade as users within their + * namespace by giving a ``user_id`` query parameter. In this + * situation, the server should verify that the given ``user_id`` + * is registered by the appservice, and return it in the response + * body. + */ +class GetTokenOwnerJob : public BaseJob { +public: + /// Gets information about the owner of an access token. + explicit GetTokenOwnerJob(); -namespace QMatrixClient -{ - // Operations + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetTokenOwnerJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); - /// Gets information about the owner of an access token. - /// - /// Gets information about the owner of a given access token. - /// - /// Note that, as with the rest of the Client-Server API, - /// Application Services may masquerade as users within their - /// namespace by giving a ``user_id`` query parameter. In this - /// situation, the server should verify that the given ``user_id`` - /// is registered by the appservice, and return it in the response - /// body. - class GetTokenOwnerJob : public BaseJob - { - public: - explicit GetTokenOwnerJob(); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetTokenOwnerJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl); - - ~GetTokenOwnerJob() override; - - // Result properties - - /// The user id that owns the access token. - const QString& userId() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; -} // namespace QMatrixClient + // Result properties + + /// The user id that owns the access token. + QString userId() const { return loadFromJson<QString>("user_id"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/{{base}}.cpp.mustache b/lib/csapi/{{base}}.cpp.mustache deleted file mode 100644 index ff888d76..00000000 --- a/lib/csapi/{{base}}.cpp.mustache +++ /dev/null @@ -1,123 +0,0 @@ -{{>preamble}} -#include "{{filenameBase}}.h" -{{^models}} -#include "converters.h" -{{/models}}{{#operations}} -{{#producesNonJson?}}#include <QtNetwork/QNetworkReply> -{{/producesNonJson?}}#include <QtCore/QStringBuilder> -{{/operations}} -using namespace QMatrixClient; -{{#models.model}}{{#in?}} -void JsonObjectConverter<{{qualifiedName}}>::dumpTo( - QJsonObject& jo, const {{qualifiedName}}& pod) -{ -{{#propertyMap}} fillJson(jo, pod.{{nameCamelCase}}); -{{/propertyMap}}{{#parents}} fillJson<{{name}}>(jo, pod); -{{/parents}}{{#vars}} addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}}); -{{/vars}}}{{!<- dumpTo() ends here}} -{{/in?}}{{#out?}} -void JsonObjectConverter<{{qualifiedName}}>::fillFrom( - {{^propertyMap}}const QJsonObject&{{/propertyMap - }}{{#propertyMap}}QJsonObject{{/propertyMap}} jo, {{qualifiedName}}& result) -{ -{{#parents}} fillFromJson<{{qualifiedName}}>(jo, result); -{{/parents}}{{#vars}} fromJson(jo.{{#propertyMap}}take{{/propertyMap - }}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls), result.{{nameCamelCase}}); -{{/vars}}{{#propertyMap}} - fromJson(jo, result.{{nameCamelCase}}); -{{/propertyMap}}} -{{/out?}}{{/models.model}}{{#operations}} -static const auto basePath = QStringLiteral("{{basePathWithoutHost}}"); -{{# operation}}{{#models}} -namespace QMatrixClient -{ - // Converters -{{#model}} - template <> struct JsonObjectConverter<{{qualifiedName}}> - { -{{#in?}} static void dumpTo(QJsonObject& jo, const {{qualifiedName}}& pod) - { -{{#propertyMap}} fillJson(jo, pod.{{nameCamelCase}}); - {{/propertyMap}}{{#parents}}fillJson<{{name}}>(jo, pod); - {{/parents}}{{#vars -}} addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}}); -{{/vars}} } -{{/in?}}{{#out? -}} static void fillFrom({{^propertyMap}}const QJsonObject&{{/propertyMap - }}{{#propertyMap}}QJsonObject{{/propertyMap}} jo, {{qualifiedName}}& result) - { -{{#parents}} fillFromJson<{{qualifiedName}}{{!of the parent!}}>(jo, result); - {{/parents}}{{#vars -}} fromJson(jo.{{#propertyMap}}take{{/propertyMap - }}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls), result.{{nameCamelCase}}); -{{/vars}}{{#propertyMap}} fromJson(jo, result.{{nameCamelCase}}); -{{/propertyMap}} } -{{/out?}} }; -{{/model}}} // namespace QMatrixClient -{{/ models}}{{#responses}}{{#normalResponse?}}{{#allProperties?}} -class {{camelCaseOperationId}}Job::Private -{ - public:{{#allProperties}} - {{>maybeOmittableType}} {{paramName}};{{/allProperties}} -}; -{{/ allProperties?}}{{/normalResponse?}}{{/responses}}{{#queryParams?}} -BaseJob::Query queryTo{{camelCaseOperationId}}({{#queryParams}}{{>joinedParamDef}}{{/queryParams}}) -{ - BaseJob::Query _q;{{#queryParams}} - addParam<{{^required?}}IfNotEmpty{{/required?}}>(_q, QStringLiteral("{{baseName}}"), {{paramName}});{{/queryParams}} - return _q; -} -{{/queryParams?}}{{^bodyParams}} -QUrl {{camelCaseOperationId}}Job::makeRequestUrl(QUrl baseUrl{{#allParams?}}, {{#allParams}}{{>joinedParamDef}}{{/allParams}}{{/allParams?}}) -{ - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath{{#pathParts}} % {{_}}{{/pathParts}}{{#queryParams?}}, - queryTo{{camelCaseOperationId}}({{>passQueryParams}}){{/queryParams?}}); -} -{{/ bodyParams}} -static const auto {{camelCaseOperationId}}JobName = QStringLiteral("{{camelCaseOperationId}}Job"); - -{{camelCaseOperationId}}Job::{{camelCaseOperationId}}Job({{#allParams}}{{>joinedParamDef}}{{/allParams}}) - : BaseJob(HttpVerb::{{#_cap}}{{#_tolower}}{{httpMethod}}{{/_tolower}}{{/_cap}}, {{camelCaseOperationId}}JobName, - basePath{{#pathParts}} % {{_}}{{/pathParts}}{{#queryParams?}}, - queryTo{{camelCaseOperationId}}({{>passQueryParams}}){{/queryParams?}}{{#skipAuth}}{{#queryParams?}}, - {}{{/queryParams?}}, false{{/skipAuth}}){{#responses}}{{#normalResponse?}}{{#allProperties?}} - , d(new Private){{/allProperties?}}{{/normalResponse?}}{{/responses}} -{ -{{#headerParams?}}{{#headerParams}} setRequestHeader("{{baseName}}", {{paramName}}.toLatin1()); -{{/headerParams}} -{{/headerParams? -}}{{#bodyParams? -}}{{#inlineBody}} setRequestData(Data({{! - }}{{#consumesNonJson?}}{{nameCamelCase}}{{/consumesNonJson? - }}{{^consumesNonJson?}}toJson({{nameCamelCase}}){{/consumesNonJson?}}));{{/inlineBody -}}{{^inlineBody}} QJsonObject _data;{{#bodyParams}} - addParam<{{^required?}}IfNotEmpty{{/required?}}>(_data, QStringLiteral("{{baseName}}"), {{paramName}});{{/bodyParams}} - setRequestData(_data);{{/inlineBody}} -{{/bodyParams?}}{{#producesNonJson?}} setExpectedContentTypes({ {{#produces}}"{{_}}"{{>cjoin}}{{/produces}} }); -{{/producesNonJson?}}}{{!<- mind the actual brace}} -{{# responses}}{{#normalResponse?}}{{#allProperties?}} -{{camelCaseOperationId}}Job::~{{camelCaseOperationId}}Job() = default; -{{# allProperties}} -{{>qualifiedMaybeCrefType}} {{camelCaseOperationId}}Job::{{paramName}}(){{^moveOnly}} const{{/moveOnly}} -{ - return {{#moveOnly}}std::move({{/moveOnly}}d->{{paramName}}{{#moveOnly}}){{/moveOnly}}; -} -{{/ allProperties}}{{#producesNonJson?}} -BaseJob::Status {{camelCaseOperationId}}Job::parseReply(QNetworkReply* reply) -{ - {{#headers}}d->{{paramName}} = reply->rawHeader("{{baseName}}");{{! We don't check for required headers yet }} - {{/headers}}{{#properties}}d->{{paramName}} = reply;{{/properties}} - return Success; -}{{/ producesNonJson?}}{{^producesNonJson?}} -BaseJob::Status {{camelCaseOperationId}}Job::parseJson(const QJsonDocument& data) -{ -{{#inlineResponse}} fromJson(data, d->{{paramName}}); -{{/inlineResponse}}{{^inlineResponse}} auto json = data.object(); -{{#properties}}{{#required?}} if (!json.contains("{{baseName}}"_ls)) - return { JsonParseError, - "The key '{{baseName}}' not found in the response" }; -{{/required?}} fromJson(json.value("{{baseName}}"_ls), d->{{paramName}}); -{{/properties}}{{/inlineResponse}} return Success; -}{{/ producesNonJson?}} -{{/allProperties?}}{{/normalResponse?}}{{/responses}}{{/operation}}{{/operations}} diff --git a/lib/csapi/{{base}}.h.mustache b/lib/csapi/{{base}}.h.mustache deleted file mode 100644 index a9c3a63a..00000000 --- a/lib/csapi/{{base}}.h.mustache +++ /dev/null @@ -1,78 +0,0 @@ -{{>preamble}} -#pragma once - -{{#operations}}#include "jobs/basejob.h" -{{/operations}}{{#models}}#include "converters.h" -{{/models}} -{{#imports}}#include {{_}} -{{/imports}} -namespace QMatrixClient -{ -{{#models}} // Data structures -{{# model}}{{#description}} - /// {{_}}{{/description}} - struct {{name}}{{#parents?}} : {{#parents}}{{name}}{{>cjoin}}{{/parents}}{{/parents?}} - { -{{#vars}}{{#description}} /// {{_}} -{{/description}} {{>maybeOmittableType}} {{nameCamelCase}}; -{{/vars}}{{#propertyMap}}{{#description}} /// {{_}} -{{/description}} {{>maybeOmittableType}} {{nameCamelCase}}; -{{/propertyMap}} }; - template <> struct JsonObjectConverter<{{name}}> - { - {{#in?}}static void dumpTo(QJsonObject& jo, const {{name}}& pod); - {{/in?}}{{#out?}}static void fillFrom({{^propertyMap}}const QJsonObject&{{/propertyMap - }}{{#propertyMap}}QJsonObject{{/propertyMap}} jo, {{name}}& pod); -{{/out?}} }; -{{/model}} -{{/models}}{{#operations}} // Operations -{{# operation}}{{#summary}} - /// {{summary}}{{#description?}}{{!add a linebreak between summary and description if both exist}} - ///{{/description?}}{{/summary}}{{#description}} - /// {{_}}{{/description}} - class {{camelCaseOperationId}}Job : public BaseJob - { - public:{{#models}} - // Inner data structures -{{# model}}{{#description}} - /// {{_}}{{/description}} - struct {{name}}{{#parents?}} : {{#parents}}{{name}}{{>cjoin}}{{/parents}}{{/parents?}} - { -{{#vars}}{{#description}} /// {{_}} -{{/description}} {{>maybeOmittableType}} {{nameCamelCase}}; -{{/vars}}{{#propertyMap}}{{#description}} /// {{_}} -{{/description}} {{>maybeOmittableType}} {{nameCamelCase}}; -{{/propertyMap}} }; -{{/ model}} - // Construction/destruction -{{/ models}}{{#allParams?}} - /*! {{summary}}{{#allParams}} - * \param {{nameCamelCase}}{{#description}} - * {{_}}{{/description}}{{/allParams}} - */{{/allParams?}} - explicit {{camelCaseOperationId}}Job({{#allParams}}{{>joinedParamDecl}}{{/allParams}});{{^bodyParams}} - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * {{camelCaseOperationId}}Job is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl{{#allParams?}}, {{#allParams}}{{>joinedParamDecl}}{{/allParams}}{{/allParams?}}); -{{/bodyParams}}{{# responses}}{{#normalResponse?}}{{#allProperties?}} - ~{{camelCaseOperationId}}Job() override; - - // Result properties -{{#allProperties}}{{#description}} - /// {{_}}{{/description}} - {{>maybeCrefType}} {{paramName}}(){{^moveOnly}} const{{/moveOnly}};{{/allProperties}} - - protected: - Status {{#producesNonJson?}}parseReply(QNetworkReply* reply){{/producesNonJson?}}{{^producesNonJson?}}parseJson(const QJsonDocument& data){{/producesNonJson?}} override; - - private: - class Private; - QScopedPointer<Private> d;{{/allProperties?}}{{/normalResponse?}}{{/responses}} - }; -{{/operation}}{{/operations}}{{!skip EOL -}}} // namespace QMatrixClient diff --git a/lib/e2ee.h b/lib/e2ee.h new file mode 100644 index 00000000..f49b9748 --- /dev/null +++ b/lib/e2ee.h @@ -0,0 +1,31 @@ +#pragma once + +#include "util.h" + +#include <QtCore/QStringList> + +namespace Quotient { +inline const auto CiphertextKeyL = "ciphertext"_ls; +inline const auto SenderKeyKeyL = "sender_key"_ls; +inline const auto DeviceIdKeyL = "device_id"_ls; +inline const auto SessionIdKeyL = "session_id"_ls; + +inline const auto AlgorithmKeyL = "algorithm"_ls; +inline const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; +inline const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; + +inline const auto AlgorithmKey = QStringLiteral("algorithm"); +inline const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms"); +inline const auto RotationPeriodMsgsKey = + QStringLiteral("rotation_period_msgs"); + +inline const auto Ed25519Key = QStringLiteral("ed25519"); +inline const auto Curve25519Key = QStringLiteral("curve25519"); +inline const auto SignedCurve25519Key = QStringLiteral("signed_curve25519"); +inline const auto OlmV1Curve25519AesSha2AlgoKey = + QStringLiteral("m.olm.v1.curve25519-aes-sha2"); +inline const auto MegolmV1AesSha2AlgoKey = + QStringLiteral("m.megolm.v1.aes-sha2"); +inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey, + MegolmV1AesSha2AlgoKey }; +} // namespace Quotient diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp new file mode 100644 index 00000000..4a1025b2 --- /dev/null +++ b/lib/encryptionmanager.cpp @@ -0,0 +1,369 @@ +#ifdef Quotient_E2EE_ENABLED +#include "encryptionmanager.h" + +#include "connection.h" +#include "e2ee.h" + +#include "csapi/keys.h" + +#include <QtCore/QHash> +#include <QtCore/QStringBuilder> + +#include <account.h> // QtOlm +#include <session.h> // QtOlm +#include <message.h> // QtOlm +#include <errors.h> // QtOlm +#include <utils.h> // QtOlm +#include <functional> +#include <memory> + +using namespace Quotient; +using namespace QtOlm; +using std::move; + +class EncryptionManager::Private { +public: + explicit Private(const QByteArray& encryptionAccountPickle, + float signedKeysProportion, float oneTimeKeyThreshold) + : q(nullptr) + , signedKeysProportion(move(signedKeysProportion)) + , oneTimeKeyThreshold(move(oneTimeKeyThreshold)) + { + Q_ASSERT((0 <= signedKeysProportion) && (signedKeysProportion <= 1)); + Q_ASSERT((0 <= oneTimeKeyThreshold) && (oneTimeKeyThreshold <= 1)); + if (encryptionAccountPickle.isEmpty()) { + olmAccount.reset(new Account()); + } else { + olmAccount.reset( + new Account(encryptionAccountPickle)); // TODO: passphrase even + // with qtkeychain? + } + /* + * Note about targetKeysNumber: + * + * From: https://github.com/Zil0/matrix-python-sdk/ + * File: matrix_client/crypto/olm_device.py + * + * Try to maintain half the number of one-time keys libolm can hold + * uploaded on the HS. This is because some keys will be claimed by + * peers but not used instantly, and we want them to stay in libolm, + * until the limit is reached and it starts discarding keys, starting by + * the oldest. + */ + targetKeysNumber = olmAccount->maxOneTimeKeys() / 2; + targetOneTimeKeyCounts = { + { SignedCurve25519Key, + qRound(signedKeysProportion * targetKeysNumber) }, + { Curve25519Key, + qRound((1 - signedKeysProportion) * targetKeysNumber) } + }; + updateKeysToUpload(); + } + ~Private() = default; + + EncryptionManager* q; + + UploadKeysJob* uploadIdentityKeysJob = nullptr; + UploadKeysJob* uploadOneTimeKeysInitJob = nullptr; + UploadKeysJob* uploadOneTimeKeysJob = nullptr; + QueryKeysJob* queryKeysJob = nullptr; + + QScopedPointer<Account> olmAccount; + + float signedKeysProportion; + float oneTimeKeyThreshold; + int targetKeysNumber; + + void updateKeysToUpload(); + bool oneTimeKeyShouldUpload(); + + QHash<QString, int> oneTimeKeyCounts; + void setOneTimeKeyCounts(const QHash<QString, int> oneTimeKeyCountsNewValue) + { + oneTimeKeyCounts = oneTimeKeyCountsNewValue; + updateKeysToUpload(); + } + QHash<QString, int> oneTimeKeysToUploadCounts; + QHash<QString, int> targetOneTimeKeyCounts; + + // A map from senderKey to InboundSession + QMap<QString, InboundSession*> sessions; // TODO: cache + void updateDeviceKeys( + const QHash<QString, + QHash<QString, QueryKeysJob::DeviceInformation>>& deviceKeys) + { + for (auto userId : deviceKeys.keys()) { + for (auto deviceId : deviceKeys.value(userId).keys()) { + auto info = deviceKeys.value(userId).value(deviceId); + // TODO: ed25519Verify, etc + } + } + } + QString sessionDecrypt(Message* message, const QString& senderKey) + { + QString decrypted; + QList<InboundSession*> senderSessions = sessions.values(senderKey); + // Try to decrypt message body using one of the known sessions for that + // device + bool sessionsPassed = false; + for (auto senderSession : senderSessions) { + if (senderSession == senderSessions.last()) { + sessionsPassed = true; + } + try { + decrypted = senderSession->decrypt(message); + qCDebug(E2EE) + << "Success decrypting Olm event using existing session" + << senderSession->id(); + break; + } catch (OlmError* e) { + if (message->messageType() == 0) { + PreKeyMessage preKeyMessage = + PreKeyMessage(message->cipherText()); + if (senderSession->matches(&preKeyMessage, senderKey)) { + // We had a matching session for a pre-key message, but + // it didn't work. This means something is wrong, so we + // fail now. + qCDebug(E2EE) + << "Error decrypting pre-key message with existing " + "Olm session" + << senderSession->id() << "reason:" << e->what(); + return QString(); + } + } + // Simply keep trying otherwise + } + } + if (sessionsPassed || senderSessions.empty()) { + if (message->messageType() > 0) { + // Not a pre-key message, we should have had a matching session + if (!sessions.empty()) { + qCDebug(E2EE) << "Error decrypting with existing sessions"; + return QString(); + } + qCDebug(E2EE) << "No existing sessions"; + return QString(); + } + // We have a pre-key message without any matching session, in this + // case we should try to create one. + InboundSession* newSession; + qCDebug(E2EE) << "try to establish new InboundSession with" << senderKey; + PreKeyMessage preKeyMessage = PreKeyMessage(message->cipherText()); + try { + newSession = new InboundSession(olmAccount.data(), + &preKeyMessage, + senderKey.toLatin1(), q); + } catch (OlmError* e) { + qCDebug(E2EE) << "Error decrypting pre-key message when trying " + "to establish a new session:" + << e->what(); + return QString(); + } + qCDebug(E2EE) << "Created new Olm session" << newSession->id(); + try { + decrypted = newSession->decrypt(message); + } catch (OlmError* e) { + qCDebug(E2EE) + << "Error decrypting pre-key message with new session" + << e->what(); + return QString(); + } + olmAccount->removeOneTimeKeys(newSession); + sessions.insert(senderKey, newSession); + } + return decrypted; + } +}; + +EncryptionManager::EncryptionManager(const QByteArray& encryptionAccountPickle, + float signedKeysProportion, + float oneTimeKeyThreshold, QObject* parent) + : QObject(parent) + , d(std::make_unique<Private>(std::move(encryptionAccountPickle), + std::move(signedKeysProportion), + std::move(oneTimeKeyThreshold))) +{ + d->q = this; +} + +EncryptionManager::~EncryptionManager() = default; + +void EncryptionManager::uploadIdentityKeys(Connection* connection) +{ + // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-keys-upload + DeviceKeys deviceKeys { + /* + * The ID of the user the device belongs to. Must match the user ID used + * when logging in. The ID of the device these keys belong to. Must + * match the device ID used when logging in. The encryption algorithms + * supported by this device. + */ + connection->userId(), + connection->deviceId(), + SupportedAlgorithms, + /* + * Public identity keys. The names of the properties should be in the + * format <algorithm>:<device_id>. The keys themselves should be encoded + * as specified by the key algorithm. + */ + { { Curve25519Key + QStringLiteral(":") + connection->deviceId(), + d->olmAccount->curve25519IdentityKey() }, + { Ed25519Key + QStringLiteral(":") + connection->deviceId(), + d->olmAccount->ed25519IdentityKey() } }, + /* signatures should be provided after the unsigned deviceKeys + generation */ + {} + }; + + QJsonObject deviceKeysJsonObject = toJson(deviceKeys); + /* additionally removing signatures key, + * since we could not initialize deviceKeys + * without an empty signatures value: + */ + deviceKeysJsonObject.remove(QStringLiteral("signatures")); + /* + * Signatures for the device key object. + * A map from user ID, to a map from <algorithm>:<device_id> to the + * signature. The signature is calculated using the process called Signing + * JSON. + */ + deviceKeys.signatures = { + { connection->userId(), + { { Ed25519Key + QStringLiteral(":") + connection->deviceId(), + d->olmAccount->sign(deviceKeysJsonObject) } } } + }; + + d->uploadIdentityKeysJob = connection->callApi<UploadKeysJob>(deviceKeys); + connect(d->uploadIdentityKeysJob, &BaseJob::success, this, [this] { + d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts()); + }); +} + +void EncryptionManager::uploadOneTimeKeys(Connection* connection, + bool forceUpdate) +{ + if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) { + d->uploadOneTimeKeysInitJob = connection->callApi<UploadKeysJob>(); + connect(d->uploadOneTimeKeysInitJob, &BaseJob::success, this, [this] { + d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts()); + }); + } + + int signedKeysToUploadCount = + d->oneTimeKeysToUploadCounts.value(SignedCurve25519Key, 0); + int unsignedKeysToUploadCount = + d->oneTimeKeysToUploadCounts.value(Curve25519Key, 0); + + d->olmAccount->generateOneTimeKeys(signedKeysToUploadCount + + unsignedKeysToUploadCount); + + QHash<QString, QVariant> oneTimeKeys = {}; + const auto& olmAccountCurve25519OneTimeKeys = + d->olmAccount->curve25519OneTimeKeys(); + + int oneTimeKeysCounter = 0; + for (auto it = olmAccountCurve25519OneTimeKeys.cbegin(); + it != olmAccountCurve25519OneTimeKeys.cend(); ++it) { + QString keyId = it.key(); + QString keyType; + QVariant key; + if (oneTimeKeysCounter < signedKeysToUploadCount) { + QJsonObject message { { QStringLiteral("key"), + it.value().toString() } }; + + QByteArray signedMessage = d->olmAccount->sign(message); + QJsonObject signatures { + { connection->userId(), + QJsonObject { { Ed25519Key + QStringLiteral(":") + + connection->deviceId(), + QString::fromUtf8(signedMessage) } } } + }; + message.insert(QStringLiteral("signatures"), signatures); + key = message; + keyType = SignedCurve25519Key; + } else { + key = it.value(); + keyType = Curve25519Key; + } + ++oneTimeKeysCounter; + oneTimeKeys.insert(QString("%1:%2").arg(keyType).arg(keyId), key); + } + d->uploadOneTimeKeysJob = + connection->callApi<UploadKeysJob>(none, oneTimeKeys); + connect(d->uploadOneTimeKeysJob, &BaseJob::success, this, [this] { + d->setOneTimeKeyCounts(d->uploadOneTimeKeysJob->oneTimeKeyCounts()); + }); + d->olmAccount->markKeysAsPublished(); + qCDebug(E2EE) << QString("Uploaded new one-time keys: %1 signed, %2 unsigned.") + .arg(signedKeysToUploadCount) + .arg(unsignedKeysToUploadCount); +} + +void EncryptionManager::updateOneTimeKeyCounts( + Connection* connection, const QHash<QString, int>& deviceOneTimeKeysCount) +{ + d->oneTimeKeyCounts = deviceOneTimeKeysCount; + if (d->oneTimeKeyShouldUpload()) { + qCDebug(E2EE) << "Uploading new one-time keys."; + uploadOneTimeKeys(connection); + } +} + +void Quotient::EncryptionManager::updateDeviceKeys( + Connection* connection, const QHash<QString, QStringList>& deviceKeys) +{ + d->queryKeysJob = connection->callApi<QueryKeysJob>(deviceKeys); + connect(d->queryKeysJob, &BaseJob::success, this, + [this] { d->updateDeviceKeys(d->queryKeysJob->deviceKeys()); }); +} + +QString EncryptionManager::sessionDecryptMessage( + const QJsonObject& personalCipherObject, const QByteArray& senderKey) +{ + QString decrypted; + int type = personalCipherObject.value(TypeKeyL).toInt(-1); + QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1(); + if (type == 0) { + PreKeyMessage preKeyMessage { body }; + decrypted = d->sessionDecrypt(reinterpret_cast<Message*>(&preKeyMessage), + senderKey); + } else if (type == 1) { + Message message { body }; + decrypted = d->sessionDecrypt(&message, senderKey); + } + return decrypted; +} + +QByteArray EncryptionManager::olmAccountPickle() +{ + return d->olmAccount->pickle(); // TODO: passphrase even with qtkeychain? +} + +QtOlm::Account* EncryptionManager::account() const +{ + return d->olmAccount.data(); +} + +void EncryptionManager::Private::updateKeysToUpload() +{ + for (auto it = targetOneTimeKeyCounts.cbegin(); + it != targetOneTimeKeyCounts.cend(); ++it) { + int numKeys = oneTimeKeyCounts.value(it.key(), 0); + int numToCreate = qMax(it.value() - numKeys, 0); + oneTimeKeysToUploadCounts.insert(it.key(), numToCreate); + } +} + +bool EncryptionManager::Private::oneTimeKeyShouldUpload() +{ + if (oneTimeKeyCounts.empty()) + return true; + for (auto it = targetOneTimeKeyCounts.cbegin(); + it != targetOneTimeKeyCounts.cend(); ++it) { + if (oneTimeKeyCounts.value(it.key(), 0) + < it.value() * oneTimeKeyThreshold) + return true; + } + return false; +} +#endif // Quotient_E2EE_ENABLED diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h new file mode 100644 index 00000000..5df15e83 --- /dev/null +++ b/lib/encryptionmanager.h @@ -0,0 +1,47 @@ +#ifdef Quotient_E2EE_ENABLED +#pragma once + +#include <QtCore/QObject> + +#include <functional> +#include <memory> + +namespace QtOlm { +class Account; +} + +namespace Quotient { +class Connection; + +class EncryptionManager : public QObject { + Q_OBJECT + +public: + // TODO: store constats separately? + // TODO: 0.5 oneTimeKeyThreshold instead of 0.1? + explicit EncryptionManager( + const QByteArray& encryptionAccountPickle = QByteArray(), + float signedKeysProportion = 1, float oneTimeKeyThreshold = float(0.1), + QObject* parent = nullptr); + ~EncryptionManager(); + + void uploadIdentityKeys(Connection* connection); + void uploadOneTimeKeys(Connection* connection, bool forceUpdate = false); + void + updateOneTimeKeyCounts(Connection* connection, + const QHash<QString, int>& deviceOneTimeKeysCount); + void updateDeviceKeys(Connection* connection, + const QHash<QString, QStringList>& deviceKeys); + QString sessionDecryptMessage(const QJsonObject& personalCipherObject, + const QByteArray& senderKey); + QByteArray olmAccountPickle(); + + QtOlm::Account* account() const; + +private: + class Private; + std::unique_ptr<Private> d; +}; + +} // namespace Quotient +#endif // Quotient_E2EE_ENABLED diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index 8ec3fe48..2e2b11c0 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -13,33 +13,30 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "eventitem.h" -#include "events/roommessageevent.h" #include "events/roomavatarevent.h" +#include "events/roommessageevent.h" -using namespace QMatrixClient; +using namespace Quotient; void PendingEventItem::setFileUploaded(const QUrl& remoteUrl) { // TODO: eventually we might introduce hasFileContent to RoomEvent, // and unify the code below. - if (auto* rme = getAs<RoomMessageEvent>()) - { + if (auto* rme = getAs<RoomMessageEvent>()) { Q_ASSERT(rme->hasFileContent()); - rme->editContent([remoteUrl] (EventContent::TypedBase& ec) { + rme->editContent([remoteUrl](EventContent::TypedBase& ec) { ec.fileInfo()->url = remoteUrl; }); } - if (auto* rae = getAs<RoomAvatarEvent>()) - { + if (auto* rae = getAs<RoomAvatarEvent>()) { Q_ASSERT(rae->content().fileInfo()); - rae->editContent([remoteUrl] (EventContent::FileInfo& fi) { - fi.url = remoteUrl; - }); + rae->editContent( + [remoteUrl](EventContent::FileInfo& fi) { fi.url = remoteUrl; }); } setStatus(EventStatus::FileUploaded); } diff --git a/lib/eventitem.h b/lib/eventitem.h index 36ed2132..7b2c3c44 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once @@ -22,135 +22,137 @@ #include <utility> -namespace QMatrixClient -{ - class StateEventBase; +namespace Quotient { +class StateEventBase; + +class EventStatus { + Q_GADGET +public: + /** Special marks an event can assume + * + * This is used to hint at a special status of some events in UI. + * All values except Redacted and Hidden are mutually exclusive. + */ + enum Code { + Normal = 0x0, //< No special designation + Submitted = 0x01, //< The event has just been submitted for sending + FileUploaded = 0x02, //< The file attached to the event has been + // uploaded to the server + Departed = 0x03, //< The event has left the client + ReachedServer = 0x04, //< The server has received the event + SendingFailed = 0x05, //< The server could not receive the event + Redacted = 0x08, //< The event has been redacted + Replaced = 0x10, //< The event has been replaced + Hidden = 0x100, //< The event should not be shown in the timeline + }; + Q_DECLARE_FLAGS(Status, Code) + Q_FLAG(Status) +}; - class EventStatus +class EventItemBase { +public: + explicit EventItemBase(RoomEventPtr&& e) : evt(std::move(e)) { - Q_GADGET - public: - /** Special marks an event can assume - * - * This is used to hint at a special status of some events in UI. - * All values except Redacted and Hidden are mutually exclusive. - */ - enum Code { - Normal = 0x0, //< No special designation - Submitted = 0x01, //< The event has just been submitted for sending - FileUploaded = 0x02, //< The file attached to the event has been uploaded to the server - Departed = 0x03, //< The event has left the client - ReachedServer = 0x04, //< The server has received the event - SendingFailed = 0x05, //< The server could not receive the event - Redacted = 0x08, //< The event has been redacted - Hidden = 0x10, //< The event should not be shown in the timeline - }; - Q_DECLARE_FLAGS(Status, Code) - Q_FLAG(Status) - }; + Q_ASSERT(evt); + } - class EventItemBase + const RoomEvent* event() const { return rawPtr(evt); } + const RoomEvent* get() const { return event(); } + template <typename EventT> + const EventT* viewAs() const { - public: - explicit EventItemBase(RoomEventPtr&& e) - : evt(std::move(e)) - { - Q_ASSERT(evt); - } - - const RoomEvent* event() const { return rawPtr(evt); } - const RoomEvent* get() const { return event(); } - template <typename EventT> - const EventT* viewAs() const { return eventCast<const EventT>(evt); } - const RoomEventPtr& operator->() const { return evt; } - const RoomEvent& operator*() const { return *evt; } - - // Used for event redaction - RoomEventPtr replaceEvent(RoomEventPtr&& other) - { - return std::exchange(evt, move(other)); - } - - protected: - template <typename EventT> - EventT* getAs() { return eventCast<EventT>(evt); } - private: - RoomEventPtr evt; - }; + return eventCast<const EventT>(evt); + } + const RoomEventPtr& operator->() const { return evt; } + const RoomEvent& operator*() const { return *evt; } - class TimelineItem : public EventItemBase + // Used for event redaction + RoomEventPtr replaceEvent(RoomEventPtr&& other) { - public: - // For compatibility with Qt containers, even though we use - // a std:: container now for the room timeline - using index_t = int; + return std::exchange(evt, move(other)); + } - TimelineItem(RoomEventPtr&& e, index_t number) - : EventItemBase(std::move(e)), idx(number) - { } +protected: + template <typename EventT> + EventT* getAs() + { + return eventCast<EventT>(evt); + } - index_t index() const { return idx; } +private: + RoomEventPtr evt; +}; - private: - index_t idx; - }; +class TimelineItem : public EventItemBase { +public: + // For compatibility with Qt containers, even though we use + // a std:: container now for the room timeline + using index_t = int; + + TimelineItem(RoomEventPtr&& e, index_t number) + : EventItemBase(std::move(e)), idx(number) + {} + + index_t index() const { return idx; } + +private: + index_t idx; +}; - template<> - inline const StateEventBase* EventItemBase::viewAs<StateEventBase>() const +template <> +inline const StateEventBase* EventItemBase::viewAs<StateEventBase>() const +{ + return evt->isStateEvent() ? weakPtrCast<const StateEventBase>(evt) + : nullptr; +} + +template <> +inline const CallEventBase* EventItemBase::viewAs<CallEventBase>() const +{ + return evt->isCallEvent() ? weakPtrCast<const CallEventBase>(evt) : nullptr; +} + +class PendingEventItem : public EventItemBase { + Q_GADGET +public: + using EventItemBase::EventItemBase; + + EventStatus::Code deliveryStatus() const { return _status; } + QDateTime lastUpdated() const { return _lastUpdated; } + QString annotation() const { return _annotation; } + + void setDeparted() { setStatus(EventStatus::Departed); } + void setFileUploaded(const QUrl& remoteUrl); + void setReachedServer(const QString& eventId) { - return evt->isStateEvent() ? weakPtrCast<const StateEventBase>(evt) - : nullptr; + setStatus(EventStatus::ReachedServer); + (*this)->addId(eventId); } - - template<> - inline const CallEventBase* EventItemBase::viewAs<CallEventBase>() const + void setSendingFailed(QString errorText) { - return evt->isCallEvent() ? weakPtrCast<const CallEventBase>(evt) - : nullptr; + setStatus(EventStatus::SendingFailed); + _annotation = std::move(errorText); } + void resetStatus() { setStatus(EventStatus::Submitted); } - class PendingEventItem : public EventItemBase - { - Q_GADGET - public: - using EventItemBase::EventItemBase; - - EventStatus::Code deliveryStatus() const { return _status; } - QDateTime lastUpdated() const { return _lastUpdated; } - QString annotation() const { return _annotation; } - - void setDeparted() { setStatus(EventStatus::Departed); } - void setFileUploaded(const QUrl& remoteUrl); - void setReachedServer(const QString& eventId) - { - setStatus(EventStatus::ReachedServer); - (*this)->addId(eventId); - } - void setSendingFailed(QString errorText) - { - setStatus(EventStatus::SendingFailed); - _annotation = std::move(errorText); - } - void resetStatus() { setStatus(EventStatus::Submitted); } - - private: - EventStatus::Code _status = EventStatus::Submitted; - QDateTime _lastUpdated = QDateTime::currentDateTimeUtc(); - QString _annotation; - - void setStatus(EventStatus::Code status) - { - _status = status; - _lastUpdated = QDateTime::currentDateTimeUtc(); - _annotation.clear(); - } - }; +private: + EventStatus::Code _status = EventStatus::Submitted; + QDateTime _lastUpdated = QDateTime::currentDateTimeUtc(); + QString _annotation; - inline QDebug& operator<<(QDebug& d, const TimelineItem& ti) + void setStatus(EventStatus::Code status) { - QDebugStateSaver dss(d); - d.nospace() << "(" << ti.index() << "|" << ti->id() << ")"; - return d; + _status = status; + _lastUpdated = QDateTime::currentDateTimeUtc(); + _annotation.clear(); } +}; + +inline QDebug& operator<<(QDebug& d, const TimelineItem& ti) +{ + QDebugStateSaver dss(d); + d.nospace() << "(" << ti.index() << "|" << ti->id() << ")"; + return d; } -Q_DECLARE_METATYPE(QMatrixClient::EventStatus) +} // namespace Quotient +Q_DECLARE_METATYPE(Quotient::EventStatus) diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index a43e358c..a55016d9 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -1,5 +1,3 @@ -#include <utility> - /****************************************************************************** * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> * @@ -15,87 +13,80 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once +#include "converters.h" #include "event.h" #include "eventcontent.h" -#include "converters.h" -namespace QMatrixClient -{ - constexpr const char* FavouriteTag = "m.favourite"; - constexpr const char* LowPriorityTag = "m.lowpriority"; - constexpr const char* ServerNoticeTag = "m.server_notice"; +namespace Quotient { +constexpr const char* FavouriteTag = "m.favourite"; +constexpr const char* LowPriorityTag = "m.lowpriority"; +constexpr const char* ServerNoticeTag = "m.server_notice"; - struct TagRecord - { - using order_type = Omittable<float>; +struct TagRecord { + using order_type = Omittable<float>; - order_type order; + order_type order; - TagRecord (order_type order = none) : order(order) { } + TagRecord(order_type order = none) : order(std::move(order)) {} - bool operator<(const TagRecord& other) const - { - // Per The Spec, rooms with no order should be after those with order - return !order.omitted() && - (other.order.omitted() || order.value() < other.order.value()); - } - }; + bool operator<(const TagRecord& other) const + { + // Per The Spec, rooms with no order should be after those with order, + // against optional<>::operator<() convention. + return order && (!other.order || *order < *other.order); + } +}; - template <> struct JsonObjectConverter<TagRecord> +template <> +struct JsonObjectConverter<TagRecord> { + static void fillFrom(const QJsonObject& jo, TagRecord& rec) { - static void fillFrom(const QJsonObject& jo, TagRecord& rec) - { - // Parse a float both from JSON double and JSON string because - // libqmatrixclient previously used to use strings to store order. - const auto orderJv = jo.value("order"_ls); - if (orderJv.isDouble()) - rec.order = fromJson<float>(orderJv); - if (orderJv.isString()) - { - bool ok; - rec.order = orderJv.toString().toFloat(&ok); - if (!ok) - rec.order = none; - } - } - static void dumpTo(QJsonObject& jo, const TagRecord& rec) - { - addParam<IfNotEmpty>(jo, QStringLiteral("order"), rec.order); + // Parse a float both from JSON double and JSON string because + // the library previously used to use strings to store order. + const auto orderJv = jo.value("order"_ls); + if (orderJv.isDouble()) + rec.order = fromJson<float>(orderJv); + if (orderJv.isString()) { + bool ok; + rec.order = orderJv.toString().toFloat(&ok); + if (!ok) + rec.order = none; } - }; + } + static void dumpTo(QJsonObject& jo, const TagRecord& rec) + { + addParam<IfNotEmpty>(jo, QStringLiteral("order"), rec.order); + } +}; - using TagsMap = QHash<QString, TagRecord>; +using TagsMap = QHash<QString, TagRecord>; -#define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ - class _Name : public Event \ - { \ - public: \ - using content_type = _ContentType; \ - DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name(QJsonObject obj) \ - : Event(typeId(), std::move(obj)) \ - { } \ - explicit _Name(_ContentType content) \ - : Event(typeId(), matrixTypeId(), \ - QJsonObject { { QStringLiteral(#_ContentKey), \ - toJson(std::move(content)) } }) \ - { } \ - auto _ContentKey() const \ - { return fromJson<content_type>(contentJson()[#_ContentKey##_ls]); } \ - }; \ - REGISTER_EVENT_TYPE(_Name) \ +#define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ + class _Name : public Event { \ + public: \ + using content_type = _ContentType; \ + DEFINE_EVENT_TYPEID(_TypeId, _Name) \ + explicit _Name(QJsonObject obj) : Event(typeId(), std::move(obj)) {} \ + explicit _Name(_ContentType content) \ + : Event(typeId(), matrixTypeId(), \ + QJsonObject { { QStringLiteral(#_ContentKey), \ + toJson(std::move(content)) } }) \ + {} \ + auto _ContentKey() const \ + { \ + return content<content_type>(#_ContentKey##_ls); \ + } \ + }; \ + REGISTER_EVENT_TYPE(_Name) \ // End of macro - DEFINE_SIMPLE_EVENT(TagEvent, "m.tag", TagsMap, tags) - DEFINE_SIMPLE_EVENT(ReadMarkerEvent, "m.fully_read", QString, event_id) - DEFINE_SIMPLE_EVENT(IgnoredUsersEvent, "m.ignored_user_list", - QSet<QString>, ignored_users) - - DEFINE_EVENTTYPE_ALIAS(Tag, TagEvent) - DEFINE_EVENTTYPE_ALIAS(ReadMarker, ReadMarkerEvent) -} +DEFINE_SIMPLE_EVENT(TagEvent, "m.tag", TagsMap, tags) +DEFINE_SIMPLE_EVENT(ReadMarkerEvent, "m.fully_read", QString, event_id) +DEFINE_SIMPLE_EVENT(IgnoredUsersEvent, "m.ignored_user_list", QSet<QString>, + ignored_users) +} // namespace Quotient diff --git a/lib/events/callanswerevent.cpp b/lib/events/callanswerevent.cpp index d2862241..d6622b30 100644 --- a/lib/events/callanswerevent.cpp +++ b/lib/events/callanswerevent.cpp @@ -13,13 +13,12 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "callanswerevent.h" #include "event.h" - #include "logging.h" #include <QtCore/QJsonDocument> @@ -45,7 +44,7 @@ m.call.answer } */ -using namespace QMatrixClient; +using namespace Quotient; CallAnswerEvent::CallAnswerEvent(const QJsonObject& obj) : CallEventBase(typeId(), obj) @@ -55,18 +54,18 @@ CallAnswerEvent::CallAnswerEvent(const QJsonObject& obj) CallAnswerEvent::CallAnswerEvent(const QString& callId, const int lifetime, const QString& sdp) - : CallEventBase(typeId(), matrixTypeId(), callId, 0, - { { QStringLiteral("lifetime"), lifetime } - , { QStringLiteral("answer"), QJsonObject { - { QStringLiteral("type"), QStringLiteral("answer") }, - { QStringLiteral("sdp"), sdp } } } - }) -{ } + : CallEventBase( + typeId(), matrixTypeId(), callId, 0, + { { QStringLiteral("lifetime"), lifetime }, + { QStringLiteral("answer"), + QJsonObject { { QStringLiteral("type"), QStringLiteral("answer") }, + { QStringLiteral("sdp"), sdp } } } }) +{} CallAnswerEvent::CallAnswerEvent(const QString& callId, const QString& sdp) - : CallEventBase(typeId(), matrixTypeId(), callId, 0, - { { QStringLiteral("answer"), QJsonObject { - { QStringLiteral("type"), QStringLiteral("answer") }, - { QStringLiteral("sdp"), sdp } } } - }) -{ } + : CallEventBase( + typeId(), matrixTypeId(), callId, 0, + { { QStringLiteral("answer"), + QJsonObject { { QStringLiteral("type"), QStringLiteral("answer") }, + { QStringLiteral("sdp"), sdp } } } }) +{} diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h index 2d9e5bb0..2709882b 100644 --- a/lib/events/callanswerevent.h +++ b/lib/events/callanswerevent.h @@ -13,33 +13,33 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "roomevent.h" -namespace QMatrixClient -{ - class CallAnswerEvent: public CallEventBase - { - public: - DEFINE_EVENT_TYPEID("m.call.answer", CallAnswerEvent) +namespace Quotient { +class CallAnswerEvent : public CallEventBase { +public: + DEFINE_EVENT_TYPEID("m.call.answer", CallAnswerEvent) - explicit CallAnswerEvent(const QJsonObject& obj); + explicit CallAnswerEvent(const QJsonObject& obj); - explicit CallAnswerEvent(const QString& callId, const int lifetime, - const QString& sdp); - explicit CallAnswerEvent(const QString& callId, const QString& sdp); + explicit CallAnswerEvent(const QString& callId, const int lifetime, + const QString& sdp); + explicit CallAnswerEvent(const QString& callId, const QString& sdp); - int lifetime() const { return content<int>("lifetime"_ls); } // FIXME: Omittable<>? - QString sdp() const { - return contentJson()["answer"_ls].toObject() - .value("sdp"_ls).toString(); - } - }; + int lifetime() const + { + return content<int>("lifetime"_ls); + } // FIXME: Omittable<>? + QString sdp() const + { + return contentJson()["answer"_ls].toObject().value("sdp"_ls).toString(); + } +}; - REGISTER_EVENT_TYPE(CallAnswerEvent) - DEFINE_EVENTTYPE_ALIAS(CallAnswer, CallAnswerEvent) -} // namespace QMatrixClient +REGISTER_EVENT_TYPE(CallAnswerEvent) +} // namespace Quotient diff --git a/lib/events/callcandidatesevent.cpp b/lib/events/callcandidatesevent.cpp index 52cd1856..24f0dd46 100644 --- a/lib/events/callcandidatesevent.cpp +++ b/lib/events/callcandidatesevent.cpp @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "callcandidatesevent.h" @@ -26,9 +26,8 @@ m.call.candidates "call_id": "12345", "candidates": [ { - "candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0", - "sdpMLineIndex": 0, - "sdpMid": "audio" + "candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156 +43670 typ host generation 0", "sdpMLineIndex": 0, "sdpMid": "audio" } ], "version": 0 diff --git a/lib/events/callcandidatesevent.h b/lib/events/callcandidatesevent.h index 4618832c..e224f048 100644 --- a/lib/events/callcandidatesevent.h +++ b/lib/events/callcandidatesevent.h @@ -13,36 +13,33 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "roomevent.h" -namespace QMatrixClient -{ - class CallCandidatesEvent: public CallEventBase - { - public: - DEFINE_EVENT_TYPEID("m.call.candidates", CallCandidatesEvent) +namespace Quotient { +class CallCandidatesEvent : public CallEventBase { +public: + DEFINE_EVENT_TYPEID("m.call.candidates", CallCandidatesEvent) - explicit CallCandidatesEvent(const QJsonObject& obj) - : CallEventBase(typeId(), obj) - { } + explicit CallCandidatesEvent(const QJsonObject& obj) + : CallEventBase(typeId(), obj) + {} - explicit CallCandidatesEvent(const QString& callId, - const QJsonArray& candidates) - : CallEventBase(typeId(), matrixTypeId(), callId, 0, - {{ QStringLiteral("candidates"), candidates }}) - { } + explicit CallCandidatesEvent(const QString& callId, + const QJsonArray& candidates) + : CallEventBase(typeId(), matrixTypeId(), callId, 0, + { { QStringLiteral("candidates"), candidates } }) + {} - QJsonArray candidates() const - { - return content<QJsonArray>("candidates"_ls); - } - }; + QJsonArray candidates() const + { + return content<QJsonArray>("candidates"_ls); + } +}; - REGISTER_EVENT_TYPE(CallCandidatesEvent) - DEFINE_EVENTTYPE_ALIAS(CallCandidates, CallCandidatesEvent) -} +REGISTER_EVENT_TYPE(CallCandidatesEvent) +} // namespace Quotient diff --git a/lib/events/callhangupevent.cpp b/lib/events/callhangupevent.cpp index b1154806..d41849c3 100644 --- a/lib/events/callhangupevent.cpp +++ b/lib/events/callhangupevent.cpp @@ -13,13 +13,12 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "callhangupevent.h" #include "event.h" - #include "logging.h" #include <QtCore/QJsonDocument> @@ -40,8 +39,7 @@ m.call.hangup } */ -using namespace QMatrixClient; - +using namespace Quotient; CallHangupEvent::CallHangupEvent(const QJsonObject& obj) : CallEventBase(typeId(), obj) @@ -51,4 +49,4 @@ CallHangupEvent::CallHangupEvent(const QJsonObject& obj) CallHangupEvent::CallHangupEvent(const QString& callId) : CallEventBase(typeId(), matrixTypeId(), callId, 0) -{ } +{} diff --git a/lib/events/callhangupevent.h b/lib/events/callhangupevent.h index c74e20d5..5d73fb62 100644 --- a/lib/events/callhangupevent.h +++ b/lib/events/callhangupevent.h @@ -13,24 +13,21 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "roomevent.h" -namespace QMatrixClient -{ - class CallHangupEvent: public CallEventBase - { - public: - DEFINE_EVENT_TYPEID("m.call.hangup", CallHangupEvent) - - explicit CallHangupEvent(const QJsonObject& obj); - explicit CallHangupEvent(const QString& callId); - }; +namespace Quotient { +class CallHangupEvent : public CallEventBase { +public: + DEFINE_EVENT_TYPEID("m.call.hangup", CallHangupEvent) - REGISTER_EVENT_TYPE(CallHangupEvent) - DEFINE_EVENTTYPE_ALIAS(CallHangup, CallHangupEvent) -} + explicit CallHangupEvent(const QJsonObject& obj); + explicit CallHangupEvent(const QString& callId); +}; + +REGISTER_EVENT_TYPE(CallHangupEvent) +} // namespace Quotient diff --git a/lib/events/callinviteevent.cpp b/lib/events/callinviteevent.cpp index bca3f296..54faac8d 100644 --- a/lib/events/callinviteevent.cpp +++ b/lib/events/callinviteevent.cpp @@ -13,13 +13,12 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "callinviteevent.h" #include "event.h" - #include "logging.h" #include <QtCore/QJsonDocument> @@ -45,7 +44,7 @@ m.call.invite } */ -using namespace QMatrixClient; +using namespace Quotient; CallInviteEvent::CallInviteEvent(const QJsonObject& obj) : CallEventBase(typeId(), obj) @@ -55,10 +54,10 @@ CallInviteEvent::CallInviteEvent(const QJsonObject& obj) CallInviteEvent::CallInviteEvent(const QString& callId, const int lifetime, const QString& sdp) - : CallEventBase(typeId(), matrixTypeId(), callId, lifetime, - { { QStringLiteral("lifetime"), lifetime } - , { QStringLiteral("offer"), QJsonObject { - { QStringLiteral("type"), QStringLiteral("offer") }, - { QStringLiteral("sdp"), sdp } } - }}) -{ } + : CallEventBase( + typeId(), matrixTypeId(), callId, lifetime, + { { QStringLiteral("lifetime"), lifetime }, + { QStringLiteral("offer"), + QJsonObject { { QStringLiteral("type"), QStringLiteral("offer") }, + { QStringLiteral("sdp"), sdp } } } }) +{} diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h index d5315309..b067a492 100644 --- a/lib/events/callinviteevent.h +++ b/lib/events/callinviteevent.h @@ -13,32 +13,32 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "roomevent.h" -namespace QMatrixClient -{ - class CallInviteEvent: public CallEventBase - { - public: - DEFINE_EVENT_TYPEID("m.call.invite", CallInviteEvent) - - explicit CallInviteEvent(const QJsonObject& obj); +namespace Quotient { +class CallInviteEvent : public CallEventBase { +public: + DEFINE_EVENT_TYPEID("m.call.invite", CallInviteEvent) + + explicit CallInviteEvent(const QJsonObject& obj); - explicit CallInviteEvent(const QString& callId, const int lifetime, - const QString& sdp); + explicit CallInviteEvent(const QString& callId, const int lifetime, + const QString& sdp); - int lifetime() const { return content<int>("lifetime"_ls); } // FIXME: Omittable<>? - QString sdp() const { - return contentJson()["offer"_ls].toObject() - .value("sdp"_ls).toString(); - } - }; + int lifetime() const + { + return content<int>("lifetime"_ls); + } // FIXME: Omittable<>? + QString sdp() const + { + return contentJson()["offer"_ls].toObject().value("sdp"_ls).toString(); + } +}; - REGISTER_EVENT_TYPE(CallInviteEvent) - DEFINE_EVENTTYPE_ALIAS(CallInvite, CallInviteEvent) -} +REGISTER_EVENT_TYPE(CallInviteEvent) +} // namespace Quotient diff --git a/lib/events/directchatevent.cpp b/lib/events/directchatevent.cpp index 266d60d8..b4027e16 100644 --- a/lib/events/directchatevent.cpp +++ b/lib/events/directchatevent.cpp @@ -13,26 +13,25 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "directchatevent.h" #include <QtCore/QJsonArray> -using namespace QMatrixClient; +using namespace Quotient; QMultiHash<QString, QString> DirectChatEvent::usersToDirectChats() const { QMultiHash<QString, QString> result; const auto& json = contentJson(); - for (auto it = json.begin(); it != json.end(); ++it) - { + for (auto it = json.begin(); it != json.end(); ++it) { // Beware of range-for's over temporary returned from temporary // (see the bottom of // http://en.cppreference.com/w/cpp/language/range-for#Explanation) const auto roomIds = it.value().toArray(); - for (const auto& roomIdValue: roomIds) + for (const auto& roomIdValue : roomIds) result.insert(it.key(), roomIdValue.toString()); } return result; diff --git a/lib/events/directchatevent.h b/lib/events/directchatevent.h index 7559796b..bb091c5c 100644 --- a/lib/events/directchatevent.h +++ b/lib/events/directchatevent.h @@ -13,26 +13,21 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "event.h" -namespace QMatrixClient -{ - class DirectChatEvent : public Event - { - public: - DEFINE_EVENT_TYPEID("m.direct", DirectChatEvent) +namespace Quotient { +class DirectChatEvent : public Event { +public: + DEFINE_EVENT_TYPEID("m.direct", DirectChatEvent) - explicit DirectChatEvent(const QJsonObject& obj) - : Event(typeId(), obj) - { } + explicit DirectChatEvent(const QJsonObject& obj) : Event(typeId(), obj) {} - QMultiHash<QString, QString> usersToDirectChats() const; - }; - REGISTER_EVENT_TYPE(DirectChatEvent) - DEFINE_EVENTTYPE_ALIAS(DirectChat, DirectChatEvent) -} + QMultiHash<QString, QString> usersToDirectChats() const; +}; +REGISTER_EVENT_TYPE(DirectChatEvent) +} // namespace Quotient diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp new file mode 100644 index 00000000..dccfa540 --- /dev/null +++ b/lib/events/encryptedevent.cpp @@ -0,0 +1,32 @@ +#include "encryptedevent.h" + +#include "room.h" + +using namespace Quotient; +using namespace QtOlm; + +EncryptedEvent::EncryptedEvent(const QJsonObject& ciphertext, + const QString& senderKey) + : RoomEvent(typeId(), matrixTypeId(), + { { AlgorithmKeyL, OlmV1Curve25519AesSha2AlgoKey }, + { CiphertextKeyL, ciphertext }, + { SenderKeyKeyL, senderKey } }) +{} + +EncryptedEvent::EncryptedEvent(QByteArray ciphertext, const QString& senderKey, + const QString& deviceId, const QString& sessionId) + : RoomEvent(typeId(), matrixTypeId(), + { + { AlgorithmKeyL, MegolmV1AesSha2AlgoKey }, + { CiphertextKeyL, QString(ciphertext) }, + { DeviceIdKeyL, deviceId }, + { SenderKeyKeyL, senderKey }, + { SessionIdKeyL, sessionId }, + }) +{} + +EncryptedEvent::EncryptedEvent(const QJsonObject& obj) + : RoomEvent(typeId(), obj) +{ + qCDebug(E2EE) << "Encrypted event from" << senderId(); +} diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h new file mode 100644 index 00000000..235b2aa4 --- /dev/null +++ b/lib/events/encryptedevent.h @@ -0,0 +1,66 @@ +#pragma once + +#include "e2ee.h" +#include "roomevent.h" + +namespace Quotient { +class Room; +/* + * While the specification states: + * + * "This event type is used when sending encrypted events. + * It can be used either within a room + * (in which case it will have all of the Room Event fields), + * or as a to-device event." + * "The encrypted payload can contain any message event." + * https://matrix.org/docs/spec/client_server/latest#id493 + * + * -- for most of the cases the message event is the room message event. + * And even for the to-device events the context is for the room. + * + * So, to simplify integration to the timeline, EncryptedEvent is a RoomEvent + * inheritor. Strictly speaking though, it's not always a RoomEvent, but an Event + * in general. It's possible, because RoomEvent interface is similar to Event's + * one and doesn't add new restrictions, just provides additional features. + */ +class EncryptedEvent : public RoomEvent { + Q_GADGET +public: + DEFINE_EVENT_TYPEID("m.room.encrypted", EncryptedEvent) + + /* In case with Olm, the encrypted content of the event is + * a map from the recipient Curve25519 identity key to ciphertext + * information */ + explicit EncryptedEvent(const QJsonObject& ciphertext, + const QString& senderKey); + /* In case with Megolm, device_id and session_id are required */ + explicit EncryptedEvent(QByteArray ciphertext, const QString& senderKey, + const QString& deviceId, const QString& sessionId); + explicit EncryptedEvent(const QJsonObject& obj); + + QString algorithm() const + { + QString algo = content<QString>(AlgorithmKeyL); + if (!SupportedAlgorithms.contains(algo)) { + qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo + << "is not supported"; + } + return algo; + } + QByteArray ciphertext() const + { + return content<QString>(CiphertextKeyL).toLatin1(); + } + QJsonObject ciphertext(const QString& identityKey) const + { + return content<QJsonObject>(CiphertextKeyL).value(identityKey).toObject(); + } + QString senderKey() const { return content<QString>(SenderKeyKeyL); } + + /* device_id and session_id are required with Megolm */ + QString deviceId() const { return content<QString>(DeviceIdKeyL); } + QString sessionId() const { return content<QString>(SessionIdKeyL); } +}; +REGISTER_EVENT_TYPE(EncryptedEvent) + +} // namespace Quotient diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp new file mode 100644 index 00000000..f1bde621 --- /dev/null +++ b/lib/events/encryptionevent.cpp @@ -0,0 +1,45 @@ +#include "encryptionevent.h" + +#include "e2ee.h" + +#include <array> + +namespace Quotient { +static const std::array<QString, 1> encryptionStrings = { + { MegolmV1AesSha2AlgoKey } +}; + +template <> +struct JsonConverter<EncryptionType> { + static EncryptionType load(const QJsonValue& jv) + { + const auto& encryptionString = jv.toString(); + for (auto it = encryptionStrings.begin(); it != encryptionStrings.end(); + ++it) + if (encryptionString == *it) + return EncryptionType(it - encryptionStrings.begin()); + + if (!encryptionString.isEmpty()) + qCWarning(EVENTS) << "Unknown EncryptionType: " << encryptionString; + return EncryptionType::Undefined; + } +}; +} // namespace Quotient + +using namespace Quotient; + +EncryptionEventContent::EncryptionEventContent(const QJsonObject& json) + : encryption(fromJson<EncryptionType>(json[AlgorithmKeyL])) + , algorithm(sanitized(json[AlgorithmKeyL].toString())) + , rotationPeriodMs(json[RotationPeriodMsKeyL].toInt(604800000)) + , rotationPeriodMsgs(json[RotationPeriodMsgsKeyL].toInt(100)) +{} + +void EncryptionEventContent::fillJson(QJsonObject* o) const +{ + Q_ASSERT(o); + if (encryption != EncryptionType::Undefined) + o->insert(AlgorithmKey, algorithm); + o->insert(RotationPeriodMsKey, rotationPeriodMs); + o->insert(RotationPeriodMsgsKey, rotationPeriodMsgs); +} diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h new file mode 100644 index 00000000..cbd3ba4a --- /dev/null +++ b/lib/events/encryptionevent.h @@ -0,0 +1,73 @@ +/****************************************************************************** + * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "eventcontent.h" +#include "stateevent.h" + +namespace Quotient { +class EncryptionEventContent : public EventContent::Base { +public: + enum EncryptionType : size_t { MegolmV1AesSha2 = 0, Undefined }; + + explicit EncryptionEventContent(EncryptionType et = Undefined) + : encryption(et) + {} + explicit EncryptionEventContent(const QJsonObject& json); + + EncryptionType encryption; + QString algorithm; + int rotationPeriodMs; + int rotationPeriodMsgs; + +protected: + void fillJson(QJsonObject* o) const override; +}; + +using EncryptionType = EncryptionEventContent::EncryptionType; + +class EncryptionEvent : public StateEvent<EncryptionEventContent> { + Q_GADGET +public: + DEFINE_EVENT_TYPEID("m.room.encryption", EncryptionEvent) + + using EncryptionType = EncryptionEventContent::EncryptionType; + + explicit EncryptionEvent(const QJsonObject& obj = {}) // TODO: apropriate + // default value + : StateEvent(typeId(), obj) + {} + template <typename... ArgTs> + EncryptionEvent(ArgTs&&... contentArgs) + : StateEvent(typeId(), matrixTypeId(), QString(), + std::forward<ArgTs>(contentArgs)...) + {} + + EncryptionType encryption() const { return content().encryption; } + + QString algorithm() const { return content().algorithm; } + int rotationPeriodMs() const { return content().rotationPeriodMs; } + int rotationPeriodMsgs() const { return content().rotationPeriodMsgs; } + +private: + Q_ENUM(EncryptionType) +}; + +REGISTER_EVENT_TYPE(EncryptionEvent) +} // namespace Quotient diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 6505d89a..7b34114d 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "event.h" @@ -22,7 +22,7 @@ #include <QtCore/QJsonDocument> -using namespace QMatrixClient; +using namespace Quotient; event_type_t EventTypeRegistry::initializeTypeId(event_mtype_t matrixTypeId) { @@ -31,23 +31,21 @@ event_type_t EventTypeRegistry::initializeTypeId(event_mtype_t matrixTypeId) if (strncmp(matrixTypeId, "", 1) == 0) qDebug(EVENTS) << "Initialized unknown event type with id" << id; else - qDebug(EVENTS) << "Initialized event type" << matrixTypeId - << "with id" << id; + qDebug(EVENTS) << "Initialized event type" << matrixTypeId << "with id" + << id; return id; } QString EventTypeRegistry::getMatrixType(event_type_t typeId) { - return typeId < get().eventTypes.size() - ? get().eventTypes[typeId] : QString(); + return typeId < get().eventTypes.size() ? get().eventTypes[typeId] + : QString(); } -Event::Event(Type type, const QJsonObject& json) - : _type(type), _json(json) +Event::Event(Type type, const QJsonObject& json) : _type(type), _json(json) { - if (!json.contains(ContentKeyL) && - !json.value(UnsignedKeyL).toObject().contains(RedactedCauseKeyL)) - { + if (!json.contains(ContentKeyL) + && !json.value(UnsignedKeyL).toObject().contains(RedactedCauseKeyL)) { qCWarning(EVENTS) << "Event without 'content' node"; qCWarning(EVENTS) << formatJson << json; } @@ -55,25 +53,22 @@ Event::Event(Type type, const QJsonObject& json) Event::Event(Type type, event_mtype_t matrixType, const QJsonObject& contentJson) : Event(type, basicEventJson(matrixType, contentJson)) -{ } +{} Event::~Event() = default; -QString Event::matrixType() const -{ - return fullJson()[TypeKeyL].toString(); -} +QString Event::matrixType() const { return fullJson()[TypeKeyL].toString(); } -QByteArray Event::originalJson() const -{ - return QJsonDocument(_json).toJson(); -} +QByteArray Event::originalJson() const { return QJsonDocument(_json).toJson(); } +// On const below: this is to catch accidental attempts to change event JSON +// NOLINTNEXTLINE(readability-const-return-type) const QJsonObject Event::contentJson() const { return fullJson()[ContentKeyL].toObject(); } +// NOLINTNEXTLINE(readability-const-return-type) const QJsonObject Event::unsignedJson() const { return fullJson()[UnsignedKeyL].toObject(); diff --git a/lib/events/event.h b/lib/events/event.h index b7bbd83e..6c8961ad 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once @@ -22,393 +22,372 @@ #include "logging.h" #ifdef ENABLE_EVENTTYPE_ALIAS -#define USE_EVENTTYPE_ALIAS 1 +# define USE_EVENTTYPE_ALIAS 1 #endif -namespace QMatrixClient +namespace Quotient { +// === event_ptr_tt<> and type casting facilities === + +template <typename EventT> +using event_ptr_tt = std::unique_ptr<EventT>; + +/// Unwrap a plain pointer from a smart pointer +template <typename EventT> +inline EventT* rawPtr(const event_ptr_tt<EventT>& ptr) { - // === event_ptr_tt<> and type casting facilities === + return ptr.get(); +} - template <typename EventT> - using event_ptr_tt = std::unique_ptr<EventT>; +/// Unwrap a plain pointer and downcast it to the specified type +template <typename TargetEventT, typename EventT> +inline TargetEventT* weakPtrCast(const event_ptr_tt<EventT>& ptr) +{ + return static_cast<TargetEventT*>(rawPtr(ptr)); +} - /// Unwrap a plain pointer from a smart pointer - template <typename EventT> - inline EventT* rawPtr(const event_ptr_tt<EventT>& ptr) - { - return ptr.get(); - } +/// Re-wrap a smart pointer to base into a smart pointer to derived +template <typename TargetT, typename SourceT> +[[deprecated("Consider using eventCast() or visit() instead")]] +inline event_ptr_tt<TargetT> ptrCast(event_ptr_tt<SourceT>&& ptr) +{ + return std::unique_ptr<TargetT>(static_cast<TargetT*>(ptr.release())); +} + +// === Standard Matrix key names and basicEventJson() === + +static const auto TypeKey = QStringLiteral("type"); +static const auto BodyKey = QStringLiteral("body"); +static const auto ContentKey = QStringLiteral("content"); +static const auto EventIdKey = QStringLiteral("event_id"); +static const auto UnsignedKey = QStringLiteral("unsigned"); +static const auto StateKeyKey = QStringLiteral("state_key"); +static const auto TypeKeyL = "type"_ls; +static const auto BodyKeyL = "body"_ls; +static const auto ContentKeyL = "content"_ls; +static const auto EventIdKeyL = "event_id"_ls; +static const auto UnsignedKeyL = "unsigned"_ls; +static const auto RedactedCauseKeyL = "redacted_because"_ls; +static const auto PrevContentKeyL = "prev_content"_ls; +static const auto StateKeyKeyL = "state_key"_ls; + +/// Make a minimal correct Matrix event JSON +template <typename StrT> +inline QJsonObject basicEventJson(StrT matrixType, const QJsonObject& content) +{ + return { { TypeKey, std::forward<StrT>(matrixType) }, + { ContentKey, content } }; +} - /// Unwrap a plain pointer and downcast it to the specified type - template <typename TargetEventT, typename EventT> - inline TargetEventT* weakPtrCast(const event_ptr_tt<EventT>& ptr) - { - return static_cast<TargetEventT*>(rawPtr(ptr)); - } +// === Event types and event types registry === - /// Re-wrap a smart pointer to base into a smart pointer to derived - template <typename TargetT, typename SourceT> - [[deprecated("Consider using eventCast() or visit() instead")]] - inline event_ptr_tt<TargetT> ptrCast(event_ptr_tt<SourceT>&& ptr) - { - return unique_ptr_cast<TargetT>(ptr); - } +using event_type_t = size_t; +using event_mtype_t = const char*; + +class EventTypeRegistry { +public: + ~EventTypeRegistry() = default; - // === Standard Matrix key names and basicEventJson() === - - static const auto TypeKey = QStringLiteral("type"); - static const auto ContentKey = QStringLiteral("content"); - static const auto EventIdKey = QStringLiteral("event_id"); - static const auto UnsignedKey = QStringLiteral("unsigned"); - static const auto TypeKeyL = "type"_ls; - static const auto ContentKeyL = "content"_ls; - static const auto EventIdKeyL = "event_id"_ls; - static const auto UnsignedKeyL = "unsigned"_ls; - static const auto RedactedCauseKeyL = "redacted_because"_ls; - static const auto PrevContentKeyL = "prev_content"_ls; - - // Minimal correct Matrix event JSON - template <typename StrT> - inline QJsonObject basicEventJson(StrT matrixType, - const QJsonObject& content) + static event_type_t initializeTypeId(event_mtype_t matrixTypeId); + + template <typename EventT> + static inline event_type_t initializeTypeId() { - return { { TypeKey, std::forward<StrT>(matrixType) }, - { ContentKey, content } }; + return initializeTypeId(EventT::matrixTypeId()); } - // === Event types and event types registry === + static QString getMatrixType(event_type_t typeId); - using event_type_t = size_t; - using event_mtype_t = const char*; +private: + EventTypeRegistry() = default; + Q_DISABLE_COPY(EventTypeRegistry) + DISABLE_MOVE(EventTypeRegistry) - class EventTypeRegistry + static EventTypeRegistry& get() { - public: - ~EventTypeRegistry() = default; + static EventTypeRegistry etr; + return etr; + } - static event_type_t initializeTypeId(event_mtype_t matrixTypeId); + std::vector<event_mtype_t> eventTypes; +}; - template <typename EventT> - static inline event_type_t initializeTypeId() - { - return initializeTypeId(EventT::matrixTypeId()); - } +template <> +inline event_type_t EventTypeRegistry::initializeTypeId<void>() +{ + return initializeTypeId(""); +} - static QString getMatrixType(event_type_t typeId); +template <typename EventT> +struct EventTypeTraits { + static event_type_t id() + { + static const auto id = EventTypeRegistry::initializeTypeId<EventT>(); + return id; + } +}; - private: - EventTypeRegistry() = default; - Q_DISABLE_COPY(EventTypeRegistry) - DISABLE_MOVE(EventTypeRegistry) +template <typename EventT> +inline event_type_t typeId() +{ + return EventTypeTraits<std::decay_t<EventT>>::id(); +} - static EventTypeRegistry& get() - { - static EventTypeRegistry etr; - return etr; - } +inline event_type_t unknownEventTypeId() { return typeId<void>(); } - std::vector<event_mtype_t> eventTypes; - }; +// === EventFactory === - template <> - inline event_type_t EventTypeRegistry::initializeTypeId<void>() +/** Create an event of arbitrary type from its arguments */ +template <typename EventT, typename... ArgTs> +inline event_ptr_tt<EventT> makeEvent(ArgTs&&... args) +{ + return std::make_unique<EventT>(std::forward<ArgTs>(args)...); +} + +template <typename BaseEventT> +class EventFactory { +public: + template <typename FnT> + static auto addMethod(FnT&& method) { - return initializeTypeId(""); + factories().emplace_back(std::forward<FnT>(method)); + return 0; } + /** Chain two type factories + * Adds the factory class of EventT2 (EventT2::factory_t) to + * the list in factory class of EventT1 (EventT1::factory_t) so + * that when EventT1::factory_t::make() is invoked, types of + * EventT2 factory are looked through as well. This is used + * to include RoomEvent types into the more general Event factory, + * and state event types into the RoomEvent factory. + */ template <typename EventT> - struct EventTypeTraits + static auto chainFactory() { - static event_type_t id() - { - static const auto id = EventTypeRegistry::initializeTypeId<EventT>(); - return id; - } - }; + return addMethod(&EventT::factory_t::make); + } - template <typename EventT> - inline event_type_t typeId() + static event_ptr_tt<BaseEventT> make(const QJsonObject& json, + const QString& matrixType) { - return EventTypeTraits<std::decay_t<EventT>>::id(); + for (const auto& f : factories()) + if (auto e = f(json, matrixType)) + return e; + return nullptr; } - inline event_type_t unknownEventTypeId() { return typeId<void>(); } +private: + static auto& factories() + { + using inner_factory_tt = std::function<event_ptr_tt<BaseEventT>( + const QJsonObject&, const QString&)>; + static std::vector<inner_factory_tt> _factories {}; + return _factories; + } +}; - // === EventFactory === +/** Add a type to its default factory + * Adds a standard factory method (via makeEvent<>) for a given + * type to EventT::factory_t factory class so that it can be + * created dynamically from loadEvent<>(). + * + * \tparam EventT the type to enable dynamic creation of + * \return the registered type id + * \sa loadEvent, Event::type + */ +template <typename EventT> +inline auto setupFactory() +{ + qDebug(EVENTS) << "Adding factory method for" << EventT::matrixTypeId(); + return EventT::factory_t::addMethod([](const QJsonObject& json, + const QString& jsonMatrixType) { + return EventT::matrixTypeId() == jsonMatrixType ? makeEvent<EventT>(json) + : nullptr; + }); +} + +template <typename EventT> +inline auto registerEventType() +{ + // Initialise exactly once, even if this function is called twice for + // the same type (for whatever reason - you never know the ways of + // static initialisation is done). + static const auto _ = setupFactory<EventT>(); + return _; // Only to facilitate usage in static initialisation +} + +// === Event === + +class Event { + Q_GADGET + Q_PROPERTY(Type type READ type CONSTANT) + Q_PROPERTY(QJsonObject contentJson READ contentJson CONSTANT) +public: + using Type = event_type_t; + using factory_t = EventFactory<Event>; + + explicit Event(Type type, const QJsonObject& json); + explicit Event(Type type, event_mtype_t matrixType, + const QJsonObject& contentJson = {}); + Q_DISABLE_COPY(Event) + Event(Event&&) = default; + Event& operator=(Event&&) = delete; + virtual ~Event(); + + Type type() const { return _type; } + QString matrixType() const; + QByteArray originalJson() const; + QJsonObject originalJsonObject() const { return fullJson(); } + + const QJsonObject& fullJson() const { return _json; } + + // According to the CS API spec, every event also has + // a "content" object; but since its structure is different for + // different types, we're implementing it per-event type. + + const QJsonObject contentJson() const; + const QJsonObject unsignedJson() const; - /** Create an event of arbitrary type from its arguments */ - template <typename EventT, typename... ArgTs> - inline event_ptr_tt<EventT> makeEvent(ArgTs&&... args) + template <typename T> + T content(const QString& key) const { - return std::make_unique<EventT>(std::forward<ArgTs>(args)...); + return fromJson<T>(contentJson()[key]); } - template <typename BaseEventT> - class EventFactory - { - public: - template <typename FnT> - static auto addMethod(FnT&& method) - { - factories().emplace_back(std::forward<FnT>(method)); - return 0; - } - - /** Chain two type factories - * Adds the factory class of EventT2 (EventT2::factory_t) to - * the list in factory class of EventT1 (EventT1::factory_t) so - * that when EventT1::factory_t::make() is invoked, types of - * EventT2 factory are looked through as well. This is used - * to include RoomEvent types into the more general Event factory, - * and state event types into the RoomEvent factory. - */ - template <typename EventT> - static auto chainFactory() - { - return addMethod(&EventT::factory_t::make); - } - - static event_ptr_tt<BaseEventT> make(const QJsonObject& json, - const QString& matrixType) - { - for (const auto& f: factories()) - if (auto e = f(json, matrixType)) - return e; - return nullptr; - } - - private: - static auto& factories() - { - using inner_factory_tt = - std::function<event_ptr_tt<BaseEventT>(const QJsonObject&, - const QString&)>; - static std::vector<inner_factory_tt> _factories {}; - return _factories; - } - }; - - /** Add a type to its default factory - * Adds a standard factory method (via makeEvent<>) for a given - * type to EventT::factory_t factory class so that it can be - * created dynamically from loadEvent<>(). - * - * \tparam EventT the type to enable dynamic creation of - * \return the registered type id - * \sa loadEvent, Event::type - */ - template <typename EventT> - inline auto setupFactory() + template <typename T> + T content(QLatin1String key) const { - qDebug(EVENTS) << "Adding factory method for" << EventT::matrixTypeId(); - return EventT::factory_t::addMethod( - [] (const QJsonObject& json, const QString& jsonMatrixType) - { - return EventT::matrixTypeId() == jsonMatrixType - ? makeEvent<EventT>(json) : nullptr; - }); + return fromJson<T>(contentJson()[key]); } - template <typename EventT> - inline auto registerEventType() + friend QDebug operator<<(QDebug dbg, const Event& e) { - // Initialise exactly once, even if this function is called twice for - // the same type (for whatever reason - you never know the ways of - // static initialisation is done). - static const auto _ = setupFactory<EventT>(); - return _; // Only to facilitate usage in static initialisation + QDebugStateSaver _dss { dbg }; + dbg.noquote().nospace() << e.matrixType() << '(' << e.type() << "): "; + e.dumpTo(dbg); + return dbg; } - // === Event === + virtual bool isStateEvent() const { return false; } + virtual bool isCallEvent() const { return false; } + virtual void dumpTo(QDebug dbg) const; - class Event - { - Q_GADGET - Q_PROPERTY(Type type READ type CONSTANT) - Q_PROPERTY(QJsonObject contentJson READ contentJson CONSTANT) - public: - using Type = event_type_t; - using factory_t = EventFactory<Event>; - - explicit Event(Type type, const QJsonObject& json); - explicit Event(Type type, event_mtype_t matrixType, - const QJsonObject& contentJson = {}); - Q_DISABLE_COPY(Event) - Event(Event&&) = default; - Event& operator=(Event&&) = delete; - virtual ~Event(); - - Type type() const { return _type; } - QString matrixType() const; - QByteArray originalJson() const; - QJsonObject originalJsonObject() const { return fullJson(); } - - const QJsonObject& fullJson() const { return _json; } - - // According to the CS API spec, every event also has - // a "content" object; but since its structure is different for - // different types, we're implementing it per-event type. - - const QJsonObject contentJson() const; - const QJsonObject unsignedJson() const; - - template <typename T> - T content(const QString& key) const - { - return fromJson<T>(contentJson()[key]); - } - - template <typename T> - T content(const QLatin1String& key) const - { - return fromJson<T>(contentJson()[key]); - } - - friend QDebug operator<<(QDebug dbg, const Event& e) - { - QDebugStateSaver _dss { dbg }; - dbg.noquote().nospace() - << e.matrixType() << '(' << e.type() << "): "; - e.dumpTo(dbg); - return dbg; - } - - virtual bool isStateEvent() const { return false; } - virtual bool isCallEvent() const { return false; } - virtual void dumpTo(QDebug dbg) const; - - protected: - QJsonObject& editJson() { return _json; } - - private: - Type _type; - QJsonObject _json; - }; - using EventPtr = event_ptr_tt<Event>; +protected: + QJsonObject& editJson() { return _json; } - template <typename EventT> - using EventsArray = std::vector<event_ptr_tt<EventT>>; - using Events = EventsArray<Event>; +private: + Type _type; + QJsonObject _json; +}; +using EventPtr = event_ptr_tt<Event>; - // === Macros used with event class definitions === +template <typename EventT> +using EventsArray = std::vector<event_ptr_tt<EventT>>; +using Events = EventsArray<Event>; - // This macro should be used in a public section of an event class to - // provide matrixTypeId() and typeId(). -#define DEFINE_EVENT_TYPEID(_Id, _Type) \ - static constexpr event_mtype_t matrixTypeId() { return _Id; } \ - static auto typeId() { return QMatrixClient::typeId<_Type>(); } \ - // End of macro +// === Macros used with event class definitions === - // This macro should be put after an event class definition (in .h or .cpp) - // to enable its deserialisation from a /sync and other - // polymorphic event arrays -#define REGISTER_EVENT_TYPE(_Type) \ - namespace { \ - [[gnu::unused]] \ - static const auto _factoryAdded##_Type = registerEventType<_Type>(); \ - } \ +// This macro should be used in a public section of an event class to +// provide matrixTypeId() and typeId(). +#define DEFINE_EVENT_TYPEID(_Id, _Type) \ + static constexpr event_mtype_t matrixTypeId() { return _Id; } \ + static auto typeId() { return Quotient::typeId<_Type>(); } \ // End of macro -#ifdef USE_EVENTTYPE_ALIAS - namespace EventType - { - inline event_type_t logEventType(event_type_t id, const char* idName) - { - qDebug(EVENTS) << "Using id" << id << "for" << idName; - return id; - } - } - - // This macro provides constants in EventType:: namespace for - // back-compatibility with libQMatrixClient 0.3 event type system. -#define DEFINE_EVENTTYPE_ALIAS(_Id, _Type) \ - namespace EventType \ - { \ - [[deprecated("Use is<>(), eventCast<>() or visit<>()")]] \ - static const auto _Id = logEventType(typeId<_Type>(), #_Id); \ - } \ +// This macro should be put after an event class definition (in .h or .cpp) +// to enable its deserialisation from a /sync and other +// polymorphic event arrays +#define REGISTER_EVENT_TYPE(_Type) \ + namespace { \ + [[maybe_unused]] static const auto _factoryAdded##_Type = \ + registerEventType<_Type>(); \ + } \ // End of macro -#else -#define DEFINE_EVENTTYPE_ALIAS(_Id, _Type) // Nothing -#endif - - // === is<>(), eventCast<>() and visit<>() === - template <typename EventT> - inline bool is(const Event& e) { return e.type() == typeId<EventT>(); } +// === is<>(), eventCast<>() and visit<>() === - inline bool isUnknown(const Event& e) { return e.type() == unknownEventTypeId(); } - - template <typename EventT, typename BasePtrT> - inline auto eventCast(const BasePtrT& eptr) - -> decltype(static_cast<EventT*>(&*eptr)) - { - Q_ASSERT(eptr); - return is<std::decay_t<EventT>>(*eptr) - ? static_cast<EventT*>(&*eptr) : nullptr; - } +template <typename EventT> +inline bool is(const Event& e) +{ + return e.type() == typeId<EventT>(); +} - // A single generic catch-all visitor - template <typename BaseEventT, typename FnT> - inline auto visit(const BaseEventT& event, FnT&& visitor) - -> decltype(visitor(event)) - { - return visitor(event); - } +inline bool isUnknown(const Event& e) +{ + return e.type() == unknownEventTypeId(); +} - template <typename T> - constexpr auto is_event() - { - return std::is_base_of<Event, std::decay_t<T>>::value; - } +template <typename EventT, typename BasePtrT> +inline auto eventCast(const BasePtrT& eptr) + -> decltype(static_cast<EventT*>(&*eptr)) +{ + Q_ASSERT(eptr); + return is<std::decay_t<EventT>>(*eptr) ? static_cast<EventT*>(&*eptr) + : nullptr; +} + +// A single generic catch-all visitor +template <typename BaseEventT, typename FnT> +inline auto visit(const BaseEventT& event, FnT&& visitor) + -> decltype(visitor(event)) +{ + return visitor(event); +} +namespace _impl { template <typename T, typename FnT> - constexpr auto needs_cast() - { - return !std::is_convertible<T, fn_arg_t<FnT>>::value; - } - - // A single type-specific void visitor - template <typename BaseEventT, typename FnT> - inline - std::enable_if_t< - is_event<BaseEventT>() && needs_cast<BaseEventT, FnT>() && - std::is_void<fn_return_t<FnT>>::value> - visit(const BaseEventT& event, FnT&& visitor) + constexpr auto needs_downcast() { - using event_type = fn_arg_t<FnT>; - if (is<std::decay_t<event_type>>(event)) - visitor(static_cast<event_type>(event)); + return !std::is_convertible_v<T, fn_arg_t<FnT>>; } +} - // A single type-specific non-void visitor with an optional default value - template <typename BaseEventT, typename FnT> - inline - std::enable_if_t< - is_event<BaseEventT>() && needs_cast<BaseEventT, FnT>(), - fn_return_t<FnT>> // non-voidness is guarded by defaultValue type - visit(const BaseEventT& event, FnT&& visitor, - fn_return_t<FnT>&& defaultValue = {}) - { - using event_type = fn_arg_t<FnT>; - if (is<std::decay_t<event_type>>(event)) - return visitor(static_cast<event_type>(event)); - return std::forward<fn_return_t<FnT>>(defaultValue); - } - - // A chain of 2 or more visitors - template <typename BaseEventT, typename FnT1, typename FnT2, typename... FnTs> - inline - std::enable_if_t<is_event<BaseEventT>(), fn_return_t<FnT1>> - visit(const BaseEventT& event, FnT1&& visitor1, FnT2&& visitor2, - FnTs&&... visitors) - { - using event_type1 = fn_arg_t<FnT1>; - if (is<std::decay_t<event_type1>>(event)) - return visitor1(static_cast<event_type1&>(event)); - return visit(event, std::forward<FnT2>(visitor2), - std::forward<FnTs>(visitors)...); - } -} // namespace QMatrixClient -Q_DECLARE_METATYPE(QMatrixClient::Event*) -Q_DECLARE_METATYPE(const QMatrixClient::Event*) +// A single type-specific void visitor +template <typename BaseEventT, typename FnT> +inline std::enable_if_t<_impl::needs_downcast<BaseEventT, FnT>() + && std::is_void_v<fn_return_t<FnT>>> +visit(const BaseEventT& event, FnT&& visitor) +{ + using event_type = fn_arg_t<FnT>; + if (is<std::decay_t<event_type>>(event)) + visitor(static_cast<event_type>(event)); +} + +// A single type-specific non-void visitor with an optional default value +// non-voidness is guarded by defaultValue type +template <typename BaseEventT, typename FnT> +inline std::enable_if_t<_impl::needs_downcast<BaseEventT, FnT>(), fn_return_t<FnT>> +visit(const BaseEventT& event, FnT&& visitor, + fn_return_t<FnT>&& defaultValue = {}) +{ + using event_type = fn_arg_t<FnT>; + if (is<std::decay_t<event_type>>(event)) + return visitor(static_cast<event_type>(event)); + return std::forward<fn_return_t<FnT>>(defaultValue); +} + +// A chain of 2 or more visitors +template <typename BaseEventT, typename FnT1, typename FnT2, typename... FnTs> +inline fn_return_t<FnT1> visit(const BaseEventT& event, FnT1&& visitor1, + FnT2&& visitor2, FnTs&&... visitors) +{ + using event_type1 = fn_arg_t<FnT1>; + if (is<std::decay_t<event_type1>>(event)) + return visitor1(static_cast<event_type1&>(event)); + return visit(event, std::forward<FnT2>(visitor2), + std::forward<FnTs>(visitors)...); +} + +// A facility overload that calls void-returning visit() on each event +// over a range of event pointers +template <typename RangeT, typename... FnTs> +inline auto visitEach(RangeT&& events, FnTs&&... visitors) + -> std::enable_if_t<std::is_convertible_v< + std::decay_t<decltype(**events.begin())>, Event>> +{ + for (auto&& evtPtr: events) + visit(*evtPtr, std::forward<FnTs>(visitors)...); +} +} // namespace Quotient +Q_DECLARE_METATYPE(Quotient::Event*) +Q_DECLARE_METATYPE(const Quotient::Event*) diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 77f756cd..802d8176 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "eventcontent.h" @@ -23,7 +23,7 @@ #include <QtCore/QMimeDatabase> -using namespace QMatrixClient::EventContent; +using namespace Quotient::EventContent; QJsonObject Base::toJson() const { @@ -34,14 +34,17 @@ QJsonObject Base::toJson() const FileInfo::FileInfo(const QUrl& u, qint64 payloadSize, const QMimeType& mimeType, const QString& originalFilename) - : mimeType(mimeType), url(u), payloadSize(payloadSize) + : mimeType(mimeType) + , url(u) + , payloadSize(payloadSize) , originalName(originalFilename) -{ } +{} FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, const QString& originalFilename) : originalInfoJson(infoJson) - , mimeType(QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString())) + , mimeType( + QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString())) , url(u) , payloadSize(fromJson<qint64>(infoJson["size"_ls])) , originalName(originalFilename) @@ -53,7 +56,7 @@ FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, bool FileInfo::isValid() const { return url.scheme() == "mxc" - && (url.authority() + url.path()).count('/') == 1; + && (url.authority() + url.path()).count('/') == 1; } void FileInfo::fillInfoJson(QJsonObject* infoJson) const @@ -68,13 +71,13 @@ void FileInfo::fillInfoJson(QJsonObject* infoJson) const ImageInfo::ImageInfo(const QUrl& u, qint64 fileSize, QMimeType mimeType, const QSize& imageSize, const QString& originalFilename) : FileInfo(u, fileSize, mimeType, originalFilename), imageSize(imageSize) -{ } +{} ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson, const QString& originalFilename) : FileInfo(u, infoJson, originalFilename) , imageSize(infoJson["w"_ls].toInt(), infoJson["h"_ls].toInt()) -{ } +{} void ImageInfo::fillInfoJson(QJsonObject* infoJson) const { @@ -88,7 +91,7 @@ void ImageInfo::fillInfoJson(QJsonObject* infoJson) const Thumbnail::Thumbnail(const QJsonObject& infoJson) : ImageInfo(infoJson["thumbnail_url"_ls].toString(), infoJson["thumbnail_info"_ls].toObject()) -{ } +{} void Thumbnail::fillInfoJson(QJsonObject* infoJson) const { diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index ab31a75d..0d4c047e 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once @@ -23,262 +23,260 @@ #include <QtCore/QJsonObject> #include <QtCore/QMimeType> -#include <QtCore/QUrl> #include <QtCore/QSize> +#include <QtCore/QUrl> +#include <QtCore/QMetaType> -namespace QMatrixClient -{ - namespace EventContent - { - /** - * A base class for all content types that can be stored - * in a RoomMessageEvent - * - * Each content type class should have a constructor taking - * a QJsonObject and override fillJson() with an implementation - * that will fill the target QJsonObject with stored values. It is - * assumed but not required that a content object can also be created - * from plain data. - */ - class Base - { - public: - explicit Base (QJsonObject o = {}) : originalJson(std::move(o)) { } - virtual ~Base() = default; +namespace Quotient { +namespace EventContent { + /** + * A base class for all content types that can be stored + * in a RoomMessageEvent + * + * Each content type class should have a constructor taking + * a QJsonObject and override fillJson() with an implementation + * that will fill the target QJsonObject with stored values. It is + * assumed but not required that a content object can also be created + * from plain data. + */ + class Base { + public: + explicit Base(QJsonObject o = {}) : originalJson(std::move(o)) {} + virtual ~Base() = default; + + // FIXME: make toJson() from converters.* work on base classes + QJsonObject toJson() const; + + public: + QJsonObject originalJson; - // FIXME: make toJson() from converters.* work on base classes - QJsonObject toJson() const; + protected: + Base(const Base&) = default; + Base(Base&&) = default; - public: - QJsonObject originalJson; + virtual void fillJson(QJsonObject* o) const = 0; + }; - protected: - virtual void fillJson(QJsonObject* o) const = 0; - }; + // The below structures fairly follow CS spec 11.2.1.6. The overall + // set of attributes for each content types is a superset of the spec + // but specific aggregation structure is altered. See doc comments to + // each type for the list of available attributes. + + // A quick classes inheritance structure follows: + // FileInfo + // FileContent : UrlBasedContent<FileInfo, Thumbnail> + // AudioContent : UrlBasedContent<FileInfo, Duration> + // ImageInfo : FileInfo + imageSize attribute + // ImageContent : UrlBasedContent<ImageInfo, Thumbnail> + // VideoContent : UrlBasedContent<ImageInfo, Thumbnail, Duration> + + /** + * A base/mixin class for structures representing an "info" object for + * some content types. These include most attachment types currently in + * the CS API spec. + * + * In order to use it in a content class, derive both from TypedBase + * (or Base) and from FileInfo (or its derivative, such as \p ImageInfo) + * and call fillInfoJson() to fill the "info" subobject. Make sure + * to pass an "info" part of JSON to FileInfo constructor, not the whole + * JSON content, as well as contents of "url" (or a similar key) and + * optionally "filename" node from the main JSON content. Assuming you + * don't do unusual things, you should use \p UrlBasedContent<> instead + * of doing multiple inheritance and overriding Base::fillJson() by hand. + * + * This class is not polymorphic. + */ + class FileInfo { + public: + explicit FileInfo(const QUrl& u, qint64 payloadSize = -1, + const QMimeType& mimeType = {}, + const QString& originalFilename = {}); + FileInfo(const QUrl& u, const QJsonObject& infoJson, + const QString& originalFilename = {}); - // The below structures fairly follow CS spec 11.2.1.6. The overall - // set of attributes for each content types is a superset of the spec - // but specific aggregation structure is altered. See doc comments to - // each type for the list of available attributes. + bool isValid() const; - // A quick classes inheritance structure follows: - // FileInfo - // FileContent : UrlBasedContent<FileInfo, Thumbnail> - // AudioContent : UrlBasedContent<FileInfo, Duration> - // ImageInfo : FileInfo + imageSize attribute - // ImageContent : UrlBasedContent<ImageInfo, Thumbnail> - // VideoContent : UrlBasedContent<ImageInfo, Thumbnail, Duration> + void fillInfoJson(QJsonObject* infoJson) const; /** - * A base/mixin class for structures representing an "info" object for - * some content types. These include most attachment types currently in - * the CS API spec. + * \brief Extract media id from the URL * - * In order to use it in a content class, derive both from TypedBase - * (or Base) and from FileInfo (or its derivative, such as \p ImageInfo) - * and call fillInfoJson() to fill the "info" subobject. Make sure - * to pass an "info" part of JSON to FileInfo constructor, not the whole - * JSON content, as well as contents of "url" (or a similar key) and - * optionally "filename" node from the main JSON content. Assuming you - * don't do unusual things, you should use \p UrlBasedContent<> instead - * of doing multiple inheritance and overriding Base::fillJson() by hand. - * - * This class is not polymorphic. + * This can be used, e.g., to construct a QML-facing image:// + * URI as follows: + * \code "image://provider/" + info.mediaId() \endcode */ - class FileInfo - { - public: - explicit FileInfo(const QUrl& u, qint64 payloadSize = -1, - const QMimeType& mimeType = {}, - const QString& originalFilename = {}); - FileInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename = {}); - - bool isValid() const; - - void fillInfoJson(QJsonObject* infoJson) const; - - /** - * \brief Extract media id from the URL - * - * This can be used, e.g., to construct a QML-facing image:// - * URI as follows: - * \code "image://provider/" + info.mediaId() \endcode - */ - QString mediaId() const { return url.authority() + url.path(); } - - public: - QJsonObject originalInfoJson; - QMimeType mimeType; - QUrl url; - qint64 payloadSize; - QString originalName; - }; - - template <typename InfoT> - QJsonObject toInfoJson(const InfoT& info) - { - QJsonObject infoJson; - info.fillInfoJson(&infoJson); - return infoJson; - } + QString mediaId() const { return url.authority() + url.path(); } - /** - * A content info class for image content types: image, thumbnail, video - */ - class ImageInfo : public FileInfo - { - public: - explicit ImageInfo(const QUrl& u, qint64 fileSize = -1, - QMimeType mimeType = {}, - const QSize& imageSize = {}, - const QString& originalFilename = {}); - ImageInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename = {}); + public: + QJsonObject originalInfoJson; + QMimeType mimeType; + QUrl url; + qint64 payloadSize; + QString originalName; + }; + + template <typename InfoT> + QJsonObject toInfoJson(const InfoT& info) + { + QJsonObject infoJson; + info.fillInfoJson(&infoJson); + return infoJson; + } + + /** + * A content info class for image content types: image, thumbnail, video + */ + class ImageInfo : public FileInfo { + public: + explicit ImageInfo(const QUrl& u, qint64 fileSize = -1, + QMimeType mimeType = {}, const QSize& imageSize = {}, + const QString& originalFilename = {}); + ImageInfo(const QUrl& u, const QJsonObject& infoJson, + const QString& originalFilename = {}); + + void fillInfoJson(QJsonObject* infoJson) const; - void fillInfoJson(QJsonObject* infoJson) const; + public: + QSize imageSize; + }; - public: - QSize imageSize; - }; + /** + * An auxiliary class for an info type that carries a thumbnail + * + * This class saves/loads a thumbnail to/from "info" subobject of + * the JSON representation of event content; namely, + * "info/thumbnail_url" and "info/thumbnail_info" fields are used. + */ + class Thumbnail : public ImageInfo { + public: + Thumbnail() : ImageInfo(QUrl()) {} // To allow empty thumbnails + Thumbnail(const QJsonObject& infoJson); + Thumbnail(const ImageInfo& info) : ImageInfo(info) {} + using ImageInfo::ImageInfo; /** - * An auxiliary class for an info type that carries a thumbnail - * - * This class saves/loads a thumbnail to/from "info" subobject of - * the JSON representation of event content; namely, - * "info/thumbnail_url" and "info/thumbnail_info" fields are used. + * Writes thumbnail information to "thumbnail_info" subobject + * and thumbnail URL to "thumbnail_url" node inside "info". */ - class Thumbnail : public ImageInfo + void fillInfoJson(QJsonObject* infoJson) const; + }; + + class TypedBase : public Base { + public: + explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {} + virtual QMimeType type() const = 0; + virtual const FileInfo* fileInfo() const { return nullptr; } + virtual FileInfo* fileInfo() { return nullptr; } + virtual const Thumbnail* thumbnailInfo() const { return nullptr; } + + protected: + using Base::Base; + }; + + /** + * A base class for content types that have a URL and additional info + * + * Types that derive from this class template take "url" and, + * optionally, "filename" values from the top-level JSON object and + * the rest of information from the "info" subobject, as defined by + * the parameter type. + * + * \tparam InfoT base info class + */ + template <class InfoT> + class UrlBasedContent : public TypedBase, public InfoT { + public: + using InfoT::InfoT; + explicit UrlBasedContent(const QJsonObject& json) + : TypedBase(json) + , InfoT(json["url"].toString(), json["info"].toObject(), + json["filename"].toString()) { - public: - Thumbnail() : ImageInfo(QUrl()) { } // To allow empty thumbnails - Thumbnail(const QJsonObject& infoJson); - Thumbnail(const ImageInfo& info) : ImageInfo(info) { } - using ImageInfo::ImageInfo; - - /** - * Writes thumbnail information to "thumbnail_info" subobject - * and thumbnail URL to "thumbnail_url" node inside "info". - */ - void fillInfoJson(QJsonObject* infoJson) const; - }; - - class TypedBase: public Base + // A small hack to facilitate links creation in QML. + originalJson.insert("mediaId", InfoT::mediaId()); + } + + QMimeType type() const override { return InfoT::mimeType; } + const FileInfo* fileInfo() const override { return this; } + FileInfo* fileInfo() override { return this; } + + protected: + void fillJson(QJsonObject* json) const override { - public: - explicit TypedBase(const QJsonObject& o = {}) : Base(o) { } - virtual QMimeType type() const = 0; - virtual const FileInfo* fileInfo() const { return nullptr; } - virtual FileInfo* fileInfo() { return nullptr; } - virtual const Thumbnail* thumbnailInfo() const { return nullptr; } - }; + Q_ASSERT(json); + json->insert("url", InfoT::url.toString()); + if (!InfoT::originalName.isEmpty()) + json->insert("filename", InfoT::originalName); + json->insert("info", toInfoJson<InfoT>(*this)); + } + }; - /** - * A base class for content types that have a URL and additional info - * - * Types that derive from this class template take "url" and, - * optionally, "filename" values from the top-level JSON object and - * the rest of information from the "info" subobject, as defined by - * the parameter type. - * - * \tparam InfoT base info class - */ - template <class InfoT> - class UrlBasedContent : public TypedBase, public InfoT + template <typename InfoT> + class UrlWithThumbnailContent : public UrlBasedContent<InfoT> { + public: + // NB: when using inherited constructors, thumbnail has to be + // initialised separately + using UrlBasedContent<InfoT>::UrlBasedContent; + explicit UrlWithThumbnailContent(const QJsonObject& json) + : UrlBasedContent<InfoT>(json), thumbnail(InfoT::originalInfoJson) { - public: - using InfoT::InfoT; - explicit UrlBasedContent(const QJsonObject& json) - : TypedBase(json) - , InfoT(json["url"].toString(), json["info"].toObject(), - json["filename"].toString()) - { - // A small hack to facilitate links creation in QML. - originalJson.insert("mediaId", InfoT::mediaId()); - } - - QMimeType type() const override { return InfoT::mimeType; } - const FileInfo* fileInfo() const override { return this; } - FileInfo* fileInfo() override { return this; } - - protected: - void fillJson(QJsonObject* json) const override - { - Q_ASSERT(json); - json->insert("url", InfoT::url.toString()); - if (!InfoT::originalName.isEmpty()) - json->insert("filename", InfoT::originalName); - json->insert("info", toInfoJson<InfoT>(*this)); - } - }; - - template <typename InfoT> - class UrlWithThumbnailContent : public UrlBasedContent<InfoT> + // Another small hack, to simplify making a thumbnail link + UrlBasedContent<InfoT>::originalJson.insert("thumbnailMediaId", + thumbnail.mediaId()); + } + + const Thumbnail* thumbnailInfo() const override { return &thumbnail; } + + public: + Thumbnail thumbnail; + + protected: + void fillJson(QJsonObject* json) const override { - public: - using UrlBasedContent<InfoT>::UrlBasedContent; - explicit UrlWithThumbnailContent(const QJsonObject& json) - : UrlBasedContent<InfoT>(json) - , thumbnail(InfoT::originalInfoJson) - { - // Another small hack, to simplify making a thumbnail link - UrlBasedContent<InfoT>::originalJson.insert( - "thumbnailMediaId", thumbnail.mediaId()); - } - - const Thumbnail* thumbnailInfo() const override - { return &thumbnail; } - - public: - Thumbnail thumbnail; - - protected: - void fillJson(QJsonObject* json) const override - { - UrlBasedContent<InfoT>::fillJson(json); - auto infoJson = json->take("info").toObject(); - thumbnail.fillInfoJson(&infoJson); - json->insert("info", infoJson); - } - }; + UrlBasedContent<InfoT>::fillJson(json); + auto infoJson = json->take("info").toObject(); + thumbnail.fillInfoJson(&infoJson); + json->insert("info", infoJson); + } + }; - /** - * Content class for m.image - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename (extension to the spec) - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - imageSize (QSize for a combination of "h" and "w" in JSON) - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: contents of - * thumbnail field, in the same vein as for the main image: - * - payloadSize - * - mimeType - * - imageSize - */ - using ImageContent = UrlWithThumbnailContent<ImageInfo>; + /** + * Content class for m.image + * + * Available fields: + * - corresponding to the top-level JSON: + * - url + * - filename (extension to the spec) + * - corresponding to the "info" subobject: + * - payloadSize ("size" in JSON) + * - mimeType ("mimetype" in JSON) + * - imageSize (QSize for a combination of "h" and "w" in JSON) + * - thumbnail.url ("thumbnail_url" in JSON) + * - corresponding to the "info/thumbnail_info" subobject: contents of + * thumbnail field, in the same vein as for the main image: + * - payloadSize + * - mimeType + * - imageSize + */ + using ImageContent = UrlWithThumbnailContent<ImageInfo>; - /** - * Content class for m.file - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: - * - thumbnail.payloadSize - * - thumbnail.mimeType - * - thumbnail.imageSize (QSize for "h" and "w" in JSON) - */ - using FileContent = UrlWithThumbnailContent<FileInfo>; - } // namespace EventContent -} // namespace QMatrixClient + /** + * Content class for m.file + * + * Available fields: + * - corresponding to the top-level JSON: + * - url + * - filename + * - corresponding to the "info" subobject: + * - payloadSize ("size" in JSON) + * - mimeType ("mimetype" in JSON) + * - thumbnail.url ("thumbnail_url" in JSON) + * - corresponding to the "info/thumbnail_info" subobject: + * - thumbnail.payloadSize + * - thumbnail.mimeType + * - thumbnail.imageSize (QSize for "h" and "w" in JSON) + */ + using FileContent = UrlWithThumbnailContent<FileInfo>; +} // namespace EventContent +} // namespace Quotient +Q_DECLARE_METATYPE(const Quotient::EventContent::TypedBase*) diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index da663392..ebb96441 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -1,71 +1,86 @@ /****************************************************************************** -* Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ + * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ #pragma once #include "stateevent.h" -namespace QMatrixClient { - namespace _impl { - template <typename BaseEventT> - static inline auto loadEvent(const QJsonObject& json, - const QString& matrixType) - { - if (auto e = EventFactory<BaseEventT>::make(json, matrixType)) - return e; - return makeEvent<BaseEventT>(unknownEventTypeId(), json); - } - } - - /** Create an event with proper type from a JSON object - * Use this factory template to detect the type from the JSON object - * contents (the detected event type should derive from the template - * parameter type) and create an event object of that type. - */ +namespace Quotient { +namespace _impl { template <typename BaseEventT> - inline event_ptr_tt<BaseEventT> loadEvent(const QJsonObject& fullJson) + static inline auto loadEvent(const QJsonObject& json, + const QString& matrixType) { - return _impl::loadEvent<BaseEventT>(fullJson, - fullJson[TypeKeyL].toString()); + if (auto e = EventFactory<BaseEventT>::make(json, matrixType)) + return e; + return makeEvent<BaseEventT>(unknownEventTypeId(), json); } +} // namespace _impl - /** Create an event from a type string and content JSON - * Use this factory template to resolve the C++ type from the Matrix - * type string in \p matrixType and create an event of that type that has - * its content part set to \p content. - */ - template <typename BaseEventT> - inline event_ptr_tt<BaseEventT> loadEvent(const QString& matrixType, - const QJsonObject& content) +/*! Create an event with proper type from a JSON object + * + * Use this factory template to detect the type from the JSON object + * contents (the detected event type should derive from the template + * parameter type) and create an event object of that type. + */ +template <typename BaseEventT> +inline event_ptr_tt<BaseEventT> loadEvent(const QJsonObject& fullJson) +{ + return _impl::loadEvent<BaseEventT>(fullJson, fullJson[TypeKeyL].toString()); +} + +/*! Create an event from a type string and content JSON + * + * Use this factory template to resolve the C++ type from the Matrix + * type string in \p matrixType and create an event of that type that has + * its content part set to \p content. + */ +template <typename BaseEventT> +inline event_ptr_tt<BaseEventT> loadEvent(const QString& matrixType, + const QJsonObject& content) +{ + return _impl::loadEvent<BaseEventT>(basicEventJson(matrixType, content), + matrixType); +} + +/*! Create a state event from a type string, content JSON and state key + * + * Use this factory to resolve the C++ type from the Matrix type string + * in \p matrixType and create a state event of that type with content part + * set to \p content and state key set to \p stateKey (empty by default). + */ +inline StateEventPtr loadStateEvent(const QString& matrixType, + const QJsonObject& content, + const QString& stateKey = {}) +{ + return _impl::loadEvent<StateEventBase>( + basicStateEventJson(matrixType, content, stateKey), matrixType); +} + +template <typename EventT> +struct JsonConverter<event_ptr_tt<EventT>> { + static auto load(const QJsonValue& jv) { - return _impl::loadEvent<BaseEventT>(basicEventJson(matrixType, content), - matrixType); + return loadEvent<EventT>(jv.toObject()); } - - template <typename EventT> struct JsonConverter<event_ptr_tt<EventT>> + static auto load(const QJsonDocument& jd) { - static auto load(const QJsonValue& jv) - { - return loadEvent<EventT>(jv.toObject()); - } - static auto load(const QJsonDocument& jd) - { - return loadEvent<EventT>(jd.object()); - } - }; -} // namespace QMatrixClient + return loadEvent<EventT>(jd.object()); + } +}; +} // namespace Quotient diff --git a/lib/events/reactionevent.cpp b/lib/events/reactionevent.cpp index 0081edc2..003c8ead 100644 --- a/lib/events/reactionevent.cpp +++ b/lib/events/reactionevent.cpp @@ -13,15 +13,15 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "reactionevent.h" -using namespace QMatrixClient; +using namespace Quotient; -void QMatrixClient::JsonObjectConverter<EventRelation>::dumpTo( - QJsonObject& jo, const EventRelation& pod) +void JsonObjectConverter<EventRelation>::dumpTo( + QJsonObject& jo, const EventRelation& pod) { if (pod.type.isEmpty()) { qCWarning(MAIN) << "Empty relation type; won't dump to JSON"; @@ -33,8 +33,8 @@ void QMatrixClient::JsonObjectConverter<EventRelation>::dumpTo( jo.insert(QStringLiteral("key"), pod.key); } -void QMatrixClient::JsonObjectConverter<EventRelation>::fillFrom( - const QJsonObject& jo, EventRelation& pod) +void JsonObjectConverter<EventRelation>::fillFrom( + const QJsonObject& jo, EventRelation& pod) { // The experimental logic for generic relationships (MSC1849) fromJson(jo["rel_type"_ls], pod.type); diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h index 7a4c9b5a..75c6528c 100644 --- a/lib/events/reactionevent.h +++ b/lib/events/reactionevent.h @@ -20,13 +20,9 @@ #include "roomevent.h" -namespace QMatrixClient { +namespace Quotient { struct EventRelation { - // To please MSVC 2015 that doesn't handle initialiser lists like proper - EventRelation(QString type = {}, QString eventId = {}, QString key = {}) - : type(std::move(type)), eventId(std::move(eventId)), key(std::move(key)) - {} using reltypeid_t = const char*; static constexpr reltypeid_t Reply() { return "m.in_reply_to"; } static constexpr reltypeid_t Annotation() { return "m.annotation"; } @@ -38,26 +34,24 @@ struct EventRelation { static EventRelation replyTo(QString eventId) { - return EventRelation(Reply(), std::move(eventId)); + return { Reply(), std::move(eventId) }; } static EventRelation annotate(QString eventId, QString key) { - return EventRelation(Annotation(), std::move(eventId), std::move(key)); + return { Annotation(), std::move(eventId), std::move(key) }; } static EventRelation replace(QString eventId) { - return EventRelation(Replacement(), std::move(eventId)); + return { Replacement(), std::move(eventId) }; } }; template <> -struct JsonObjectConverter<EventRelation> -{ +struct JsonObjectConverter<EventRelation> { static void dumpTo(QJsonObject& jo, const EventRelation& pod); static void fillFrom(const QJsonObject& jo, EventRelation& pod); }; -class ReactionEvent : public RoomEvent -{ +class ReactionEvent : public RoomEvent { public: DEFINE_EVENT_TYPEID("m.reaction", ReactionEvent) @@ -65,17 +59,15 @@ public: : RoomEvent(typeId(), matrixTypeId(), { { QStringLiteral("m.relates_to"), toJson(value) } }) {} - explicit ReactionEvent(const QJsonObject& obj) - : RoomEvent(typeId(), obj) - {} + explicit ReactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) {} EventRelation relation() const { return content<EventRelation>(QStringLiteral("m.relates_to")); } -//private: -// EventRelation _relation; +private: + EventRelation _relation; }; REGISTER_EVENT_TYPE(ReactionEvent) -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/events/receiptevent.cpp b/lib/events/receiptevent.cpp index 47e1398c..bf050cb2 100644 --- a/lib/events/receiptevent.cpp +++ b/lib/events/receiptevent.cpp @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ /* @@ -38,32 +38,28 @@ Example of a Receipt Event: #include "converters.h" #include "logging.h" -using namespace QMatrixClient; +using namespace Quotient; -ReceiptEvent::ReceiptEvent(const QJsonObject& obj) - : Event(typeId(), obj) +ReceiptEvent::ReceiptEvent(const QJsonObject& obj) : Event(typeId(), obj) { const auto& contents = contentJson(); _eventsWithReceipts.reserve(contents.size()); - for( auto eventIt = contents.begin(); eventIt != contents.end(); ++eventIt ) - { - if (eventIt.key().isEmpty()) - { - qCWarning(EPHEMERAL) << "ReceiptEvent has an empty event id, skipping"; + for (auto eventIt = contents.begin(); eventIt != contents.end(); ++eventIt) { + if (eventIt.key().isEmpty()) { + qCWarning(EPHEMERAL) + << "ReceiptEvent has an empty event id, skipping"; qCDebug(EPHEMERAL) << "ReceiptEvent content follows:\n" << contents; continue; } - const QJsonObject reads = eventIt.value().toObject() - .value("m.read"_ls).toObject(); + const QJsonObject reads = + eventIt.value().toObject().value("m.read"_ls).toObject(); QVector<Receipt> receipts; receipts.reserve(reads.size()); - for( auto userIt = reads.begin(); userIt != reads.end(); ++userIt ) - { + for (auto userIt = reads.begin(); userIt != reads.end(); ++userIt) { const QJsonObject user = userIt.value().toObject(); - receipts.push_back({userIt.key(), - fromJson<QDateTime>(user["ts"_ls])}); + receipts.push_back( + { userIt.key(), fromJson<QDateTime>(user["ts"_ls]) }); } - _eventsWithReceipts.push_back({eventIt.key(), std::move(receipts)}); + _eventsWithReceipts.push_back({ eventIt.key(), std::move(receipts) }); } } - diff --git a/lib/events/receiptevent.h b/lib/events/receiptevent.h index c15a01c2..dd54a476 100644 --- a/lib/events/receiptevent.h +++ b/lib/events/receiptevent.h @@ -13,42 +13,39 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "event.h" -#include <QtCore/QVector> #include <QtCore/QDateTime> +#include <QtCore/QVector> -namespace QMatrixClient -{ - struct Receipt - { - QString userId; - QDateTime timestamp; - }; - struct ReceiptsForEvent - { - QString evtId; - QVector<Receipt> receipts; - }; - using EventsWithReceipts = QVector<ReceiptsForEvent>; +namespace Quotient { +struct Receipt { + QString userId; + QDateTime timestamp; +}; +struct ReceiptsForEvent { + QString evtId; + QVector<Receipt> receipts; +}; +using EventsWithReceipts = QVector<ReceiptsForEvent>; - class ReceiptEvent: public Event - { - public: - DEFINE_EVENT_TYPEID("m.receipt", ReceiptEvent) - explicit ReceiptEvent(const QJsonObject& obj); +class ReceiptEvent : public Event { +public: + DEFINE_EVENT_TYPEID("m.receipt", ReceiptEvent) + explicit ReceiptEvent(const QJsonObject& obj); - const EventsWithReceipts& eventsWithReceipts() const - { return _eventsWithReceipts; } + const EventsWithReceipts& eventsWithReceipts() const + { + return _eventsWithReceipts; + } - private: - EventsWithReceipts _eventsWithReceipts; - }; - REGISTER_EVENT_TYPE(ReceiptEvent) - DEFINE_EVENTTYPE_ALIAS(Receipt, ReceiptEvent) -} // namespace QMatrixClient +private: + EventsWithReceipts _eventsWithReceipts; +}; +REGISTER_EVENT_TYPE(ReceiptEvent) +} // namespace Quotient diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h index a72a8ff9..3b3af18e 100644 --- a/lib/events/redactionevent.h +++ b/lib/events/redactionevent.h @@ -13,29 +13,26 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "roomevent.h" -namespace QMatrixClient -{ - class RedactionEvent : public RoomEvent - { - public: - DEFINE_EVENT_TYPEID("m.room.redaction", RedactionEvent) +namespace Quotient { +class RedactionEvent : public RoomEvent { +public: + DEFINE_EVENT_TYPEID("m.room.redaction", RedactionEvent) - explicit RedactionEvent(const QJsonObject& obj) - : RoomEvent(typeId(), obj) - { } + explicit RedactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) + {} - QString redactedEvent() const - { return fullJson()["redacts"_ls].toString(); } - QString reason() const - { return contentJson()["reason"_ls].toString(); } - }; - REGISTER_EVENT_TYPE(RedactionEvent) - DEFINE_EVENTTYPE_ALIAS(Redaction, RedactionEvent) -} // namespace QMatrixClient + QString redactedEvent() const + { + return fullJson()["redacts"_ls].toString(); + } + QString reason() const { return contentJson()["reason"_ls].toString(); } +}; +REGISTER_EVENT_TYPE(RedactionEvent) +} // namespace Quotient diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index a43d3a85..c2100eaa 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -13,29 +13,37 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once -#include "stateevent.h" #include "eventcontent.h" +#include "stateevent.h" + +namespace Quotient { +class RoomAvatarEvent : public StateEvent<EventContent::ImageContent> { + // It's a bit of an overkill to use a full-fledged ImageContent + // because in reality m.room.avatar usually only has a single URL, + // without a thumbnail. But The Spec says there be thumbnails, and + // we follow The Spec. +public: + DEFINE_EVENT_TYPEID("m.room.avatar", RoomAvatarEvent) + explicit RoomAvatarEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) + {} + explicit RoomAvatarEvent(const EventContent::ImageContent& avatar) + : StateEvent(typeId(), matrixTypeId(), QString(), avatar) + {} + // A replica of EventContent::ImageInfo constructor + explicit RoomAvatarEvent(const QUrl& u, qint64 fileSize = -1, + QMimeType mimeType = {}, + const QSize& imageSize = {}, + const QString& originalFilename = {}) + : RoomAvatarEvent(EventContent::ImageContent { + u, fileSize, mimeType, imageSize, originalFilename }) + {} -namespace QMatrixClient -{ - class RoomAvatarEvent: public StateEvent<EventContent::ImageContent> - { - // It's a bit of an overkill to use a full-fledged ImageContent - // because in reality m.room.avatar usually only has a single URL, - // without a thumbnail. But The Spec says there be thumbnails, and - // we follow The Spec. - public: - DEFINE_EVENT_TYPEID("m.room.avatar", RoomAvatarEvent) - explicit RoomAvatarEvent(const QJsonObject& obj) - : StateEvent(typeId(), obj) - { } - QUrl url() const { return content().url; } - }; - REGISTER_EVENT_TYPE(RoomAvatarEvent) - DEFINE_EVENTTYPE_ALIAS(RoomAvatar, RoomAvatarEvent) -} // namespace QMatrixClient + QUrl url() const { return content().url; } +}; +REGISTER_EVENT_TYPE(RoomAvatarEvent) +} // namespace Quotient diff --git a/lib/events/roomcanonicalaliasevent.h b/lib/events/roomcanonicalaliasevent.h new file mode 100644 index 00000000..fadfece0 --- /dev/null +++ b/lib/events/roomcanonicalaliasevent.h @@ -0,0 +1,78 @@ +/****************************************************************************** + * Copyright (C) 2020 QMatrixClient project + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "stateevent.h" + +namespace Quotient { +namespace EventContent{ + class AliasesEventContent { + + public: + + template<typename T1, typename T2> + AliasesEventContent(T1&& canonicalAlias, T2&& altAliases) + : canonicalAlias(std::forward<T1>(canonicalAlias)) + , altAliases(std::forward<T2>(altAliases)) + { } + + AliasesEventContent(const QJsonObject& json) + : canonicalAlias(fromJson<QString>(json["alias"])) + , altAliases(fromJson<QStringList>(json["alt_aliases"])) + { } + + auto toJson() const + { + QJsonObject jo; + addParam<IfNotEmpty>(jo, QStringLiteral("alias"), canonicalAlias); + addParam<IfNotEmpty>(jo, QStringLiteral("alt_aliases"), altAliases); + return jo; + } + + QString canonicalAlias; + QStringList altAliases; + }; +} // namespace EventContent + +class RoomCanonicalAliasEvent + : public StateEvent<EventContent::AliasesEventContent> { +public: + DEFINE_EVENT_TYPEID("m.room.canonical_alias", RoomCanonicalAliasEvent) + + explicit RoomCanonicalAliasEvent(const QJsonObject& obj) + : StateEvent(typeId(), obj) + { } + + explicit RoomCanonicalAliasEvent(const QString& canonicalAlias, + const QStringList& altAliases = {}) + : StateEvent(typeId(), matrixTypeId(), QString(), + canonicalAlias, altAliases) + { } + + explicit RoomCanonicalAliasEvent(QString&& canonicalAlias, + QStringList&& altAliases = {}) + : StateEvent(typeId(), matrixTypeId(), QString(), + std::move(canonicalAlias), std::move(altAliases)) + { } + + QString alias() const { return content().canonicalAlias; } + QStringList altAliases() const { return content().altAliases; } +}; +REGISTER_EVENT_TYPE(RoomCanonicalAliasEvent) +} // namespace Quotient diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp index 8fd0f1de..c72b5bc2 100644 --- a/lib/events/roomcreateevent.cpp +++ b/lib/events/roomcreateevent.cpp @@ -1,24 +1,24 @@ /****************************************************************************** -* Copyright (C) 2019 QMatrixClient project -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ + * Copyright (C) 2019 QMatrixClient project + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ #include "roomcreateevent.h" -using namespace QMatrixClient; +using namespace Quotient; bool RoomCreateEvent::isFederated() const { @@ -33,10 +33,8 @@ QString RoomCreateEvent::version() const RoomCreateEvent::Predecessor RoomCreateEvent::predecessor() const { const auto predJson = contentJson()["predecessor"_ls].toObject(); - return { - fromJson<QString>(predJson["room_id"_ls]), - fromJson<QString>(predJson["event_id"_ls]) - }; + return { fromJson<QString>(predJson["room_id"_ls]), + fromJson<QString>(predJson["event_id"_ls]) }; } bool RoomCreateEvent::isUpgrade() const diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h index 0a8f27cc..91aefe9e 100644 --- a/lib/events/roomcreateevent.h +++ b/lib/events/roomcreateevent.h @@ -1,49 +1,44 @@ /****************************************************************************** -* Copyright (C) 2019 QMatrixClient project -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ + * Copyright (C) 2019 QMatrixClient project + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ #pragma once #include "stateevent.h" -namespace QMatrixClient -{ - class RoomCreateEvent : public StateEventBase - { - public: - DEFINE_EVENT_TYPEID("m.room.create", RoomCreateEvent) +namespace Quotient { +class RoomCreateEvent : public StateEventBase { +public: + DEFINE_EVENT_TYPEID("m.room.create", RoomCreateEvent) - explicit RoomCreateEvent() - : StateEventBase(typeId(), matrixTypeId()) - { } - explicit RoomCreateEvent(const QJsonObject& obj) - : StateEventBase(typeId(), obj) - { } + explicit RoomCreateEvent() : StateEventBase(typeId(), matrixTypeId()) {} + explicit RoomCreateEvent(const QJsonObject& obj) + : StateEventBase(typeId(), obj) + {} - struct Predecessor - { - QString roomId; - QString eventId; - }; - - bool isFederated() const; - QString version() const; - Predecessor predecessor() const; - bool isUpgrade() const; + struct Predecessor { + QString roomId; + QString eventId; }; - REGISTER_EVENT_TYPE(RoomCreateEvent) -} + + bool isFederated() const; + QString version() const; + Predecessor predecessor() const; + bool isUpgrade() const; +}; +REGISTER_EVENT_TYPE(RoomCreateEvent) +} // namespace Quotient diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 5e2d0b3c..a59cd6e0 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -1,59 +1,52 @@ /****************************************************************************** -* Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ + * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ #include "roomevent.h" -#include "redactionevent.h" #include "converters.h" #include "logging.h" +#include "redactionevent.h" -using namespace QMatrixClient; +using namespace Quotient; -[[gnu::unused]] static auto roomEventTypeInitialised = - Event::factory_t::chainFactory<RoomEvent>(); +[[maybe_unused]] static auto roomEventTypeInitialised = + Event::factory_t::chainFactory<RoomEvent>(); RoomEvent::RoomEvent(Type type, event_mtype_t matrixType, const QJsonObject& contentJson) : Event(type, matrixType, contentJson) -{ } +{} -RoomEvent::RoomEvent(Type type, const QJsonObject& json) - : Event(type, json) +RoomEvent::RoomEvent(Type type, const QJsonObject& json) : Event(type, json) { const auto unsignedData = json[UnsignedKeyL].toObject(); const auto redaction = unsignedData[RedactedCauseKeyL]; if (redaction.isObject()) - { _redactedBecause = makeEvent<RedactionEvent>(redaction.toObject()); - return; - } } RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job -QString RoomEvent::id() const -{ - return fullJson()[EventIdKeyL].toString(); -} +QString RoomEvent::id() const { return fullJson()[EventIdKeyL].toString(); } -QDateTime RoomEvent::timestamp() const +QDateTime RoomEvent::originTimestamp() const { - return QMatrixClient::fromJson<QDateTime>(fullJson()["origin_server_ts"_ls]); + return Quotient::fromJson<QDateTime>(fullJson()["origin_server_ts"_ls]); } QString RoomEvent::roomId() const @@ -82,7 +75,7 @@ QString RoomEvent::replacedBy() const QString RoomEvent::redactionReason() const { - return isRedacted() ? _redactedBecause->reason() : QString{}; + return isRedacted() ? _redactedBecause->reason() : QString {}; } QString RoomEvent::transactionId() const @@ -92,7 +85,17 @@ QString RoomEvent::transactionId() const QString RoomEvent::stateKey() const { - return fullJson()["state_key"_ls].toString(); + return fullJson()[StateKeyKeyL].toString(); +} + +void RoomEvent::setRoomId(const QString& roomId) +{ + editJson().insert(QStringLiteral("room_id"), roomId); +} + +void RoomEvent::setSender(const QString& senderId) +{ + editJson().insert(QStringLiteral("sender"), senderId); } void RoomEvent::setTransactionId(const QString& txnId) @@ -105,7 +108,8 @@ void RoomEvent::setTransactionId(const QString& txnId) void RoomEvent::addId(const QString& newId) { - Q_ASSERT(id().isEmpty()); Q_ASSERT(!newId.isEmpty()); + Q_ASSERT(id().isEmpty()); + Q_ASSERT(!newId.isEmpty()); editJson().insert(EventIdKey, newId); qCDebug(EVENTS) << "Event txnId -> id:" << transactionId() << "->" << id(); Q_ASSERT(id() == newId); @@ -124,7 +128,7 @@ CallEventBase::CallEventBase(Type type, event_mtype_t matrixType, const QJsonObject& contentJson) : RoomEvent(type, matrixType, makeCallContentJson(callId, version, contentJson)) -{ } +{} CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json) : RoomEvent(type, json) diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 8926ab0f..621652cb 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -1,20 +1,20 @@ /****************************************************************************** -* Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ + * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ #pragma once @@ -22,87 +22,89 @@ #include <QtCore/QDateTime> -namespace QMatrixClient -{ - class RedactionEvent; +namespace Quotient { +class RedactionEvent; - /** This class corresponds to m.room.* events */ - class RoomEvent : public Event - { - Q_GADGET - Q_PROPERTY(QString id READ id) - Q_PROPERTY(QDateTime timestamp READ timestamp CONSTANT) - Q_PROPERTY(QString roomId READ roomId CONSTANT) - Q_PROPERTY(QString senderId READ senderId CONSTANT) - Q_PROPERTY(QString redactionReason READ redactionReason) - Q_PROPERTY(bool isRedacted READ isRedacted) - Q_PROPERTY(QString transactionId READ transactionId WRITE setTransactionId) - public: - using factory_t = EventFactory<RoomEvent>; +/** This class corresponds to m.room.* events */ +class RoomEvent : public Event { + Q_GADGET + Q_PROPERTY(QString id READ id) + Q_PROPERTY(QDateTime timestamp READ timestamp CONSTANT) + Q_PROPERTY(QString roomId READ roomId CONSTANT) + Q_PROPERTY(QString senderId READ senderId CONSTANT) + Q_PROPERTY(QString redactionReason READ redactionReason) + Q_PROPERTY(bool isRedacted READ isRedacted) + Q_PROPERTY(QString transactionId READ transactionId WRITE setTransactionId) +public: + using factory_t = EventFactory<RoomEvent>; + + // RedactionEvent is an incomplete type here so we cannot inline + // constructors and destructors and we cannot use 'using'. + RoomEvent(Type type, event_mtype_t matrixType, + const QJsonObject& contentJson = {}); + RoomEvent(Type type, const QJsonObject& json); + ~RoomEvent() override; - // RedactionEvent is an incomplete type here so we cannot inline - // constructors and destructors and we cannot use 'using'. - RoomEvent(Type type, event_mtype_t matrixType, - const QJsonObject& contentJson = {}); - RoomEvent(Type type, const QJsonObject& json); - ~RoomEvent() override; + QString id() const; + QDateTime originTimestamp() const; + [[deprecated("Use originTimestamp()")]] QDateTime timestamp() const { + return originTimestamp(); + } + QString roomId() const; + QString senderId() const; + bool isReplaced() const; + QString replacedBy() const; + bool isRedacted() const { return bool(_redactedBecause); } + const event_ptr_tt<RedactionEvent>& redactedBecause() const + { + return _redactedBecause; + } + QString redactionReason() const; + QString transactionId() const; + QString stateKey() const; - QString id() const; - QDateTime timestamp() const; - QString roomId() const; - QString senderId() const; - bool isReplaced() const; - QString replacedBy() const; - bool isRedacted() const { return bool(_redactedBecause); } - const event_ptr_tt<RedactionEvent>& redactedBecause() const - { - return _redactedBecause; - } - QString redactionReason() const; - QString transactionId() const; - QString stateKey() const; + void setRoomId(const QString& roomId); + void setSender(const QString& senderId); - /** - * Sets the transaction id for locally created events. This should be - * done before the event is exposed to any code using the respective - * Q_PROPERTY. - * - * \param txnId - transaction id, normally obtained from - * Connection::generateTxnId() - */ - void setTransactionId(const QString& txnId); + /** + * Sets the transaction id for locally created events. This should be + * done before the event is exposed to any code using the respective + * Q_PROPERTY. + * + * \param txnId - transaction id, normally obtained from + * Connection::generateTxnId() + */ + void setTransactionId(const QString& txnId); - /** - * Sets event id for locally created events - * - * When a new event is created locally, it has no server id yet. - * This function allows to add the id once the confirmation from - * the server is received. There should be no id set previously - * in the event. It's the responsibility of the code calling addId() - * to notify clients that use Q_PROPERTY(id) about its change - */ - void addId(const QString& newId); + /** + * Sets event id for locally created events + * + * When a new event is created locally, it has no server id yet. + * This function allows to add the id once the confirmation from + * the server is received. There should be no id set previously + * in the event. It's the responsibility of the code calling addId() + * to notify clients that use Q_PROPERTY(id) about its change + */ + void addId(const QString& newId); - private: - event_ptr_tt<RedactionEvent> _redactedBecause; - }; - using RoomEventPtr = event_ptr_tt<RoomEvent>; - using RoomEvents = EventsArray<RoomEvent>; - using RoomEventsRange = Range<RoomEvents>; +private: + event_ptr_tt<RedactionEvent> _redactedBecause; +}; +using RoomEventPtr = event_ptr_tt<RoomEvent>; +using RoomEvents = EventsArray<RoomEvent>; +using RoomEventsRange = Range<RoomEvents>; - class CallEventBase: public RoomEvent - { - public: - CallEventBase(Type type, event_mtype_t matrixType, - const QString& callId, int version, - const QJsonObject& contentJson = {}); - CallEventBase(Type type, const QJsonObject& json); - ~CallEventBase() override = default; - bool isCallEvent() const override { return true; } +class CallEventBase : public RoomEvent { +public: + CallEventBase(Type type, event_mtype_t matrixType, const QString& callId, + int version, const QJsonObject& contentJson = {}); + CallEventBase(Type type, const QJsonObject& json); + ~CallEventBase() override = default; + bool isCallEvent() const override { return true; } - QString callId() const { return content<QString>("call_id"_ls); } - int version() const { return content<int>("version"_ls); } - }; -} // namespace QMatrixClient -Q_DECLARE_METATYPE(QMatrixClient::RoomEvent*) -Q_DECLARE_METATYPE(const QMatrixClient::RoomEvent*) + QString callId() const { return content<QString>("call_id"_ls); } + int version() const { return content<int>("version"_ls); } +}; +} // namespace Quotient +Q_DECLARE_METATYPE(Quotient::RoomEvent*) +Q_DECLARE_METATYPE(const Quotient::RoomEvent*) diff --git a/lib/events/roomkeyevent.cpp b/lib/events/roomkeyevent.cpp new file mode 100644 index 00000000..66580430 --- /dev/null +++ b/lib/events/roomkeyevent.cpp @@ -0,0 +1,9 @@ +#include "roomkeyevent.h" + +using namespace Quotient; + +RoomKeyEvent::RoomKeyEvent(const QJsonObject &obj) : Event(typeId(), obj) +{ + if (roomId().isEmpty()) + qCWarning(E2EE) << "Room key event has empty room id"; +} diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h new file mode 100644 index 00000000..679cbf7c --- /dev/null +++ b/lib/events/roomkeyevent.h @@ -0,0 +1,19 @@ +#pragma once + +#include "event.h" + +namespace Quotient { +class RoomKeyEvent : public Event +{ +public: + DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent) + + RoomKeyEvent(const QJsonObject& obj); + + QString algorithm() const { return content<QString>("algorithm"_ls); } + QString roomId() const { return content<QString>("room_id"_ls); } + QString sessionId() const { return content<QString>("session_id"_ls); } + QString sessionKey() const { return content<QString>("session_key"_ls); } +}; +REGISTER_EVENT_TYPE(RoomKeyEvent) +} // namespace Quotient diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index 6da76526..3193a54d 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "roommemberevent.h" @@ -23,68 +23,91 @@ #include <array> -static const std::array<QString, 5> membershipStrings = { { - QStringLiteral("invite"), QStringLiteral("join"), - QStringLiteral("knock"), QStringLiteral("leave"), - QStringLiteral("ban") -} }; +static const std::array<QString, 5> membershipStrings = { + { QStringLiteral("invite"), QStringLiteral("join"), QStringLiteral("knock"), + QStringLiteral("leave"), QStringLiteral("ban") } +}; -namespace QMatrixClient { - template <> - struct JsonConverter<MembershipType> +namespace Quotient { +template <> +struct JsonConverter<MembershipType> { + static MembershipType load(const QJsonValue& jv) { - static MembershipType load(const QJsonValue& jv) - { - const auto& membershipString = jv.toString(); - for (auto it = membershipStrings.begin(); - it != membershipStrings.end(); ++it) - if (membershipString == *it) - return MembershipType(it - membershipStrings.begin()); + const auto& membershipString = jv.toString(); + for (auto it = membershipStrings.begin(); it != membershipStrings.end(); + ++it) + if (membershipString == *it) + return MembershipType(it - membershipStrings.begin()); + if (!membershipString.isEmpty()) qCWarning(EVENTS) << "Unknown MembershipType: " << membershipString; - return MembershipType::Undefined; - } - }; -} + return MembershipType::Undefined; + } +}; +} // namespace Quotient -using namespace QMatrixClient; +using namespace Quotient; MemberEventContent::MemberEventContent(const QJsonObject& json) : membership(fromJson<MembershipType>(json["membership"_ls])) , isDirect(json["is_direct"_ls].toBool()) , displayName(sanitized(json["displayname"_ls].toString())) , avatarUrl(json["avatar_url"_ls].toString()) -{ } + , reason(json["reason"_ls].toString()) +{} void MemberEventContent::fillJson(QJsonObject* o) const { Q_ASSERT(o); Q_ASSERT_X(membership != MembershipType::Undefined, __FUNCTION__, - "The key 'membership' must be explicit in MemberEventContent"); + "The key 'membership' must be explicit in MemberEventContent"); if (membership != MembershipType::Undefined) o->insert(QStringLiteral("membership"), membershipStrings[membership]); o->insert(QStringLiteral("displayname"), displayName); if (avatarUrl.isValid()) o->insert(QStringLiteral("avatar_url"), avatarUrl.toString()); + if (!reason.isEmpty()) + o->insert(QStringLiteral("reason"), reason); +} + +bool RoomMemberEvent::changesMembership() const +{ + return !prevContent() || prevContent()->membership != membership(); } bool RoomMemberEvent::isInvite() const { - return membership() == MembershipType::Invite && - (!prevContent() || prevContent()->membership != membership()); + return membership() == MembershipType::Invite && changesMembership(); +} + +bool RoomMemberEvent::isRejectedInvite() const +{ + return membership() == MembershipType::Leave && prevContent() + && prevContent()->membership == MembershipType::Invite; } bool RoomMemberEvent::isJoin() const { - return membership() == MembershipType::Join && - (!prevContent() || prevContent()->membership != membership()); + return membership() == MembershipType::Join && changesMembership(); } bool RoomMemberEvent::isLeave() const { - return membership() == MembershipType::Leave && - prevContent() && prevContent()->membership != membership() && - prevContent()->membership != MembershipType::Ban; + return membership() == MembershipType::Leave && prevContent() + && prevContent()->membership != membership() + && prevContent()->membership != MembershipType::Ban + && prevContent()->membership != MembershipType::Invite; +} + +bool RoomMemberEvent::isBan() const +{ + return membership() == MembershipType::Ban && changesMembership(); +} + +bool RoomMemberEvent::isUnban() const +{ + return membership() == MembershipType::Leave && prevContent() + && prevContent()->membership == MembershipType::Ban; } bool RoomMemberEvent::isRename() const diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index b8224033..783b8207 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -13,94 +13,103 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once -#include "stateevent.h" #include "eventcontent.h" +#include "stateevent.h" -namespace QMatrixClient -{ - class MemberEventContent: public EventContent::Base - { - public: - enum MembershipType : size_t { Invite = 0, Join, Knock, Leave, Ban, - Undefined }; +namespace Quotient { +class MemberEventContent : public EventContent::Base { +public: + enum MembershipType : size_t { + Invite = 0, + Join, + Knock, + Leave, + Ban, + Undefined + }; - explicit MemberEventContent(MembershipType mt = Join) - : membership(mt) - { } - explicit MemberEventContent(const QJsonObject& json); + explicit MemberEventContent(MembershipType mt = Join) : membership(mt) {} + explicit MemberEventContent(const QJsonObject& json); - MembershipType membership; - bool isDirect = false; - QString displayName; - QUrl avatarUrl; + MembershipType membership; + bool isDirect = false; + QString displayName; + QUrl avatarUrl; + QString reason; - protected: - void fillJson(QJsonObject* o) const override; - }; +protected: + void fillJson(QJsonObject* o) const override; +}; - using MembershipType = MemberEventContent::MembershipType; +using MembershipType = MemberEventContent::MembershipType; - class RoomMemberEvent: public StateEvent<MemberEventContent> - { - Q_GADGET - public: - DEFINE_EVENT_TYPEID("m.room.member", RoomMemberEvent) +class RoomMemberEvent : public StateEvent<MemberEventContent> { + Q_GADGET +public: + DEFINE_EVENT_TYPEID("m.room.member", RoomMemberEvent) - using MembershipType = MemberEventContent::MembershipType; + using MembershipType = MemberEventContent::MembershipType; - explicit RoomMemberEvent(const QJsonObject& obj) - : StateEvent(typeId(), obj) - { } - RoomMemberEvent(MemberEventContent&& c) - : StateEvent(typeId(), matrixTypeId(), c) - { } + explicit RoomMemberEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) + {} + [[deprecated("Use RoomMemberEvent(userId, contentArgs) instead")]] + RoomMemberEvent(MemberEventContent&& c) + : StateEvent(typeId(), matrixTypeId(), QString(), c) + {} + template <typename... ArgTs> + RoomMemberEvent(const QString& userId, ArgTs&&... contentArgs) + : StateEvent(typeId(), matrixTypeId(), userId, + std::forward<ArgTs>(contentArgs)...) + {} - /// A special constructor to create unknown RoomMemberEvents - /** - * This is needed in order to use RoomMemberEvent as a "base event - * class" in cases like GetMembersByRoomJob when RoomMemberEvents - * (rather than RoomEvents or StateEvents) are resolved from JSON. - * For such cases loadEvent<> requires an underlying class to be - * constructible with unknownTypeId() instead of its genuine id. - * Don't use it directly. - * \sa GetMembersByRoomJob, loadEvent, unknownTypeId - */ - RoomMemberEvent(Type type, const QJsonObject& fullJson) - : StateEvent(type, fullJson) - { } + /// A special constructor to create unknown RoomMemberEvents + /** + * This is needed in order to use RoomMemberEvent as a "base event + * class" in cases like GetMembersByRoomJob when RoomMemberEvents + * (rather than RoomEvents or StateEvents) are resolved from JSON. + * For such cases loadEvent<> requires an underlying class to be + * constructible with unknownTypeId() instead of its genuine id. + * Don't use it directly. + * \sa GetMembersByRoomJob, loadEvent, unknownTypeId + */ + RoomMemberEvent(Type type, const QJsonObject& fullJson) + : StateEvent(type, fullJson) + {} - MembershipType membership() const { return content().membership; } - QString userId() const - { return fullJson()["state_key"_ls].toString(); } - bool isDirect() const { return content().isDirect; } - QString displayName() const { return content().displayName; } - QUrl avatarUrl() const { return content().avatarUrl; } - bool isInvite() const; - bool isJoin() const; - bool isLeave() const; - bool isRename() const; - bool isAvatarUpdate() const; + MembershipType membership() const { return content().membership; } + QString userId() const { return fullJson()[StateKeyKeyL].toString(); } + bool isDirect() const { return content().isDirect; } + QString displayName() const { return content().displayName; } + QUrl avatarUrl() const { return content().avatarUrl; } + QString reason() const { return content().reason; } + bool changesMembership() const; + bool isBan() const; + bool isUnban() const; + bool isInvite() const; + bool isRejectedInvite() const; + bool isJoin() const; + bool isLeave() const; + bool isRename() const; + bool isAvatarUpdate() const; - private: - REGISTER_ENUM(MembershipType) - }; +private: + Q_ENUM(MembershipType) +}; - template <> - class EventFactory<RoomMemberEvent> +template <> +class EventFactory<RoomMemberEvent> { +public: + static event_ptr_tt<RoomMemberEvent> make(const QJsonObject& json, + const QString&) { - public: - static event_ptr_tt<RoomMemberEvent> make(const QJsonObject& json, - const QString&) - { - return makeEvent<RoomMemberEvent>(json); - } - }; + return makeEvent<RoomMemberEvent>(json); + } +}; - REGISTER_EVENT_TYPE(RoomMemberEvent) - DEFINE_EVENTTYPE_ALIAS(RoomMember, RoomMemberEvent) -} // namespace QMatrixClient +REGISTER_EVENT_TYPE(RoomMemberEvent) +} // namespace Quotient diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 1edf82e4..616a034f 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -13,26 +13,25 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "roommessageevent.h" #include "logging.h" -#include <QtCore/QMimeDatabase> #include <QtCore/QFileInfo> +#include <QtCore/QMimeDatabase> #include <QtGui/QImageReader> #include <QtMultimedia/QMediaResource> -using namespace QMatrixClient; +using namespace Quotient; using namespace EventContent; using MsgType = RoomMessageEvent::MsgType; static const auto RelatesToKeyL = "m.relates_to"_ls; static const auto MsgTypeKeyL = "msgtype"_ls; -static const auto BodyKeyL = "body"_ls; static const auto FormattedBodyKeyL = "formatted_body"_ls; static const auto TextTypeKey = "m.text"; @@ -51,31 +50,33 @@ template <> TypedBase* make<TextContent>(const QJsonObject& json) { return json.contains(FormattedBodyKeyL) || json.contains(RelatesToKeyL) - ? new TextContent(json) : nullptr; + ? new TextContent(json) + : nullptr; } -struct MsgTypeDesc -{ +struct MsgTypeDesc { QString matrixType; MsgType enumType; TypedBase* (*maker)(const QJsonObject&); }; -const std::vector<MsgTypeDesc> msgTypes = - { { TextTypeKey, MsgType::Text, make<TextContent> } - , { EmoteTypeKey, MsgType::Emote, make<TextContent> } - , { NoticeTypeKey, MsgType::Notice, make<TextContent> } - , { QStringLiteral("m.image"), MsgType::Image, make<ImageContent> } - , { QStringLiteral("m.file"), MsgType::File, make<FileContent> } - , { QStringLiteral("m.location"), MsgType::Location, make<LocationContent> } - , { QStringLiteral("m.video"), MsgType::Video, make<VideoContent> } - , { QStringLiteral("m.audio"), MsgType::Audio, make<AudioContent> } - }; +const std::vector<MsgTypeDesc> msgTypes = { + { TextTypeKey, MsgType::Text, make<TextContent> }, + { EmoteTypeKey, MsgType::Emote, make<TextContent> }, + { NoticeTypeKey, MsgType::Notice, make<TextContent> }, + { QStringLiteral("m.image"), MsgType::Image, make<ImageContent> }, + { QStringLiteral("m.file"), MsgType::File, make<FileContent> }, + { QStringLiteral("m.location"), MsgType::Location, make<LocationContent> }, + { QStringLiteral("m.video"), MsgType::Video, make<VideoContent> }, + { QStringLiteral("m.audio"), MsgType::Audio, make<AudioContent> } +}; QString msgTypeToJson(MsgType enumType) { auto it = std::find_if(msgTypes.begin(), msgTypes.end(), - [=](const MsgTypeDesc& mtd) { return mtd.enumType == enumType; }); + [=](const MsgTypeDesc& mtd) { + return mtd.enumType == enumType; + }); if (it != msgTypes.end()) return it->matrixType; @@ -85,15 +86,23 @@ QString msgTypeToJson(MsgType enumType) MsgType jsonToMsgType(const QString& matrixType) { auto it = std::find_if(msgTypes.begin(), msgTypes.end(), - [=](const MsgTypeDesc& mtd) { return mtd.matrixType == matrixType; }); + [=](const MsgTypeDesc& mtd) { + return mtd.matrixType == matrixType; + }); if (it != msgTypes.end()) return it->enumType; return MsgType::Unknown; } +inline bool isReplacement(const Omittable<RelatesTo>& rel) +{ + return rel && rel->type == RelatesTo::ReplacementTypeId(); +} + QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, - const QString& jsonMsgType, TypedBase* content) + const QString& jsonMsgType, + TypedBase* content) { auto json = content ? content->toJson() : QJsonObject(); if (json.contains(RelatesToKeyL)) { @@ -107,9 +116,10 @@ QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, // After the above, we know for sure that the content is TextContent // and that its RelatesTo structure is not omitted auto* textContent = static_cast<const TextContent*>(content); + Q_ASSERT(textContent && textContent->relatesTo.has_value()); if (textContent->relatesTo->type == RelatesTo::ReplacementTypeId()) { auto newContentJson = json.take("m.new_content"_ls).toObject(); - newContentJson.insert(BodyKeyL, plainBody); + newContentJson.insert(BodyKey, plainBody); newContentJson.insert(MsgTypeKeyL, jsonMsgType); json.insert(QStringLiteral("m.new_content"), newContentJson); json[MsgTypeKeyL] = jsonMsgType; @@ -124,24 +134,24 @@ QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, } RoomMessageEvent::RoomMessageEvent(const QString& plainBody, - const QString& jsonMsgType, TypedBase* content) + const QString& jsonMsgType, + TypedBase* content) : RoomEvent(typeId(), matrixTypeId(), assembleContentJson(plainBody, jsonMsgType, content)) , _content(content) -{ } +{} -RoomMessageEvent::RoomMessageEvent(const QString& plainBody, - MsgType msgType, TypedBase* content) +RoomMessageEvent::RoomMessageEvent(const QString& plainBody, MsgType msgType, + TypedBase* content) : RoomMessageEvent(plainBody, msgTypeToJson(msgType), content) -{ } +{} TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) { auto filePath = file.absoluteFilePath(); auto localUrl = QUrl::fromLocalFile(filePath); auto mimeType = QMimeDatabase().mimeTypeForFile(file); - if (!asGenericFile) - { + if (!asGenericFile) { auto mimeTypeName = mimeType.name(); if (mimeTypeName.startsWith("image/")) return new ImageContent(localUrl, file.size(), mimeType, @@ -163,11 +173,12 @@ TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) } RoomMessageEvent::RoomMessageEvent(const QString& plainBody, - const QFileInfo& file, bool asGenericFile) + const QFileInfo& file, bool asGenericFile) : RoomMessageEvent(plainBody, - asGenericFile ? QStringLiteral("m.file") : rawMsgTypeForFile(file), - contentFromFile(file, asGenericFile)) -{ } + asGenericFile ? QStringLiteral("m.file") + : rawMsgTypeForFile(file), + contentFromFile(file, asGenericFile)) +{} RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj), _content(nullptr) @@ -175,26 +186,21 @@ RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) if (isRedacted()) return; const QJsonObject content = contentJson(); - if ( content.contains(MsgTypeKeyL) && content.contains(BodyKeyL) ) - { + if (content.contains(MsgTypeKeyL) && content.contains(BodyKeyL)) { auto msgtype = content[MsgTypeKeyL].toString(); bool msgTypeFound = false; - for (const auto& mt: msgTypes) - if (mt.matrixType == msgtype) - { + for (const auto& mt : msgTypes) + if (mt.matrixType == msgtype) { _content.reset(mt.maker(content)); msgTypeFound = true; } - if (!msgTypeFound) - { + if (!msgTypeFound) { qCWarning(EVENTS) << "RoomMessageEvent: unknown msg_type," << " full content dump follows"; qCWarning(EVENTS) << formatJson << content; } - } - else - { + } else { qCWarning(EVENTS) << "No body or msgtype in room message event"; qCWarning(EVENTS) << formatJson << obj; } @@ -218,15 +224,15 @@ QString RoomMessageEvent::plainBody() const QMimeType RoomMessageEvent::mimeType() const { static const auto PlainTextMimeType = - QMimeDatabase().mimeTypeForName("text/plain"); + QMimeDatabase().mimeTypeForName("text/plain"); return _content ? _content->type() : PlainTextMimeType; } bool RoomMessageEvent::hasTextContent() const { - return !content() || - (msgtype() == MsgType::Text || msgtype() == MsgType::Emote || - msgtype() == MsgType::Notice); + return !content() + || (msgtype() == MsgType::Text || msgtype() == MsgType::Emote + || msgtype() == MsgType::Notice); } bool RoomMessageEvent::hasFileContent() const @@ -245,17 +251,18 @@ QString RoomMessageEvent::replacedEvent() const return {}; const auto& rel = static_cast<const TextContent*>(content())->relatesTo; - return !rel.omitted() && rel->type == RelatesTo::ReplacementTypeId() - ? rel->eventId : QString(); + return isReplacement(rel) ? rel->eventId : QString(); } QString rawMsgTypeForMimeType(const QMimeType& mimeType) { auto name = mimeType.name(); - return name.startsWith("image/") ? QStringLiteral("m.image") : - name.startsWith("video/") ? QStringLiteral("m.video") : - name.startsWith("audio/") ? QStringLiteral("m.audio") : - QStringLiteral("m.file"); + return name.startsWith("image/") + ? QStringLiteral("m.image") + : name.startsWith("video/") + ? QStringLiteral("m.video") + : name.startsWith("audio/") ? QStringLiteral("m.audio") + : QStringLiteral("m.file"); } QString RoomMessageEvent::rawMsgTypeForUrl(const QUrl& url) @@ -268,18 +275,21 @@ QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi) return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForFile(fi)); } -TextContent::TextContent(const QString& text, const QString& contentType, +TextContent::TextContent(QString text, const QString& contentType, Omittable<RelatesTo> relatesTo) - : mimeType(QMimeDatabase().mimeTypeForName(contentType)), body(text) + : mimeType(QMimeDatabase().mimeTypeForName(contentType)) + , body(std::move(text)) , relatesTo(std::move(relatesTo)) { if (contentType == HtmlContentTypeId) mimeType = QMimeDatabase().mimeTypeForName("text/html"); } -namespace QMatrixClient -{ -Omittable<RelatesTo> relationFromJson(const QJsonValue& jv) +namespace Quotient { +// Overload the default fromJson<> logic that defined in converters.h +// as we want +template <> +Omittable<RelatesTo> fromJson(const QJsonValue& jv) { const auto jo = jv.toObject(); if (jo.isEmpty()) @@ -291,22 +301,21 @@ Omittable<RelatesTo> relationFromJson(const QJsonValue& jv) return RelatesTo { jo.value("rel_type"_ls).toString(), jo.value(EventIdKeyL).toString() }; } -} +} // namespace Quotient TextContent::TextContent(const QJsonObject& json) - : relatesTo(relationFromJson(json[RelatesToKeyL])) + : relatesTo(fromJson<Omittable<RelatesTo>>(json[RelatesToKeyL])) { QMimeDatabase db; static const auto PlainTextMimeType = db.mimeTypeForName("text/plain"); static const auto HtmlMimeType = db.mimeTypeForName("text/html"); - const auto actualJson = - relatesTo.omitted() || relatesTo->type != RelatesTo::ReplacementTypeId() - ? json : json.value("m.new_content"_ls).toObject(); + const auto actualJson = isReplacement(relatesTo) + ? json.value("m.new_content"_ls).toObject() + : json; // Special-casing the custom matrix.org's (actually, Riot's) way // of sending HTML messages. - if (actualJson["format"_ls].toString() == HtmlContentTypeId) - { + if (actualJson["format"_ls].toString() == HtmlContentTypeId) { mimeType = HtmlMimeType; body = actualJson[FormattedBodyKeyL].toString(); } else { @@ -320,22 +329,21 @@ TextContent::TextContent(const QJsonObject& json) void TextContent::fillJson(QJsonObject* json) const { static const auto FormatKey = QStringLiteral("format"); - static const auto RichBodyKey = QStringLiteral("formatted_body"); + static const auto FormattedBodyKey = QStringLiteral("formatted_body"); Q_ASSERT(json); - if (mimeType.inherits("text/html")) - { + if (mimeType.inherits("text/html")) { json->insert(FormatKey, HtmlContentTypeId); - json->insert(RichBodyKey, body); + json->insert(FormattedBodyKey, body); } - if (!relatesTo.omitted()) { + if (relatesTo) { json->insert(QStringLiteral("m.relates_to"), - QJsonObject { { relatesTo->type, relatesTo->eventId } }); + QJsonObject { { "rel_type", relatesTo->type }, { EventIdKey, relatesTo->eventId } }); if (relatesTo->type == RelatesTo::ReplacementTypeId()) { QJsonObject newContentJson; if (mimeType.inherits("text/html")) { json->insert(FormatKey, HtmlContentTypeId); - json->insert(RichBodyKey, body); + json->insert(FormattedBodyKey, body); } json->insert(QStringLiteral("m.new_content"), newContentJson); } @@ -345,13 +353,13 @@ void TextContent::fillJson(QJsonObject* json) const LocationContent::LocationContent(const QString& geoUri, const Thumbnail& thumbnail) : geoUri(geoUri), thumbnail(thumbnail) -{ } +{} LocationContent::LocationContent(const QJsonObject& json) : TypedBase(json) , geoUri(json["geo_uri"_ls].toString()) , thumbnail(json["info"_ls].toObject()) -{ } +{} QMimeType LocationContent::type() const { diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 7320e4ea..2501d097 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -13,211 +13,211 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once -#include "roomevent.h" #include "eventcontent.h" +#include "roomevent.h" class QFileInfo; -namespace QMatrixClient -{ - namespace MessageEventContent = EventContent; // Back-compatibility +namespace Quotient { +namespace MessageEventContent = EventContent; // Back-compatibility + +/** + * The event class corresponding to m.room.message events + */ +class RoomMessageEvent : public RoomEvent { + Q_GADGET + Q_PROPERTY(QString msgType READ rawMsgtype CONSTANT) + Q_PROPERTY(QString plainBody READ plainBody CONSTANT) + Q_PROPERTY(QMimeType mimeType READ mimeType STORED false CONSTANT) + Q_PROPERTY(const EventContent::TypedBase* content READ content CONSTANT) +public: + DEFINE_EVENT_TYPEID("m.room.message", RoomMessageEvent) + + enum class MsgType { + Text, + Emote, + Notice, + Image, + File, + Location, + Video, + Audio, + Unknown + }; + + RoomMessageEvent(const QString& plainBody, const QString& jsonMsgType, + EventContent::TypedBase* content = nullptr); + explicit RoomMessageEvent(const QString& plainBody, + MsgType msgType = MsgType::Text, + EventContent::TypedBase* content = nullptr); + explicit RoomMessageEvent(const QString& plainBody, const QFileInfo& file, + bool asGenericFile = false); + explicit RoomMessageEvent(const QJsonObject& obj); + + MsgType msgtype() const; + QString rawMsgtype() const; + QString plainBody() const; + const EventContent::TypedBase* content() const { return _content.data(); } + template <typename VisitorT> + void editContent(VisitorT&& visitor) + { + visitor(*_content); + editJson()[ContentKeyL] = assembleContentJson(plainBody(), rawMsgtype(), + _content.data()); + } + QMimeType mimeType() const; + bool hasTextContent() const; + bool hasFileContent() const; + bool hasThumbnail() const; + QString replacedEvent() const; + + static QString rawMsgTypeForUrl(const QUrl& url); + static QString rawMsgTypeForFile(const QFileInfo& fi); + +private: + QScopedPointer<EventContent::TypedBase> _content; + + // FIXME: should it really be static? + static QJsonObject assembleContentJson(const QString& plainBody, + const QString& jsonMsgType, + EventContent::TypedBase* content); + + Q_ENUM(MsgType) +}; +REGISTER_EVENT_TYPE(RoomMessageEvent) +using MessageEventType = RoomMessageEvent::MsgType; + +namespace EventContent { + // Additional event content types + + struct RelatesTo { + static constexpr const char* ReplyTypeId() { return "m.in_reply_to"; } + static constexpr const char* ReplacementTypeId() { return "m.replace"; } + QString type; // The only supported relation so far + QString eventId; + }; + inline RelatesTo replyTo(QString eventId) + { + return { RelatesTo::ReplyTypeId(), std::move(eventId) }; + } + inline RelatesTo replacementOf(QString eventId) + { + return { RelatesTo::ReplacementTypeId(), std::move(eventId) }; + } /** - * The event class corresponding to m.room.message events + * Rich text content for m.text, m.emote, m.notice + * + * Available fields: mimeType, body. The body can be either rich text + * or plain text, depending on what mimeType specifies. */ - class RoomMessageEvent: public RoomEvent - { - Q_GADGET - Q_PROPERTY(QString msgType READ rawMsgtype CONSTANT) - Q_PROPERTY(QString plainBody READ plainBody CONSTANT) - Q_PROPERTY(QMimeType mimeType READ mimeType STORED false CONSTANT) - Q_PROPERTY(EventContent::TypedBase* content READ content CONSTANT) - public: - DEFINE_EVENT_TYPEID("m.room.message", RoomMessageEvent) - - enum class MsgType - { - Text, Emote, Notice, Image, File, Location, Video, Audio, Unknown - }; - - RoomMessageEvent(const QString& plainBody, - const QString& jsonMsgType, - EventContent::TypedBase* content = nullptr); - explicit RoomMessageEvent(const QString& plainBody, - MsgType msgType = MsgType::Text, - EventContent::TypedBase* content = nullptr); - explicit RoomMessageEvent(const QString& plainBody, - const QFileInfo& file, - bool asGenericFile = false); - explicit RoomMessageEvent(const QJsonObject& obj); - - MsgType msgtype() const; - QString rawMsgtype() const; - QString plainBody() const; - EventContent::TypedBase* content() const - { return _content.data(); } - template <typename VisitorT> - void editContent(VisitorT visitor) - { - visitor(*_content); - editJson()[ContentKeyL] = - assembleContentJson(plainBody(), rawMsgtype(), content()); - } - QMimeType mimeType() const; - bool hasTextContent() const; - bool hasFileContent() const; - bool hasThumbnail() const; - QString replacedEvent() const; - - static QString rawMsgTypeForUrl(const QUrl& url); - static QString rawMsgTypeForFile(const QFileInfo& fi); - - private: - QScopedPointer<EventContent::TypedBase> _content; - - // FIXME: should it really be static? - static QJsonObject assembleContentJson(const QString& plainBody, - const QString& jsonMsgType, EventContent::TypedBase* content); - - REGISTER_ENUM(MsgType) + class TextContent : public TypedBase { + public: + TextContent(QString text, const QString& contentType, + Omittable<RelatesTo> relatesTo = none); + explicit TextContent(const QJsonObject& json); + + QMimeType type() const override { return mimeType; } + + QMimeType mimeType; + QString body; + Omittable<RelatesTo> relatesTo; + + protected: + void fillJson(QJsonObject* json) const override; }; - REGISTER_EVENT_TYPE(RoomMessageEvent) - DEFINE_EVENTTYPE_ALIAS(RoomMessage, RoomMessageEvent) - using MessageEventType = RoomMessageEvent::MsgType; - namespace EventContent - { - // Additional event content types + /** + * Content class for m.location + * + * Available fields: + * - corresponding to the top-level JSON: + * - geoUri ("geo_uri" in JSON) + * - corresponding to the "info" subobject: + * - thumbnail.url ("thumbnail_url" in JSON) + * - corresponding to the "info/thumbnail_info" subobject: + * - thumbnail.payloadSize + * - thumbnail.mimeType + * - thumbnail.imageSize + */ + class LocationContent : public TypedBase { + public: + LocationContent(const QString& geoUri, const Thumbnail& thumbnail = {}); + explicit LocationContent(const QJsonObject& json); - struct RelatesTo - { - static constexpr const char* ReplyTypeId() { return "m.in_reply_to"; } - static constexpr const char* ReplacementTypeId() { return "m.replace"; } - QString type; // The only supported relation so far - QString eventId; - }; - inline RelatesTo replyTo(QString eventId) + QMimeType type() const override; + + public: + QString geoUri; + Thumbnail thumbnail; + + protected: + void fillJson(QJsonObject* o) const override; + }; + + /** + * A base class for info types that include duration: audio and video + */ + template <typename ContentT> + class PlayableContent : public ContentT { + public: + using ContentT::ContentT; + PlayableContent(const QJsonObject& json) + : ContentT(json) + , duration(ContentT::originalInfoJson["duration"_ls].toInt()) + {} + + protected: + void fillJson(QJsonObject* json) const override { - return { RelatesTo::ReplyTypeId(), std::move(eventId) }; + ContentT::fillJson(json); + auto infoJson = json->take("info"_ls).toObject(); + infoJson.insert(QStringLiteral("duration"), duration); + json->insert(QStringLiteral("info"), infoJson); } - /** - * Rich text content for m.text, m.emote, m.notice - * - * Available fields: mimeType, body. The body can be either rich text - * or plain text, depending on what mimeType specifies. - */ - class TextContent: public TypedBase - { - public: - TextContent(const QString& text, const QString& contentType, - Omittable<RelatesTo> relatesTo = none); - explicit TextContent(const QJsonObject& json); - - QMimeType type() const override { return mimeType; } - - QMimeType mimeType; - QString body; - Omittable<RelatesTo> relatesTo; - - protected: - void fillJson(QJsonObject* json) const override; - }; - - /** - * Content class for m.location - * - * Available fields: - * - corresponding to the top-level JSON: - * - geoUri ("geo_uri" in JSON) - * - corresponding to the "info" subobject: - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: - * - thumbnail.payloadSize - * - thumbnail.mimeType - * - thumbnail.imageSize - */ - class LocationContent: public TypedBase - { - public: - LocationContent(const QString& geoUri, - const Thumbnail& thumbnail = {}); - explicit LocationContent(const QJsonObject& json); - - QMimeType type() const override; - - public: - QString geoUri; - Thumbnail thumbnail; - - protected: - void fillJson(QJsonObject* o) const override; - }; - - /** - * A base class for info types that include duration: audio and video - */ - template <typename ContentT> - class PlayableContent : public ContentT - { - public: - using ContentT::ContentT; - PlayableContent(const QJsonObject& json) - : ContentT(json) - , duration(ContentT::originalInfoJson["duration"_ls].toInt()) - { } - - protected: - void fillJson(QJsonObject* json) const override - { - ContentT::fillJson(json); - auto infoJson = json->take("info"_ls).toObject(); - infoJson.insert(QStringLiteral("duration"), duration); - json->insert(QStringLiteral("info"), infoJson); - } - - public: - int duration; - }; - - /** - * Content class for m.video - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename (extension to the CS API spec) - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - duration - * - imageSize (QSize for a combination of "h" and "w" in JSON) - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: contents of - * thumbnail field, in the same vein as for "info": - * - payloadSize - * - mimeType - * - imageSize - */ - using VideoContent = PlayableContent<UrlWithThumbnailContent<ImageInfo>>; - - /** - * Content class for m.audio - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename (extension to the CS API spec) - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - duration - */ - using AudioContent = PlayableContent<UrlBasedContent<FileInfo>>; - } // namespace EventContent -} // namespace QMatrixClient + public: + int duration; + }; + + /** + * Content class for m.video + * + * Available fields: + * - corresponding to the top-level JSON: + * - url + * - filename (extension to the CS API spec) + * - corresponding to the "info" subobject: + * - payloadSize ("size" in JSON) + * - mimeType ("mimetype" in JSON) + * - duration + * - imageSize (QSize for a combination of "h" and "w" in JSON) + * - thumbnail.url ("thumbnail_url" in JSON) + * - corresponding to the "info/thumbnail_info" subobject: contents of + * thumbnail field, in the same vein as for "info": + * - payloadSize + * - mimeType + * - imageSize + */ + using VideoContent = PlayableContent<UrlWithThumbnailContent<ImageInfo>>; + + /** + * Content class for m.audio + * + * Available fields: + * - corresponding to the top-level JSON: + * - url + * - filename (extension to the CS API spec) + * - corresponding to the "info" subobject: + * - payloadSize ("size" in JSON) + * - mimeType ("mimetype" in JSON) + * - duration + */ + using AudioContent = PlayableContent<UrlBasedContent<FileInfo>>; +} // namespace EventContent +} // namespace Quotient diff --git a/lib/events/roompowerlevelsevent.cpp b/lib/events/roompowerlevelsevent.cpp new file mode 100644 index 00000000..0a401752 --- /dev/null +++ b/lib/events/roompowerlevelsevent.cpp @@ -0,0 +1,62 @@ +#include "roompowerlevelsevent.h" + +#include <QJsonDocument> + +using namespace Quotient; + +PowerLevelsEventContent::PowerLevelsEventContent(const QJsonObject& json) : + invite(json["invite"_ls].toInt(50)), + kick(json["kick"_ls].toInt(50)), + ban(json["ban"_ls].toInt(50)), + redact(json["redact"_ls].toInt(50)), + events(fromJson<QHash<QString, int>>(json["events"_ls])), + eventsDefault(json["events_default"_ls].toInt(0)), + stateDefault(json["state_default"_ls].toInt(0)), + users(fromJson<QHash<QString, int>>(json["users"_ls])), + usersDefault(json["users_default"_ls].toInt(0)), + notifications(Notifications{json["notifications"_ls].toObject()["room"_ls].toInt(50)}) +{ +} + +void PowerLevelsEventContent::fillJson(QJsonObject* o) const { + o->insert(QStringLiteral("invite"), invite); + o->insert(QStringLiteral("kick"), kick); + o->insert(QStringLiteral("ban"), ban); + o->insert(QStringLiteral("redact"), redact); + o->insert(QStringLiteral("events"), Quotient::toJson(events)); + o->insert(QStringLiteral("events_default"), eventsDefault); + o->insert(QStringLiteral("state_default"), stateDefault); + o->insert(QStringLiteral("users"), Quotient::toJson(users)); + o->insert(QStringLiteral("users_default"), usersDefault); + o->insert(QStringLiteral("notifications"), QJsonObject{{"room", notifications.room}}); +} + +int RoomPowerLevelsEvent::powerLevelForEvent(const QString &eventId) const { + auto e = events(); + + if (e.contains(eventId)) { + return e[eventId]; + } + + return eventsDefault(); +} + +int RoomPowerLevelsEvent::powerLevelForState(const QString &eventId) const { + auto e = events(); + + if (e.contains(eventId)) { + return e[eventId]; + } + + return stateDefault(); +} + +int RoomPowerLevelsEvent::powerLevelForUser(const QString &userId) const { + auto u = users(); + + if (u.contains(userId)) { + return u[userId]; + } + + return usersDefault(); +} diff --git a/lib/events/roompowerlevelsevent.h b/lib/events/roompowerlevelsevent.h new file mode 100644 index 00000000..f0f7207f --- /dev/null +++ b/lib/events/roompowerlevelsevent.h @@ -0,0 +1,76 @@ +#pragma once + +#include "eventcontent.h" +#include "stateevent.h" + +namespace Quotient { +class PowerLevelsEventContent : public EventContent::Base { +public: + struct Notifications { + int room; + }; + + explicit PowerLevelsEventContent(const QJsonObject& json); + + int invite; + int kick; + int ban; + + int redact; + + QHash<QString, int> events; + int eventsDefault; + int stateDefault; + + QHash<QString, int> users; + int usersDefault; + + Notifications notifications; + +protected: + void fillJson(QJsonObject* o) const override; +}; + +class RoomPowerLevelsEvent : public StateEvent<PowerLevelsEventContent> { + Q_GADGET +public: + DEFINE_EVENT_TYPEID("m.room.power_levels", RoomPowerLevelsEvent) + + explicit RoomPowerLevelsEvent(const QJsonObject& obj) + : StateEvent(typeId(), obj) + {} + + int invite() const { return content().invite; } + int kick() const { return content().kick; } + int ban() const { return content().ban; } + + int redact() const { return content().redact; } + + QHash<QString, int> events() const { return content().events; } + int eventsDefault() const { return content().eventsDefault; } + int stateDefault() const { return content().stateDefault; } + + QHash<QString, int> users() const { return content().users; } + int usersDefault() const { return content().usersDefault; } + + int roomNotification() const { return content().notifications.room; } + + int powerLevelForEvent(const QString& eventId) const; + int powerLevelForState(const QString& eventId) const; + int powerLevelForUser(const QString& userId) const; + +private: +}; + +template <> +class EventFactory<RoomPowerLevelsEvent> { +public: + static event_ptr_tt<RoomPowerLevelsEvent> make(const QJsonObject& json, + const QString&) + { + return makeEvent<RoomPowerLevelsEvent>(json); + } +}; + +REGISTER_EVENT_TYPE(RoomPowerLevelsEvent) +} // namespace Quotient diff --git a/lib/events/roomtombstoneevent.cpp b/lib/events/roomtombstoneevent.cpp index 9c3bafd4..f93eb60d 100644 --- a/lib/events/roomtombstoneevent.cpp +++ b/lib/events/roomtombstoneevent.cpp @@ -1,24 +1,24 @@ /****************************************************************************** -* Copyright (C) 2019 QMatrixClient project -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ + * Copyright (C) 2019 QMatrixClient project + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ #include "roomtombstoneevent.h" -using namespace QMatrixClient; +using namespace Quotient; QString RoomTombstoneEvent::serverMessage() const { diff --git a/lib/events/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h index c7008ec4..2c2f0663 100644 --- a/lib/events/roomtombstoneevent.h +++ b/lib/events/roomtombstoneevent.h @@ -1,41 +1,37 @@ /****************************************************************************** -* Copyright (C) 2019 QMatrixClient project -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ + * Copyright (C) 2019 QMatrixClient project + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ #pragma once #include "stateevent.h" -namespace QMatrixClient -{ - class RoomTombstoneEvent : public StateEventBase - { - public: - DEFINE_EVENT_TYPEID("m.room.tombstone", RoomTombstoneEvent) +namespace Quotient { +class RoomTombstoneEvent : public StateEventBase { +public: + DEFINE_EVENT_TYPEID("m.room.tombstone", RoomTombstoneEvent) - explicit RoomTombstoneEvent() - : StateEventBase(typeId(), matrixTypeId()) - { } - explicit RoomTombstoneEvent(const QJsonObject& obj) - : StateEventBase(typeId(), obj) - { } + explicit RoomTombstoneEvent() : StateEventBase(typeId(), matrixTypeId()) {} + explicit RoomTombstoneEvent(const QJsonObject& obj) + : StateEventBase(typeId(), obj) + {} - QString serverMessage() const; - QString successorRoomId() const; - }; - REGISTER_EVENT_TYPE(RoomTombstoneEvent) -} + QString serverMessage() const; + QString successorRoomId() const; +}; +REGISTER_EVENT_TYPE(RoomTombstoneEvent) +} // namespace Quotient diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 2c23d9ca..cde5b0fd 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -13,80 +13,77 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "stateevent.h" -#include "converters.h" +namespace Quotient { +namespace EventContent { + template <typename T> + class SimpleContent { + public: + using value_type = T; -namespace QMatrixClient -{ - namespace EventContent - { - template <typename T> - class SimpleContent + // The constructor is templated to enable perfect forwarding + template <typename TT> + SimpleContent(QString keyName, TT&& value) + : value(std::forward<TT>(value)), key(std::move(keyName)) + {} + SimpleContent(const QJsonObject& json, QString keyName) + : value(fromJson<T>(json[keyName])), key(std::move(keyName)) + {} + QJsonObject toJson() const { - public: - using value_type = T; + return { { key, Quotient::toJson(value) } }; + } - // The constructor is templated to enable perfect forwarding - template <typename TT> - SimpleContent(QString keyName, TT&& value) - : value(std::forward<TT>(value)), key(std::move(keyName)) - { } - SimpleContent(const QJsonObject& json, QString keyName) - : value(fromJson<T>(json[keyName])) - , key(std::move(keyName)) - { } - QJsonObject toJson() const - { - return { { key, QMatrixClient::toJson(value) } }; - } + public: + T value; - public: - T value; + protected: + QString key; + }; +} // namespace EventContent - protected: - QString key; - }; - } // namespace EventContent - -#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \ - class _Name : public StateEvent<EventContent::SimpleContent<_ValueType>> \ - { \ - public: \ - using value_type = content_type::value_type; \ - DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name() : _Name(value_type()) { } \ - template <typename T> \ - explicit _Name(T&& value) \ - : StateEvent(typeId(), matrixTypeId(), \ - QStringLiteral(#_ContentKey), \ - std::forward<T>(value)) \ - { } \ - explicit _Name(QJsonObject obj) \ - : StateEvent(typeId(), std::move(obj), \ - QStringLiteral(#_ContentKey)) \ - { } \ - auto _ContentKey() const { return content().value; } \ - }; \ - REGISTER_EVENT_TYPE(_Name) \ +#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \ + class _Name : public StateEvent<EventContent::SimpleContent<_ValueType>> { \ + public: \ + using value_type = content_type::value_type; \ + DEFINE_EVENT_TYPEID(_TypeId, _Name) \ + explicit _Name() : _Name(value_type()) {} \ + template <typename T> \ + explicit _Name(T&& value) \ + : StateEvent(typeId(), matrixTypeId(), QString(), \ + QStringLiteral(#_ContentKey), std::forward<T>(value)) \ + {} \ + explicit _Name(QJsonObject obj) \ + : StateEvent(typeId(), std::move(obj), \ + QStringLiteral(#_ContentKey)) \ + {} \ + auto _ContentKey() const { return content().value; } \ + }; \ + REGISTER_EVENT_TYPE(_Name) \ // End of macro - DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name) - DEFINE_EVENTTYPE_ALIAS(RoomName, RoomNameEvent) - DEFINE_SIMPLE_STATE_EVENT(RoomAliasesEvent, "m.room.aliases", - QStringList, aliases) - DEFINE_EVENTTYPE_ALIAS(RoomAliases, RoomAliasesEvent) - DEFINE_SIMPLE_STATE_EVENT(RoomCanonicalAliasEvent, "m.room.canonical_alias", - QString, alias) - DEFINE_EVENTTYPE_ALIAS(RoomCanonicalAlias, RoomCanonicalAliasEvent) - DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic) - DEFINE_EVENTTYPE_ALIAS(RoomTopic, RoomTopicEvent) - DEFINE_SIMPLE_STATE_EVENT(EncryptionEvent, "m.room.encryption", - QString, algorithm) - DEFINE_EVENTTYPE_ALIAS(RoomEncryption, EncryptionEvent) -} // namespace QMatrixClient +DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name) +DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic) + +class RoomAliasesEvent + : public StateEvent<EventContent::SimpleContent<QStringList>> { +public: + DEFINE_EVENT_TYPEID("m.room.aliases", RoomAliasesEvent) + explicit RoomAliasesEvent(const QJsonObject& obj) + : StateEvent(typeId(), obj, QStringLiteral("aliases")) + {} + RoomAliasesEvent(const QString& server, const QStringList& aliases) + : StateEvent(typeId(), matrixTypeId(), server, + QStringLiteral("aliases"), aliases) + {} + QString server() const { return stateKey(); } + QStringList aliases() const { return content().value; } +}; +REGISTER_EVENT_TYPE(RoomAliasesEvent) +} // namespace Quotient diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index a84f302b..5909e8a6 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -1,33 +1,32 @@ /****************************************************************************** -* Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ + * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ #include "stateevent.h" -using namespace QMatrixClient; +using namespace Quotient; // Aside from the normal factory to instantiate StateEventBase inheritors // StateEventBase itself can be instantiated if there's a state_key JSON key // but the event type is unknown. -[[gnu::unused]] static auto stateEventTypeInitialised = +[[maybe_unused]] static auto stateEventTypeInitialised = RoomEvent::factory_t::addMethod( - [] (const QJsonObject& json, const QString& matrixType) -> StateEventPtr - { - if (!json.contains("state_key"_ls)) + [](const QJsonObject& json, const QString& matrixType) -> StateEventPtr { + if (!json.contains(StateKeyKeyL)) return nullptr; if (auto e = StateEventBase::factory_t::make(json, matrixType)) @@ -36,6 +35,12 @@ using namespace QMatrixClient; return makeEvent<StateEventBase>(unknownEventTypeId(), json); }); +StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, + const QString& stateKey, + const QJsonObject& contentJson) + : RoomEvent(type, basicStateEventJson(matrixType, contentJson, stateKey)) +{} + bool StateEventBase::repeatsState() const { const auto prevContentJson = unsignedJson().value(PrevContentKeyL); @@ -53,6 +58,7 @@ void StateEventBase::dumpTo(QDebug dbg) const dbg << '<' << stateKey() << "> "; if (unsignedJson().contains(PrevContentKeyL)) dbg << QJsonDocument(unsignedJson()[PrevContentKeyL].toObject()) - .toJson(QJsonDocument::Compact) << " -> "; + .toJson(QJsonDocument::Compact) + << " -> "; RoomEvent::dumpTo(dbg); } diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 3f54f7bf..710b4271 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -1,121 +1,131 @@ /****************************************************************************** -* Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ + * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ #pragma once #include "roomevent.h" -namespace QMatrixClient { - class StateEventBase: public RoomEvent - { - public: - using factory_t = EventFactory<StateEventBase>; +namespace Quotient { + +/// Make a minimal correct Matrix state event JSON +template <typename StrT> +inline QJsonObject basicStateEventJson(StrT matrixType, + const QJsonObject& content, + const QString& stateKey = {}) +{ + return { { TypeKey, std::forward<StrT>(matrixType) }, + { StateKeyKey, stateKey }, + { ContentKey, content } }; +} + +class StateEventBase : public RoomEvent { +public: + using factory_t = EventFactory<StateEventBase>; - using RoomEvent::RoomEvent; - ~StateEventBase() override = default; + StateEventBase(Type type, const QJsonObject& json) : RoomEvent(type, json) + {} + StateEventBase(Type type, event_mtype_t matrixType, + const QString& stateKey = {}, + const QJsonObject& contentJson = {}); + ~StateEventBase() override = default; - bool isStateEvent() const override { return true; } - QString replacedState() const; - void dumpTo(QDebug dbg) const override; + bool isStateEvent() const override { return true; } + QString replacedState() const; + void dumpTo(QDebug dbg) const override; + + virtual bool repeatsState() const; +}; +using StateEventPtr = event_ptr_tt<StateEventBase>; +using StateEvents = EventsArray<StateEventBase>; + +template <> +inline bool is<StateEventBase>(const Event& e) +{ + return e.isStateEvent(); +} - virtual bool repeatsState() const; - }; - using StateEventPtr = event_ptr_tt<StateEventBase>; - using StateEvents = EventsArray<StateEventBase>; +/** + * A combination of event type and state key uniquely identifies a piece + * of state in Matrix. + * \sa + * https://matrix.org/docs/spec/client_server/unstable.html#types-of-room-events + */ +using StateEventKey = QPair<QString, QString>; - template <> - inline bool is<StateEventBase>(const Event& e) { return e.isStateEvent(); } +template <typename ContentT> +struct Prev { + template <typename... ContentParamTs> + explicit Prev(const QJsonObject& unsignedJson, + ContentParamTs&&... contentParams) + : senderId(unsignedJson.value("prev_sender"_ls).toString()) + , content(unsignedJson.value(PrevContentKeyL).toObject(), + std::forward<ContentParamTs>(contentParams)...) + {} - /** - * A combination of event type and state key uniquely identifies a piece - * of state in Matrix. - * \sa https://matrix.org/docs/spec/client_server/unstable.html#types-of-room-events - */ - using StateEventKey = QPair<QString, QString>; + QString senderId; + ContentT content; +}; - template <typename ContentT> - struct Prev +template <typename ContentT> +class StateEvent : public StateEventBase { +public: + using content_type = ContentT; + + template <typename... ContentParamTs> + explicit StateEvent(Type type, const QJsonObject& fullJson, + ContentParamTs&&... contentParams) + : StateEventBase(type, fullJson) + , _content(contentJson(), std::forward<ContentParamTs>(contentParams)...) { - template <typename... ContentParamTs> - explicit Prev(const QJsonObject& unsignedJson, - ContentParamTs&&... contentParams) - : senderId(unsignedJson.value("prev_sender"_ls).toString()) - , content(unsignedJson.value(PrevContentKeyL).toObject(), - std::forward<ContentParamTs>(contentParams)...) - { } - - QString senderId; - ContentT content; - }; - - template <typename ContentT> - class StateEvent: public StateEventBase + const auto& unsignedData = unsignedJson(); + if (unsignedData.contains(PrevContentKeyL)) + _prev = std::make_unique<Prev<ContentT>>( + unsignedData, std::forward<ContentParamTs>(contentParams)...); + } + template <typename... ContentParamTs> + explicit StateEvent(Type type, event_mtype_t matrixType, + const QString& stateKey, + ContentParamTs&&... contentParams) + : StateEventBase(type, matrixType, stateKey) + , _content(std::forward<ContentParamTs>(contentParams)...) { - public: - using content_type = ContentT; - - template <typename... ContentParamTs> - explicit StateEvent(Type type, const QJsonObject& fullJson, - ContentParamTs&&... contentParams) - : StateEventBase(type, fullJson) - , _content(contentJson(), - std::forward<ContentParamTs>(contentParams)...) - { - const auto& unsignedData = unsignedJson(); - if (unsignedData.contains(PrevContentKeyL)) - _prev = std::make_unique<Prev<ContentT>>(unsignedData, - std::forward<ContentParamTs>(contentParams)...); - } - template <typename... ContentParamTs> - explicit StateEvent(Type type, event_mtype_t matrixType, - ContentParamTs&&... contentParams) - : StateEventBase(type, matrixType) - , _content(std::forward<ContentParamTs>(contentParams)...) - { - editJson().insert(ContentKey, _content.toJson()); - } - - const ContentT& content() const { return _content; } - template <typename VisitorT> - void editContent(VisitorT&& visitor) - { - visitor(_content); - editJson()[ContentKeyL] = _content.toJson(); - } - [[deprecated("Use prevContent instead")]] - const ContentT* prev_content() const { return prevContent(); } - const ContentT* prevContent() const - { return _prev ? &_prev->content : nullptr; } - QString prevSenderId() const - { return _prev ? _prev->senderId : QString(); } - - private: - ContentT _content; - std::unique_ptr<Prev<ContentT>> _prev; - }; -} // namespace QMatrixClient - -namespace std { - template <> struct hash<QMatrixClient::StateEventKey> + editJson().insert(ContentKey, _content.toJson()); + } + + const ContentT& content() const { return _content; } + template <typename VisitorT> + void editContent(VisitorT&& visitor) { - size_t operator()(const QMatrixClient::StateEventKey& k) const Q_DECL_NOEXCEPT - { - return qHash(k); - } - }; -} + visitor(_content); + editJson()[ContentKeyL] = _content.toJson(); + } + [[deprecated("Use prevContent instead")]] const ContentT* prev_content() const + { + return prevContent(); + } + const ContentT* prevContent() const + { + return _prev ? &_prev->content : nullptr; + } + QString prevSenderId() const { return _prev ? _prev->senderId : QString(); } + +private: + ContentT _content; + std::unique_ptr<Prev<ContentT>> _prev; +}; +} // namespace Quotient diff --git a/lib/events/typingevent.cpp b/lib/events/typingevent.cpp index 0d39d1be..a95d2f0d 100644 --- a/lib/events/typingevent.cpp +++ b/lib/events/typingevent.cpp @@ -13,20 +13,19 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "typingevent.h" #include <QtCore/QJsonArray> -using namespace QMatrixClient; +using namespace Quotient; -TypingEvent::TypingEvent(const QJsonObject& obj) - : Event(typeId(), obj) +TypingEvent::TypingEvent(const QJsonObject& obj) : Event(typeId(), obj) { const auto& array = contentJson()["user_ids"_ls].toArray(); - for(const auto& user: array ) + _users.reserve(array.size()); + for (const auto& user : array) _users.push_back(user.toString()); } - diff --git a/lib/events/typingevent.h b/lib/events/typingevent.h index 27b668b4..1cf4e69d 100644 --- a/lib/events/typingevent.h +++ b/lib/events/typingevent.h @@ -13,27 +13,24 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "event.h" -namespace QMatrixClient -{ - class TypingEvent: public Event - { - public: - DEFINE_EVENT_TYPEID("m.typing", TypingEvent) +namespace Quotient { +class TypingEvent : public Event { +public: + DEFINE_EVENT_TYPEID("m.typing", TypingEvent) - TypingEvent(const QJsonObject& obj); + TypingEvent(const QJsonObject& obj); - const QStringList& users() const { return _users; } + const QStringList& users() const { return _users; } - private: - QStringList _users; - }; - REGISTER_EVENT_TYPE(TypingEvent) - DEFINE_EVENTTYPE_ALIAS(Typing, TypingEvent) -} // namespace QMatrixClient +private: + QStringList _users; +}; +REGISTER_EVENT_TYPE(TypingEvent) +} // namespace Quotient diff --git a/lib/identity/definitions/request_email_validation.cpp b/lib/identity/definitions/request_email_validation.cpp deleted file mode 100644 index 47463a8b..00000000 --- a/lib/identity/definitions/request_email_validation.cpp +++ /dev/null @@ -1,26 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "request_email_validation.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<RequestEmailValidation>::dumpTo( - QJsonObject& jo, const RequestEmailValidation& pod) -{ - addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); - addParam<>(jo, QStringLiteral("email"), pod.email); - addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt); - addParam<IfNotEmpty>(jo, QStringLiteral("next_link"), pod.nextLink); -} - -void JsonObjectConverter<RequestEmailValidation>::fillFrom( - const QJsonObject& jo, RequestEmailValidation& result) -{ - fromJson(jo.value("client_secret"_ls), result.clientSecret); - fromJson(jo.value("email"_ls), result.email); - fromJson(jo.value("send_attempt"_ls), result.sendAttempt); - fromJson(jo.value("next_link"_ls), result.nextLink); -} - diff --git a/lib/identity/definitions/request_email_validation.h b/lib/identity/definitions/request_email_validation.h index eb7d8ed6..079da953 100644 --- a/lib/identity/definitions/request_email_validation.h +++ b/lib/identity/definitions/request_email_validation.h @@ -6,37 +6,50 @@ #include "converters.h" -#include "converters.h" +namespace Quotient { + +struct RequestEmailValidation { + /// A unique string generated by the client, and used to identify the + /// validation attempt. It must be a string consisting of the characters + /// ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it + /// must not be empty. + QString clientSecret; + + /// The email address to validate. + QString email; + + /// The server will only send an email if the ``send_attempt`` + /// is a number greater than the most recent one which it has seen, + /// scoped to that ``email`` + ``client_secret`` pair. This is to + /// avoid repeatedly sending the same email in the case of request + /// retries between the POSTing user and the identity server. + /// The client should increment this value if they desire a new + /// email (e.g. a reminder) to be sent. If they do not, the server + /// should respond with success but not resend the email. + int sendAttempt; -namespace QMatrixClient -{ - // Data structures + /// Optional. When the validation is completed, the identity server will + /// redirect the user to this URL. This option is ignored when submitting + /// 3PID validation information through a POST request. + QString nextLink; +}; - struct RequestEmailValidation +template <> +struct JsonObjectConverter<RequestEmailValidation> { + static void dumpTo(QJsonObject& jo, const RequestEmailValidation& pod) { - /// A unique string generated by the client, and used to identify the - /// validation attempt. It must be a string consisting of the characters - /// ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it - /// must not be empty. - QString clientSecret; - /// The email address to validate. - QString email; - /// The server will only send an email if the ``send_attempt`` - /// is a number greater than the most recent one which it has seen, - /// scoped to that ``email`` + ``client_secret`` pair. This is to - /// avoid repeatedly sending the same email in the case of request - /// retries between the POSTing user and the identity server. - /// The client should increment this value if they desire a new - /// email (e.g. a reminder) to be sent. - int sendAttempt; - /// Optional. When the validation is completed, the identity - /// server will redirect the user to this URL. - QString nextLink; - }; - template <> struct JsonObjectConverter<RequestEmailValidation> + addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); + addParam<>(jo, QStringLiteral("email"), pod.email); + addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt); + addParam<IfNotEmpty>(jo, QStringLiteral("next_link"), pod.nextLink); + } + static void fillFrom(const QJsonObject& jo, RequestEmailValidation& pod) { - static void dumpTo(QJsonObject& jo, const RequestEmailValidation& pod); - static void fillFrom(const QJsonObject& jo, RequestEmailValidation& pod); - }; + fromJson(jo.value("client_secret"_ls), pod.clientSecret); + fromJson(jo.value("email"_ls), pod.email); + fromJson(jo.value("send_attempt"_ls), pod.sendAttempt); + fromJson(jo.value("next_link"_ls), pod.nextLink); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/identity/definitions/request_msisdn_validation.cpp b/lib/identity/definitions/request_msisdn_validation.cpp deleted file mode 100644 index a123d326..00000000 --- a/lib/identity/definitions/request_msisdn_validation.cpp +++ /dev/null @@ -1,28 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "request_msisdn_validation.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<RequestMsisdnValidation>::dumpTo( - QJsonObject& jo, const RequestMsisdnValidation& pod) -{ - addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); - addParam<>(jo, QStringLiteral("country"), pod.country); - addParam<>(jo, QStringLiteral("phone_number"), pod.phoneNumber); - addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt); - addParam<IfNotEmpty>(jo, QStringLiteral("next_link"), pod.nextLink); -} - -void JsonObjectConverter<RequestMsisdnValidation>::fillFrom( - const QJsonObject& jo, RequestMsisdnValidation& result) -{ - fromJson(jo.value("client_secret"_ls), result.clientSecret); - fromJson(jo.value("country"_ls), result.country); - fromJson(jo.value("phone_number"_ls), result.phoneNumber); - fromJson(jo.value("send_attempt"_ls), result.sendAttempt); - fromJson(jo.value("next_link"_ls), result.nextLink); -} - diff --git a/lib/identity/definitions/request_msisdn_validation.h b/lib/identity/definitions/request_msisdn_validation.h index b48ed6d5..a29fd0de 100644 --- a/lib/identity/definitions/request_msisdn_validation.h +++ b/lib/identity/definitions/request_msisdn_validation.h @@ -6,40 +6,55 @@ #include "converters.h" -#include "converters.h" +namespace Quotient { + +struct RequestMsisdnValidation { + /// A unique string generated by the client, and used to identify the + /// validation attempt. It must be a string consisting of the characters + /// ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it + /// must not be empty. + QString clientSecret; + + /// The two-letter uppercase ISO-3166-1 alpha-2 country code that the + /// number in ``phone_number`` should be parsed as if it were dialled from. + QString country; + + /// The phone number to validate. + QString phoneNumber; + + /// The server will only send an SMS if the ``send_attempt`` is a + /// number greater than the most recent one which it has seen, + /// scoped to that ``country`` + ``phone_number`` + ``client_secret`` + /// triple. This is to avoid repeatedly sending the same SMS in + /// the case of request retries between the POSTing user and the + /// identity server. The client should increment this value if + /// they desire a new SMS (e.g. a reminder) to be sent. + int sendAttempt; -namespace QMatrixClient -{ - // Data structures + /// Optional. When the validation is completed, the identity server will + /// redirect the user to this URL. This option is ignored when submitting + /// 3PID validation information through a POST request. + QString nextLink; +}; - struct RequestMsisdnValidation +template <> +struct JsonObjectConverter<RequestMsisdnValidation> { + static void dumpTo(QJsonObject& jo, const RequestMsisdnValidation& pod) { - /// A unique string generated by the client, and used to identify the - /// validation attempt. It must be a string consisting of the characters - /// ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it - /// must not be empty. - QString clientSecret; - /// The two-letter uppercase ISO country code that the number in - /// ``phone_number`` should be parsed as if it were dialled from. - QString country; - /// The phone number to validate. - QString phoneNumber; - /// The server will only send an SMS if the ``send_attempt`` is a - /// number greater than the most recent one which it has seen, - /// scoped to that ``country`` + ``phone_number`` + ``client_secret`` - /// triple. This is to avoid repeatedly sending the same SMS in - /// the case of request retries between the POSTing user and the - /// identity server. The client should increment this value if - /// they desire a new SMS (e.g. a reminder) to be sent. - int sendAttempt; - /// Optional. When the validation is completed, the identity - /// server will redirect the user to this URL. - QString nextLink; - }; - template <> struct JsonObjectConverter<RequestMsisdnValidation> + addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); + addParam<>(jo, QStringLiteral("country"), pod.country); + addParam<>(jo, QStringLiteral("phone_number"), pod.phoneNumber); + addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt); + addParam<IfNotEmpty>(jo, QStringLiteral("next_link"), pod.nextLink); + } + static void fillFrom(const QJsonObject& jo, RequestMsisdnValidation& pod) { - static void dumpTo(QJsonObject& jo, const RequestMsisdnValidation& pod); - static void fillFrom(const QJsonObject& jo, RequestMsisdnValidation& pod); - }; + fromJson(jo.value("client_secret"_ls), pod.clientSecret); + fromJson(jo.value("country"_ls), pod.country); + fromJson(jo.value("phone_number"_ls), pod.phoneNumber); + fromJson(jo.value("send_attempt"_ls), pod.sendAttempt); + fromJson(jo.value("next_link"_ls), pod.nextLink); + } +}; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/identity/definitions/sid.cpp b/lib/identity/definitions/sid.cpp deleted file mode 100644 index 1ba4b3b5..00000000 --- a/lib/identity/definitions/sid.cpp +++ /dev/null @@ -1,20 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#include "sid.h" - -using namespace QMatrixClient; - -void JsonObjectConverter<Sid>::dumpTo( - QJsonObject& jo, const Sid& pod) -{ - addParam<>(jo, QStringLiteral("sid"), pod.sid); -} - -void JsonObjectConverter<Sid>::fillFrom( - const QJsonObject& jo, Sid& result) -{ - fromJson(jo.value("sid"_ls), result.sid); -} - diff --git a/lib/identity/definitions/sid.h b/lib/identity/definitions/sid.h deleted file mode 100644 index ac8c4130..00000000 --- a/lib/identity/definitions/sid.h +++ /dev/null @@ -1,28 +0,0 @@ -/****************************************************************************** - * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN - */ - -#pragma once - -#include "converters.h" - - -namespace QMatrixClient -{ - // Data structures - - struct Sid - { - /// The session ID. Session IDs are opaque strings generated by the identity - /// server. They must consist entirely of the characters - /// ``[0-9a-zA-Z.=_-]``. Their length must not exceed 255 characters and they - /// must not be empty. - QString sid; - }; - template <> struct JsonObjectConverter<Sid> - { - static void dumpTo(QJsonObject& jo, const Sid& pod); - static void fillFrom(const QJsonObject& jo, Sid& pod); - }; - -} // namespace QMatrixClient diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 0e6a8403..5960203d 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -13,92 +13,195 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "basejob.h" #include "connectiondata.h" -#include "util.h" +#include <QtCore/QRegularExpression> +#include <QtCore/QTimer> +#include <QtCore/QStringBuilder> +#include <QtCore/QMetaEnum> +#include <QtCore/QPointer> #include <QtNetwork/QNetworkAccessManager> -#include <QtNetwork/QNetworkRequest> #include <QtNetwork/QNetworkReply> -#include <QtCore/QTimer> -#include <QtCore/QRegularExpression> -#include <QtCore/QJsonObject> +#include <QtNetwork/QNetworkRequest> #include <array> -using namespace QMatrixClient; +using namespace Quotient; +using std::chrono::seconds, std::chrono::milliseconds; +using namespace std::chrono_literals; + +BaseJob::StatusCode BaseJob::Status::fromHttpCode(int httpCode) +{ + // Based on https://en.wikipedia.org/wiki/List_of_HTTP_status_codes + if (httpCode / 10 == 41) // 41x errors + return httpCode == 410 ? IncorrectRequestError : NotFoundError; + switch (httpCode) { + case 401: + return Unauthorised; + // clang-format off + case 403: case 407: // clang-format on + return ContentAccessError; + case 404: + return NotFoundError; + // clang-format off + case 400: case 405: case 406: case 426: case 428: case 505: // clang-format on + case 494: // Unofficial nginx "Request header too large" + case 497: // Unofficial nginx "HTTP request sent to HTTPS port" + return IncorrectRequestError; + case 429: + return TooManyRequestsError; + case 501: + case 510: + return RequestNotImplementedError; + case 511: + return NetworkAuthRequiredError; + default: + return NetworkError; + } +} + +QDebug BaseJob::Status::dumpToLog(QDebug dbg) const +{ + QDebugStateSaver _s(dbg); + dbg.noquote().nospace(); + if (auto* const k = QMetaEnum::fromType<StatusCode>().valueToKey(code)) { + const QByteArray b = k; + dbg << b.mid(b.lastIndexOf(':')); + } else + dbg << code; + return dbg << ": " << message; +} -struct NetworkReplyDeleter : public QScopedPointerDeleteLater +template <typename... Ts> +constexpr auto make_array(Ts&&... items) { - static inline void cleanup(QNetworkReply* reply) + return std::array<std::common_type_t<Ts...>, sizeof...(Ts)>({items...}); +} + +class BaseJob::Private { +public: + struct JobTimeoutConfig { + seconds jobTimeout; + seconds nextRetryInterval; + }; + + // Using an idiom from clang-tidy: + // http://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html + Private(HttpVerb v, QString endpoint, const QUrlQuery& q, Data&& data, + bool nt) + : verb(v) + , apiEndpoint(std::move(endpoint)) + , requestQuery(q) + , requestData(std::move(data)) + , needsToken(nt) { - if (reply && reply->isRunning()) - reply->abort(); - QScopedPointerDeleteLater::cleanup(reply); + timer.setSingleShot(true); + retryTimer.setSingleShot(true); + } + + ~Private() + { + if (reply) { + if (reply->isRunning()) { + reply->abort(); + } + delete reply; + } + } + + void sendRequest(); + /*! \brief Parse the response byte array into JSON + * + * This calls QJsonDocument::fromJson() on rawResponse, converts + * the QJsonParseError result to BaseJob::Status and stores the resulting + * JSON in jsonResponse. + */ + Status parseJson(); + + ConnectionData* connection = nullptr; + + // Contents for the network request + HttpVerb verb; + QString apiEndpoint; + QHash<QByteArray, QByteArray> requestHeaders; + QUrlQuery requestQuery; + Data requestData; + bool needsToken; + + bool inBackground = false; + + // There's no use of QMimeType here because we don't want to match + // content types against the known MIME type hierarchy; and at the same + // type QMimeType is of little help with MIME type globs (`text/*` etc.) + QByteArrayList expectedContentTypes { "application/json" }; + + QByteArrayList expectedKeys; + + // When the QNetworkAccessManager is destroyed it destroys all pending replies. + // Using QPointer allows us to know when that happend. + QPointer<QNetworkReply> reply; + + Status status = Unprepared; + QByteArray rawResponse; + /// Contains a null document in case of non-JSON body (for a successful + /// or unsuccessful response); a document with QJsonObject or QJsonArray + /// in case of a successful response with JSON payload, as per the API + /// definition (including an empty JSON object - QJsonObject{}); + /// and QJsonObject in case of an API error. + QJsonDocument jsonResponse; + QUrl errorUrl; //< May contain a URL to help with some errors + + LoggingCategory logCat = JOBS; + + QTimer timer; + QTimer retryTimer; + + static constexpr std::array<const JobTimeoutConfig, 3> errorStrategy { + { { 90s, 5s }, { 90s, 10s }, { 120s, 30s } } + }; + int maxRetries = int(errorStrategy.size()); + int retriesTaken = 0; + + [[nodiscard]] const JobTimeoutConfig& getCurrentTimeoutConfig() const + { + return errorStrategy[std::min(size_t(retriesTaken), + errorStrategy.size() - 1)]; } -}; -class BaseJob::Private -{ - public: - // Using an idiom from clang-tidy: - // http://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html - Private(HttpVerb v, QString endpoint, const QUrlQuery& q, - Data&& data, bool nt) - : verb(v), apiEndpoint(std::move(endpoint)), requestQuery(q) - , requestData(std::move(data)), needsToken(nt) - { } - - void sendRequest(bool inBackground); - const JobTimeoutConfig& getCurrentTimeoutConfig() const; - - const ConnectionData* connection = nullptr; - - // Contents for the network request - HttpVerb verb; - QString apiEndpoint; - QHash<QByteArray, QByteArray> requestHeaders; - QUrlQuery requestQuery; - Data requestData; - bool needsToken; - - // There's no use of QMimeType here because we don't want to match - // content types against the known MIME type hierarchy; and at the same - // type QMimeType is of little help with MIME type globs (`text/*` etc.) - QByteArrayList expectedContentTypes; - - QScopedPointer<QNetworkReply, NetworkReplyDeleter> reply; - Status status = Pending; - QByteArray rawResponse; - QUrl errorUrl; //< May contain a URL to help with some errors - - QTimer timer; - QTimer retryTimer; - - QVector<JobTimeoutConfig> errorStrategy = - { { 90, 5 }, { 90, 10 }, { 120, 30 } }; - int maxRetries = errorStrategy.size(); - int retriesTaken = 0; - - LoggingCategory logCat = JOBS; + [[nodiscard]] QString dumpRequest() const + { + // FIXME: use std::array {} when Apple stdlib gets deduction guides for it + static const auto verbs = + make_array(QStringLiteral("GET"), QStringLiteral("PUT"), + QStringLiteral("POST"), QStringLiteral("DELETE")); + const auto verbWord = verbs.at(size_t(verb)); + return verbWord % ' ' + % (reply ? reply->url().toString(QUrl::RemoveQuery) + : makeRequestUrl(connection->baseUrl(), apiEndpoint) + .toString()); + } }; -BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, bool needsToken) - : BaseJob(verb, name, endpoint, Query { }, Data { }, needsToken) -{ } +BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, + bool needsToken) + : BaseJob(verb, name, endpoint, Query {}, Data {}, needsToken) +{} BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, const Query& query, Data&& data, bool needsToken) : d(new Private(verb, endpoint, query, std::move(data), needsToken)) { setObjectName(name); - setExpectedContentTypes({ "application/json" }); - d->timer.setSingleShot(true); - connect (&d->timer, &QTimer::timeout, this, &BaseJob::timeout); + connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout); + connect(&d->retryTimer, &QTimer::timeout, this, [this] { + qCDebug(d->logCat) << "Retrying" << this; + d->connection->submit(this); + }); } BaseJob::~BaseJob() @@ -108,28 +211,18 @@ BaseJob::~BaseJob() qCDebug(d->logCat) << this << "destroyed"; } -QUrl BaseJob::requestUrl() const -{ - return d->reply ? d->reply->request().url() : QUrl(); -} +QUrl BaseJob::requestUrl() const { return d->reply ? d->reply->url() : QUrl(); } -bool BaseJob::isBackground() const -{ - return d->reply && d->reply->request().attribute( - QNetworkRequest::BackgroundRequestAttribute).toBool(); -} +bool BaseJob::isBackground() const { return d->inBackground; } -const QString& BaseJob::apiEndpoint() const -{ - return d->apiEndpoint; -} +const QString& BaseJob::apiEndpoint() const { return d->apiEndpoint; } void BaseJob::setApiEndpoint(const QString& apiEndpoint) { d->apiEndpoint = apiEndpoint; } -const BaseJob::headers_t&BaseJob::requestHeaders() const +const BaseJob::headers_t& BaseJob::requestHeaders() const { return d->requestHeaders; } @@ -145,25 +238,16 @@ void BaseJob::setRequestHeaders(const BaseJob::headers_t& headers) d->requestHeaders = headers; } -const QUrlQuery& BaseJob::query() const -{ - return d->requestQuery; -} +const QUrlQuery& BaseJob::query() const { return d->requestQuery; } void BaseJob::setRequestQuery(const QUrlQuery& query) { d->requestQuery = query; } -const BaseJob::Data& BaseJob::requestData() const -{ - return d->requestData; -} +const BaseJob::Data& BaseJob::requestData() const { return d->requestData; } -void BaseJob::setRequestData(Data&& data) -{ - std::swap(d->requestData, data); -} +void BaseJob::setRequestData(Data&& data) { std::swap(d->requestData, data); } const QByteArrayList& BaseJob::expectedContentTypes() const { @@ -180,182 +264,195 @@ void BaseJob::setExpectedContentTypes(const QByteArrayList& contentTypes) d->expectedContentTypes = contentTypes; } -QUrl BaseJob::makeRequestUrl(QUrl baseUrl, - const QString& path, const QUrlQuery& query) +const QByteArrayList BaseJob::expectedKeys() const { return d->expectedKeys; } + +void BaseJob::addExpectedKey(const QByteArray& key) { d->expectedKeys << key; } + +void BaseJob::setExpectedKeys(const QByteArrayList& keys) +{ + d->expectedKeys = keys; +} + +const QNetworkReply* BaseJob::reply() const { return d->reply.data(); } + +QNetworkReply* BaseJob::reply() { return d->reply.data(); } + +QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QString& path, + const QUrlQuery& query) { auto pathBase = baseUrl.path(); - if (!pathBase.endsWith('/') && !path.startsWith('/')) - pathBase.push_back('/'); + // QUrl::adjusted(QUrl::StripTrailingSlashes) doesn't help with root '/' + while (pathBase.endsWith('/')) + pathBase.chop(1); + if (!path.startsWith('/')) // Normally API files do start with '/' + pathBase.push_back('/'); // so this shouldn't be needed these days baseUrl.setPath(pathBase + path, QUrl::TolerantMode); baseUrl.setQuery(query); return baseUrl; } -void BaseJob::Private::sendRequest(bool inBackground) +void BaseJob::Private::sendRequest() { - QNetworkRequest req - { makeRequestUrl(connection->baseUrl(), apiEndpoint, requestQuery) }; + QNetworkRequest req { makeRequestUrl(connection->baseUrl(), apiEndpoint, + requestQuery) }; if (!requestHeaders.contains("Content-Type")) req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); if (needsToken) req.setRawHeader("Authorization", QByteArray("Bearer ") + connection->accessToken()); req.setAttribute(QNetworkRequest::BackgroundRequestAttribute, inBackground); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); req.setMaximumRedirectsAllowed(10); -#endif req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true); -#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) - // some sources claim that there are issues with QT 5.8 - req.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true); + req.setAttribute( +#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) + QNetworkRequest::Http2AllowedAttribute +#else + QNetworkRequest::HTTP2AllowedAttribute #endif + // Qt doesn't combine HTTP2 with SSL quite right, occasionally crashing at + // what seems like an attempt to write to a closed channel. If/when that + // changes, false should be turned to true below. + , false); Q_ASSERT(req.url().isValid()); for (auto it = requestHeaders.cbegin(); it != requestHeaders.cend(); ++it) req.setRawHeader(it.key(), it.value()); - switch( verb ) - { - case HttpVerb::Get: - reply.reset( connection->nam()->get(req) ); - break; - case HttpVerb::Post: - reply.reset( connection->nam()->post(req, requestData.source()) ); - break; - case HttpVerb::Put: - reply.reset( connection->nam()->put(req, requestData.source()) ); - break; - case HttpVerb::Delete: - reply.reset( connection->nam()->deleteResource(req) ); - break; + + switch (verb) { + case HttpVerb::Get: + reply = connection->nam()->get(req); + break; + case HttpVerb::Post: + reply = connection->nam()->post(req, requestData.source()); + break; + case HttpVerb::Put: + reply = connection->nam()->put(req, requestData.source()); + break; + case HttpVerb::Delete: + reply = connection->nam()->sendCustomRequest(req, "DELETE", requestData.source()); + break; } } -void BaseJob::beforeStart(const ConnectionData*) -{ } +void BaseJob::doPrepare() { } -void BaseJob::afterStart(const ConnectionData*, QNetworkReply*) -{ } +void BaseJob::onSentRequest(QNetworkReply*) { } -void BaseJob::beforeAbandon(QNetworkReply*) -{ } +void BaseJob::beforeAbandon() { } -void BaseJob::start(const ConnectionData* connData, bool inBackground) +void BaseJob::initiate(ConnectionData* connData, bool inBackground) { - if (connData && connData->baseUrl().isValid()) { + if (Q_LIKELY(connData && connData->baseUrl().isValid())) { + d->inBackground = inBackground; d->connection = connData; - d->retryTimer.setSingleShot(true); - connect(&d->retryTimer, &QTimer::timeout, this, - [this, inBackground] { sendRequest(inBackground); }); - - beforeStart(connData); - if (status().good()) - sendRequest(inBackground); - if (status().good()) - afterStart(connData, d->reply.data()); + doPrepare(); + + if (d->needsToken && d->connection->accessToken().isEmpty()) + setStatus(Unauthorised); + else if ((d->verb == HttpVerb::Post || d->verb == HttpVerb::Put) + && d->requestData.source() + && !d->requestData.source()->isReadable()) { + setStatus(FileError, "Request data not ready"); + } + Q_ASSERT(status().code != Pending); // doPrepare() must NOT set this + if (Q_LIKELY(status().code == Unprepared)) { + d->connection->submit(this); + return; + } + qCWarning(d->logCat).noquote() + << "Request failed preparation and won't be sent:" + << d->dumpRequest(); } else { qCCritical(d->logCat) << "Developers, ensure the Connection is valid before using it"; Q_ASSERT(false); setStatus(IncorrectRequestError, tr("Invalid server connection")); } - if (!status().good()) - QTimer::singleShot(0, this, &BaseJob::finishJob); + // The status is no good, finalise + QTimer::singleShot(0, this, &BaseJob::finishJob); } -void BaseJob::sendRequest(bool inBackground) +void BaseJob::sendRequest() { - emit aboutToStart(); - d->retryTimer.stop(); // In case we were counting down at the moment - qCDebug(d->logCat) << this << "sending request to" << d->apiEndpoint; - if (!d->requestQuery.isEmpty()) - qCDebug(d->logCat) << " query:" << d->requestQuery.toString(); - d->sendRequest(inBackground); - connect( d->reply.data(), &QNetworkReply::finished, this, &BaseJob::gotReply ); - if (d->reply->isRunning()) - { - connect( d->reply.data(), &QNetworkReply::metaDataChanged, - this, &BaseJob::checkReply); - connect( d->reply.data(), &QNetworkReply::uploadProgress, - this, &BaseJob::uploadProgress); - connect( d->reply.data(), &QNetworkReply::downloadProgress, - this, &BaseJob::downloadProgress); - d->timer.start(getCurrentTimeout()); - qCDebug(d->logCat) << this << "request has been sent"; - emit started(); + if (status().code == Abandoned) { + qCDebug(d->logCat) << "Won't proceed with the abandoned request:" + << d->dumpRequest(); + return; } - else - qCWarning(d->logCat) << this << "request could not start"; + Q_ASSERT(d->connection && status().code == Pending); + qCDebug(d->logCat).noquote() << "Making" << d->dumpRequest(); + d->needsToken |= d->connection->needsToken(objectName()); + emit aboutToSendRequest(); + d->sendRequest(); + Q_ASSERT(d->reply); + connect(reply(), &QNetworkReply::finished, this, [this] { + gotReply(); + finishJob(); + }); + if (d->reply->isRunning()) { + connect(reply(), &QNetworkReply::metaDataChanged, this, + [this] { checkReply(reply()); }); + connect(reply(), &QNetworkReply::uploadProgress, this, + &BaseJob::uploadProgress); + connect(reply(), &QNetworkReply::downloadProgress, this, + &BaseJob::downloadProgress); + d->timer.start(getCurrentTimeout()); + qCInfo(d->logCat).noquote() << "Sent" << d->dumpRequest(); + onSentRequest(reply()); + emit sentRequest(); + } else + qCCritical(d->logCat).noquote() + << "Request could not start:" << d->dumpRequest(); } -void BaseJob::checkReply() +BaseJob::Status BaseJob::Private::parseJson() { - setStatus(doCheckReply(d->reply.data())); + QJsonParseError error { 0, QJsonParseError::MissingObject }; + jsonResponse = QJsonDocument::fromJson(rawResponse, &error); + return { error.error == QJsonParseError::NoError ? NoError + : IncorrectResponse, + error.errorString() }; } void BaseJob::gotReply() { - checkReply(); + setStatus(checkReply(reply())); + + if (status().good() + && d->expectedContentTypes == QByteArrayList { "application/json" }) { + d->rawResponse = reply()->readAll(); + setStatus(d->parseJson()); + if (status().good() && !expectedKeys().empty()) { + const auto& responseObject = jsonData(); + QByteArrayList missingKeys; + for (const auto& k: expectedKeys()) + if (!responseObject.contains(k)) + missingKeys.push_back(k); + if (!missingKeys.empty()) + setStatus(IncorrectResponse, tr("Required JSON keys missing: ") + + missingKeys.join()); + } + if (!status().good()) // Bad JSON in a "good" reply: bail out + return; + } // else { + // If the endpoint expects anything else than just (API-related) JSON + // reply()->readAll() is not performed and the whole reply processing + // is left to derived job classes: they may read it piecemeal or customise + // per content type in prepareResult(), or even have read it already + // (see, e.g., DownloadFileJob). + // } + if (status().good()) - setStatus(parseReply(d->reply.data())); + setStatus(prepareResult()); else { - // FIXME: Factor out to smth like BaseJob::handleError() - d->rawResponse = d->reply->readAll(); - const auto jsonBody = - d->reply->rawHeader("Content-Type") == "application/json"; + d->rawResponse = reply()->readAll(); qCDebug(d->logCat).noquote() - << "Error body (truncated if long):" << d->rawResponse.left(500); - if (jsonBody) - { - auto json = QJsonDocument::fromJson(d->rawResponse).object(); - const auto errCode = json.value("errcode"_ls).toString(); - if (error() == TooManyRequestsError || - errCode == "M_LIMIT_EXCEEDED") - { - QString msg = tr("Too many requests"); - auto retryInterval = json.value("retry_after_ms"_ls).toInt(-1); - if (retryInterval != -1) - msg += tr(", next retry advised after %1 ms") - .arg(retryInterval); - else // We still have to figure some reasonable interval - retryInterval = getNextRetryInterval(); - - setStatus(TooManyRequestsError, msg); - - // Shortcut to retry instead of executing finishJob() - stop(); - qCWarning(d->logCat) - << this << "will retry in" << retryInterval << "ms"; - d->retryTimer.start(retryInterval); - emit retryScheduled(d->retriesTaken, retryInterval); - return; - } - if (errCode == "M_CONSENT_NOT_GIVEN") - { - d->status.code = UserConsentRequiredError; - d->errorUrl = json.value("consent_uri"_ls).toString(); - } - else if (errCode == "M_UNSUPPORTED_ROOM_VERSION" || - errCode == "M_INCOMPATIBLE_ROOM_VERSION") - { - d->status.code = UnsupportedRoomVersionError; - if (json.contains("room_version")) - d->status.message = - tr("Requested room version: %1") - .arg(json.value("room_version").toString()); - } - else if (errCode == "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM") - setStatus(IncorrectRequestError, - tr("It's not allowed to leave a server notices room")); - else if (errCode == "M_USER_DEACTIVATED") - setStatus(ContentAccessError, - tr("The user has been deactivated")); - else if (!json.isEmpty()) // Not localisable on the client side - setStatus(d->status.code, json.value("error"_ls).toString()); - } + << "Error body (truncated if long):" << rawDataSample(500); + // Parse the error payload and update the status if needed + if (const auto newStatus = prepareError(); !newStatus.good()) + setStatus(newStatus); } - - finishJob(); } bool checkContentType(const QByteArray& type, const QByteArrayList& patterns) @@ -366,138 +463,183 @@ bool checkContentType(const QByteArray& type, const QByteArrayList& patterns) // ignore possible appendixes of the content type const auto ctype = type.split(';').front(); - for (const auto& pattern: patterns) - { + for (const auto& pattern: patterns) { if (pattern.startsWith('*') || ctype == pattern) // Fast lane return true; auto patternParts = pattern.split('/'); Q_ASSERT_X(patternParts.size() <= 2, __FUNCTION__, - "BaseJob: Expected content type should have up to two" - " /-separated parts; violating pattern: " + pattern); + "BaseJob: Expected content type should have up to two" + " /-separated parts; violating pattern: " + + pattern); - if (ctype.split('/').front() == patternParts.front() && - patternParts.back() == "*") + if (ctype.split('/').front() == patternParts.front() + && patternParts.back() == "*") return true; // Exact match already went on fast lane } return false; } -BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const +BaseJob::Status BaseJob::checkReply(const QNetworkReply* reply) const { - // QNetworkReply error codes seem to be flawed when it comes to HTTP; - // see, e.g., https://github.com/QMatrixClient/libqmatrixclient/issues/200 - // so check genuine HTTP codes. The below processing is based on - // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes + // QNetworkReply error codes are insufficient for our purposes (e.g. they + // don't allow to discern HTTP code 429) so check the original code instead const auto httpCodeHeader = - reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); - if (!httpCodeHeader.isValid()) - { - qCWarning(d->logCat) << this << "didn't get valid HTTP headers"; + reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + if (!httpCodeHeader.isValid()) { + qCWarning(d->logCat).noquote() + << "No valid HTTP headers from" << d->dumpRequest(); return { NetworkError, reply->errorString() }; } - const QString replyState = reply->isRunning() ? - QStringLiteral("(tentative)") : QStringLiteral("(final)"); - const auto urlString = '|' + d->reply->url().toDisplayString(); const auto httpCode = httpCodeHeader.toInt(); - const auto reason = - reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); if (httpCode / 100 == 2) // 2xx { - qCDebug(d->logCat).noquote().nospace() << this << urlString; - qCDebug(d->logCat).noquote() << " " << httpCode << reason << replyState; + if (reply->isFinished()) + qCInfo(d->logCat).noquote() << httpCode << "<-" << d->dumpRequest(); if (!checkContentType(reply->rawHeader("Content-Type"), - d->expectedContentTypes)) + d->expectedContentTypes)) return { UnexpectedResponseTypeWarning, "Unexpected content type of the response" }; return NoError; } + if (reply->isFinished()) + qCWarning(d->logCat).noquote() << httpCode << "<-" << d->dumpRequest(); - qCWarning(d->logCat).noquote().nospace() << this << urlString; - qCWarning(d->logCat).noquote() << " " << httpCode << reason << replyState; - return { [httpCode]() -> StatusCode { - if (httpCode / 10 == 41) - return httpCode == 410 ? IncorrectRequestError : NotFoundError; - switch (httpCode) - { - case 401: case 403: case 407: - return ContentAccessError; - case 404: - return NotFoundError; - case 400: case 405: case 406: case 426: case 428: - case 505: - case 494: // Unofficial nginx "Request header too large" - case 497: // Unofficial nginx "HTTP request sent to HTTPS port" - return IncorrectRequestError; - case 429: - return TooManyRequestsError; - case 501: case 510: - return RequestNotImplementedError; - case 511: - return NetworkAuthRequiredError; - default: - return NetworkError; - } - }(), reply->errorString() }; + auto message = reply->errorString(); + if (message.isEmpty()) + message = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute) + .toString(); + + return Status::fromHttpCode(httpCode, message); } -BaseJob::Status BaseJob::parseReply(QNetworkReply* reply) +BaseJob::Status BaseJob::prepareResult() { return Success; } + +BaseJob::Status BaseJob::prepareError() { - d->rawResponse = reply->readAll(); - QJsonParseError error { 0, QJsonParseError::MissingObject }; - const auto& json = QJsonDocument::fromJson(d->rawResponse, &error); - if( error.error == QJsonParseError::NoError ) - return parseJson(json); + // Try to make sense of the error payload but be prepared for all kinds + // of unexpected stuff (raw HTML, plain text, foreign JSON among those) + if (!d->rawResponse.isEmpty() + && reply()->rawHeader("Content-Type") == "application/json") + d->parseJson(); + + // By now, if d->parseJson() above succeeded then jsonData() will return + // a valid JSON object - or an empty object otherwise (in which case most + // of if's below will fall through to `return NoError` at the end + const auto& errorJson = jsonData(); + const auto errCode = errorJson.value("errcode"_ls).toString(); + if (error() == TooManyRequestsError || errCode == "M_LIMIT_EXCEEDED") { + QString msg = tr("Too many requests"); + int64_t retryAfterMs = errorJson.value("retry_after_ms"_ls).toInt(-1); + if (retryAfterMs >= 0) + msg += tr(", next retry advised after %1 ms").arg(retryAfterMs); + else // We still have to figure some reasonable interval + retryAfterMs = getNextRetryMs(); + + d->connection->limitRate(milliseconds(retryAfterMs)); + + return { TooManyRequestsError, msg }; + } + + if (errCode == "M_CONSENT_NOT_GIVEN") { + d->errorUrl = errorJson.value("consent_uri"_ls).toString(); + return { UserConsentRequiredError }; + } + if (errCode == "M_UNSUPPORTED_ROOM_VERSION" + || errCode == "M_INCOMPATIBLE_ROOM_VERSION") + return { UnsupportedRoomVersionError, + errorJson.contains("room_version"_ls) + ? tr("Requested room version: %1") + .arg(errorJson.value("room_version"_ls).toString()) + : errorJson.value("error"_ls).toString() }; + if (errCode == "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM") + return { CannotLeaveRoom, + tr("It's not allowed to leave a server notices room") }; + if (errCode == "M_USER_DEACTIVATED") + return { UserDeactivated }; - return { IncorrectResponseError, error.errorString() }; + // Not localisable on the client side + if (errorJson.contains("error"_ls)) // Keep the code, update the message + return { d->status.code, errorJson.value("error"_ls).toString() }; + + return NoError; // Retain the status if the error payload is not recognised } -BaseJob::Status BaseJob::parseJson(const QJsonDocument&) +QJsonValue BaseJob::takeValueFromJson(const QString& key) { - return Success; + if (!d->jsonResponse.isObject()) + return QJsonValue::Undefined; + auto o = d->jsonResponse.object(); + auto v = o.take(key); + d->jsonResponse.setObject(o); + return v; } void BaseJob::stop() { + // This method is (also) used to semi-finalise the job before retrying; so + // stop the timeout timer but keep the retry timer running. d->timer.stop(); - if (d->reply) - { + if (d->reply) { d->reply->disconnect(this); // Ignore whatever comes from the reply - if (d->reply->isRunning()) - { - qCWarning(d->logCat) << this << "stopped without ready network reply"; - d->reply->abort(); + if (d->reply->isRunning()) { + qCWarning(d->logCat) + << this << "stopped without ready network reply"; + d->reply->abort(); // Keep the reply object in case clients need it } - } - else + } else qCWarning(d->logCat) << this << "stopped with empty network reply"; } void BaseJob::finishJob() { stop(); - if ((error() == NetworkError || error() == TimeoutError) - && d->retriesTaken < d->maxRetries) - { - // TODO: The whole retrying thing should be put to ConnectionManager - // otherwise independently retrying jobs make a bit of notification - // storm towards the UI. - const auto retryInterval = - error() == TimeoutError ? 0 : getNextRetryInterval(); - ++d->retriesTaken; - qCWarning(d->logCat).nospace() << this << ": retry #" << d->retriesTaken - << " in " << retryInterval/1000 << " s"; - d->retryTimer.start(retryInterval); - emit retryScheduled(d->retriesTaken, retryInterval); + switch(error()) { + case TooManyRequests: + emit rateLimited(); + d->connection->submit(this); return; + case Unauthorised: + if (!d->needsToken && !d->connection->accessToken().isEmpty()) { + // Rerun with access token (extension of the spec while + // https://github.com/matrix-org/matrix-doc/issues/701 is pending) + d->connection->setNeedsToken(objectName()); + qCWarning(d->logCat) << this << "re-running with authentication"; + emit retryScheduled(d->retriesTaken, 0); + d->connection->submit(this); + return; + } + break; + case NetworkError: + case IncorrectResponse: + case Timeout: + if (d->retriesTaken < d->maxRetries) { + // TODO: The whole retrying thing should be put to + // Connection(Manager) otherwise independently retrying jobs make a + // bit of notification storm towards the UI. + const seconds retryIn = error() == Timeout ? 0s + : getNextRetryInterval(); + ++d->retriesTaken; + qCWarning(d->logCat).nospace() + << this << ": retry #" << d->retriesTaken << " in " + << retryIn.count() << " s"; + setStatus(Pending, "Pending retry"); + d->retryTimer.start(retryIn); + emit retryScheduled(d->retriesTaken, milliseconds(retryIn).count()); + return; + } + [[fallthrough]]; + default:; } - // Notify those interested in any completion of the job (including killing) + Q_ASSERT(status().code != Pending); + + // Notify those interested in any completion of the job including abandon() emit finished(this); - emit result(this); + emit result(this); // abandon() doesn't emit this if (error()) emit failure(this); else @@ -506,135 +648,144 @@ void BaseJob::finishJob() deleteLater(); } -const JobTimeoutConfig& BaseJob::Private::getCurrentTimeoutConfig() const +seconds BaseJob::getCurrentTimeout() const { - return errorStrategy[std::min(retriesTaken, errorStrategy.size() - 1)]; + return d->getCurrentTimeoutConfig().jobTimeout; } -BaseJob::duration_t BaseJob::getCurrentTimeout() const +BaseJob::duration_ms_t BaseJob::getCurrentTimeoutMs() const { - return d->getCurrentTimeoutConfig().jobTimeout * 1000; + return milliseconds(getCurrentTimeout()).count(); } -BaseJob::duration_t BaseJob::getNextRetryInterval() const +seconds BaseJob::getNextRetryInterval() const { - return d->getCurrentTimeoutConfig().nextRetryInterval * 1000; + return d->getCurrentTimeoutConfig().nextRetryInterval; } -BaseJob::duration_t BaseJob::millisToRetry() const +BaseJob::duration_ms_t BaseJob::getNextRetryMs() const { - return d->retryTimer.isActive() ? d->retryTimer.remainingTime() : 0; + return milliseconds(getNextRetryInterval()).count(); } -int BaseJob::maxRetries() const +milliseconds BaseJob::timeToRetry() const { - return d->maxRetries; + return d->retryTimer.isActive() ? d->retryTimer.remainingTimeAsDuration() + : 0s; } -void BaseJob::setMaxRetries(int newMaxRetries) +BaseJob::duration_ms_t BaseJob::millisToRetry() const { - d->maxRetries = newMaxRetries; + return timeToRetry().count(); } -BaseJob::Status BaseJob::status() const +int BaseJob::maxRetries() const { return d->maxRetries; } + +void BaseJob::setMaxRetries(int newMaxRetries) { - return d->status; + d->maxRetries = newMaxRetries; } +BaseJob::Status BaseJob::status() const { return d->status; } + QByteArray BaseJob::rawData(int bytesAtMost) const { return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost - ? d->rawResponse.left(bytesAtMost) : d->rawResponse; + ? d->rawResponse.left(bytesAtMost) + : d->rawResponse; } +const QByteArray& BaseJob::rawData() const { return 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()); - + ? data + : data + tr("...(truncated, %Ln bytes in total)", + "Comes after trimmed raw network response", + d->rawResponse.size()); } -QString BaseJob::statusCaption() const +QJsonObject BaseJob::jsonData() const { - switch (d->status.code) - { - case Success: - return tr("Success"); - case Pending: - return tr("Request still pending response"); - case UnexpectedResponseTypeWarning: - return tr("Warning: Unexpected response type"); - case Abandoned: - return tr("Request was abandoned"); - case NetworkError: - return tr("Network problems"); - case JsonParseError: - return tr("Response could not be parsed"); - case TimeoutError: - return tr("Request timed out"); - case ContentAccessError: - return tr("Access error"); - case NotFoundError: - return tr("Not found"); - case IncorrectRequestError: - return tr("Invalid request"); - case IncorrectResponseError: - return tr("Response could not be parsed"); - case TooManyRequestsError: - return tr("Too many requests"); - case RequestNotImplementedError: - return tr("Function not implemented by the server"); - case NetworkAuthRequiredError: - return tr("Network authentication required"); - case UserConsentRequiredError: - return tr("User consent required"); - case UnsupportedRoomVersionError: - return tr("The server does not support the needed room version"); - default: - return tr("Request failed"); - } + return d->jsonResponse.object(); } -int BaseJob::error() const +QJsonArray BaseJob::jsonItems() const { - return d->status.code; + return d->jsonResponse.array(); } -QString BaseJob::errorString() const +QString BaseJob::statusCaption() const { - return d->status.message; + switch (d->status.code) { + case Success: + return tr("Success"); + case Pending: + return tr("Request still pending response"); + case UnexpectedResponseTypeWarning: + return tr("Warning: Unexpected response type"); + case Abandoned: + return tr("Request was abandoned"); + case NetworkError: + return tr("Network problems"); + case TimeoutError: + return tr("Request timed out"); + case Unauthorised: + return tr("Unauthorised request"); + case ContentAccessError: + return tr("Access error"); + case NotFoundError: + return tr("Not found"); + case IncorrectRequestError: + return tr("Invalid request"); + case IncorrectResponseError: + return tr("Response could not be parsed"); + case TooManyRequestsError: + return tr("Too many requests"); + case RequestNotImplementedError: + return tr("Function not implemented by the server"); + case NetworkAuthRequiredError: + return tr("Network authentication required"); + case UserConsentRequiredError: + return tr("User consent required"); + case UnsupportedRoomVersionError: + return tr("The server does not support the needed room version"); + default: + return tr("Request failed"); + } } -QUrl BaseJob::errorUrl() const -{ - return d->errorUrl; -} +int BaseJob::error() const { return d->status.code; } + +QString BaseJob::errorString() const { return d->status.message; } + +QUrl BaseJob::errorUrl() const { return d->errorUrl; } void BaseJob::setStatus(Status s) { // The crash that led to this code has been reported in - // https://github.com/QMatrixClient/Quaternion/issues/566 - basically, - // when cleaning up childrent of a deleted Connection, there's a chance + // https://github.com/quotient-im/Quaternion/issues/566 - basically, + // when cleaning up children of a deleted Connection, there's a chance // of pending jobs being abandoned, calling setStatus(Abandoned). // There's nothing wrong with this; however, the safety check for // cleartext access tokens below uses d->connection - which is a dangling // pointer. // To alleviate that, a stricter condition is applied, that for Abandoned // and to-be-Abandoned jobs the status message will be disregarded entirely. - // 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(); - + // We could rectify the situation by making d->connection a QPointer<> + // (and deriving ConnectionData from QObject, respectively) but it's + // a too edge case for the hassle. if (d->status == s) return; - if (!s.message.isEmpty() - && d->connection && !d->connection->accessToken().isEmpty()) + if (d->status.code == Abandoned || s.code == Abandoned) + s.message.clear(); + + if (!s.message.isEmpty() && d->connection + && !d->connection->accessToken().isEmpty()) s.message.replace(d->connection->accessToken(), "(REDACTED)"); if (!s.good()) qCWarning(d->logCat) << this << "status" << s; @@ -649,7 +800,7 @@ void BaseJob::setStatus(int code, QString message) void BaseJob::abandon() { - beforeAbandon(d->reply ? d->reply.data() : nullptr); + beforeAbandon(); d->timer.stop(); d->retryTimer.stop(); // In case abandon() was called between retries setStatus(Abandoned); @@ -662,11 +813,8 @@ void BaseJob::abandon() void BaseJob::timeout() { - setStatus( TimeoutError, "The job has timed out" ); + setStatus(TimeoutError, "The job has timed out"); finishJob(); } -void BaseJob::setLoggingCategory(LoggingCategory lcf) -{ - d->logCat = lcf; -} +void BaseJob::setLoggingCategory(LoggingCategory lcf) { d->logCat = lcf; } diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 4c1c7706..be2926be 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -13,340 +13,465 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once -#include "../logging.h" #include "requestdata.h" +#include "../logging.h" +#include "../converters.h" #include <QtCore/QObject> -#include <QtCore/QUrlQuery> -#include <QtCore/QJsonDocument> class QNetworkReply; class QSslError; -namespace QMatrixClient -{ - class ConnectionData; +namespace Quotient { +class ConnectionData; + +enum class HttpVerb { Get, Put, Post, Delete }; + +class BaseJob : public QObject { + Q_OBJECT + Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT) + Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries) + Q_PROPERTY(int statusCode READ error NOTIFY statusChanged) +public: + /*! The status code of a job + * + * Every job is created in Unprepared status; upon calling prepare() + * from Connection (if things are fine) it go to Pending status. After + * that, the next transition comes after the reply arrives and its contents + * are analysed. At any point in time the job can be abandon()ed, causing + * it to switch to status Abandoned for a brief period before deletion. + */ + enum StatusCode { + Success = 0, + NoError = Success, // To be compatible with Qt conventions + Pending = 1, + WarningLevel = 20, //< Warnings have codes starting from this + UnexpectedResponseType = 21, + UnexpectedResponseTypeWarning = UnexpectedResponseType, + Unprepared = 25, //< Initial job state is incomplete, hence warning level + Abandoned = 50, //< A tiny period between abandoning and object deletion + ErrorLevel = 100, //< Errors have codes starting from this + NetworkError = 101, + Timeout, + TimeoutError = Timeout, + Unauthorised, + ContentAccessError, + NotFoundError, + IncorrectRequest, + IncorrectRequestError = IncorrectRequest, + IncorrectResponse, + IncorrectResponseError = IncorrectResponse, + JsonParseError //< \deprecated Use IncorrectResponse instead + = IncorrectResponse, + TooManyRequests, + TooManyRequestsError = TooManyRequests, + RateLimited = TooManyRequests, + RequestNotImplemented, + RequestNotImplementedError = RequestNotImplemented, + UnsupportedRoomVersion, + UnsupportedRoomVersionError = UnsupportedRoomVersion, + NetworkAuthRequired, + NetworkAuthRequiredError = NetworkAuthRequired, + UserConsentRequired, + UserConsentRequiredError = UserConsentRequired, + CannotLeaveRoom, + UserDeactivated, + FileError, + UserDefinedError = 256 + }; + Q_ENUM(StatusCode) + + /** + * 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); + } + }; - enum class HttpVerb { Get, Put, Post, Delete }; + 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 + */ + struct Status { + Status(StatusCode c) : code(c) {} + Status(int c, QString m) : code(c), message(std::move(m)) {} + + static StatusCode fromHttpCode(int httpCode); + static Status fromHttpCode(int httpCode, QString msg) + { + return { fromHttpCode(httpCode), std::move(msg) }; + } + + bool good() const { return code < ErrorLevel; } + QDebug dumpToLog(QDebug dbg) const; + friend QDebug operator<<(const QDebug& dbg, const Status& s) + { + return s.dumpToLog(dbg); + } + + bool operator==(const Status& other) const + { + return code == other.code && message == other.message; + } + bool operator!=(const Status& other) const + { + return !operator==(other); + } + + int code; + QString message; + }; - struct JobTimeoutConfig +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 first bytes of the raw response body as received from the server + * + * \param bytesAtMost the number of leftmost bytes to return + * + * \sa rawDataSample + */ + QByteArray rawData(int bytesAtMost) const; + + /*! Access the whole response body as received from the server */ + const QByteArray& rawData() const; + + /** Get UI-friendly sample of raw data + * + * This is almost the same as rawData but appends the "truncated" + * suffix if not all data fit in bytesAtMost. This call is + * recommended to present a sample of raw data as "details" next to + * error messages. Note that the default \p bytesAtMost value is + * also tailored to UI cases. + * + * \sa rawData + */ + QString rawDataSample(int bytesAtMost = 65535) const; + + /** Get the response body as a JSON object + * + * If the job's returned content type is not `application/json` + * or if the top-level JSON entity is not an object, an empty object + * is returned. + */ + QJsonObject jsonData() const; + + /** Get the response body as a JSON array + * + * If the job's returned content type is not `application/json` + * or if the top-level JSON entity is not an array, an empty array + * is returned. + */ + QJsonArray jsonItems() const; + + /** Load the property from the JSON response assuming a given C++ type + * + * If there's no top-level JSON object in the response or if there's + * no node with the key \p keyName, \p defaultValue is returned. + */ + template <typename T, typename StrT> // Waiting for QStringViews... + T loadFromJson(const StrT& keyName, T&& defaultValue = {}) const { - int jobTimeout; - int nextRetryInterval; - }; + const auto& jv = jsonData().value(keyName); + return jv.isUndefined() ? std::forward<T>(defaultValue) + : fromJson<T>(jv); + } - class BaseJob: public QObject + /** Load the property from the JSON response and delete it from JSON + * + * If there's no top-level JSON object in the response or if there's + * no node with the key \p keyName, \p defaultValue is returned. + */ + template <typename T> + T takeFromJson(const QString& key, T&& defaultValue = {}) { - 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: - using QUrlQuery::QUrlQuery; - Query() = default; - Query(const std::initializer_list< QPair<QString, QString> >& l) - { - setQueryItems(l); - } - }; - - using Data = RequestData; - - /** - * This structure stores the status of a server call job. The status consists - * of a code, that is described (but not delimited) by the respective enum, - * and a freeform message. - * - * To extend the list of error codes, define an (anonymous) enum - * along the lines of StatusCode, with additional values - * starting at UserDefinedError - */ - class Status - { - public: - Status(StatusCode c) : code(c) { } - Status(int c, QString m) : code(c), message(std::move(m)) { } - - bool good() const { return code < ErrorLevel; } - friend QDebug operator<<(QDebug dbg, const Status& s) - { - QDebugStateSaver _s(dbg); - return dbg.noquote().nospace() - << s.code << ": " << s.message; - } - - bool operator==(const Status& other) const - { - return code == other.code && message == other.message; - } - bool operator!=(const Status& other) const - { - return !operator==(other); - } - - int code; - QString message; - }; - - using duration_t = int; // milliseconds - - public: - BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, - bool needsToken = true); - BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, - const Query& query, Data&& data = {}, - bool needsToken = true); - - QUrl requestUrl() const; - bool isBackground() const; - - /** Current status of the job */ - Status status() const; - /** Short human-friendly message on the job status */ - QString statusCaption() const; - /** 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 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; - }; + if (const auto& jv = takeValueFromJson(key); !jv.isUndefined()) + return fromJson<T>(jv); - inline bool isJobRunning(BaseJob* job) + return std::forward<T>(defaultValue); + } + + /** Error (more generally, status) code + * Equivalent to status().code + * \sa status + */ + int error() const; + + /** Error-specific message, as returned by the server */ + virtual QString errorString() const; + + /** A URL to help/clarify the error, if provided by the server */ + QUrl errorUrl() const; + + int maxRetries() const; + void setMaxRetries(int newMaxRetries); + + using duration_ms_t = std::chrono::milliseconds::rep; // normally int64_t + + std::chrono::seconds getCurrentTimeout() const; + Q_INVOKABLE Quotient::BaseJob::duration_ms_t getCurrentTimeoutMs() const; + std::chrono::seconds getNextRetryInterval() const; + Q_INVOKABLE Quotient::BaseJob::duration_ms_t getNextRetryMs() const; + std::chrono::milliseconds timeToRetry() const; + Q_INVOKABLE Quotient::BaseJob::duration_ms_t millisToRetry() const; + + friend QDebug operator<<(QDebug dbg, const BaseJob* j) { - return job && job->error() == BaseJob::Pending; + return dbg << j->objectName(); } -} // namespace QMatrixClient + +public slots: + void initiate(ConnectionData* connData, bool inBackground); + + /** + * Abandons the result of this job, arrived or unarrived. + * + * This aborts waiting for a reply from the server (if there was + * any pending) and deletes the job object. No result signals + * (result, success, failure) are emitted. + */ + void abandon(); + +signals: + /** The job is about to send a network request */ + void aboutToSendRequest(); + + /** The job has sent a network request */ + void sentRequest(); + + /** The job has changed its status */ + void statusChanged(Quotient::BaseJob::Status newStatus); + + /** + * The previous network request has failed; the next attempt will + * be done in the specified time + * @param nextAttempt the 1-based number of attempt (will always be more + * than 1) + * @param inMilliseconds the interval after which the next attempt will be + * taken + */ + void retryScheduled(int nextAttempt, + Quotient::BaseJob::duration_ms_t inMilliseconds); + + /** + * The previous network request has been rate-limited; the next attempt + * will be queued and run sometime later. Since other jobs may already + * wait in the queue, it's not possible to predict the wait time. + */ + void rateLimited(); + + /** + * Emitted when the job is finished, in any case. It is used to notify + * observers that the job is terminated and that progress can be hidden. + * + * This should not be emitted directly by subclasses; + * use finishJob() instead. + * + * In general, to be notified of a job's completion, client code + * should connect to result(), success(), or failure() + * rather than finished(). However if you need to track the job's + * lifecycle you should connect to this instead of result(); + * in particular, only this signal will be emitted on abandoning. + * + * @param job the job that emitted this signal + * + * @see result, success, failure + */ + void finished(Quotient::BaseJob* job); + + /** + * Emitted when the job is finished (except when abandoned). + * + * Use error() to know if the job was finished with error. + * + * @param job the job that emitted this signal + * + * @see success, failure + */ + void result(Quotient::BaseJob* job); + + /** + * Emitted together with result() in case there's no error. + * + * @see result, failure + */ + void success(Quotient::BaseJob*); + + /** + * Emitted together with result() if there's an error. + * Similar to result(), this won't be emitted in case of abandon(). + * + * @see result, success + */ + void failure(Quotient::BaseJob*); + + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void uploadProgress(qint64 bytesSent, qint64 bytesTotal); + +protected: + using headers_t = QHash<QByteArray, QByteArray>; + + 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); + const QByteArrayList expectedKeys() const; + void addExpectedKey(const QByteArray &key); + void setExpectedKeys(const QByteArrayList &keys); + + const QNetworkReply* reply() const; + QNetworkReply* reply(); + + /** Construct a URL out of baseUrl, path and query + * + * The function ensures exactly one '/' between the path component of + * \p baseUrl and \p path. The query component of \p baseUrl is ignored. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& path, + const QUrlQuery& query = {}); + + /*! Prepares the job for execution + * + * This method is called no more than once per job lifecycle, + * when it's first scheduled for execution; in particular, it is not called + * on retries. + */ + virtual void doPrepare(); + + /*! Postprocessing after the network request has been sent + * + * This method is called every time the job receives a running + * QNetworkReply object from NetworkAccessManager - basically, after + * successfully sending a network request (including retries). + */ + virtual void onSentRequest(QNetworkReply*); + virtual void beforeAbandon(); + + /*! \brief An extension point for additional reply processing. + * + * The base implementation does nothing and returns Success. + * + * \sa gotReply + */ + virtual Status prepareResult(); + + /*! \brief Process details of the error + * + * The function processes the reply in case when status from checkReply() + * was not good (usually because of an unsuccessful HTTP code). + * The base implementation assumes Matrix JSON error object in the body; + * overrides are strongly recommended to call it for all stock Matrix + * responses as early as possible but in addition can process custom errors, + * with JSON or non-JSON payload. + */ + virtual Status prepareError(); + + /*! \brief Get direct access to the JSON response object in the job + * + * This allows to implement deserialisation with "move" semantics for parts + * of the response. Assuming that the response body is a valid JSON object, + * the function calls QJsonObject::take(key) on it and returns the result. + * + * \return QJsonValue::Null, if the response content type is not + * advertised as `application/json`; + * QJsonValue::Undefined, if the response is a JSON object but + * doesn't have \p key; + * the value for \p key otherwise. + * + * \sa takeFromJson + */ + QJsonValue takeValueFromJson(const QString& key); + + void setStatus(Status s); + void setStatus(int code, QString message); + + // Q_DECLARE_LOGGING_CATEGORY return different function types + // in different versions + using LoggingCategory = decltype(JOBS)*; + void setLoggingCategory(LoggingCategory lcf); + + // Job objects should only be deleted via QObject::deleteLater + ~BaseJob() override; + +protected slots: + void timeout(); + + /*! \brief Check the pending or received reply for upfront issues + * + * This is invoked when headers are first received and also once + * the complete reply is obtained; the base implementation checks the HTTP + * headers to detect general issues such as network errors or access denial + * and it's strongly recommended to call it from overrides, + * as early as possible. + * This slot is const and cannot read the response body. If you need to read + * the body on the fly, override onSentRequest() and connect in it + * to reply->readyRead(); and if you only need to validate the body after + * it fully arrived, use prepareResult() for that). Returning anything + * except NoError/Success switches further processing from prepareResult() + * to prepareError(). + * + * @return the result of checking the reply + * + * @see gotReply + */ + virtual Status checkReply(const QNetworkReply *reply) const; + +private slots: + void sendRequest(); + void gotReply(); + + friend class ConnectionData; // to provide access to sendRequest() + +private: + void stop(); + void finishJob(); + + class Private; + QScopedPointer<Private> d; +}; + +inline bool isJobRunning(BaseJob* job) +{ + return job && job->error() == BaseJob::Pending; +} +} // namespace Quotient diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 672a7b2d..0011a97c 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -1,29 +1,28 @@ #include "downloadfilejob.h" -#include <QtNetwork/QNetworkReply> #include <QtCore/QFile> #include <QtCore/QTemporaryFile> +#include <QtNetwork/QNetworkReply> -using namespace QMatrixClient; +using namespace Quotient; -class DownloadFileJob::Private -{ - public: - Private() : tempFile(new QTemporaryFile()) { } +class DownloadFileJob::Private { +public: + Private() : tempFile(new QTemporaryFile()) {} - explicit Private(const QString& localFilename) - : targetFile(new QFile(localFilename)) - , tempFile(new QFile(targetFile->fileName() + ".qmcdownload")) - { } + explicit Private(const QString& localFilename) + : targetFile(new QFile(localFilename)) + , tempFile(new QFile(targetFile->fileName() + ".qtntdownload")) + {} - QScopedPointer<QFile> targetFile; - QScopedPointer<QFile> tempFile; + QScopedPointer<QFile> targetFile; + QScopedPointer<QFile> tempFile; }; QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri) { - return makeRequestUrl( - std::move(baseUrl), mxcUri.authority(), mxcUri.path().mid(1)); + return makeRequestUrl(std::move(baseUrl), mxcUri.authority(), + mxcUri.path().mid(1)); } DownloadFileJob::DownloadFileJob(const QString& serverName, @@ -40,18 +39,16 @@ QString DownloadFileJob::targetFileName() const return (d->targetFile ? d->targetFile : d->tempFile)->fileName(); } -void DownloadFileJob::beforeStart(const ConnectionData*) +void DownloadFileJob::doPrepare() { - if (d->targetFile && !d->targetFile->isReadable() && - !d->targetFile->open(QIODevice::WriteOnly)) - { - qCWarning(JOBS) << "Couldn't open the file" - << d->targetFile->fileName() << "for writing"; + if (d->targetFile && !d->targetFile->isReadable() + && !d->targetFile->open(QIODevice::WriteOnly)) { + qCWarning(JOBS) << "Couldn't open the file" << d->targetFile->fileName() + << "for writing"; setStatus(FileError, "Could not open the target file for writing"); return; } - if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::WriteOnly)) - { + if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::WriteOnly)) { qCWarning(JOBS) << "Couldn't open the temporary file" << d->tempFile->fileName() << "for writing"; setStatus(FileError, "Could not open the temporary download file"); @@ -60,18 +57,16 @@ void DownloadFileJob::beforeStart(const ConnectionData*) qCDebug(JOBS) << "Downloading to" << d->tempFile->fileName(); } -void DownloadFileJob::afterStart(const ConnectionData*, QNetworkReply* reply) +void DownloadFileJob::onSentRequest(QNetworkReply* reply) { - connect(reply, &QNetworkReply::metaDataChanged, this, [this,reply] { + connect(reply, &QNetworkReply::metaDataChanged, this, [this, reply] { if (!status().good()) return; auto sizeHeader = reply->header(QNetworkRequest::ContentLengthHeader); - if (sizeHeader.isValid()) - { - auto targetSize = sizeHeader.value<qint64>(); + if (sizeHeader.isValid()) { + auto targetSize = sizeHeader.toLongLong(); if (targetSize != -1) - if (!d->tempFile->resize(targetSize)) - { + if (!d->tempFile->resize(targetSize)) { qCWarning(JOBS) << "Failed to allocate" << targetSize << "bytes for" << d->tempFile->fileName(); setStatus(FileError, @@ -79,44 +74,39 @@ void DownloadFileJob::afterStart(const ConnectionData*, QNetworkReply* reply) } } }); - connect(reply, &QIODevice::readyRead, this, [this,reply] { + connect(reply, &QIODevice::readyRead, this, [this, reply] { if (!status().good()) return; auto bytes = reply->read(reply->bytesAvailable()); if (!bytes.isEmpty()) d->tempFile->write(bytes); else - qCWarning(JOBS) - << "Unexpected empty chunk when downloading from" - << reply->url() << "to" << d->tempFile->fileName(); + qCWarning(JOBS) << "Unexpected empty chunk when downloading from" + << reply->url() << "to" << d->tempFile->fileName(); }); } -void DownloadFileJob::beforeAbandon(QNetworkReply*) +void DownloadFileJob::beforeAbandon() { if (d->targetFile) d->targetFile->remove(); d->tempFile->remove(); } -BaseJob::Status DownloadFileJob::parseReply(QNetworkReply*) +BaseJob::Status DownloadFileJob::prepareResult() { - if (d->targetFile) - { + if (d->targetFile) { d->targetFile->close(); - if (!d->targetFile->remove()) - { + if (!d->targetFile->remove()) { qCWarning(JOBS) << "Failed to remove the target file placeholder"; return { FileError, "Couldn't finalise the download" }; } - if (!d->tempFile->rename(d->targetFile->fileName())) - { + if (!d->tempFile->rename(d->targetFile->fileName())) { qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() << "to" << d->targetFile->fileName(); return { FileError, "Couldn't finalise the download" }; } - } - else + } else d->tempFile->close(); qCDebug(JOBS) << "Saved a file as" << targetFileName(); return Success; diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h index ce47ab1c..e00fd9e4 100644 --- a/lib/jobs/downloadfilejob.h +++ b/lib/jobs/downloadfilejob.h @@ -2,29 +2,24 @@ #include "csapi/content-repo.h" -namespace QMatrixClient -{ - class DownloadFileJob : public GetContentJob - { - public: - enum { FileError = BaseJob::UserDefinedError + 1 }; +namespace Quotient { +class DownloadFileJob : public GetContentJob { +public: + 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 doPrepare() override; + void onSentRequest(QNetworkReply* reply) override; + void beforeAbandon() override; + Status prepareResult() override; +}; +} // namespace Quotient diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp index edb9b156..a69f00e9 100644 --- a/lib/jobs/mediathumbnailjob.cpp +++ b/lib/jobs/mediathumbnailjob.cpp @@ -13,51 +13,45 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "mediathumbnailjob.h" -using namespace QMatrixClient; +using namespace Quotient; -QUrl MediaThumbnailJob::makeRequestUrl(QUrl baseUrl, - const QUrl& mxcUri, QSize requestedSize) +QUrl MediaThumbnailJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri, + QSize requestedSize) { - return makeRequestUrl(std::move(baseUrl), - mxcUri.authority(), mxcUri.path().mid(1), - requestedSize.width(), requestedSize.height()); + return makeRequestUrl(std::move(baseUrl), mxcUri.authority(), + mxcUri.path().mid(1), requestedSize.width(), + requestedSize.height()); } MediaThumbnailJob::MediaThumbnailJob(const QString& serverName, const QString& mediaId, QSize requestedSize) - : GetContentThumbnailJob(serverName, mediaId, - requestedSize.width(), requestedSize.height()) -{ } + : GetContentThumbnailJob(serverName, mediaId, requestedSize.width(), + requestedSize.height(), "scale") +{} MediaThumbnailJob::MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize) - : MediaThumbnailJob(mxcUri.authority(), mxcUri.path().mid(1), // sans leading '/' + : MediaThumbnailJob(mxcUri.authority(), + mxcUri.path().mid(1), // sans leading '/' requestedSize) -{ } +{} -QImage MediaThumbnailJob::thumbnail() const -{ - return _thumbnail; -} +QImage MediaThumbnailJob::thumbnail() const { return _thumbnail; } QImage MediaThumbnailJob::scaledThumbnail(QSize toSize) const { - return _thumbnail.scaled(toSize, - Qt::KeepAspectRatio, Qt::SmoothTransformation); + return _thumbnail.scaled(toSize, Qt::KeepAspectRatio, + Qt::SmoothTransformation); } -BaseJob::Status MediaThumbnailJob::parseReply(QNetworkReply* reply) +BaseJob::Status MediaThumbnailJob::prepareResult() { - auto result = GetContentThumbnailJob::parseReply(reply); - if (!result.good()) - return result; - - if( _thumbnail.loadFromData(data()->readAll()) ) + if (_thumbnail.loadFromData(data()->readAll())) return Success; - return { IncorrectResponseError, QStringLiteral("Could not read image data") }; + return { IncorrectResponse, QStringLiteral("Could not read image data") }; } diff --git a/lib/jobs/mediathumbnailjob.h b/lib/jobs/mediathumbnailjob.h index 7963796e..e6d39085 100644 --- a/lib/jobs/mediathumbnailjob.h +++ b/lib/jobs/mediathumbnailjob.h @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once @@ -22,26 +22,24 @@ #include <QtGui/QPixmap> -namespace QMatrixClient -{ - class MediaThumbnailJob: public GetContentThumbnailJob - { - public: - using GetContentThumbnailJob::makeRequestUrl; - static QUrl makeRequestUrl(QUrl baseUrl, - const QUrl& mxcUri, QSize requestedSize); - - MediaThumbnailJob(const QString& serverName, const QString& mediaId, - QSize requestedSize); - MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize); - - QImage thumbnail() const; - QImage scaledThumbnail(QSize toSize) const; - - protected: - Status parseReply(QNetworkReply* reply) override; - - private: - QImage _thumbnail; - }; -} // namespace QMatrixClient +namespace Quotient { +class MediaThumbnailJob : public GetContentThumbnailJob { +public: + using GetContentThumbnailJob::makeRequestUrl; + static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri, + QSize requestedSize); + + MediaThumbnailJob(const QString& serverName, const QString& mediaId, + QSize requestedSize); + MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize); + + QImage thumbnail() const; + QImage scaledThumbnail(QSize toSize) const; + +protected: + Status prepareResult() override; + +private: + QImage _thumbnail; +}; +} // namespace Quotient diff --git a/lib/jobs/postreadmarkersjob.h b/lib/jobs/postreadmarkersjob.h index 63a8e1d0..5a4d942c 100644 --- a/lib/jobs/postreadmarkersjob.h +++ b/lib/jobs/postreadmarkersjob.h @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once @@ -22,18 +22,17 @@ #include <QtCore/QJsonObject> -using namespace QMatrixClient; +using namespace Quotient; -class PostReadMarkersJob : public BaseJob -{ - public: - explicit PostReadMarkersJob(const QString& roomId, - const QString& readUpToEventId) - : BaseJob(HttpVerb::Post, "PostReadMarkersJob", - QStringLiteral("_matrix/client/r0/rooms/%1/read_markers") - .arg(roomId)) - { - setRequestData(QJsonObject {{ - QStringLiteral("m.fully_read"), readUpToEventId }}); - } +class PostReadMarkersJob : public BaseJob { +public: + explicit PostReadMarkersJob(const QString& roomId, + const QString& readUpToEventId) + : BaseJob( + HttpVerb::Post, "PostReadMarkersJob", + QStringLiteral("_matrix/client/r0/rooms/%1/read_markers").arg(roomId)) + { + setRequestData( + QJsonObject { { QStringLiteral("m.fully_read"), readUpToEventId } }); + } }; diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp index 5cb62221..cec15954 100644 --- a/lib/jobs/requestdata.cpp +++ b/lib/jobs/requestdata.cpp @@ -1,19 +1,18 @@ #include "requestdata.h" +#include <QtCore/QBuffer> #include <QtCore/QByteArray> -#include <QtCore/QJsonObject> #include <QtCore/QJsonArray> #include <QtCore/QJsonDocument> -#include <QtCore/QBuffer> +#include <QtCore/QJsonObject> -using namespace QMatrixClient; +using namespace Quotient; auto fromData(const QByteArray& data) { auto source = std::make_unique<QBuffer>(); - source->open(QIODevice::WriteOnly); - source->write(data); - source->close(); + source->setData(data); + source->open(QIODevice::ReadOnly); return source; } @@ -23,16 +22,10 @@ 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 db011b61..9cb5ecaf 100644 --- a/lib/jobs/requestdata.h +++ b/lib/jobs/requestdata.h @@ -13,49 +13,43 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once +#include <QtCore/QByteArray> + #include <memory> -class QByteArray; class QJsonObject; class QJsonArray; class QJsonDocument; class QIODevice; -namespace QMatrixClient -{ - /** - * A simple wrapper that represents the request body. - * Provides a unified interface to dump an unstructured byte stream - * as well as JSON (and possibly other structures in the future) to - * a QByteArray consumed by QNetworkAccessManager request methods. - */ - class RequestData - { - public: - RequestData() = default; - RequestData(const QByteArray& a); - RequestData(const QJsonObject& jo); - RequestData(const QJsonArray& ja); - RequestData(QIODevice* source) - : _source(std::unique_ptr<QIODevice>(source)) - { } - RequestData(const RequestData&) = delete; - RequestData& operator=(const RequestData&) = delete; - RequestData(RequestData&&) = default; - RequestData& operator=(RequestData&&) = default; - ~RequestData(); +namespace Quotient { +/** + * A simple wrapper that represents the request body. + * Provides a unified interface to dump an unstructured byte stream + * as well as JSON (and possibly other structures in the future) to + * a QByteArray consumed by QNetworkAccessManager request methods. + */ +class RequestData { +public: + RequestData(const QByteArray& a = {}); + RequestData(const QJsonObject& jo); + RequestData(const QJsonArray& ja); + RequestData(QIODevice* source) : _source(std::unique_ptr<QIODevice>(source)) + {} + 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; - }; -} // namespace QMatrixClient +private: + std::unique_ptr<QIODevice> _source; +}; +} // namespace Quotient +/// \deprecated Use namespace Quotient instead +namespace QMatrixClient = Quotient; diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index 84385b55..9087fe50 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -13,12 +13,12 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "syncjob.h" -using namespace QMatrixClient; +using namespace Quotient; static size_t jobId = 0; @@ -29,34 +29,33 @@ SyncJob::SyncJob(const QString& since, const QString& filter, int timeout, { setLoggingCategory(SYNCJOB); QUrlQuery query; - if( !filter.isEmpty() ) + if (!filter.isEmpty()) query.addQueryItem(QStringLiteral("filter"), filter); - if( !presence.isEmpty() ) + if (!presence.isEmpty()) query.addQueryItem(QStringLiteral("set_presence"), presence); - if( timeout >= 0 ) + if (timeout >= 0) query.addQueryItem(QStringLiteral("timeout"), QString::number(timeout)); - if( !since.isEmpty() ) + if (!since.isEmpty()) query.addQueryItem(QStringLiteral("since"), since); setRequestQuery(query); setMaxRetries(std::numeric_limits<int>::max()); } -SyncJob::SyncJob(const QString& since, const Filter& filter, - int timeout, const QString& presence) +SyncJob::SyncJob(const QString& since, const Filter& filter, int timeout, + const QString& presence) : SyncJob(since, QJsonDocument(toJson(filter)).toJson(QJsonDocument::Compact), timeout, presence) -{ } +{} -BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) +BaseJob::Status SyncJob::prepareResult() { - d.parseJson(data.object()); + d.parseJson(jsonData()); if (d.unresolvedRooms().isEmpty()) - return BaseJob::Success; + return Success; qCCritical(MAIN).noquote() << "Incomplete sync response, missing rooms:" << d.unresolvedRooms().join(','); - return BaseJob::IncorrectResponseError; + return IncorrectResponse; } - diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h index 036b25d0..bf139a7b 100644 --- a/lib/jobs/syncjob.h +++ b/lib/jobs/syncjob.h @@ -13,33 +13,29 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once -#include "basejob.h" - -#include "../syncdata.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 Quotient { +class SyncJob : public BaseJob { +public: + explicit SyncJob(const QString& since = {}, const QString& filter = {}, + int timeout = -1, const QString& presence = {}); + explicit SyncJob(const QString& since, const Filter& filter, + int timeout = -1, const QString& presence = {}); - SyncData &&takeData() { return std::move(d); } + SyncData&& takeData() { return std::move(d); } - protected: - Status parseJson(const QJsonDocument& data) override; +protected: + Status prepareResult() override; - private: - SyncData d; - }; -} // namespace QMatrixClient +private: + SyncData d; +}; +} // namespace Quotient diff --git a/lib/joinstate.h b/lib/joinstate.h index 379183f6..31c2b6a7 100644 --- a/lib/joinstate.h +++ b/lib/joinstate.h @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once @@ -22,27 +22,26 @@ #include <array> -namespace QMatrixClient -{ - enum class JoinState : unsigned int - { - Join = 0x1, - Invite = 0x2, - Leave = 0x4, - }; +namespace Quotient { +enum class JoinState : unsigned int { + Join = 0x1, + Invite = 0x2, + Leave = 0x4, +}; - Q_DECLARE_FLAGS(JoinStates, JoinState) +Q_DECLARE_FLAGS(JoinStates, JoinState) - // We cannot use REGISTER_ENUM outside of a Q_OBJECT and besides, we want - // to use strings that match respective JSON keys. - static const std::array<const char*, 3> JoinStateStrings - { { "join", "invite", "leave" } }; +// We cannot use Q_ENUM outside of a Q_OBJECT and besides, we want +// to use strings that match respective JSON keys. +static const std::array<const char*, 3> JoinStateStrings { { "join", "invite", + "leave" } }; - inline const char* toCString(JoinState js) - { - size_t state = size_t(js), index = 0; - while (state >>= 1) ++index; - return JoinStateStrings[index]; - } -} // namespace QMatrixClient -Q_DECLARE_OPERATORS_FOR_FLAGS(QMatrixClient::JoinStates) +inline const char* toCString(JoinState js) +{ + size_t state = size_t(js), index = 0; + while (state >>= 1u) + ++index; + return JoinStateStrings[index]; +} +} // namespace Quotient +Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::JoinStates) diff --git a/lib/logging.cpp b/lib/logging.cpp index 7476781f..c346fbf1 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -13,21 +13,20 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "logging.h" -#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) #define LOGGING_CATEGORY(Name, Id) Q_LOGGING_CATEGORY((Name), (Id), QtInfoMsg) -#else -#define LOGGING_CATEGORY(Name, Id) Q_LOGGING_CATEGORY((Name), (Id)) -#endif // Use LOGGING_CATEGORY instead of Q_LOGGING_CATEGORY in the rest of the code -LOGGING_CATEGORY(MAIN, "libqmatrixclient.main") -LOGGING_CATEGORY(PROFILER, "libqmatrixclient.profiler") -LOGGING_CATEGORY(EVENTS, "libqmatrixclient.events") -LOGGING_CATEGORY(EPHEMERAL, "libqmatrixclient.events.ephemeral") -LOGGING_CATEGORY(JOBS, "libqmatrixclient.jobs") -LOGGING_CATEGORY(SYNCJOB, "libqmatrixclient.jobs.sync") +LOGGING_CATEGORY(MAIN, "quotient.main") +LOGGING_CATEGORY(EVENTS, "quotient.events") +LOGGING_CATEGORY(STATE, "quotient.events.state") +LOGGING_CATEGORY(MESSAGES, "quotient.events.messages") +LOGGING_CATEGORY(EPHEMERAL, "quotient.events.ephemeral") +LOGGING_CATEGORY(E2EE, "quotient.e2ee") +LOGGING_CATEGORY(JOBS, "quotient.jobs") +LOGGING_CATEGORY(SYNCJOB, "quotient.jobs.sync") +LOGGING_CATEGORY(PROFILER, "quotient.profiler") diff --git a/lib/logging.h b/lib/logging.h index a3a65887..ce4131bb 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once @@ -22,63 +22,67 @@ #include <QtCore/QLoggingCategory> Q_DECLARE_LOGGING_CATEGORY(MAIN) -Q_DECLARE_LOGGING_CATEGORY(PROFILER) +Q_DECLARE_LOGGING_CATEGORY(STATE) +Q_DECLARE_LOGGING_CATEGORY(MESSAGES) Q_DECLARE_LOGGING_CATEGORY(EVENTS) Q_DECLARE_LOGGING_CATEGORY(EPHEMERAL) +Q_DECLARE_LOGGING_CATEGORY(E2EE) Q_DECLARE_LOGGING_CATEGORY(JOBS) Q_DECLARE_LOGGING_CATEGORY(SYNCJOB) +Q_DECLARE_LOGGING_CATEGORY(PROFILER) -namespace QMatrixClient -{ - // QDebug manipulators +namespace Quotient { +// QDebug manipulators - using QDebugManip = QDebug (*)(QDebug); +using QDebugManip = QDebug (*)(QDebug); - /** - * @brief QDebug manipulator to setup the stream for JSON output - * - * Originally made to encapsulate the change in QDebug behavior in Qt 5.4 - * and the respective addition of QDebug::noquote(). - * Together with the operator<<() helper, the proposed usage is - * (similar to std:: I/O manipulators): - * - * @example qCDebug() << formatJson << json_object; // (QJsonObject, etc.) - */ - inline QDebug formatJson(QDebug debug_object) - { +/** + * @brief QDebug manipulator to setup the stream for JSON output + * + * Originally made to encapsulate the change in QDebug behavior in Qt 5.4 + * and the respective addition of QDebug::noquote(). + * Together with the operator<<() helper, the proposed usage is + * (similar to std:: I/O manipulators): + * + * @example qCDebug() << formatJson << json_object; // (QJsonObject, etc.) + */ +inline QDebug formatJson(QDebug debug_object) +{ #if QT_VERSION < QT_VERSION_CHECK(5, 4, 0) - return debug_object; + return debug_object; #else - return debug_object.noquote(); + return debug_object.noquote(); #endif - } +} - /** - * @brief A helper operator to facilitate usage of formatJson (and possibly - * other manipulators) - * - * @param debug_object to output the json to - * @param qdm a QDebug manipulator - * @return a copy of debug_object that has its mode altered by qdm - */ - inline QDebug operator<< (QDebug debug_object, QDebugManip qdm) - { - return qdm(debug_object); - } +/** + * @brief A helper operator to facilitate usage of formatJson (and possibly + * other manipulators) + * + * @param debug_object to output the json to + * @param qdm a QDebug manipulator + * @return a copy of debug_object that has its mode altered by qdm + */ +inline QDebug operator<<(QDebug debug_object, QDebugManip qdm) +{ + return qdm(debug_object); +} - inline qint64 profilerMinNsecs() - { - return +inline qint64 profilerMinNsecs() +{ + return #ifdef PROFILER_LOG_USECS - PROFILER_LOG_USECS + PROFILER_LOG_USECS #else - 200 + 200 #endif * 1000; - } } +} // namespace Quotient +/// \deprecated Use namespace Quotient instead +namespace QMatrixClient = Quotient; -inline QDebug operator<< (QDebug debug_object, const QElapsedTimer& et) +inline QDebug operator<<(QDebug debug_object, const QElapsedTimer& et) { auto val = et.nsecsElapsed() / 1000; if (val < 1000) diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 7d9cb360..e8aa85df 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -13,25 +13,24 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "networkaccessmanager.h" -#include <QtNetwork/QNetworkReply> #include <QtCore/QCoreApplication> +#include <QtNetwork/QNetworkReply> -using namespace QMatrixClient; +using namespace Quotient; -class NetworkAccessManager::Private -{ - public: - QList<QSslError> ignoredSslErrors; +class NetworkAccessManager::Private { +public: + QList<QSslError> ignoredSslErrors; }; NetworkAccessManager::NetworkAccessManager(QObject* parent) : QNetworkAccessManager(parent), d(std::make_unique<Private>()) -{ } +{} QList<QSslError> NetworkAccessManager::ignoredSslErrors() const { @@ -51,10 +50,13 @@ void NetworkAccessManager::clearIgnoredSslErrors() static NetworkAccessManager* createNam() { auto nam = new NetworkAccessManager(QCoreApplication::instance()); - // See #109. Once Qt bearer management gets better, this workaround - // should become unnecessary. - nam->connect(nam, &QNetworkAccessManager::networkAccessibleChanged, - [nam] { nam->setNetworkAccessible(QNetworkAccessManager::Accessible); }); +#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) + // See #109; in newer Qt, bearer management is deprecated altogether + NetworkAccessManager::connect(nam, + &QNetworkAccessManager::networkAccessibleChanged, [nam] { + nam->setNetworkAccessible(QNetworkAccessManager::Accessible); + }); +#endif return nam; } @@ -66,11 +68,10 @@ NetworkAccessManager* NetworkAccessManager::instance() NetworkAccessManager::~NetworkAccessManager() = default; -QNetworkReply* NetworkAccessManager::createRequest(Operation op, - const QNetworkRequest& request, QIODevice* outgoingData) +QNetworkReply* NetworkAccessManager::createRequest( + Operation op, const QNetworkRequest& request, QIODevice* outgoingData) { - auto reply = - QNetworkAccessManager::createRequest(op, request, outgoingData); + auto reply = QNetworkAccessManager::createRequest(op, request, outgoingData); reply->ignoreSslErrors(d->ignoredSslErrors); return reply; } diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h index ae847582..a678b80f 100644 --- a/lib/networkaccessmanager.h +++ b/lib/networkaccessmanager.h @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once @@ -22,28 +22,25 @@ #include <memory> -namespace QMatrixClient -{ - class NetworkAccessManager : public QNetworkAccessManager - { - Q_OBJECT - public: - NetworkAccessManager(QObject* parent = nullptr); - ~NetworkAccessManager() override; - - QList<QSslError> ignoredSslErrors() const; - void addIgnoredSslError(const QSslError& error); - void clearIgnoredSslErrors(); - - /** Get a pointer to the singleton */ - static NetworkAccessManager* instance(); - - private: - QNetworkReply * createRequest(Operation op, - const QNetworkRequest &request, - QIODevice *outgoingData = Q_NULLPTR) override; - - class Private; - std::unique_ptr<Private> d; - }; -} // namespace QMatrixClient +namespace Quotient { +class NetworkAccessManager : public QNetworkAccessManager { + Q_OBJECT +public: + NetworkAccessManager(QObject* parent = nullptr); + ~NetworkAccessManager() override; + + QList<QSslError> ignoredSslErrors() const; + void addIgnoredSslError(const QSslError& error); + void clearIgnoredSslErrors(); + + /** Get a pointer to the singleton */ + static NetworkAccessManager* instance(); + +private: + QNetworkReply* createRequest(Operation op, const QNetworkRequest& request, + QIODevice* outgoingData = Q_NULLPTR) override; + + class Private; + std::unique_ptr<Private> d; +}; +} // namespace Quotient diff --git a/lib/networksettings.cpp b/lib/networksettings.cpp index 6ff2bc1f..40ecba11 100644 --- a/lib/networksettings.cpp +++ b/lib/networksettings.cpp @@ -13,12 +13,12 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "networksettings.h" -using namespace QMatrixClient; +using namespace Quotient; void NetworkSettings::setupApplicationProxy() const { @@ -26,6 +26,9 @@ void NetworkSettings::setupApplicationProxy() const { proxyType(), proxyHostName(), proxyPort() }); } -QMC_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, "proxy_type", QNetworkProxy::DefaultProxy, setProxyType) -QMC_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", {}, setProxyHostName) -QMC_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, setProxyPort) +QTNT_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, + "proxy_type", QNetworkProxy::DefaultProxy, setProxyType) +QTNT_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", + {}, setProxyHostName) +QTNT_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, + setProxyPort) diff --git a/lib/networksettings.h b/lib/networksettings.h index 83613060..2399cf5f 100644 --- a/lib/networksettings.h +++ b/lib/networksettings.h @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once @@ -24,21 +24,20 @@ Q_DECLARE_METATYPE(QNetworkProxy::ProxyType) -namespace QMatrixClient { - class NetworkSettings: public SettingsGroup - { - Q_OBJECT - QMC_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType) - QMC_DECLARE_SETTING(QString, proxyHostName, setProxyHostName) - QMC_DECLARE_SETTING(quint16, proxyPort, setProxyPort) - Q_PROPERTY(QString proxyHost READ proxyHostName WRITE setProxyHostName) - public: - template <typename... ArgTs> - explicit NetworkSettings(ArgTs... qsettingsArgs) - : SettingsGroup(QStringLiteral("Network"), qsettingsArgs...) - { } - ~NetworkSettings() override = default; +namespace Quotient { +class NetworkSettings : public SettingsGroup { + Q_OBJECT + QTNT_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType) + QTNT_DECLARE_SETTING(QString, proxyHostName, setProxyHostName) + QTNT_DECLARE_SETTING(quint16, proxyPort, setProxyPort) + Q_PROPERTY(QString proxyHost READ proxyHostName WRITE setProxyHostName) +public: + template <typename... ArgTs> + explicit NetworkSettings(ArgTs... qsettingsArgs) + : SettingsGroup(QStringLiteral("Network"), qsettingsArgs...) + {} + ~NetworkSettings() override = default; - Q_INVOKABLE void setupApplicationProxy() const; - }; -} + Q_INVOKABLE void setupApplicationProxy() const; +}; +} // namespace Quotient diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index c2bde8df..699735d4 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once @@ -22,86 +22,144 @@ #include <QtCore/QPointer> -namespace QMatrixClient { - namespace _impl { - template <typename SenderT, typename SignalT, - typename ContextT, typename... ArgTs> - inline QMetaObject::Connection connectUntil( - SenderT* sender, SignalT signal, ContextT* context, - std::function<bool(ArgTs...)> slot, Qt::ConnectionType connType) - { - // See https://bugreports.qt.io/browse/QTBUG-60339 +namespace Quotient { +namespace _impl { + template <typename... ArgTs> + using decorated_slot_tt = + std::function<void(QMetaObject::Connection&, const ArgTs&...)>; + + template <typename SenderT, typename SignalT, typename ContextT, typename... ArgTs> + inline QMetaObject::Connection + connectDecorated(SenderT* sender, SignalT signal, ContextT* context, + decorated_slot_tt<ArgTs...> decoratedSlot, + Qt::ConnectionType connType) + { + // See https://bugreports.qt.io/browse/QTBUG-60339 #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) - auto pc = std::make_shared<QMetaObject::Connection>(); + auto pc = std::make_shared<QMetaObject::Connection>(); #else - auto pc = std::make_unique<QMetaObject::Connection>(); + auto pc = std::make_unique<QMetaObject::Connection>(); #endif - auto& c = *pc; // Resolve a reference before pc is moved to lambda - c = QObject::connect(sender, signal, context, - [pc=std::move(pc),slot] (ArgTs... args) { - Q_ASSERT(*pc); // If it's been triggered, it should exist - if (slot(std::forward<ArgTs>(args)...)) - QObject::disconnect(*pc); - }, connType); - return c; - } - } + auto& c = *pc; // Resolve a reference before pc is moved to lambda - template <typename SenderT, typename SignalT, - typename ContextT, typename FunctorT> - inline auto connectUntil(SenderT* sender, SignalT signal, ContextT* context, - const FunctorT& slot, - Qt::ConnectionType connType = Qt::AutoConnection) + // Perfect forwarding doesn't work through signal-slot connections - + // arguments are always copied (at best - COWed) to the context of + // the slot. Therefore the slot decorator receives const ArgTs&... + // rather than ArgTs&&... + // TODO (C++20): std::bind_front() instead of lambda. + c = QObject::connect(sender, signal, context, + [pc = std::move(pc), + decoratedSlot = std::move(decoratedSlot)](const ArgTs&... args) { + Q_ASSERT(*pc); // If it's been triggered, it should exist + decoratedSlot(*pc, args...); + }, + connType); + return c; + } + template <typename SenderT, typename SignalT, typename ContextT, + typename... ArgTs> + inline QMetaObject::Connection + connectUntil(SenderT* sender, SignalT signal, ContextT* context, + std::function<bool(ArgTs...)> functor, + Qt::ConnectionType connType) { - return _impl::connectUntil(sender, signal, context, - typename function_traits<FunctorT>::function_type(slot), - connType); + return connectDecorated(sender, signal, context, + decorated_slot_tt<ArgTs...>( + [functor = std::move(functor)](QMetaObject::Connection& c, + const ArgTs&... args) { + if (functor(args...)) + QObject::disconnect(c); + }), + connType); } - - /** Create a single-shot connection that triggers on the signal and - * then self-disconnects - * - * Only supports DirectConnection type. - */ - template <typename SenderT, typename SignalT, - typename ReceiverT, typename SlotT> - inline auto connectSingleShot(SenderT* sender, SignalT signal, - ReceiverT* receiver, SlotT slot) + template <typename SenderT, typename SignalT, typename ContextT, + typename... ArgTs> + inline QMetaObject::Connection + connectSingleShot(SenderT* sender, SignalT signal, ContextT* context, + std::function<void(ArgTs...)> slot, + Qt::ConnectionType connType) { - QMetaObject::Connection connection; - connection = QObject::connect(sender, signal, receiver, slot, - Qt::DirectConnection); - Q_ASSERT(connection); - QObject::connect(sender, signal, receiver, - [connection] { QObject::disconnect(connection); }, - Qt::DirectConnection); - return connection; + return connectDecorated(sender, signal, context, + decorated_slot_tt<ArgTs...>( + [slot = std::move(slot)](QMetaObject::Connection& c, + const ArgTs&... args) { + QObject::disconnect(c); + slot(args...); + }), + connType); } +} // namespace _impl - /** A guard pointer that disconnects an interested object upon destruction - * It's almost QPointer<> except that you have to initialise it with one - * more additional parameter - a pointer to a QObject that will be - * disconnected from signals of the underlying pointer upon the guard's - * destruction. - */ - template <typename T> - class ConnectionsGuard : public QPointer<T> - { - public: - ConnectionsGuard(T* publisher, QObject* subscriber) - : QPointer<T>(publisher), subscriber(subscriber) - { } - ~ConnectionsGuard() - { - if (*this) - (*this)->disconnect(subscriber); - } - ConnectionsGuard(ConnectionsGuard&&) = default; - ConnectionsGuard& operator=(ConnectionsGuard&&) = default; - Q_DISABLE_COPY(ConnectionsGuard) - using QPointer<T>::operator=; +/*! \brief Create a connection that self-disconnects when its "slot" returns true + * + * A slot accepted by connectUntil() is different from classic Qt slots + * in that its return value must be bool, not void. The slot's return value + * controls whether the connection should be kept; if the slot returns false, + * the connection remains; upon returning true, the slot is disconnected from + * the signal. Because of a different slot signature connectUntil() doesn't + * accept member functions as QObject::connect or Quotient::connectSingleShot + * do; you should pass a lambda or a pre-bound member function to it. + */ +template <typename SenderT, typename SignalT, typename ContextT, typename FunctorT> +inline auto connectUntil(SenderT* sender, SignalT signal, ContextT* context, + const FunctorT& slot, + Qt::ConnectionType connType = Qt::AutoConnection) +{ + return _impl::connectUntil(sender, signal, context, wrap_in_function(slot), + connType); +} - private: - QObject* subscriber; - }; +/// Create a connection that self-disconnects after triggering on the signal +template <typename SenderT, typename SignalT, typename ContextT, typename FunctorT> +inline auto connectSingleShot(SenderT* sender, SignalT signal, + ContextT* context, const FunctorT& slot, + Qt::ConnectionType connType = Qt::AutoConnection) +{ + return _impl::connectSingleShot( + sender, signal, context, wrap_in_function(slot), connType); } + +// Specialisation for usual Qt slots passed as pointers-to-members. +template <typename SenderT, typename SignalT, typename ReceiverT, + typename SlotObjectT, typename... ArgTs> +inline auto connectSingleShot(SenderT* sender, SignalT signal, + ReceiverT* receiver, + void (SlotObjectT::*slot)(ArgTs...), + Qt::ConnectionType connType = Qt::AutoConnection) +{ + // TODO: when switching to C++20, use std::bind_front() instead + return _impl::connectSingleShot(sender, signal, receiver, + wrap_in_function( + [receiver, slot](const ArgTs&... args) { + (receiver->*slot)(args...); + }), + connType); +} + +/*! \brief A guard pointer that disconnects an interested object upon destruction + * + * It's almost QPointer<> except that you have to initialise it with one + * more additional parameter - a pointer to a QObject that will be + * disconnected from signals of the underlying pointer upon the guard's + * destruction. Note that destructing the guide doesn't destruct either QObject. + */ +template <typename T> +class ConnectionsGuard : public QPointer<T> { +public: + ConnectionsGuard(T* publisher, QObject* subscriber) + : QPointer<T>(publisher), subscriber(subscriber) + {} + ~ConnectionsGuard() + { + if (*this) + (*this)->disconnect(subscriber); + } + ConnectionsGuard(ConnectionsGuard&&) = default; + ConnectionsGuard& operator=(ConnectionsGuard&&) = default; + Q_DISABLE_COPY(ConnectionsGuard) + using QPointer<T>::operator=; + +private: + QObject* subscriber; +}; +} // namespace Quotient diff --git a/lib/quotient_common.h b/lib/quotient_common.h new file mode 100644 index 00000000..bb05af05 --- /dev/null +++ b/lib/quotient_common.h @@ -0,0 +1,29 @@ +#pragma once + +#include <qobjectdefs.h> + +namespace Quotient { +Q_NAMESPACE + +/** Enumeration with flags defining the network job running policy + * So far only background/foreground flags are available. + * + * \sa Connection::callApi, Connection::run + */ +enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 }; + +Q_ENUM_NS(RunningPolicy) + +enum UriResolveResult : short { + StillResolving = -1, + UriResolved = 0, + CouldNotResolve, + IncorrectAction, + InvalidUri, + NoAccount +}; +Q_ENUM_NS(UriResolveResult) + +} // namespace Quotient +/// \deprecated Use namespace Quotient instead +namespace QMatrixClient = Quotient; diff --git a/lib/room.cpp b/lib/room.cpp index 3cabe948..c42e618e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -13,58 +13,71 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "room.h" -#include "csapi/kicking.h" -#include "csapi/inviting.h" +#include "avatar.h" +#include "connection.h" +#include "converters.h" +#include "e2ee.h" +#include "syncdata.h" +#include "user.h" + +#include "csapi/account-data.h" #include "csapi/banning.h" +#include "csapi/inviting.h" +#include "csapi/kicking.h" #include "csapi/leaving.h" #include "csapi/receipts.h" #include "csapi/redaction.h" -#include "csapi/account-data.h" -#include "csapi/room_state.h" #include "csapi/room_send.h" +#include "csapi/room_state.h" +#include "csapi/room_upgrades.h" #include "csapi/rooms.h" #include "csapi/tags.h" -#include "csapi/room_upgrades.h" -#include "events/simplestateevents.h" -#include "events/roomcreateevent.h" -#include "events/roomtombstoneevent.h" -#include "events/roomavatarevent.h" -#include "events/roommemberevent.h" -#include "events/typingevent.h" -#include "events/receiptevent.h" -#include "events/reactionevent.h" -#include "events/callinviteevent.h" -#include "events/callcandidatesevent.h" + #include "events/callanswerevent.h" +#include "events/callcandidatesevent.h" #include "events/callhangupevent.h" +#include "events/callinviteevent.h" +#include "events/encryptionevent.h" +#include "events/reactionevent.h" +#include "events/receiptevent.h" #include "events/redactionevent.h" -#include "jobs/mediathumbnailjob.h" +#include "events/roomavatarevent.h" +#include "events/roomcreateevent.h" +#include "events/roommemberevent.h" +#include "events/roomtombstoneevent.h" +#include "events/simplestateevents.h" +#include "events/typingevent.h" +#include "events/roompowerlevelsevent.h" #include "jobs/downloadfilejob.h" +#include "jobs/mediathumbnailjob.h" #include "jobs/postreadmarkersjob.h" -#include "avatar.h" -#include "connection.h" -#include "user.h" -#include "converters.h" -#include "syncdata.h" +#include "events/roomcanonicalaliasevent.h" +#include <QtCore/QDir> #include <QtCore/QHash> -#include <QtCore/QStringBuilder> // for efficient string concats (operator%) +#include <QtCore/QMimeDatabase> #include <QtCore/QPointer> -#include <QtCore/QDir> -#include <QtCore/QTemporaryFile> #include <QtCore/QRegularExpression> -#include <QtCore/QMimeDatabase> +#include <QtCore/QStringBuilder> // for efficient string concats (operator%) +#include <QtCore/QTemporaryFile> #include <array> -#include <functional> #include <cmath> +#include <functional> -using namespace QMatrixClient; +#ifdef Quotient_E2EE_ENABLED +#include <account.h> // QtOlm +#include <errors.h> // QtOlm +#include <groupsession.h> // QtOlm +#endif // Quotient_E2EE_ENABLED + +using namespace Quotient; +using namespace QtOlm; using namespace std::placeholders; using std::move; #if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123) @@ -73,259 +86,386 @@ using std::llround; enum EventsPlacement : int { Older = -1, Newer = 1 }; -class Room::Private -{ - public: - /** Map of user names to users. User names potentially duplicate, hence a multi-hashmap. */ - using members_map_t = QMultiHash<QString, User*>; - - Private(Connection* c, QString id_, JoinState initialJoinState) - : q(nullptr), connection(c), id(move(id_)) - , joinState(initialJoinState) - { } - - Room* q; - - Connection* connection; - QString id; - JoinState joinState; - RoomSummary summary = { none, 0, none }; - /// The state of the room at timeline position before-0 - /// \sa timelineBase - std::unordered_map<StateEventKey, StateEventPtr> baseState; - /// The state of the room at timeline position after-maxTimelineIndex() - /// \sa Room::syncEdge - QHash<StateEventKey, const StateEventBase*> currentState; - Timeline timeline; - PendingEvents unsyncedEvents; - QHash<QString, TimelineItem::index_t> eventsIndex; - // A map from evtId to a map of relation type to a vector of event - // pointers. Not using QMultiHash, because we want to quickly return - // a number of relations for a given event without enumerating them. - QHash<QPair<QString, QString>, RelatedEvents> relations; - QString displayname; - Avatar avatar; - int highlightCount = 0; - int notificationCount = 0; - members_map_t membersMap; - QList<User*> usersTyping; - QMultiHash<QString, User*> eventIdReadUsers; - QList<User*> usersInvited; - QList<User*> membersLeft; - int unreadMessages = 0; - bool displayed = false; - QString firstDisplayedEventId; - QString lastDisplayedEventId; - QHash<const User*, QString> lastReadEventIds; - QString serverReadMarker; - TagsMap tags; - std::unordered_map<QString, EventPtr> accountData; - QString prevBatch; - QPointer<GetRoomEventsJob> eventsHistoryJob; - QPointer<GetMembersByRoomJob> allMembersJob; - - struct FileTransferPrivateInfo +class Room::Private { +public: + /// Map of user names to users + /** User names potentially duplicate, hence QMultiHash. */ + using members_map_t = QMultiHash<QString, User*>; + + Private(Connection* c, QString id_, JoinState initialJoinState) + : q(nullptr), connection(c), id(move(id_)), joinState(initialJoinState) + {} + + Room* q; + + Connection* connection; + QString id; + JoinState joinState; + RoomSummary summary = { none, 0, none }; + /// The state of the room at timeline position before-0 + /// \sa timelineBase + UnorderedMap<StateEventKey, StateEventPtr> baseState; + /// State event stubs - events without content, just type and state key + static decltype(baseState) stubbedState; + /// The state of the room at timeline position after-maxTimelineIndex() + /// \sa Room::syncEdge + QHash<StateEventKey, const StateEventBase*> currentState; + /// Servers with aliases for this room except the one of the local user + /// \sa Room::remoteAliases + QSet<QString> aliasServers; + + Timeline timeline; + PendingEvents unsyncedEvents; + QHash<QString, TimelineItem::index_t> eventsIndex; + // A map from evtId to a map of relation type to a vector of event + // pointers. Not using QMultiHash, because we want to quickly return + // a number of relations for a given event without enumerating them. + QHash<QPair<QString, QString>, RelatedEvents> relations; + QString displayname; + Avatar avatar; + int highlightCount = 0; + int notificationCount = 0; + members_map_t membersMap; + QList<User*> usersTyping; + QMultiHash<QString, User*> eventIdReadUsers; + QList<User*> usersInvited; + QList<User*> membersLeft; + int unreadMessages = 0; + bool displayed = false; + QString firstDisplayedEventId; + QString lastDisplayedEventId; + QHash<const User*, QString> lastReadEventIds; + QString serverReadMarker; + TagsMap tags; + UnorderedMap<QString, EventPtr> accountData; + QString prevBatch; + QPointer<GetRoomEventsJob> eventsHistoryJob; + QPointer<GetMembersByRoomJob> allMembersJob; + + struct FileTransferPrivateInfo { + FileTransferPrivateInfo() = default; + FileTransferPrivateInfo(BaseJob* j, const QString& fileName, + bool isUploading = false) + : status(FileTransferInfo::Started) + , job(j) + , localFileInfo(fileName) + , isUpload(isUploading) + {} + + FileTransferInfo::Status status = FileTransferInfo::None; + QPointer<BaseJob> job = nullptr; + QFileInfo localFileInfo {}; + bool isUpload = false; + qint64 progress = 0; + qint64 total = -1; + + void update(qint64 p, qint64 t) { - FileTransferPrivateInfo() = default; - FileTransferPrivateInfo(BaseJob* j, const QString& fileName, - bool isUploading = false) - : status(FileTransferInfo::Started), job(j) - , localFileInfo(fileName), isUpload(isUploading) - { } - - FileTransferInfo::Status status = FileTransferInfo::None; - QPointer<BaseJob> job = nullptr; - QFileInfo localFileInfo { }; - bool isUpload = false; - qint64 progress = 0; - qint64 total = -1; - - void update(qint64 p, qint64 t) - { - if (t == 0) - { - t = -1; - if (p == 0) - p = -1; - } - if (p != -1) - qCDebug(PROFILER) << "Transfer progress:" << p << "/" << t - << "=" << llround(double(p) / t * 100) << "%"; - progress = p; total = t; + if (t == 0) { + t = -1; + if (p == 0) + p = -1; } - }; - void failedTransfer(const QString& tid, const QString& errorMessage = {}) - { - qCWarning(MAIN) << "File transfer failed for id" << tid; - if (!errorMessage.isEmpty()) - qCWarning(MAIN) << "Message:" << errorMessage; - fileTransfers[tid].status = FileTransferInfo::Failed; - emit q->fileTransferFailed(tid, errorMessage); + if (p != -1) + qCDebug(PROFILER) << "Transfer progress:" << p << "/" << t + << "=" << llround(double(p) / t * 100) << "%"; + progress = p; + total = t; } - /// A map from event/txn ids to information about the long operation; - /// used for both download and upload operations - QHash<QString, FileTransferPrivateInfo> fileTransfers; + }; + void failedTransfer(const QString& tid, const QString& errorMessage = {}) + { + qCWarning(MAIN) << "File transfer failed for id" << tid; + if (!errorMessage.isEmpty()) + qCWarning(MAIN) << "Message:" << errorMessage; + fileTransfers[tid].status = FileTransferInfo::Failed; + emit q->fileTransferFailed(tid, errorMessage); + } + /// A map from event/txn ids to information about the long operation; + /// used for both download and upload operations + QHash<QString, FileTransferPrivateInfo> fileTransfers; - const RoomMessageEvent* getEventWithFile(const QString& eventId) const; - QString fileNameToDownload(const RoomMessageEvent* event) const; + const RoomMessageEvent* getEventWithFile(const QString& eventId) const; + QString fileNameToDownload(const RoomMessageEvent* event) const; - Changes setSummary(RoomSummary&& newSummary); + Changes setSummary(RoomSummary&& newSummary); - //void inviteUser(User* u); // We might get it at some point in time. - void insertMemberIntoMap(User* u); - void renameMember(User* u, const QString& oldName); - void removeMemberFromMap(const QString& username, User* u); + // void inviteUser(User* u); // We might get it at some point in time. + void insertMemberIntoMap(User* u); + void removeMemberFromMap(User* u); - // This updates the room displayname field (which is the way a room - // should be shown in the room list); called whenever the list of - // members, the room name (m.room.name) or canonical alias change. - void updateDisplayname(); - // This is used by updateDisplayname() but only calculates the new name - // without any updates. - QString calculateDisplayname() const; + // This updates the room displayname field (which is the way a room + // should be shown in the room list); called whenever the list of + // members, the room name (m.room.name) or canonical alias change. + void updateDisplayname(); + // This is used by updateDisplayname() but only calculates the new name + // without any updates. + QString calculateDisplayname() const; - /// A point in the timeline corresponding to baseState - rev_iter_t timelineBase() const { return q->findInTimeline(-1); } + /// A point in the timeline corresponding to baseState + rev_iter_t timelineBase() const { return q->findInTimeline(-1); } - void getPreviousContent(int limit = 10); - bool allHistoryLoaded() const; + void getPreviousContent(int limit = 10); - template <typename EventT> - const EventT* getCurrentState(const QString& stateKey = {}) const - { - static const EventT empty; - const auto* evt = - currentState.value({EventT::matrixTypeId(), stateKey}, &empty); - Q_ASSERT(evt->type() == EventT::typeId() && - evt->matrixType() == EventT::matrixTypeId()); - return static_cast<const EventT*>(evt); + const StateEventBase* getCurrentState(const StateEventKey& evtKey) const + { + const auto* evt = currentState.value(evtKey, nullptr); + if (!evt) { + if (stubbedState.find(evtKey) == stubbedState.end()) { + // In the absence of a real event, make a stub as-if an event + // with empty content has been received. Event classes should be + // prepared for empty/invalid/malicious content anyway. + stubbedState.emplace(evtKey, loadStateEvent(evtKey.first, {}, + evtKey.second)); + qCDebug(STATE) << "A new stub event created for key {" + << evtKey.first << evtKey.second << "}"; + } + evt = stubbedState[evtKey].get(); + Q_ASSERT(evt); } + Q_ASSERT(evt->matrixType() == evtKey.first + && evt->stateKey() == evtKey.second); + return evt; + } - bool isEventNotable(const TimelineItem& ti) const - { - return !ti->isRedacted() && - ti->senderId() != connection->userId() && - is<RoomMessageEvent>(*ti); + template <typename EventT> + const EventT* getCurrentState(const QString& stateKey = {}) const + { + const StateEventKey evtKey { EventT::matrixTypeId(), stateKey }; + const auto* evt = currentState.value(evtKey, nullptr); + if (!evt) { + if (stubbedState.find(evtKey) == stubbedState.end()) { + // In the absence of a real event, make a stub as-if an event + // with empty content has been received. Event classes should be + // prepared for empty/invalid/malicious content anyway. + stubbedState.emplace( + evtKey, makeEvent<EventT>(basicStateEventJson( + EventT::matrixTypeId(), {}, evtKey.second))); + qCDebug(STATE) << "A new stub event created for key {" + << evtKey.first << evtKey.second << "}"; + } + evt = stubbedState[evtKey].get(); + Q_ASSERT(evt); } + Q_ASSERT(evt->type() == EventT::typeId() + && evt->matrixType() == EventT::matrixTypeId() + && evt->stateKey() == stateKey); + return static_cast<const EventT*>(evt); + } - template <typename EventArrayT> - Changes updateStateFrom(EventArrayT&& events) - { - Changes changes = NoChange; - if (!events.empty()) - { - QElapsedTimer et; et.start(); - for (auto&& eptr: events) - { - const auto& evt = *eptr; - Q_ASSERT(evt.isStateEvent()); - // Update baseState afterwards to make sure that the old state - // is valid and usable inside processStateEvent - changes |= q->processStateEvent(evt); - baseState[{evt.matrixType(),evt.stateKey()}] = move(eptr); - } - if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::Private::updateStateFrom():" - << events.size() << "event(s)," << et; +// template <typename EventT> +// const auto& getCurrentStateContent(const QString& stateKey = {}) const +// { +// if (const auto* evt = +// currentState.value({ EventT::matrixTypeId(), stateKey }, nullptr)) +// return evt->content(); +// return EventT::content_type() +// } + + bool isEventNotable(const TimelineItem& ti) const + { + return !ti->isRedacted() && ti->senderId() != connection->userId() + && is<RoomMessageEvent>(*ti) + && ti.viewAs<RoomMessageEvent>()->replacedEvent().isEmpty(); + } + + template <typename EventArrayT> + Changes updateStateFrom(EventArrayT&& events) + { + Changes changes = NoChange; + if (!events.empty()) { + QElapsedTimer et; + et.start(); + for (auto&& eptr : events) { + const auto& evt = *eptr; + Q_ASSERT(evt.isStateEvent()); + // Update baseState afterwards to make sure that the old state + // is valid and usable inside processStateEvent + changes |= q->processStateEvent(evt); + baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr); } - return changes; - } - Changes addNewMessageEvents(RoomEvents&& events); - void addHistoricalMessageEvents(RoomEvents&& events); - - /** Move events into the timeline - * - * Insert events into the timeline, either new or historical. - * Pointers in the original container become empty, the ownership - * is passed to the timeline container. - * @param events - the range of events to be inserted - * @param placement - position and direction of insertion: Older for - * historical messages, Newer for new ones - */ - Timeline::size_type moveEventsToTimeline(RoomEventsRange events, - EventsPlacement placement); - - /** - * Remove events from the passed container that are already in the timeline - */ - void dropDuplicateEvents(RoomEvents& events) const; - - Changes setLastReadEvent(User* u, QString eventId); - void updateUnreadCount(rev_iter_t from, rev_iter_t to); - Changes promoteReadMarker(User* u, rev_iter_t newMarker, - bool force = false); - - Changes markMessagesAsRead(rev_iter_t upToMarker); - - void getAllMembers(); - - QString sendEvent(RoomEventPtr&& event); - - template <typename EventT, typename... ArgTs> - QString sendEvent(ArgTs&&... eventArgs) - { - return sendEvent(makeEvent<EventT>(std::forward<ArgTs>(eventArgs)...)); + if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) + << "*** Room::Private::updateStateFrom():" << events.size() + << "event(s)," << et; } + return changes; + } + Changes addNewMessageEvents(RoomEvents&& events); + void addHistoricalMessageEvents(RoomEvents&& events); + + /** Move events into the timeline + * + * Insert events into the timeline, either new or historical. + * Pointers in the original container become empty, the ownership + * is passed to the timeline container. + * @param events - the range of events to be inserted + * @param placement - position and direction of insertion: Older for + * historical messages, Newer for new ones + */ + Timeline::size_type moveEventsToTimeline(RoomEventsRange events, + EventsPlacement placement); + + /** + * Remove events from the passed container that are already in the timeline + */ + void dropDuplicateEvents(RoomEvents& events) const; + + Changes setLastReadEvent(User* u, QString eventId); + void updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to); + Changes promoteReadMarker(User* u, const rev_iter_t& newMarker, bool force = false); + + Changes markMessagesAsRead(rev_iter_t upToMarker); + + void getAllMembers(); + + QString sendEvent(RoomEventPtr&& event); + + template <typename EventT, typename... ArgTs> + QString sendEvent(ArgTs&&... eventArgs) + { + return sendEvent(makeEvent<EventT>(std::forward<ArgTs>(eventArgs)...)); + } - RoomEvent* addAsPending(RoomEventPtr&& event); + RoomEvent* addAsPending(RoomEventPtr&& event); - QString doSendEvent(const RoomEvent* pEvent); - void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); + QString doSendEvent(const RoomEvent* pEvent); + void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); - template <typename EvT> - SetRoomStateWithKeyJob* requestSetState(const QString& stateKey, - const EvT& event) - { - if (q->successorId().isEmpty()) - { - // TODO: Queue up state events sending (see #133). - return connection->callApi<SetRoomStateWithKeyJob>( - id, EvT::matrixTypeId(), stateKey, event.contentJson()); - } - qCWarning(MAIN) << q << "has been upgraded, state won't be set"; - return nullptr; + SetRoomStateWithKeyJob* requestSetState(const StateEventBase& event) + { + // if (event.roomId().isEmpty()) + // event.setRoomId(id); + // if (event.senderId().isEmpty()) + // event.setSender(connection->userId()); + // TODO: Queue up state events sending (see #133). + // TODO: Maybe addAsPending() as well, despite having no txnId + return connection->callApi<SetRoomStateWithKeyJob>( + id, event.matrixType(), event.stateKey(), event.contentJson()); + } + + template <typename EvT, typename... ArgTs> + auto requestSetState(ArgTs&&... args) + { + return requestSetState(EvT(std::forward<ArgTs>(args)...)); + } + + /*! Apply redaction to the timeline + * + * Tries to find an event in the timeline and redact it; deletes the + * redaction event whether the redacted event was found or not. + * \return true if the event has been found and redacted; false otherwise + */ + bool processRedaction(const RedactionEvent& redaction); + + /*! Apply a new revision of the event to the timeline + * + * Tries to find an event in the timeline and replace it with the new + * content passed in \p newMessage. + * \return true if the event has been found and replaced; false otherwise + */ + bool processReplacement(const RoomMessageEvent& newEvent); + + void setTags(TagsMap&& newTags); + + QJsonObject toJson() const; + + bool isLocalUser(const User* u) const { return u == q->localUser(); } + +#ifdef Quotient_E2EE_ENABLED + // A map from <sessionId, messageIndex> to <event_id, origin_server_ts> + QHash<QPair<QString, uint32_t>, QPair<QString, QDateTime>> + groupSessionIndexRecord; // TODO: cache + // A map from senderKey to a map of sessionId to InboundGroupSession + // Not using QMultiHash, because we want to quickly return + // a number of relations for a given event without enumerating them. + QHash<QPair<QString, QString>, InboundGroupSession*> groupSessions; // TODO: + // cache + bool addInboundGroupSession(QString senderKey, QString sessionId, + QString sessionKey) + { + if (groupSessions.contains({ senderKey, sessionId })) { + qCDebug(E2EE) << "Inbound Megolm session" << sessionId + << "with senderKey" << senderKey << "already exists"; + return false; } - template <typename EvT> - auto requestSetState(const EvT& event) - { - return connection->callApi<SetRoomStateJob>( - id, EvT::matrixTypeId(), event.contentJson()); + InboundGroupSession* megolmSession; + try { + megolmSession = new InboundGroupSession(sessionKey.toLatin1(), + InboundGroupSession::Init, + q); + } catch (OlmError* e) { + qCDebug(E2EE) << "Unable to create new InboundGroupSession" + << e->what(); + return false; } + if (megolmSession->id() != sessionId) { + qCDebug(E2EE) << "Session ID mismatch in m.room_key event sent " + "from sender with key" + << senderKey; + return false; + } + groupSessions.insert({ senderKey, sessionId }, megolmSession); + return true; + } - /** - * @brief Apply redaction to the timeline - * - * Tries to find an event in the timeline and redact it; deletes the - * redaction event whether the redacted event was found or not. - * \return true if the event has been found and redacted; false otherwise - */ - bool processRedaction(const RedactionEvent& redaction); - - /*! Apply a new revision of the event to the timeline - * - * Tries to find an event in the timeline and replace it with the new - * content passed in \p newMessage. - * \return true if the event has been found and replaced; false otherwise - */ - bool processReplacement(const RoomMessageEvent& newMessage); - - void setTags(TagsMap newTags); - - QJsonObject toJson() const; - - private: - using users_shortlist_t = std::array<User*, 3>; - template<typename ContT> - users_shortlist_t buildShortlist(const ContT& users) const; - users_shortlist_t buildShortlist(const QStringList& userIds) const; - - bool isLocalUser(const User* u) const - { - return u == q->localUser(); + QString groupSessionDecryptMessage(QByteArray cipher, + const QString& senderKey, + const QString& sessionId, + const QString& eventId, + QDateTime timestamp) + { + std::pair<QString, uint32_t> decrypted; + QPair<QString, QString> senderSessionPairKey = + qMakePair(senderKey, sessionId); + if (!groupSessions.contains(senderSessionPairKey)) { + qCDebug(E2EE) << "Unable to decrypt event" << eventId + << "The sender's device has not sent us the keys for " + "this message"; + return QString(); + } + InboundGroupSession* senderSession = + groupSessions.value(senderSessionPairKey); + if (!senderSession) { + qCDebug(E2EE) << "Unable to decrypt event" << eventId + << "senderSessionPairKey:" << senderSessionPairKey; + return QString(); } + try { + decrypted = senderSession->decrypt(cipher); + } catch (OlmError* e) { + qCDebug(E2EE) << "Unable to decrypt event" << eventId + << "with matching megolm session:" << e->what(); + return QString(); + } + QPair<QString, QDateTime> properties = groupSessionIndexRecord.value( + qMakePair(senderSession->id(), decrypted.second)); + if (properties.first.isEmpty()) { + groupSessionIndexRecord.insert(qMakePair(senderSession->id(), + decrypted.second), + qMakePair(eventId, timestamp)); + } else { + if ((properties.first != eventId) + || (properties.second != timestamp)) { + qCDebug(E2EE) << "Detected a replay attack on event" << eventId; + return QString(); + } + } + + return decrypted.first; + } +#endif // Quotient_E2EE_ENABLED + +private: + using users_shortlist_t = std::array<User*, 3>; + template <typename ContT> + users_shortlist_t buildShortlist(const ContT& users) const; + users_shortlist_t buildShortlist(const QStringList& userIds) const; }; +decltype(Room::Private::baseState) Room::Private::stubbedState {}; + Room::Room(Connection* connection, QString id, JoinState initialJoinState) : QObject(connection), d(new Private(connection, id, initialJoinState)) { @@ -334,24 +474,17 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/ d->q = this; d->displayname = d->calculateDisplayname(); // Set initial "Empty room" name - connectUntil(connection, &Connection::loadedRoomState, this, - [this] (Room* r) { - if (this == r) - emit baseStateLoaded(); - return this == r; // loadedRoomState fires only once per room - }); - qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id; + connectUntil(connection, &Connection::loadedRoomState, this, [this](Room* r) { + if (this == r) + emit baseStateLoaded(); + return this == r; // loadedRoomState fires only once per room + }); + qCDebug(STATE) << "New" << toCString(initialJoinState) << "Room:" << id; } -Room::~Room() -{ - delete d; -} +Room::~Room() { delete d; } -const QString& Room::id() const -{ - return d->id; -} +const QString& Room::id() const { return d->id; } QString Room::version() const { @@ -361,8 +494,8 @@ QString Room::version() const bool Room::isUnstable() const { - return !connection()->loadingCapabilities() && - !connection()->stableRoomVersions().contains(version()); + return !connection()->loadingCapabilities() + && !connection()->stableRoomVersions().contains(version()); } QString Room::predecessorId() const @@ -370,24 +503,41 @@ QString Room::predecessorId() const return d->getCurrentState<RoomCreateEvent>()->predecessor().roomId; } +Room* Room::predecessor(JoinStates statesFilter) const +{ + if (const auto& predId = predecessorId(); !predId.isEmpty()) + if (auto* r = connection()->room(predId, statesFilter); + r && r->successorId() == id()) + return r; + + return nullptr; +} + QString Room::successorId() const { return d->getCurrentState<RoomTombstoneEvent>()->successorRoomId(); } -const Room::Timeline& Room::messageEvents() const +Room* Room::successor(JoinStates statesFilter) const { - return d->timeline; + if (const auto& succId = successorId(); !succId.isEmpty()) + if (auto* r = connection()->room(succId, statesFilter); + r && r->predecessorId() == id()) + return r; + + return nullptr; } +const Room::Timeline& Room::messageEvents() const { return d->timeline; } + const Room::PendingEvents& Room::pendingEvents() const { return d->unsyncedEvents; } -bool Room::Private::allHistoryLoaded() const +bool Room::allHistoryLoaded() const { - return !timeline.empty() && is<RoomCreateEvent>(*timeline.front()); + return !d->timeline.empty() && is<RoomCreateEvent>(*d->timeline.front()); } QString Room::name() const @@ -398,57 +548,53 @@ QString Room::name() const QStringList Room::aliases() const { const auto* evt = d->getCurrentState<RoomCanonicalAliasEvent>(); - auto aliases = fromJson<QStringList>(evt->contentJson()["alt_aliases"]); + auto result = evt->altAliases(); if (!evt->alias().isEmpty()) - aliases << evt->alias(); - return aliases; + result << evt->alias(); + return result; } QStringList Room::altAliases() const { - const auto* evt = d->getCurrentState<RoomCanonicalAliasEvent>(); - return fromJson<QStringList>(evt->contentJson()["alt_aliases"]); + return d->getCurrentState<RoomCanonicalAliasEvent>()->altAliases(); } -QString Room::canonicalAlias() const +QStringList Room::localAliases() const { - return d->getCurrentState<RoomCanonicalAliasEvent>()->alias(); + return d->getCurrentState<RoomAliasesEvent>( + connection()->domain()) + ->aliases(); } -QString Room::displayName() const +QStringList Room::remoteAliases() const { - return d->displayname; + QStringList result; + for (const auto& s : std::as_const(d->aliasServers)) + result += d->getCurrentState<RoomAliasesEvent>(s)->aliases(); + return result; } -void Room::refreshDisplayName() +QString Room::canonicalAlias() const { - d->updateDisplayname(); + return d->getCurrentState<RoomCanonicalAliasEvent>()->alias(); } +QString Room::displayName() const { return d->displayname; } + +void Room::refreshDisplayName() { d->updateDisplayname(); } + QString Room::topic() const { return d->getCurrentState<RoomTopicEvent>()->topic(); } -QString Room::avatarMediaId() const -{ - return d->avatar.mediaId(); -} +QString Room::avatarMediaId() const { return d->avatar.mediaId(); } -QUrl Room::avatarUrl() const -{ - return d->avatar.url(); -} +QUrl Room::avatarUrl() const { return d->avatar.url(); } -const Avatar& Room::avatarObject() const -{ - return d->avatar; -} +const Avatar& Room::avatarObject() const { return d->avatar; } -QImage Room::avatar(int dimension) -{ - return avatar(dimension, dimension); -} +QImage Room::avatar(int dimension) { return avatar(dimension, dimension); } QImage Room::avatar(int width, int height) { @@ -458,7 +604,7 @@ QImage Room::avatar(int width, int height) // Use the first (excluding self) user's avatar for direct chats const auto dcUsers = directChatUsers(); - for (auto* u: dcUsers) + for (auto* u : dcUsers) if (u != localUser()) return u->avatar(width, height, this, [=] { emit avatarChanged(); }); @@ -472,24 +618,20 @@ User* Room::user(const QString& userId) const JoinState Room::memberJoinState(User* user) const { - return - d->membersMap.contains(user->name(this), user) ? JoinState::Join : - JoinState::Leave; + return d->membersMap.contains(user->name(this), user) ? JoinState::Join + : JoinState::Leave; } -JoinState Room::joinState() const -{ - return d->joinState; -} +JoinState Room::joinState() const { return d->joinState; } void Room::setJoinState(JoinState state) { JoinState oldState = d->joinState; - if( state == oldState ) + if (state == oldState) return; d->joinState = state; - qCDebug(MAIN) << "Room" << id() << "changed state: " - << int(oldState) << "->" << int(state); + qCDebug(STATE) << "Room" << id() << "changed state: " << int(oldState) + << "->" << int(state); emit changed(Change::JoinStateChange); emit joinStateChanged(oldState, state); } @@ -504,17 +646,18 @@ Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId) swap(storedId, eventId); emit q->lastReadEventChanged(u); emit q->readMarkerForUserMoved(u, eventId, storedId); - if (isLocalUser(u)) - { + if (isLocalUser(u)) { if (storedId != serverReadMarker) - connection->callApi<PostReadMarkersJob>(id, storedId); + connection->callApi<PostReadMarkersJob>(BackgroundRequest, id, + storedId); emit q->readMarkerMoved(eventId, storedId); return Change::ReadMarkerChange; } return Change::NoChange; } -void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) +void Room::Private::updateUnreadCount(const rev_iter_t& from, + const rev_iter_t& to) { Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend()); Q_ASSERT(to >= from && to <= timeline.crend()); @@ -524,78 +667,81 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) // unreadMessages and might need to promote the read marker further // over local-origin messages. auto readMarker = q->readMarker(); - if (readMarker == timeline.crend() && allHistoryLoaded()) + if (readMarker == timeline.crend() && q->allHistoryLoaded()) --readMarker; // Read marker not found in the timeline, initialise it - if (readMarker >= from && readMarker < to) - { + if (readMarker >= from && readMarker < to) { promoteReadMarker(q->localUser(), readMarker, true); return; } Q_ASSERT(to <= readMarker); - QElapsedTimer et; et.start(); - const auto newUnreadMessages = count_if(from, to, - std::bind(&Room::Private::isEventNotable, this, _1)); + QElapsedTimer et; + et.start(); + const auto newUnreadMessages = + count_if(from, to, std::bind(&Room::Private::isEventNotable, this, _1)); if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Counting gained unread messages took" << et; - if(newUnreadMessages > 0) - { - // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count + if (newUnreadMessages > 0) { + // See https://github.com/quotient-im/libQuotient/wiki/unread_count if (unreadMessages < 0) unreadMessages = 0; unreadMessages += newUnreadMessages; - qCDebug(MAIN) << "Room" << q->objectName() << "has gained" - << newUnreadMessages << "unread message(s)," - << (q->readMarker() == timeline.crend() ? - "in total at least" : "in total") - << unreadMessages << "unread message(s)"; + qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained" + << newUnreadMessages << "unread message(s)," + << (q->readMarker() == timeline.crend() + ? "in total at least" + : "in total") + << unreadMessages << "unread message(s)"; emit q->unreadMessagesChanged(q); } } -Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, +Room::Changes Room::Private::promoteReadMarker(User* u, + const rev_iter_t& newMarker, bool force) { Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr"); Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend()); const auto prevMarker = q->readMarker(u); - if (!force && prevMarker <= newMarker) // Remember, we deal with reverse iterators + if (!force && prevMarker <= newMarker) // Remember, we deal with reverse + // iterators return Change::NoChange; Q_ASSERT(newMarker < timeline.crend()); // Try to auto-promote the read marker over the user's own messages // (switch to direct iterators for that). - auto eagerMarker = find_if(newMarker.base(), timeline.cend(), - [=](const TimelineItem& ti) { return ti->senderId() != u->id(); }); + auto eagerMarker = + find_if(newMarker.base(), timeline.cend(), [=](const TimelineItem& ti) { + return ti->senderId() != u->id(); + }); auto changes = setLastReadEvent(u, (*(eagerMarker - 1))->id()); - if (isLocalUser(u)) - { + if (isLocalUser(u)) { const auto oldUnreadCount = unreadMessages; - QElapsedTimer et; et.start(); - unreadMessages = int(count_if(eagerMarker, timeline.cend(), - std::bind(&Room::Private::isEventNotable, this, _1))); + QElapsedTimer et; + et.start(); + unreadMessages = + int(count_if(eagerMarker, timeline.cend(), + [this](const auto& ti) { return isEventNotable(ti); })); if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Recounting unread messages took" << et; - // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count + // See https://github.com/quotient-im/libQuotient/wiki/unread_count if (unreadMessages == 0) unreadMessages = -1; - if (force || unreadMessages != oldUnreadCount) - { - if (unreadMessages == -1) - { - qCDebug(MAIN) << "Room" << displayname - << "has no more unread messages"; + if (force || unreadMessages != oldUnreadCount) { + if (unreadMessages == -1) { + qCDebug(MESSAGES) + << "Room" << displayname << "has no more unread messages"; } else - qCDebug(MAIN) << "Room" << displayname << "still has" - << unreadMessages << "unread message(s)"; + qCDebug(MESSAGES) << "Room" << displayname << "still has" + << unreadMessages << "unread message(s)"; emit q->unreadMessagesChanged(q); changes |= Change::UnreadNotifsChange; } @@ -608,17 +754,17 @@ Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker) const auto prevMarker = q->readMarker(); auto changes = promoteReadMarker(q->localUser(), upToMarker); if (prevMarker != upToMarker) - qCDebug(MAIN) << "Marked messages as read until" << *q->readMarker(); + qCDebug(MESSAGES) << "Marked messages as read until" << *q->readMarker(); // We shouldn't send read receipts for the local user's own messages - so // search earlier messages for the latest message not from the local user // until the previous last-read message, whichever comes first. - for (; upToMarker < prevMarker; ++upToMarker) - { - if ((*upToMarker)->senderId() != q->localUser()->id()) - { - connection->callApi<PostReceiptJob>(id, QStringLiteral("m.read"), - QUrl::toPercentEncoding((*upToMarker)->id())); + for (; upToMarker < prevMarker; ++upToMarker) { + if ((*upToMarker)->senderId() != q->localUser()->id()) { + connection->callApi<PostReceiptJob>(BackgroundRequest, + id, QStringLiteral("m.read"), + QUrl::toPercentEncoding( + (*upToMarker)->id())); break; } } @@ -639,50 +785,29 @@ void Room::markAllMessagesAsRead() bool Room::canSwitchVersions() const { if (!successorId().isEmpty()) - return false; // Noone can upgrade a room that's already upgraded + return false; // No one can upgrade a room that's already upgraded - // TODO, #276: m.room.power_levels - const auto* plEvt = - d->currentState.value({QStringLiteral("m.room.power_levels"), {}}); - if (!plEvt) - return true; - - const auto plJson = plEvt->contentJson(); - const auto currentUserLevel = - plJson.value("users"_ls).toObject() - .value(localUser()->id()).toInt( - plJson.value("users_default"_ls).toInt()); - const auto tombstonePowerLevel = - plJson.value("events"_ls).toObject() - .value("m.room.tombstone"_ls).toInt( - plJson.value("state_default"_ls).toInt()); - return currentUserLevel >= tombstonePowerLevel; + if (const auto* plEvt = d->getCurrentState<RoomPowerLevelsEvent>()) { + const auto currentUserLevel = plEvt->powerLevelForUser(localUser()->id()); + const auto tombstonePowerLevel = + plEvt->powerLevelForState("m.room.tombstone"_ls); + return currentUserLevel >= tombstonePowerLevel; + } + return true; } -bool Room::hasUnreadMessages() const -{ - return unreadCount() >= 0; -} +bool Room::hasUnreadMessages() const { return unreadCount() >= 0; } -int Room::unreadCount() const -{ - return d->unreadMessages; -} +int Room::unreadCount() const { return d->unreadMessages; } -Room::rev_iter_t Room::historyEdge() const -{ - return d->timeline.crend(); -} +Room::rev_iter_t Room::historyEdge() const { return d->timeline.crend(); } Room::Timeline::const_iterator Room::syncEdge() const { return d->timeline.cend(); } -Room::rev_iter_t Room::timelineEdge() const -{ - return historyEdge(); -} +Room::rev_iter_t Room::timelineEdge() const { return historyEdge(); } TimelineItem::index_t Room::minTimelineIndex() const { @@ -696,21 +821,19 @@ TimelineItem::index_t Room::maxTimelineIndex() const bool Room::isValidIndex(TimelineItem::index_t timelineIndex) const { - return !d->timeline.empty() && - timelineIndex >= minTimelineIndex() && - timelineIndex <= maxTimelineIndex(); + return !d->timeline.empty() && timelineIndex >= minTimelineIndex() + && timelineIndex <= maxTimelineIndex(); } Room::rev_iter_t Room::findInTimeline(TimelineItem::index_t index) const { - return timelineEdge() - - (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0); + return timelineEdge() + - (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0); } Room::rev_iter_t Room::findInTimeline(const QString& evtId) const { - if (!d->timeline.empty() && d->eventsIndex.contains(evtId)) - { + if (!d->timeline.empty() && d->eventsIndex.contains(evtId)) { auto it = findInTimeline(d->eventsIndex.value(evtId)); Q_ASSERT(it != historyEdge() && (*it)->id() == evtId); return it; @@ -721,14 +844,18 @@ Room::rev_iter_t Room::findInTimeline(const QString& evtId) const Room::PendingEvents::iterator Room::findPendingEvent(const QString& txnId) { return std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), - [txnId] (const auto& item) { return item->transactionId() == txnId; }); + [txnId](const auto& item) { + return item->transactionId() == txnId; + }); } Room::PendingEvents::const_iterator Room::findPendingEvent(const QString& txnId) const { return std::find_if(d->unsyncedEvents.cbegin(), d->unsyncedEvents.cend(), - [txnId] (const auto& item) { return item->transactionId() == txnId; }); + [txnId](const auto& item) { + return item->transactionId() == txnId; + }); } const Room::RelatedEvents Room::relatedEvents(const QString& evtId, @@ -750,28 +877,25 @@ void Room::Private::getAllMembers() return; allMembersJob = connection->callApi<GetMembersByRoomJob>( - id, connection->nextBatchToken(), "join"); + id, connection->nextBatchToken(), "join"); auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1; - connect( allMembersJob, &BaseJob::success, q, [=] { + connect(allMembersJob, &BaseJob::success, q, [=] { Q_ASSERT(timeline.empty() || nextIndex <= q->maxTimelineIndex() + 1); auto roomChanges = updateStateFrom(allMembersJob->chunk()); // Replay member events that arrived after the point for which // the full members list was requested. - if (!timeline.empty() ) + if (!timeline.empty()) for (auto it = q->findInTimeline(nextIndex).base(); it != timeline.cend(); ++it) if (is<RoomMemberEvent>(**it)) roomChanges |= q->processStateEvent(**it); - if (roomChanges&MembersChange) + if (roomChanges & MembersChange) emit q->memberListChanged(); emit q->allMembersLoaded(); }); } -bool Room::displayed() const -{ - return d->displayed; -} +bool Room::displayed() const { return d->displayed; } void Room::setDisplayed(bool displayed) { @@ -780,18 +904,14 @@ void Room::setDisplayed(bool displayed) d->displayed = displayed; emit displayedChanged(displayed); - if( displayed ) - { + if (displayed) { resetHighlightCount(); resetNotificationCount(); d->getAllMembers(); } } -QString Room::firstDisplayedEventId() const -{ - return d->firstDisplayedEventId; -} +QString Room::firstDisplayedEventId() const { return d->firstDisplayedEventId; } Room::rev_iter_t Room::firstDisplayedMarker() const { @@ -813,10 +933,7 @@ void Room::setFirstDisplayedEvent(TimelineItem::index_t index) setFirstDisplayedEventId(findInTimeline(index)->event()->id()); } -QString Room::lastDisplayedEventId() const -{ - return d->lastDisplayedEventId; -} +QString Room::lastDisplayedEventId() const { return d->lastDisplayedEventId; } Room::rev_iter_t Room::lastDisplayedMarker() const { @@ -844,52 +961,49 @@ Room::rev_iter_t Room::readMarker(const User* user) const return findInTimeline(d->lastReadEventIds.value(user)); } -Room::rev_iter_t Room::readMarker() const -{ - return readMarker(localUser()); -} +Room::rev_iter_t Room::readMarker() const { return readMarker(localUser()); } QString Room::readMarkerEventId() const { return d->lastReadEventIds.value(localUser()); } -QList<User*> Room::usersAtEventId(const QString& eventId) { +QList<User*> Room::usersAtEventId(const QString& eventId) +{ return d->eventIdReadUsers.values(eventId); } -int Room::notificationCount() const -{ - return d->notificationCount; -} +int Room::notificationCount() const { return d->notificationCount; } void Room::resetNotificationCount() { - if( d->notificationCount == 0 ) + if (d->notificationCount == 0) return; d->notificationCount = 0; - emit notificationCountChanged(this); + emit notificationCountChanged(); } -int Room::highlightCount() const -{ - return d->highlightCount; -} +int Room::highlightCount() const { return d->highlightCount; } void Room::resetHighlightCount() { - if( d->highlightCount == 0 ) + if (d->highlightCount == 0) return; d->highlightCount = 0; - emit highlightCountChanged(this); + emit highlightCountChanged(); } void Room::switchVersion(QString newVersion) { - auto* job = connection()->callApi<UpgradeRoomJob>(id(), newVersion); - connect(job, &BaseJob::failure, this, [this,job] { - emit upgradeFailed(job->errorString()); - }); + if (!successorId().isEmpty()) { + Q_ASSERT(!successorId().isEmpty()); + emit upgradeFailed(tr("The room is already upgraded")); + } + if (auto* job = connection()->callApi<UpgradeRoomJob>(id(), newVersion)) + connect(job, &BaseJob::failure, this, + [this, job] { emit upgradeFailed(job->errorString()); }); + else + emit upgradeFailed(tr("Couldn't initiate upgrade")); } bool Room::hasAccountData(const QString& type) const @@ -904,30 +1018,21 @@ const EventPtr& Room::accountData(const QString& type) const return it != d->accountData.end() ? it->second : NoEventPtr; } -QStringList Room::tagNames() const -{ - return d->tags.keys(); -} +QStringList Room::tagNames() const { return d->tags.keys(); } -TagsMap Room::tags() const -{ - return d->tags; -} +TagsMap Room::tags() const { return d->tags; } -TagRecord Room::tag(const QString& name) const -{ - return d->tags.value(name); -} +TagRecord Room::tag(const QString& name) const { return d->tags.value(name); } std::pair<bool, QString> validatedTag(QString name) { - if (name.contains('.')) + if (name.isEmpty() || name.indexOf('.', 1) != -1) return { false, name }; - qWarning(MAIN) << "The tag" << name - << "doesn't follow the CS API conventions"; + qCWarning(MAIN) << "The tag" << name + << "doesn't follow the CS API conventions"; name.prepend("u."); - qWarning(MAIN) << "Using " << name << "instead"; + qCWarning(MAIN) << "Using " << name << "instead"; return { true, name }; } @@ -935,8 +1040,8 @@ std::pair<bool, QString> validatedTag(QString name) void Room::addTag(const QString& name, const TagRecord& record) { const auto& checkRes = validatedTag(name); - if (d->tags.contains(name) || - (checkRes.first && d->tags.contains(checkRes.second))) + if (d->tags.contains(name) + || (checkRes.first && d->tags.contains(checkRes.second))) return; emit tagsAboutToChange(); @@ -948,13 +1053,12 @@ void Room::addTag(const QString& name, const TagRecord& record) void Room::addTag(const QString& name, float order) { - addTag(name, TagRecord{order}); + addTag(name, TagRecord { order }); } void Room::removeTag(const QString& name) { - if (d->tags.contains(name)) - { + if (d->tags.contains(name)) { emit tagsAboutToChange(); d->tags.remove(name); emit tagsChanged(); @@ -962,58 +1066,61 @@ void Room::removeTag(const QString& name) } else if (!name.startsWith("u.")) removeTag("u." + name); else - qWarning(MAIN) << "Tag" << name << "on room" << objectName() + qCWarning(MAIN) << "Tag" << name << "on room" << objectName() << "not found, nothing to remove"; } -void Room::setTags(TagsMap newTags) +void Room::setTags(TagsMap newTags, ActionScope applyOn) { + bool propagate = applyOn != ActionScope::ThisRoomOnly; + auto joinStates = + applyOn == ActionScope::WithinSameState ? joinState() : + applyOn == ActionScope::OmitLeftState ? JoinState::Join|JoinState::Invite : + JoinState::Join|JoinState::Invite|JoinState::Leave; + if (propagate) { + for (auto* r = this; (r = r->predecessor(joinStates));) + r->setTags(newTags, ActionScope::ThisRoomOnly); + } + d->setTags(move(newTags)); connection()->callApi<SetAccountDataPerRoomJob>( - localUser()->id(), id(), TagEvent::matrixTypeId(), - TagEvent(d->tags).contentJson()); + localUser()->id(), id(), TagEvent::matrixTypeId(), + TagEvent(d->tags).contentJson()); + + if (propagate) { + for (auto* r = this; (r = r->successor(joinStates));) + r->setTags(d->tags, ActionScope::ThisRoomOnly); + } } -void Room::Private::setTags(TagsMap newTags) +void Room::Private::setTags(TagsMap&& newTags) { emit q->tagsAboutToChange(); const auto keys = newTags.keys(); - for (const auto& k: keys) - { - const auto& checkRes = validatedTag(k); - if (checkRes.first) - { - if (newTags.contains(checkRes.second)) + for (const auto& k : keys) + if (const auto& [adjusted, adjustedTag] = validatedTag(k); adjusted) { + if (newTags.contains(adjustedTag)) newTags.remove(k); else - newTags.insert(checkRes.second, newTags.take(k)); + newTags.insert(adjustedTag, newTags.take(k)); } - } + tags = move(newTags); - qCDebug(MAIN) << "Room" << q->objectName() << "is tagged with" - << q->tagNames().join(QStringLiteral(", ")); + qCDebug(STATE) << "Room" << q->objectName() << "is tagged with" + << q->tagNames().join(QStringLiteral(", ")); emit q->tagsChanged(); } -bool Room::isFavourite() const -{ - return d->tags.contains(FavouriteTag); -} +bool Room::isFavourite() const { return d->tags.contains(FavouriteTag); } -bool Room::isLowPriority() const -{ - return d->tags.contains(LowPriorityTag); -} +bool Room::isLowPriority() const { return d->tags.contains(LowPriorityTag); } bool Room::isServerNoticeRoom() const { return d->tags.contains(ServerNoticeTag); } -bool Room::isDirectChat() const -{ - return connection()->isDirectChat(id()); -} +bool Room::isDirectChat() const { return connection()->isDirectChat(id()); } QList<User*> Room::directChatUsers() const { @@ -1029,13 +1136,12 @@ const RoomMessageEvent* Room::Private::getEventWithFile(const QString& eventId) const { auto evtIt = q->findInTimeline(eventId); - if (evtIt != timeline.rend() && is<RoomMessageEvent>(**evtIt)) - { + if (evtIt != timeline.rend() && is<RoomMessageEvent>(**evtIt)) { auto* event = evtIt->viewAs<RoomMessageEvent>(); if (event->hasFileContent()) return event; } - qWarning() << "No files to download in event" << eventId; + qCWarning(MAIN) << "No files to download in event" << eventId; return nullptr; } @@ -1046,29 +1152,23 @@ QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const QString fileName; if (!fileInfo->originalName.isEmpty()) fileName = QFileInfo(safeFileName(fileInfo->originalName)).fileName(); - else { - // Having no better options, assume that the body has - // the original file URL or at least the file name. - QUrl u { event->plainBody() }; - if (u.isValid()) - { - qDebug(MAIN) << event->id() - << "has no file name supplied but the event body " - "looks like a URL - using the file name from it"; - fileName = u.fileName(); - } + else if (QUrl u { event->plainBody() }; u.isValid()) { + qDebug(MAIN) << event->id() + << "has no file name supplied but the event body " + "looks like a URL - using the file name from it"; + fileName = u.fileName(); } if (fileName.isEmpty()) return safeFileName(fileInfo->mediaId()).replace('.', '-') % '.' % fileInfo->mimeType.preferredSuffix(); - if (QSysInfo::productType() == "windows") - { - const auto& suffixes = fileInfo->mimeType.suffixes(); - if (!suffixes.isEmpty() && - std::none_of(suffixes.begin(), suffixes.end(), - [&fileName] (const QString& s) { - return fileName.endsWith(s); })) + if (QSysInfo::productType() == "windows") { + if (const auto& suffixes = fileInfo->mimeType.suffixes(); + !suffixes.isEmpty() + && std::none_of(suffixes.begin(), suffixes.end(), + [&fileName](const QString& s) { + return fileName.endsWith(s); + })) return fileName % '.' % fileInfo->mimeType.preferredSuffix(); } return fileName; @@ -1077,21 +1177,20 @@ QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const QUrl Room::urlToThumbnail(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) - if (event->hasThumbnail()) - { + if (event->hasThumbnail()) { auto* thumbnail = event->content()->thumbnailInfo(); Q_ASSERT(thumbnail != nullptr); return MediaThumbnailJob::makeRequestUrl(connection()->homeserver(), - thumbnail->url, thumbnail->imageSize); + thumbnail->url, + thumbnail->imageSize); } - qDebug() << "Event" << eventId << "has no thumbnail"; + qCDebug(MAIN) << "Event" << eventId << "has no thumbnail"; return {}; } QUrl Room::urlToDownload(const QString& eventId) const { - if (auto* event = d->getEventWithFile(eventId)) - { + if (auto* event = d->getEventWithFile(eventId)) { auto* fileInfo = event->content()->fileInfo(); Q_ASSERT(fileInfo != nullptr); return DownloadFileJob::makeRequestUrl(connection()->homeserver(), @@ -1109,8 +1208,8 @@ QString Room::fileNameToDownload(const QString& eventId) const FileTransferInfo Room::fileTransferInfo(const QString& id) const { - auto infoIt = d->fileTransfers.find(id); - if (infoIt == d->fileTransfers.end()) + const auto infoIt = d->fileTransfers.constFind(id); + if (infoIt == d->fileTransfers.cend()) return {}; // FIXME: Add lib tests to make sure FileTransferInfo::status stays @@ -1118,27 +1217,18 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const qint64 progress = infoIt->progress; qint64 total = infoIt->total; - if (total > INT_MAX) - { + if (total > INT_MAX) { // JavaScript doesn't deal with 64-bit integers; scale down if necessary progress = llround(double(progress) / total * INT_MAX); total = INT_MAX; } -#ifdef BROKEN_INITIALIZER_LISTS - FileTransferInfo fti; - fti.status = infoIt->status; - fti.progress = int(progress); - fti.total = int(total); - fti.localDir = QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()); - fti.localPath = QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); - return fti; -#else - return { infoIt->status, infoIt->isUpload, int(progress), int(total), - QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()), - QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()) - }; -#endif + return { infoIt->status, + infoIt->isUpload, + int(progress), + int(total), + QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()), + QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()) }; } QUrl Room::fileSource(const QString& id) const @@ -1148,8 +1238,8 @@ QUrl Room::fileSource(const QString& id) const return url; // No urlToDownload means it's a pending or completed upload. - auto infoIt = d->fileTransfers.find(id); - if (infoIt != d->fileTransfers.end()) + auto infoIt = d->fileTransfers.constFind(id); + if (infoIt != d->fileTransfers.cend()) return QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); qCWarning(MAIN) << "File source for identifier" << id << "not found"; @@ -1158,91 +1248,125 @@ QUrl Room::fileSource(const QString& id) const QString Room::prettyPrint(const QString& plainText) const { - return QMatrixClient::prettyPrint(plainText); + return Quotient::prettyPrint(plainText); } -QList< User* > Room::usersTyping() const -{ - return d->usersTyping; -} +QList<User*> Room::usersTyping() const { return d->usersTyping; } -QList< User* > Room::membersLeft() const -{ - return d->membersLeft; -} +QList<User*> Room::membersLeft() const { return d->membersLeft; } -QList< User* > Room::users() const -{ - return d->membersMap.values(); -} +QList<User*> Room::users() const { return d->membersMap.values(); } QStringList Room::memberNames() const { QStringList res; + res.reserve(d->membersMap.size()); for (auto u : qAsConst(d->membersMap)) - res.append( roomMembername(u) ); + res.append(roomMembername(u)); return res; } -int Room::memberCount() const +int Room::memberCount() const { return d->membersMap.size(); } + +int Room::timelineSize() const { return int(d->timeline.size()); } + +bool Room::usesEncryption() const +{ + return !d->getCurrentState<EncryptionEvent>()->algorithm().isEmpty(); +} + +const StateEventBase* Room::getCurrentState(const QString& evtType, + const QString& stateKey) const { - return d->membersMap.size(); + return d->getCurrentState({ evtType, stateKey }); } -int Room::timelineSize() const +RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) { - return int(d->timeline.size()); +#ifndef Quotient_E2EE_ENABLED + Q_UNUSED(encryptedEvent) + qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; + return {}; +#else // Quotient_E2EE_ENABLED + if (encryptedEvent.algorithm() == MegolmV1AesSha2AlgoKey) { + QString decrypted = d->groupSessionDecryptMessage( + encryptedEvent.ciphertext(), encryptedEvent.senderKey(), + encryptedEvent.sessionId(), encryptedEvent.id(), + encryptedEvent.originTimestamp()); + if (decrypted.isEmpty()) { + return {}; + } + return makeEvent<RoomMessageEvent>( + QJsonDocument::fromJson(decrypted.toUtf8()).object()); + } + qCDebug(E2EE) << "Algorithm of the encrypted event with id" + << encryptedEvent.id() << "is not for the current device"; + return {}; +#endif // Quotient_E2EE_ENABLED } -bool Room::usesEncryption() const +void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, + const QString& senderKey) { - return !d->getCurrentState<EncryptionEvent>()->algorithm().isEmpty(); +#ifndef Quotient_E2EE_ENABLED + Q_UNUSED(roomKeyEvent) + Q_UNUSED(senderKey) + qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; +#else // Quotient_E2EE_ENABLED + if (roomKeyEvent.algorithm() != MegolmV1AesSha2AlgoKey) { + qCWarning(E2EE) << "Ignoring unsupported algorithm" + << roomKeyEvent.algorithm() << "in m.room_key event"; + } + if (d->addInboundGroupSession(senderKey, roomKeyEvent.sessionId(), + roomKeyEvent.sessionKey())) { + qCDebug(E2EE) << "added new inboundGroupSession:" + << d->groupSessions.count(); + } +#endif // Quotient_E2EE_ENABLED } int Room::joinedCount() const { - return d->summary.joinedMemberCount.omitted() - ? d->membersMap.size() - : d->summary.joinedMemberCount.value(); + return d->summary.joinedMemberCount.value_or(d->membersMap.size()); } int Room::invitedCount() const { // TODO: Store invited users in Room too - Q_ASSERT(!d->summary.invitedMemberCount.omitted()); - return d->summary.invitedMemberCount.value(); + Q_ASSERT(d->summary.invitedMemberCount.has_value()); + return d->summary.invitedMemberCount.value_or(0); } -int Room::totalMemberCount() const -{ - return joinedCount() + invitedCount(); -} +int Room::totalMemberCount() const { return joinedCount() + invitedCount(); } -GetRoomEventsJob* Room::eventsHistoryJob() const -{ - return d->eventsHistoryJob; -} +GetRoomEventsJob* Room::eventsHistoryJob() const { return d->eventsHistoryJob; } Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) { if (!summary.merge(newSummary)) return Change::NoChange; - qCDebug(MAIN).nospace().noquote() + qCDebug(STATE).nospace().noquote() << "Updated room summary for " << q->objectName() << ": " << summary; emit q->memberListChanged(); return Change::SummaryChange; } -void Room::Private::insertMemberIntoMap(User *u) +void Room::Private::insertMemberIntoMap(User* u) { - const auto userName = u->name(q); - // If there is exactly one namesake of the added user, signal member renaming - // for that other one because the two should be disambiguated now. + const auto userName = + getCurrentState<RoomMemberEvent>(u->id())->displayName(); + // If there is exactly one namesake of the added user, signal member + // renaming for that other one because the two should be disambiguated now. const auto namesakes = membersMap.values(userName); // Callers should check they are not adding an existing user once more. Q_ASSERT(!namesakes.contains(u)); + if (namesakes.contains(u)) { // Release version whines but continues + qCCritical(STATE) << "Trying to add a user" << u->id() << "to room" + << q->objectName() << "but that's already in it"; + return; + } if (namesakes.size() == 1) emit q->memberAboutToRename(namesakes.front(), @@ -1252,34 +1376,21 @@ void Room::Private::insertMemberIntoMap(User *u) emit q->memberRenamed(namesakes.front()); } -void Room::Private::renameMember(User* u, const QString& oldName) +void Room::Private::removeMemberFromMap(User* u) { - if (u->name(q) == oldName) - { - qCWarning(MAIN) << "Room::Private::renameMember(): the user " - << u->fullName(q) - << "is already known in the room under a new name."; - } - else if (membersMap.contains(oldName, u)) - { - removeMemberFromMap(oldName, u); - insertMemberIntoMap(u); - } -} + const auto userName = + getCurrentState<RoomMemberEvent>(u->id())->displayName(); -void Room::Private::removeMemberFromMap(const QString& username, User* u) -{ User* namesake = nullptr; - auto namesakes = membersMap.values(username); - if (namesakes.size() == 2) - { + auto namesakes = membersMap.values(userName); + if (namesakes.size() == 2) { namesake = namesakes.front() == u ? namesakes.back() : namesakes.front(); Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken"); - emit q->memberAboutToRename(namesake, username); + emit q->memberAboutToRename(namesake, userName); } - membersMap.remove(username, u); - // If there was one namesake besides the removed user, signal member renaming - // for it because it doesn't need to be disambiguated anymore. + membersMap.remove(userName, u); + // If there was one namesake besides the removed user, signal member + // renaming for it because it doesn't need to be disambiguated any more. if (namesake) emit q->memberRenamed(namesake); } @@ -1289,27 +1400,29 @@ inline auto makeErrorStr(const Event& e, QByteArray msg) return msg.append("; event dump follows:\n").append(e.originalJson()); } -Room::Timeline::size_type Room::Private::moveEventsToTimeline( - RoomEventsRange events, EventsPlacement placement) +Room::Timeline::size_type +Room::Private::moveEventsToTimeline(RoomEventsRange events, + EventsPlacement placement) { Q_ASSERT(!events.empty()); // Historical messages arrive in newest-to-oldest order, so the process for // them is almost symmetric to the one for new messages. New messages get // appended from index 0; old messages go backwards from index -1. - auto index = timeline.empty() ? -((placement+1)/2) /* 1 -> -1; -1 -> 0 */ : - placement == Older ? timeline.front().index() : - timeline.back().index(); + auto index = timeline.empty() + ? -((placement + 1) / 2) /* 1 -> -1; -1 -> 0 */ + : placement == Older ? timeline.front().index() + : timeline.back().index(); auto baseIndex = index; - for (auto&& e: events) - { + for (auto&& e : events) { const auto eId = e->id(); Q_ASSERT_X(e, __FUNCTION__, "Attempt to add nullptr to timeline"); - Q_ASSERT_X(!eId.isEmpty(), __FUNCTION__, - makeErrorStr(*e, - "Event with empty id cannot be in the timeline")); - Q_ASSERT_X(!eventsIndex.contains(eId), __FUNCTION__, - makeErrorStr(*e, "Event is already in the timeline; " - "incoming events were not properly deduplicated")); + Q_ASSERT_X( + !eId.isEmpty(), __FUNCTION__, + makeErrorStr(*e, "Event with empty id cannot be in the timeline")); + Q_ASSERT_X( + !eventsIndex.contains(eId), __FUNCTION__, + makeErrorStr(*e, "Event is already in the timeline; " + "incoming events were not properly deduplicated")); if (placement == Older) timeline.emplace_front(move(e), --index); else @@ -1319,7 +1432,7 @@ Room::Timeline::size_type Room::Private::moveEventsToTimeline( } const auto insertedSize = (index - baseIndex) * placement; Q_ASSERT(insertedSize == int(events.size())); - return insertedSize; + return Timeline::size_type(insertedSize); } QString Room::roomMembername(const User* u) const @@ -1338,24 +1451,11 @@ QString Room::roomMembername(const User* u) const if (namesakesIt == d->membersMap.cend()) return u->fullName(this); - auto nextUserIt = namesakesIt + 1; - if (nextUserIt == d->membersMap.cend() || nextUserIt.key() != username) + auto nextUserIt = namesakesIt; + if (++nextUserIt == d->membersMap.cend() || nextUserIt.key() != username) return username; // No disambiguation necessary - // Check if we can get away just attaching the bridge postfix - // (extension to the spec) - QVector<QString> bridges; - for (; namesakesIt != d->membersMap.cend() && namesakesIt.key() == username; - ++namesakesIt) - { - const auto bridgeName = (*namesakesIt)->bridged(); - if (bridges.contains(bridgeName)) // Two accounts on the same bridge - return u->fullName(this); // Disambiguate fully - // Don't bother sorting, not so many bridges out there - bridges.push_back(bridgeName); - } - - return u->rawName(this); // Disambiguate using the bridge postfix only + return u->fullName(this); // Disambiguate fully } QString Room::roomMembername(const QString& userId) const @@ -1363,64 +1463,63 @@ QString Room::roomMembername(const QString& userId) const return roomMembername(user(userId)); } +QString Room::safeMemberName(const QString& userId) const +{ + return sanitized(roomMembername(userId)); +} + void Room::updateData(SyncRoomData&& data, bool fromCache) { - if( d->prevBatch.isEmpty() ) + if (d->prevBatch.isEmpty()) d->prevBatch = data.timelinePrevBatch; setJoinState(data.joinState); Changes roomChanges = Change::NoChange; - QElapsedTimer et; et.start(); - for (auto&& event: data.accountData) + QElapsedTimer et; + et.start(); + for (auto&& event : data.accountData) roomChanges |= processAccountDataEvent(move(event)); roomChanges |= d->updateStateFrom(data.state); - if (!data.timeline.empty()) - { + if (!data.timeline.empty()) { et.restart(); roomChanges |= d->addNewMessageEvents(move(data.timeline)); if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" - << data.timeline.size() << "event(s)," << et; + qCDebug(PROFILER) + << "*** Room::addNewMessageEvents():" << data.timeline.size() + << "event(s)," << et; } - if (roomChanges&TopicChange) + if (roomChanges & TopicChange) emit topicChanged(); - if (roomChanges&(NameChange|CanonicalAliasChange)) + if (roomChanges & (NameChange | AliasesChange)) emit namesChanged(this); - if (roomChanges&MembersChange) + if (roomChanges & MembersChange) emit memberListChanged(); roomChanges |= d->setSummary(move(data.summary)); - for( auto&& ephemeralEvent: data.ephemeral ) + for (auto&& ephemeralEvent : data.ephemeral) roomChanges |= processEphemeralEvent(move(ephemeralEvent)); - // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count - if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) - { - qCDebug(MAIN) << "Setting unread_count to" << data.unreadCount; + // See https://github.com/quotient-im/libQuotient/wiki/unread_count + if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) { + qCDebug(MESSAGES) << "Setting unread_count to" << data.unreadCount; d->unreadMessages = data.unreadCount; - roomChanges |= Change::UnreadNotifsChange; emit unreadMessagesChanged(this); } - if( data.highlightCount != d->highlightCount ) - { + if (data.highlightCount != d->highlightCount) { d->highlightCount = data.highlightCount; - roomChanges |= Change::UnreadNotifsChange; - emit highlightCountChanged(this); + emit highlightCountChanged(); } - if( data.notificationCount != d->notificationCount ) - { + if (data.notificationCount != d->notificationCount) { d->notificationCount = data.notificationCount; - roomChanges |= Change::UnreadNotifsChange; - emit notificationCountChanged(this); + emit notificationCountChanged(); } - if (roomChanges != Change::NoChange) - { + if (roomChanges != Change::NoChange) { d->updateDisplayname(); emit changed(roomChanges); if (!fromCache) @@ -1432,6 +1531,10 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) { if (event->transactionId().isEmpty()) event->setTransactionId(connection->generateTxnId()); + if (event->roomId().isEmpty()) + event->setRoomId(id); + if (event->senderId().isEmpty()) + event->setSender(connection->userId()); auto* pEvent = rawPtr(event); emit q->pendingEventAboutToAdd(pEvent); unsyncedEvents.emplace_back(move(event)); @@ -1441,6 +1544,11 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) QString Room::Private::sendEvent(RoomEventPtr&& event) { + if (q->usesEncryption()) { + qCCritical(MAIN) << "Room" << q->objectName() + << "enforces encryption; sending encrypted messages " + "is not supported yet"; + } if (q->successorId().isEmpty()) return doSendEvent(addAsPending(std::move(event))); @@ -1452,37 +1560,37 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) { const auto txnId = pEvent->transactionId(); // TODO, #133: Enqueue the job rather than immediately trigger it. - if (auto call = connection->callApi<SendMessageJob>(BackgroundRequest, - id, pEvent->matrixType(), txnId, pEvent->contentJson())) - { - Room::connect(call, &BaseJob::started, q, - [this,txnId] { - auto it = q->findPendingEvent(txnId); - if (it == unsyncedEvents.end()) - { - qWarning(EVENTS) << "Pending event for transaction" << txnId - << "not found - got synced so soon?"; - return; - } - it->setDeparted(); - emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); - }); + if (auto call = + connection->callApi<SendMessageJob>(BackgroundRequest, id, + pEvent->matrixType(), txnId, + pEvent->contentJson())) { + Room::connect(call, &BaseJob::sentRequest, q, [this, txnId] { + auto it = q->findPendingEvent(txnId); + if (it == unsyncedEvents.end()) { + qCWarning(EVENTS) << "Pending event for transaction" << txnId + << "not found - got synced so soon?"; + return; + } + it->setDeparted(); + qCDebug(EVENTS) << "Event txn" << txnId << "has departed"; + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); + }); Room::connect(call, &BaseJob::failure, q, - std::bind(&Room::Private::onEventSendingFailure, this, txnId, call)); - Room::connect(call, &BaseJob::success, q, - [this,call,txnId] { - emit q->messageSent(txnId, call->eventId()); - auto it = q->findPendingEvent(txnId); - if (it == unsyncedEvents.end()) - { - qDebug(EVENTS) << "Pending event for transaction" << txnId - << "already merged"; - return; + std::bind(&Room::Private::onEventSendingFailure, this, + txnId, call)); + Room::connect(call, &BaseJob::success, q, [this, call, txnId] { + auto it = q->findPendingEvent(txnId); + if (it != unsyncedEvents.end()) { + if (it->deliveryStatus() != EventStatus::ReachedServer) { + it->setReachedServer(call->eventId()); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); } + } else + qCDebug(EVENTS) << "Pending event for transaction" << txnId + << "already merged"; - it->setReachedServer(call->eventId()); - emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); - }); + emit q->messageSent(txnId, call->eventId()); + }); } else onEventSendingFailure(txnId); return txnId; @@ -1491,15 +1599,13 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) void Room::Private::onEventSendingFailure(const QString& txnId, BaseJob* call) { auto it = q->findPendingEvent(txnId); - if (it == unsyncedEvents.end()) - { + if (it == unsyncedEvents.end()) { qCritical(EVENTS) << "Pending event for transaction" << txnId << "could not be sent"; return; } - it->setSendingFailed(call - ? call->statusCaption() % ": " % call->errorString() - : tr("The call could not be started")); + it->setSendingFailed(call ? call->statusCaption() % ": " % call->errorString() + : tr("The call could not be started")); emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); } @@ -1507,56 +1613,56 @@ QString Room::retryMessage(const QString& txnId) { const auto it = findPendingEvent(txnId); Q_ASSERT(it != d->unsyncedEvents.end()); - qDebug(EVENTS) << "Retrying transaction" << txnId; - const auto& transferIt = d->fileTransfers.find(txnId); - if (transferIt != d->fileTransfers.end()) - { + qCDebug(EVENTS) << "Retrying transaction" << txnId; + const auto& transferIt = d->fileTransfers.constFind(txnId); + if (transferIt != d->fileTransfers.cend()) { Q_ASSERT(transferIt->isUpload); - if (transferIt->status == FileTransferInfo::Completed) - { - qCDebug(MAIN) << "File for transaction" << txnId - << "has already been uploaded, bypassing re-upload"; + if (transferIt->status == FileTransferInfo::Completed) { + qCDebug(MESSAGES) + << "File for transaction" << txnId + << "has already been uploaded, bypassing re-upload"; } else { - if (isJobRunning(transferIt->job)) - { - qCDebug(MAIN) << "Abandoning the upload job for transaction" - << txnId << "and starting again"; + if (isJobRunning(transferIt->job)) { + qCDebug(MESSAGES) << "Abandoning the upload job for transaction" + << txnId << "and starting again"; transferIt->job->abandon(); - emit fileTransferFailed(txnId, tr("File upload will be retried")); + emit fileTransferFailed(txnId, + tr("File upload will be retried")); } - uploadFile(txnId, - QUrl::fromLocalFile(transferIt->localFileInfo.absoluteFilePath())); + uploadFile(txnId, QUrl::fromLocalFile( + transferIt->localFileInfo.absoluteFilePath())); // FIXME: Content type is no more passed here but it should } } - if (it->deliveryStatus() == EventStatus::ReachedServer) - { - qCWarning(MAIN) << "The previous attempt has reached the server; two" - " events are likely to be in the timeline after retry"; + if (it->deliveryStatus() == EventStatus::ReachedServer) { + qCWarning(MAIN) + << "The previous attempt has reached the server; two" + " events are likely to be in the timeline after retry"; } it->resetStatus(); + emit pendingEventChanged(int(it - d->unsyncedEvents.begin())); return d->doSendEvent(it->event()); } void Room::discardMessage(const QString& txnId) { auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), - [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); + [txnId](const auto& evt) { + return evt->transactionId() == txnId; + }); Q_ASSERT(it != d->unsyncedEvents.end()); - qDebug(EVENTS) << "Discarding transaction" << txnId; + qCDebug(EVENTS) << "Discarding transaction" << txnId; const auto& transferIt = d->fileTransfers.find(txnId); - if (transferIt != d->fileTransfers.end()) - { + if (transferIt != d->fileTransfers.end()) { Q_ASSERT(transferIt->isUpload); - if (isJobRunning(transferIt->job)) - { + if (isJobRunning(transferIt->job)) { transferIt->status = FileTransferInfo::Cancelled; transferIt->job->abandon(); emit fileTransferFailed(txnId, tr("File upload cancelled")); - } else if (transferIt->status == FileTransferInfo::Completed) - { - qCWarning(MAIN) << "File for transaction" << txnId - << "has been uploaded but the message was discarded"; + } else if (transferIt->status == FileTransferInfo::Completed) { + qCWarning(MAIN) + << "File for transaction" << txnId + << "has been uploaded but the message was discarded"; } } emit pendingEventAboutToDiscard(int(it - d->unsyncedEvents.begin())); @@ -1577,8 +1683,9 @@ QString Room::postPlainText(const QString& plainText) QString Room::postHtmlMessage(const QString& plainText, const QString& html, MessageEventType type) { - return d->sendEvent<RoomMessageEvent>(plainText, type, - new EventContent::TextContent(html, QStringLiteral("text/html"))); + return d->sendEvent<RoomMessageEvent>( + plainText, type, + new EventContent::TextContent(html, QStringLiteral("text/html"))); } QString Room::postHtmlText(const QString& plainText, const QString& html) @@ -1586,7 +1693,7 @@ QString Room::postHtmlText(const QString& plainText, const QString& html) return postHtmlMessage(plainText, html); } -QString Room::postReaction(const QString &eventId, const QString &key) +QString Room::postReaction(const QString& eventId, const QString& key) { return d->sendEvent<ReactionEvent>(EventRelation::annotate(eventId, key)); } @@ -1597,98 +1704,85 @@ QString Room::postFile(const QString& plainText, const QUrl& localPath, QFileInfo localFile { localPath.toLocalFile() }; Q_ASSERT(localFile.isFile()); - const auto txnId = connection()->generateTxnId(); + const auto txnId = + d->addAsPending( + makeEvent<RoomMessageEvent>(plainText, localFile, asGenericFile)) + ->transactionId(); // Remote URL will only be known after upload; fill in the local path // to enable the preview while the event is pending. uploadFile(txnId, localPath); - { - auto&& event = - makeEvent<RoomMessageEvent>(plainText, localFile, asGenericFile); - event->setTransactionId(txnId); - d->addAsPending(std::move(event)); - } - auto* context = new QObject(this); - connect(this, &Room::fileTransferCompleted, context, - [context,this,txnId] (const QString& id, QUrl, const QUrl& mxcUri) { - if (id == txnId) - { - auto it = findPendingEvent(txnId); - if (it != d->unsyncedEvents.end()) - { - it->setFileUploaded(mxcUri); - emit pendingEventChanged( - int(it - d->unsyncedEvents.begin())); - d->doSendEvent(it->get()); - } else { - // Normally in this situation we should instruct - // the media server to delete the file; alas, there's no - // API specced for that. - qCWarning(MAIN) << "File uploaded to" << mxcUri - << "but the event referring to it was cancelled"; + // Below, the upload job is used as a context object to clean up connections + const auto& transferJob = d->fileTransfers.value(txnId).job; + connect(this, &Room::fileTransferCompleted, transferJob, + [this, txnId](const QString& id, const QUrl&, const QUrl& mxcUri) { + if (id == txnId) { + auto it = findPendingEvent(txnId); + if (it != d->unsyncedEvents.end()) { + it->setFileUploaded(mxcUri); + emit pendingEventChanged( + int(it - d->unsyncedEvents.begin())); + d->doSendEvent(it->get()); + } else { + // Normally in this situation we should instruct + // the media server to delete the file; alas, there's no + // API specced for that. + qCWarning(MAIN) << "File uploaded to" << mxcUri + << "but the event referring to it was " + "cancelled"; + } } - context->deleteLater(); - } - }); - connect(this, &Room::fileTransferCancelled, this, - [context,this,txnId] (const QString& id) { - if (id == txnId) - { - auto it = findPendingEvent(txnId); - if (it != d->unsyncedEvents.end()) - { - const auto idx = int(it - d->unsyncedEvents.begin()); - emit pendingEventAboutToDiscard(idx); - // See #286 on why iterator may not be valid here. - d->unsyncedEvents.erase(d->unsyncedEvents.begin() + idx); - emit pendingEventDiscarded(); + }); + connect(this, &Room::fileTransferCancelled, transferJob, + [this, txnId](const QString& id) { + if (id == txnId) { + auto it = findPendingEvent(txnId); + if (it != d->unsyncedEvents.end()) { + const auto idx = int(it - d->unsyncedEvents.begin()); + emit pendingEventAboutToDiscard(idx); + // See #286 on why iterator may not be valid here. + d->unsyncedEvents.erase(d->unsyncedEvents.begin() + idx); + emit pendingEventDiscarded(); + } } - context->deleteLater(); - } - }); + }); return txnId; } QString Room::postEvent(RoomEvent* event) { - if (usesEncryption()) - { - qCCritical(MAIN) << "Room" << displayName() - << "enforces encryption; sending encrypted messages is not supported yet"; - } return d->sendEvent(RoomEventPtr(event)); } QString Room::postJson(const QString& matrixType, const QJsonObject& eventContent) { - return d->sendEvent(loadEvent<RoomEvent>(basicEventJson(matrixType, eventContent))); + return d->sendEvent(loadEvent<RoomEvent>(matrixType, eventContent)); +} + +SetRoomStateWithKeyJob* Room::setState(const StateEventBase& evt) const +{ + return d->requestSetState(evt); } void Room::setName(const QString& newName) { - d->requestSetState(RoomNameEvent(newName)); + d->requestSetState<RoomNameEvent>(newName); } void Room::setCanonicalAlias(const QString& newAlias) { - connection()->callApi<SetRoomStateJob>( - id(), RoomCanonicalAliasEvent::matrixTypeId(), - QJsonObject { { "alias", newAlias }, - { "alt_aliases", QMatrixClient::toJson(altAliases()) } }); + d->requestSetState<RoomCanonicalAliasEvent>(newAlias, altAliases()); } -void Room::setAliases(const QStringList& aliases) +void Room::setLocalAliases(const QStringList& aliases) { - connection()->callApi<SetRoomStateJob>( - id(), RoomCanonicalAliasEvent::matrixTypeId(), - QJsonObject { { "alias", canonicalAlias() }, - { "alt_aliases", QMatrixClient::toJson(aliases) } }); + d->requestSetState<RoomCanonicalAliasEvent>(canonicalAlias(), aliases); } void Room::setTopic(const QString& newTopic) { - d->requestSetState(RoomTopicEvent(newTopic)); + d->requestSetState<RoomTopicEvent>(newTopic); } bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) @@ -1711,10 +1805,7 @@ bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) return le->contentJson() == re->contentJson(); } -bool Room::supportsCalls() const -{ - return joinedCount() == 2; -} +bool Room::supportsCalls() const { return joinedCount() == 2; } void Room::checkVersion() { @@ -1724,12 +1815,12 @@ void Room::checkVersion() // This method is only called after the base state has been loaded // or the server capabilities have been loaded. emit stabilityUpdated(defaultVersion, stableVersions); - if (!stableVersions.contains(version())) - { - qCDebug(MAIN) << this << "version is" << version() - << "which the server doesn't count as stable"; + if (!stableVersions.contains(version())) { + qCDebug(STATE) << this << "version is" << version() + << "which the server doesn't count as stable"; if (canSwitchVersions()) - qCDebug(MAIN) << "The current user has enough privileges to fix it"; + qCDebug(STATE) + << "The current user has enough privileges to fix it"; } } @@ -1737,39 +1828,36 @@ void Room::inviteCall(const QString& callId, const int lifetime, const QString& sdp) { Q_ASSERT(supportsCalls()); - postEvent(new CallInviteEvent(callId, lifetime, sdp)); + d->sendEvent<CallInviteEvent>(callId, lifetime, sdp); } void Room::sendCallCandidates(const QString& callId, const QJsonArray& candidates) { Q_ASSERT(supportsCalls()); - postEvent(new CallCandidatesEvent(callId, candidates)); + d->sendEvent<CallCandidatesEvent>(callId, candidates); } void Room::answerCall(const QString& callId, const int lifetime, const QString& sdp) { Q_ASSERT(supportsCalls()); - postEvent(new CallAnswerEvent(callId, lifetime, sdp)); + d->sendEvent<CallAnswerEvent>(callId, lifetime, sdp); } void Room::answerCall(const QString& callId, const QString& sdp) { Q_ASSERT(supportsCalls()); - postEvent(new CallAnswerEvent(callId, sdp)); + d->sendEvent<CallAnswerEvent>(callId, sdp); } void Room::hangupCall(const QString& callId) { Q_ASSERT(supportsCalls()); - postEvent(new CallHangupEvent(callId)); + d->sendEvent<CallHangupEvent>(callId); } -void Room::getPreviousContent(int limit) -{ - d->getPreviousContent(limit); -} +void Room::getPreviousContent(int limit) { d->getPreviousContent(limit); } void Room::Private::getPreviousContent(int limit) { @@ -1779,12 +1867,12 @@ void Room::Private::getPreviousContent(int limit) eventsHistoryJob = connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit); emit q->eventsHistoryJobChanged(); - connect( eventsHistoryJob, &BaseJob::success, q, [=] { + connect(eventsHistoryJob, &BaseJob::success, q, [=] { prevBatch = eventsHistoryJob->end(); addHistoricalMessageEvents(eventsHistoryJob->chunk()); }); - connect( eventsHistoryJob, &QObject::destroyed, - q, &Room::eventsHistoryJobChanged); + connect(eventsHistoryJob, &QObject::destroyed, q, + &Room::eventsHistoryJobChanged); } void Room::inviteToRoom(const QString& memberId) @@ -1798,9 +1886,10 @@ LeaveRoomJob* Room::leaveRoom() return connection()->leaveRoom(this); } -SetRoomStateWithKeyJob*Room::setMemberState(const QString& memberId, const RoomMemberEvent& event) const +SetRoomStateWithKeyJob* Room::setMemberState(const QString& memberId, + const RoomMemberEvent& event) const { - return d->requestSetState(memberId, event); + return d->requestSetState<RoomMemberEvent>(memberId, event.content()); } void Room::kickMember(const QString& memberId, const QString& reason) @@ -1820,8 +1909,8 @@ void Room::unban(const QString& userId) void Room::redactEvent(const QString& eventId, const QString& reason) { - connection()->callApi<RedactEventJob>(id(), - QUrl::toPercentEncoding(eventId), connection()->generateTxnId(), reason); + connection()->callApi<RedactEventJob>(id(), QUrl::toPercentEncoding(eventId), + connection()->generateTxnId(), reason); } void Room::uploadFile(const QString& id, const QUrl& localFilename, @@ -1831,18 +1920,17 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, "localFilename should point at a local file"); auto fileName = localFilename.toLocalFile(); auto job = connection()->uploadFile(fileName, overrideContentType); - if (isJobRunning(job)) - { - d->fileTransfers.insert(id, { job, fileName, true }); + if (isJobRunning(job)) { + d->fileTransfers[id] = { job, fileName, true }; connect(job, &BaseJob::uploadProgress, this, - [this,id] (qint64 sent, qint64 total) { + [this, id](qint64 sent, qint64 total) { d->fileTransfers[id].update(sent, total); emit fileTransferProgress(id, sent, total); }); - connect(job, &BaseJob::success, this, [this,id,localFilename,job] { - d->fileTransfers[id].status = FileTransferInfo::Completed; - emit fileTransferCompleted(id, localFilename, job->contentUri()); - }); + connect(job, &BaseJob::success, this, [this, id, localFilename, job] { + d->fileTransfers[id].status = FileTransferInfo::Completed; + emit fileTransferCompleted(id, localFilename, job->contentUri()); + }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, id, job->errorString())); emit newFileTransfer(id, localFilename); @@ -1852,10 +1940,9 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, void Room::downloadFile(const QString& eventId, const QUrl& localFilename) { - auto ongoingTransfer = d->fileTransfers.find(eventId); - if (ongoingTransfer != d->fileTransfers.end() && - ongoingTransfer->status == FileTransferInfo::Started) - { + if (auto ongoingTransfer = d->fileTransfers.constFind(eventId); + ongoingTransfer != d->fileTransfers.cend() + && ongoingTransfer->status == FileTransferInfo::Started) { qCWarning(MAIN) << "Transfer for" << eventId << "is ongoing; download won't start"; return; @@ -1864,25 +1951,21 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) Q_ASSERT_X(localFilename.isEmpty() || localFilename.isLocalFile(), __FUNCTION__, "localFilename should point at a local file"); const auto* event = d->getEventWithFile(eventId); - if (!event) - { + if (!event) { qCCritical(MAIN) << eventId << "is not in the local timeline or has no file content"; Q_ASSERT(false); return; } const auto* const fileInfo = event->content()->fileInfo(); - if (!fileInfo->isValid()) - { + if (!fileInfo->isValid()) { qCWarning(MAIN) << "Event" << eventId << "has an empty or malformed mxc URL; won't download"; return; } const auto fileUrl = fileInfo->url; auto filePath = localFilename.toLocalFile(); - if (filePath.isEmpty()) - { - // Build our own file path, starting with temp directory and eventId. + if (filePath.isEmpty()) { // Setup default file path filePath = fileInfo->url.path().mid(1) % '_' % d->fileNameToDownload(event); @@ -1893,34 +1976,32 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) qDebug(MAIN) << "File path:" << filePath; } auto job = connection()->downloadFile(fileUrl, filePath); - if (isJobRunning(job)) - { + if (isJobRunning(job)) { // If there was a previous transfer (completed or failed), overwrite it. d->fileTransfers[eventId] = { job, job->targetFileName() }; connect(job, &BaseJob::downloadProgress, this, - [this,eventId] (qint64 received, qint64 total) { - d->fileTransfers[eventId].update(received, total); - emit fileTransferProgress(eventId, received, total); - }); - connect(job, &BaseJob::success, this, [this,eventId,fileUrl,job] { - d->fileTransfers[eventId].status = FileTransferInfo::Completed; - emit fileTransferCompleted(eventId, fileUrl, - QUrl::fromLocalFile(job->targetFileName())); - }); + [this, eventId](qint64 received, qint64 total) { + d->fileTransfers[eventId].update(received, total); + emit fileTransferProgress(eventId, received, total); + }); + connect(job, &BaseJob::success, this, [this, eventId, fileUrl, job] { + d->fileTransfers[eventId].status = FileTransferInfo::Completed; + emit fileTransferCompleted( + eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName())); + }); connect(job, &BaseJob::failure, this, - std::bind(&Private::failedTransfer, d, - eventId, job->errorString())); + std::bind(&Private::failedTransfer, d, eventId, + job->errorString())); } else d->failedTransfer(eventId); } void Room::cancelFileTransfer(const QString& id) { - auto it = d->fileTransfers.find(id); - if (it == d->fileTransfers.end()) - { - qCWarning(MAIN) << "No information on file transfer" << id - << "in room" << d->id; + const auto it = d->fileTransfers.constFind(id); + if (it == d->fileTransfers.cend()) { + qCWarning(MAIN) << "No information on file transfer" << id << "in room" + << d->id; return; } if (isJobRunning(it->job)) @@ -1936,15 +2017,16 @@ void Room::Private::dropDuplicateEvents(RoomEvents& events) const // Multiple-remove (by different criteria), single-erase // 1. Check for duplicates against the timeline. - auto dupsBegin = remove_if(events.begin(), events.end(), - [&] (const RoomEventPtr& e) - { return eventsIndex.contains(e->id()); }); + auto dupsBegin = + remove_if(events.begin(), events.end(), [&](const RoomEventPtr& e) { + return eventsIndex.contains(e->id()); + }); // 2. Check for duplicates within the batch if there are still events. for (auto eIt = events.begin(); distance(eIt, dupsBegin) > 1; ++eIt) - dupsBegin = remove_if(eIt + 1, dupsBegin, - [&] (const RoomEventPtr& e) - { return e->id() == (*eIt)->id(); }); + dupsBegin = remove_if(eIt + 1, dupsBegin, [&](const RoomEventPtr& e) { + return e->id() == (*eIt)->id(); + }); if (dupsBegin == events.end()) return; @@ -1963,46 +2045,44 @@ RoomEventPtr makeRedacted(const RoomEvent& target, const RedactionEvent& redaction) { auto originalJson = target.originalJsonObject(); - static const QStringList keepKeys { - EventIdKey, TypeKey, QStringLiteral("room_id"), - QStringLiteral("sender"), QStringLiteral("state_key"), + // clang-format off + static const QStringList keepKeys { EventIdKey, TypeKey, + QStringLiteral("room_id"), QStringLiteral("sender"), StateKeyKey, QStringLiteral("hashes"), QStringLiteral("signatures"), QStringLiteral("depth"), QStringLiteral("prev_events"), QStringLiteral("prev_state"), QStringLiteral("auth_events"), QStringLiteral("origin"), QStringLiteral("origin_server_ts"), - QStringLiteral("membership") + QStringLiteral("membership") }; + // clang-format on + + std::vector<std::pair<Event::Type, QStringList>> keepContentKeysMap { + { RoomMemberEvent::typeId(), { QStringLiteral("membership") } }, + { RoomCreateEvent::typeId(), { QStringLiteral("creator") } }, + { RoomPowerLevelsEvent::typeId(), + { QStringLiteral("ban"), QStringLiteral("events"), + QStringLiteral("events_default"), QStringLiteral("kick"), + QStringLiteral("redact"), QStringLiteral("state_default"), + QStringLiteral("users"), QStringLiteral("users_default") } }, + { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } } + // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } } + // , { RoomHistoryVisibility::typeId(), + // { QStringLiteral("history_visibility") } } }; - - std::vector<std::pair<Event::Type, QStringList>> keepContentKeysMap - { { RoomMemberEvent::typeId(), { QStringLiteral("membership") } } - , { RoomCreateEvent::typeId(), { QStringLiteral("creator") } } -// , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } } -// , { RoomPowerLevels::typeId(), -// { QStringLiteral("ban"), QStringLiteral("events"), -// QStringLiteral("events_default"), QStringLiteral("kick"), -// QStringLiteral("redact"), QStringLiteral("state_default"), -// QStringLiteral("users"), QStringLiteral("users_default") } } -// , { RoomHistoryVisibility::typeId(), -// { QStringLiteral("history_visibility") } } - }; - for (auto it = originalJson.begin(); it != originalJson.end();) - { + for (auto it = originalJson.begin(); it != originalJson.end();) { if (!keepKeys.contains(it.key())) it = originalJson.erase(it); // TODO: shred the value else ++it; } auto keepContentKeys = - find_if(keepContentKeysMap.begin(), keepContentKeysMap.end(), - [&target](const auto& t) { return target.type() == t.first; } ); - if (keepContentKeys == keepContentKeysMap.end()) - { + find_if(keepContentKeysMap.begin(), keepContentKeysMap.end(), + [&target](const auto& t) { return target.type() == t.first; }); + if (keepContentKeys == keepContentKeysMap.end()) { originalJson.remove(ContentKeyL); originalJson.remove(PrevContentKeyL); } else { auto content = originalJson.take(ContentKeyL).toObject(); - for (auto it = content.begin(); it != content.end(); ) - { + for (auto it = content.begin(); it != content.end();) { if (!keepContentKeys->second.contains(it.key())) it = content.erase(it); else @@ -2021,33 +2101,33 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) { // Can't use findInTimeline because it returns a const iterator, and // we need to change the underlying TimelineItem. - const auto pIdx = eventsIndex.find(redaction.redactedEvent()); - if (pIdx == eventsIndex.end()) + const auto pIdx = eventsIndex.constFind(redaction.redactedEvent()); + if (pIdx == eventsIndex.cend()) return false; Q_ASSERT(q->isValidIndex(*pIdx)); auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; - if (ti->isRedacted() && ti->redactedBecause()->id() == redaction.id()) - { - qCDebug(MAIN) << "Redaction" << redaction.id() - << "of event" << ti->id() << "already done, skipping"; + if (ti->isRedacted() && ti->redactedBecause()->id() == redaction.id()) { + qCDebug(EVENTS) << "Redaction" << redaction.id() << "of event" + << ti->id() << "already done, skipping"; return true; } // Make a new event from the redacted JSON and put it in the timeline // instead of the redacted one. oldEvent will be deleted on return. auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction)); - qCDebug(MAIN) << "Redacted" << oldEvent->id() << "with" << redaction.id(); - if (oldEvent->isStateEvent()) - { - const StateEventKey evtKey { oldEvent->matrixType(), oldEvent->stateKey() }; + qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" << redaction.id(); + if (oldEvent->isStateEvent()) { + const StateEventKey evtKey { oldEvent->matrixType(), + oldEvent->stateKey() }; Q_ASSERT(currentState.contains(evtKey)); - if (currentState.value(evtKey) == oldEvent.get()) - { - Q_ASSERT(ti.index() >= 0); // Historical states can't be in currentState - qCDebug(MAIN).nospace() << "Redacting state " - << oldEvent->matrixType() << "/" << oldEvent->stateKey(); + if (currentState.value(evtKey) == oldEvent.get()) { + Q_ASSERT(ti.index() >= 0); // Historical states can't be in + // currentState + qCDebug(STATE).nospace() + << "Redacting state " << oldEvent->matrixType() << "/" + << oldEvent->stateKey(); // Retarget the current state to the newly made event. if (q->processStateEvent(*ti)) emit q->namesChanged(q); @@ -2056,10 +2136,11 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) } if (const auto* reaction = eventCast<ReactionEvent>(oldEvent)) { const auto& targetEvtId = reaction->relation().eventId; - const auto lookupKey = qMakePair(targetEvtId, - EventRelation::Annotation()); + const auto lookupKey = + qMakePair(targetEvtId, EventRelation::Annotation()); if (relations.contains(lookupKey)) { relations[lookupKey].removeOne(reaction); + emit q->updatedEvent(targetEvtId); } } q->onRedaction(*oldEvent, *ti); @@ -2091,24 +2172,23 @@ bool Room::Private::processReplacement(const RoomMessageEvent& newEvent) { // Can't use findInTimeline because it returns a const iterator, and // we need to change the underlying TimelineItem. - const auto pIdx = eventsIndex.find(newEvent.replacedEvent()); - if (pIdx == eventsIndex.end()) + const auto pIdx = eventsIndex.constFind(newEvent.replacedEvent()); + if (pIdx == eventsIndex.cend()) return false; Q_ASSERT(q->isValidIndex(*pIdx)); auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; - if (ti->replacedBy() == newEvent.id()) - { - qCDebug(MAIN) << "Event" << ti->id() << "is already replaced with" - << newEvent.id(); + if (ti->replacedBy() == newEvent.id()) { + qCDebug(STATE) << "Event" << ti->id() << "is already replaced with" + << newEvent.id(); return true; } // Make a new event from the redacted JSON and put it in the timeline // instead of the redacted one. oldEvent will be deleted on return. auto oldEvent = ti.replaceEvent(makeReplaced(*ti, newEvent)); - qCDebug(MAIN) << "Replaced" << oldEvent->id() << "with" << newEvent.id(); + qCDebug(STATE) << "Replaced" << oldEvent->id() << "with" << newEvent.id(); emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); return true; } @@ -2119,10 +2199,7 @@ Connection* Room::connection() const return d->connection; } -User* Room::localUser() const -{ - return connection()->user(); -} +User* Room::localUser() const { return connection()->user(); } /// Whether the event is a redaction or a replacement inline bool isEditing(const RoomEventPtr& ep) @@ -2131,7 +2208,7 @@ inline bool isEditing(const RoomEventPtr& ep) if (is<RedactionEvent>(*ep)) return true; if (auto* msgEvent = eventCast<RoomMessageEvent>(ep)) - return msgEvent->replacedEvent().isEmpty(); + return !msgEvent->replacedEvent().isEmpty(); return false; } @@ -2149,42 +2226,39 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) // NB: We have to store redacting/replacing events to the timeline too - // see #220. auto it = std::find_if(events.begin(), events.end(), isEditing); - for (const auto& eptr: RoomEventsRange(it, events.end())) { + for (const auto& eptr : RoomEventsRange(it, events.end())) { if (auto* r = eventCast<RedactionEvent>(eptr)) { // Try to find the target in the timeline, then in the batch. if (processRedaction(*r)) continue; - auto targetIt = std::find_if(events.begin(), it, - [id = r->redactedEvent()]( - const RoomEventPtr& ep) { - return ep->id() == id; - }); - if (targetIt != it) + if (auto targetIt = std::find_if(events.begin(), events.end(), + [id = r->redactedEvent()](const RoomEventPtr& ep) { + return ep->id() == id; + }); targetIt != events.end()) *targetIt = makeRedacted(**targetIt, *r); else - qCDebug(MAIN) + qCDebug(STATE) << "Redaction" << r->id() << "ignored: target event" << r->redactedEvent() << "is not found"; // If the target event comes later, it comes already redacted. } - if (auto* msg = eventCast<RoomMessageEvent>(eptr)) { - if (!msg->replacedEvent().isEmpty()) { - if (processReplacement(*msg)) - continue; - auto targetIt = std::find_if(events.begin(), it, - [id = msg->replacedEvent()]( - const RoomEventPtr& ep) { - return ep->id() == id; - }); - if (targetIt != it) - *targetIt = makeReplaced(**targetIt, *msg); - else // FIXME: don't ignore, just show it wherever it arrived - qCDebug(MAIN) << "Replacing event" << msg->id() - << "ignored: replaced event" - << msg->replacedEvent() << "is not found"; - // Same as with redactions above, the replaced event coming - // later will come already with the new content. - } + if (auto* msg = eventCast<RoomMessageEvent>(eptr); + msg && !msg->replacedEvent().isEmpty()) { + if (processReplacement(*msg)) + continue; + auto targetIt = std::find_if(events.begin(), it, + [id = msg->replacedEvent()](const RoomEventPtr& ep) { + return ep->id() == id; + }); + if (targetIt != it) + *targetIt = makeReplaced(**targetIt, *msg); + else // FIXME: hide the replacing event when target arrives later + qCDebug(EVENTS) + << "Replacing event" << msg->id() + << "ignored: target event" << msg->replacedEvent() + << "is not found"; + // Same as with redactions above, the replaced event coming + // later will come already with the new content. } } } @@ -2195,49 +2269,52 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) // postulate that the current state is only current between syncs but not // within a sync. Changes roomChanges = Change::NoChange; - for (const auto& eptr: events) + for (const auto& eptr : events) roomChanges |= q->processStateEvent(*eptr); auto timelineSize = timeline.size(); size_t totalInserted = 0; - for (auto it = events.begin(); it != events.end();) - { - auto nextPendingPair = findFirstOf(it, events.end(), - unsyncedEvents.begin(), unsyncedEvents.end(), isEchoEvent); - auto nextPending = nextPendingPair.first; - - if (it != nextPending) - { - RoomEventsRange eventsSpan { it, nextPending }; + for (auto it = events.begin(); it != events.end();) { + auto nextPendingPair = + findFirstOf(it, events.end(), unsyncedEvents.begin(), + unsyncedEvents.end(), isEchoEvent); + const auto& remoteEcho = nextPendingPair.first; + const auto& localEcho = nextPendingPair.second; + + if (it != remoteEcho) { + RoomEventsRange eventsSpan { it, remoteEcho }; emit q->aboutToAddNewMessages(eventsSpan); auto insertedSize = moveEventsToTimeline(eventsSpan, Newer); totalInserted += insertedSize; auto firstInserted = timeline.cend() - insertedSize; q->onAddNewTimelineEvents(firstInserted); - emit q->addedMessages(firstInserted->index(), timeline.back().index()); + emit q->addedMessages(firstInserted->index(), + timeline.back().index()); } - if (nextPending == events.end()) + if (remoteEcho == events.end()) break; - it = nextPending + 1; - auto* nextPendingEvt = nextPending->get(); - const auto pendingEvtIdx = - int(nextPendingPair.second - unsyncedEvents.begin()); + it = remoteEcho + 1; + auto* nextPendingEvt = remoteEcho->get(); + const auto pendingEvtIdx = int(localEcho - unsyncedEvents.begin()); + if (localEcho->deliveryStatus() != EventStatus::ReachedServer) { + localEcho->setReachedServer(nextPendingEvt->id()); + emit q->pendingEventChanged(pendingEvtIdx); + } emit q->pendingEventAboutToMerge(nextPendingEvt, pendingEvtIdx); - qDebug(EVENTS) << "Merging pending event from transaction" - << nextPendingEvt->transactionId() << "into" - << nextPendingEvt->id(); + qCDebug(MESSAGES) << "Merging pending event from transaction" + << nextPendingEvt->transactionId() << "into" + << nextPendingEvt->id(); auto transfer = fileTransfers.take(nextPendingEvt->transactionId()); if (transfer.status != FileTransferInfo::None) fileTransfers.insert(nextPendingEvt->id(), transfer); // After emitting pendingEventAboutToMerge() above we cannot rely - // on the previously obtained nextPendingPair.second staying valid + // on the previously obtained localEcho staying valid // because a signal handler may send another message, thereby altering // unsyncedEvents (see #286). Fortunately, unsyncedEvents only grows at // its back so we can rely on the index staying valid at least. unsyncedEvents.erase(unsyncedEvents.begin() + pendingEvtIdx); - if (auto insertedSize = moveEventsToTimeline({nextPending, it}, Newer)) - { + if (auto insertedSize = moveEventsToTimeline({ remoteEcho, it }, Newer)) { totalInserted += insertedSize; q->onAddNewTimelineEvents(timeline.cend() - insertedSize); } @@ -2248,11 +2325,10 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) if (q->supportsCalls()) for (auto it = from; it != timeline.cend(); ++it) - if (auto* evt = it->viewAs<CallEventBase>()) + if (const auto* evt = it->viewAs<CallEventBase>()) emit q->callEvent(q, evt); - if (totalInserted > 0) - { + if (totalInserted > 0) { for (auto it = from; it != timeline.cend(); ++it) { if (const auto* reaction = it->viewAs<ReactionEvent>()) { const auto& relation = reaction->relation(); @@ -2261,9 +2337,9 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) } } - qCDebug(MAIN) - << "Room" << q->objectName() << "received" << totalInserted - << "new events; the last event is now" << timeline.back(); + qCDebug(MESSAGES) << "Room" << q->objectName() << "received" + << totalInserted << "new events; the last event is now" + << timeline.back(); // The first event in the just-added batch (referred to by `from`) // defines whose read marker can possibly be promoted any further over @@ -2271,12 +2347,15 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) // read receipts from the server (or, for the local user, // markMessagesAsRead() invocation) to promote their read markers over // the new message events. - auto firstWriter = q->user((*from)->senderId()); - if (q->readMarker(firstWriter) != timeline.crend()) - { - roomChanges |= promoteReadMarker(firstWriter, rev_iter_t(from) - 1); - qCDebug(MAIN) << "Auto-promoted read marker for" << firstWriter->id() - << "to" << *q->readMarker(firstWriter); + if (const auto senderId = (*from)->senderId(); !senderId.isEmpty()) { + auto* const firstWriter = q->user(senderId); + if (q->readMarker(firstWriter) != timeline.crend()) { + roomChanges |= + promoteReadMarker(firstWriter, rev_iter_t(from) - 1); + qCDebug(MESSAGES) + << "Auto-promoted read marker for" << senderId + << "to" << *q->readMarker(firstWriter); + } } updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); @@ -2289,7 +2368,8 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) { - QElapsedTimer et; et.start(); + QElapsedTimer et; + et.start(); const auto timelineSize = timeline.size(); dropDuplicateEvents(events); @@ -2300,12 +2380,10 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) // messages. Also, the cache doesn't store events with empty content; // so when such events show up in the timeline they should be properly // incorporated. - for (const auto& eptr: events) - { + for (const auto& eptr : events) { const auto& e = *eptr; - if (e.isStateEvent() && - !currentState.contains({e.matrixType(), e.stateKey()})) - { + if (e.isStateEvent() + && !currentState.contains({ e.matrixType(), e.stateKey() })) { q->processStateEvent(e); } } @@ -2314,8 +2392,8 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) const auto insertedSize = moveEventsToTimeline(events, Older); const auto from = timeline.crend() - insertedSize; - qCDebug(MAIN) << "Room" << displayname << "received" << insertedSize - << "past events; the oldest event is now" << timeline.front(); + qCDebug(STATE) << "Room" << displayname << "received" << insertedSize + << "past events; the oldest event is now" << timeline.front(); q->onAddHistoricalTimelineEvents(from); emit q->addedMessages(timeline.front().index(), from->index()); @@ -2340,45 +2418,96 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) if (!e.isStateEvent()) return Change::NoChange; - const auto* oldStateEvent = std::exchange( - d->currentState[{e.matrixType(),e.stateKey()}], - static_cast<const StateEventBase*>(&e)); - Q_ASSERT(!oldStateEvent || - (oldStateEvent->matrixType() == e.matrixType() && - oldStateEvent->stateKey() == e.stateKey())); + // Find a value (create an empty one if necessary) and get a reference + // to it. Can't use getCurrentState<>() because it (creates and) returns + // a stub if a value is not found, and what's needed here is a "real" event + // or nullptr. + auto& curStateEvent = d->currentState[{ e.matrixType(), e.stateKey() }]; + // Prepare for the state change + const auto oldRme = static_cast<const RoomMemberEvent*>(curStateEvent); + visit(e, [this, &oldRme](const RoomMemberEvent& rme) { + auto* u = user(rme.userId()); + if (!u) { // ??? + qCCritical(MAIN) + << "Could not get a user object for" << rme.userId(); + return; + } + // TODO: remove along with User::processEvent() in 0.7 + const auto prevMembership = oldRme ? oldRme->membership() + : MembershipType::Leave; + u->processEvent(rme, this, oldRme == nullptr); + + switch (prevMembership) { + case MembershipType::Invite: + if (rme.membership() != prevMembership) { + d->usersInvited.removeOne(u); + Q_ASSERT(!d->usersInvited.contains(u)); + } + break; + case MembershipType::Join: + switch (rme.membership()) { + case MembershipType::Join: // rename/avatar change or no-op + if (rme.displayName() != oldRme->displayName()) { + emit memberAboutToRename(u, rme.displayName()); + d->removeMemberFromMap(u); + } + break; + case MembershipType::Invite: + qCWarning(MAIN) << "Membership change from Join to Invite:" + << rme; + [[fallthrough]]; + default: // whatever the new membership, it's no more Join + d->removeMemberFromMap(u); + emit userRemoved(u); + } + break; + default: + if (rme.membership() == MembershipType::Invite + || rme.membership() == MembershipType::Join) { + d->membersLeft.removeOne(u); + Q_ASSERT(!d->membersLeft.contains(u)); + } + } + }); + + // Change the state + const auto* const oldStateEvent = + std::exchange(curStateEvent, static_cast<const StateEventBase*>(&e)); + Q_ASSERT(!oldStateEvent + || (oldStateEvent->matrixType() == e.matrixType() + && oldStateEvent->stateKey() == e.stateKey())); if (!is<RoomMemberEvent>(e)) // Room member events are too numerous - qCDebug(EVENTS) << "Room state event:" << e; + qCDebug(STATE) << "Updated room state:" << e; + + // Update internal structures as per the change and work out the return value + // clang-format off return visit(e , [] (const RoomNameEvent&) { return NameChange; } , [] (const RoomAliasesEvent&) { - // This event has been removed by MSC-2432 - return NoChange; + return NoChange; // This event has been removed by MSC2432 } - , [this, oldStateEvent] (const RoomCanonicalAliasEvent& evt) { - setObjectName(evt.alias().isEmpty() ? d->id : evt.alias()); - - auto prevAliases = oldStateEvent ? fromJson<QStringList>( - oldStateEvent->contentJson()["alt_aliases"]) - : QStringList(); - if (oldStateEvent) { - const auto prevCanonicalAlias = - static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent) - ->alias(); - if (!prevCanonicalAlias.isEmpty()) - prevAliases.push_back(prevCanonicalAlias); + , [this, oldStateEvent] (const RoomCanonicalAliasEvent& cae) { + // clang-format on + setObjectName(cae.alias().isEmpty() ? d->id : cae.alias()); + const auto* oldCae = + static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent); + QStringList previousAltAliases {}; + if (oldCae) { + previousAltAliases = oldCae->altAliases(); + if (!oldCae->alias().isEmpty()) + previousAltAliases.push_back(oldCae->alias()); } - auto newAliases = - fromJson<QStringList>(evt.contentJson()["alt_aliases"]); - if (!evt.alias().isEmpty()) - newAliases.push_back(evt.alias()); - - connection()->updateRoomAliases(id(), prevAliases, newAliases); + auto newAliases = cae.altAliases(); + if (!cae.alias().isEmpty()) + newAliases.push_front(cae.alias()); - return CanonicalAliasChange; + connection()->updateRoomAliases(id(), previousAltAliases, newAliases); + return AliasesChange; + // clang-format off } , [] (const RoomTopicEvent&) { return TopicChange; @@ -2388,83 +2517,59 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) emit avatarChanged(); return AvatarChange; } - , [this,oldStateEvent] (const RoomMemberEvent& evt) { + , [this,oldRme] (const RoomMemberEvent& evt) { + // clang-format on auto* u = user(evt.userId()); - const auto* oldMemberEvent = - static_cast<const RoomMemberEvent*>(oldStateEvent); - u->processEvent(evt, this, oldMemberEvent == nullptr); - const auto prevMembership = oldMemberEvent - ? oldMemberEvent->membership() : MembershipType::Leave; - if (u == localUser() && evt.membership() == MembershipType::Invite - && evt.isDirect()) - connection()->addToDirectChats(this, user(evt.senderId())); - - switch (prevMembership) - { - case MembershipType::Invite: - if (evt.membership() != prevMembership) - { - d->usersInvited.removeOne(u); - Q_ASSERT(!d->usersInvited.contains(u)); - } - break; - case MembershipType::Join: - if (evt.membership() == MembershipType::Invite) - qCWarning(MAIN) - << "Invalid membership change from Join to Invite:" - << evt; - if (evt.membership() != prevMembership) - { - disconnect(u, &User::nameAboutToChange, this, nullptr); - disconnect(u, &User::nameChanged, this, nullptr); - d->removeMemberFromMap(u->name(this), u); - emit userRemoved(u); - } - break; - default: - if (evt.membership() == MembershipType::Invite - || evt.membership() == MembershipType::Join) - { - d->membersLeft.removeOne(u); - Q_ASSERT(!d->membersLeft.contains(u)); - } - } + // TODO: remove in 0.7 + u->processEvent(evt, this, oldRme == nullptr); - switch(evt.membership()) - { + const auto prevMembership = oldRme ? oldRme->membership() + : MembershipType::Leave; + switch (evt.membership()) { case MembershipType::Join: - if (prevMembership != MembershipType::Join) - { + if (prevMembership != MembershipType::Join) { d->insertMemberIntoMap(u); - connect(u, &User::nameAboutToChange, this, - [=] (QString newName, QString, const Room* context) { - if (context == this) - emit memberAboutToRename(u, newName); - }); - connect(u, &User::nameChanged, this, - [=] (QString, QString oldName, const Room* context) { - if (context == this) - { - d->renameMember(u, oldName); - emit memberRenamed(u); - } - }); emit userAdded(u); + } else if (oldRme->displayName() != evt.displayName()) { + d->insertMemberIntoMap(u); + emit memberRenamed(u); } break; case MembershipType::Invite: if (!d->usersInvited.contains(u)) d->usersInvited.push_back(u); + if (u == localUser() && evt.isDirect()) + connection()->addToDirectChats(this, user(evt.senderId())); break; - default: + case MembershipType::Knock: + case MembershipType::Ban: + case MembershipType::Leave: if (!d->membersLeft.contains(u)) d->membersLeft.append(u); } return MembersChange; + // clang-format off } - , [this] (const EncryptionEvent&) { - emit encryption(); // It can only be done once, so emit it here. + , [this, oldEncEvt = static_cast<const EncryptionEvent*>(oldStateEvent)]( + const EncryptionEvent& ee) { + // clang-format on + if (ee.algorithm().isEmpty()) { + qWarning(STATE) + << "The encryption event for room" << objectName() + << "doesn't have 'algorithm' specified - ignoring"; + return NoChange; + } + if (oldEncEvt + && oldEncEvt->encryption() != EncryptionEventContent::Undefined) { + qCWarning(STATE) << "The room is already encrypted but a new" + " room encryption event arrived - ignoring"; + return NoChange; + } + // As encryption can only be switched on once, emit the signal here + // instead of aggregating and emitting in updateData() + emit encryption(); return OtherChange; + // clang-format off } , [this] (const RoomTombstoneEvent& evt) { const auto successorId = evt.successorRoomId(); @@ -2483,75 +2588,70 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return OtherChange; } ); + // clang-format on } Room::Changes Room::processEphemeralEvent(EventPtr&& event) { Changes changes = NoChange; - QElapsedTimer et; et.start(); - if (auto* evt = eventCast<TypingEvent>(event)) - { + QElapsedTimer et; + et.start(); + if (auto* evt = eventCast<TypingEvent>(event)) { d->usersTyping.clear(); - for( const QString& userId: qAsConst(evt->users()) ) - { + for (const QString& userId : qAsConst(evt->users())) { auto u = user(userId); if (memberJoinState(u) == JoinState::Join) d->usersTyping.append(u); } if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) << "*** Room::processEphemeralEvent(typing):" - << evt->users().size() << "users," << et; + << evt->users().size() << "users," << et; emit typingChanged(); } - if (auto* evt = eventCast<ReceiptEvent>(event)) - { + if (auto* evt = eventCast<ReceiptEvent>(event)) { int totalReceipts = 0; - for( const auto &p: qAsConst(evt->eventsWithReceipts()) ) - { + for (const auto& p : qAsConst(evt->eventsWithReceipts())) { totalReceipts += p.receipts.size(); { if (p.receipts.size() == 1) - qCDebug(EPHEMERAL) << "Marking" << p.evtId - << "as read for" << p.receipts[0].userId; + qCDebug(EPHEMERAL) << "Marking" << p.evtId << "as read for" + << p.receipts[0].userId; else - qCDebug(EPHEMERAL) << "Marking" << p.evtId - << "as read for" << p.receipts.size() << "users"; + qCDebug(EPHEMERAL) << "Marking" << p.evtId << "as read for" + << p.receipts.size() << "users"; } const auto newMarker = findInTimeline(p.evtId); - if (newMarker != timelineEdge()) - { - for( const Receipt& r: p.receipts ) - { + if (newMarker != timelineEdge()) { + for (const Receipt& r : p.receipts) { if (r.userId == connection()->userId()) continue; // FIXME, #185 auto u = user(r.userId); if (memberJoinState(u) == JoinState::Join) changes |= d->promoteReadMarker(u, newMarker); } - } else - { + } else { qCDebug(EPHEMERAL) << "Event" << p.evtId - << "not found; saving read receipts anyway"; + << "not found; saving read receipts anyway"; // If the event is not found (most likely, because it's too old // and hasn't been fetched from the server yet), but there is // a previous marker for a user, keep the previous marker. // Otherwise, blindly store the event id for this user. - for( const Receipt& r: p.receipts ) - { + for (const Receipt& r : p.receipts) { if (r.userId == connection()->userId()) continue; // FIXME, #185 auto u = user(r.userId); - if (memberJoinState(u) == JoinState::Join && - readMarker(u) == timelineEdge()) + if (memberJoinState(u) == JoinState::Join + && readMarker(u) == timelineEdge()) changes |= d->setLastReadEvent(u, p.evtId); } } } - if (evt->eventsWithReceipts().size() > 3 || totalReceipts > 10 || - et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::processEphemeralEvent(receipts):" - << evt->eventsWithReceipts().size() - << "event(s) with" << totalReceipts << "receipt(s)," << et; + if (evt->eventsWithReceipts().size() > 3 || totalReceipts > 10 + || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) + << "*** Room::processEphemeralEvent(receipts):" + << evt->eventsWithReceipts().size() << "event(s) with" + << totalReceipts << "receipt(s)," << et; } return changes; } @@ -2559,32 +2659,29 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) Room::Changes Room::processAccountDataEvent(EventPtr&& event) { Changes changes = NoChange; - if (auto* evt = eventCast<TagEvent>(event)) - { + if (auto* evt = eventCast<TagEvent>(event)) { d->setTags(evt->tags()); changes |= Change::TagsChange; } - if (auto* evt = eventCast<ReadMarkerEvent>(event)) - { + if (auto* evt = eventCast<ReadMarkerEvent>(event)) { auto readEventId = evt->event_id(); - qCDebug(MAIN) << "Server-side read marker at" << readEventId; + qCDebug(STATE) << "Server-side read marker at" << readEventId; d->serverReadMarker = readEventId; const auto newMarker = findInTimeline(readEventId); changes |= newMarker != timelineEdge() - ? d->markMessagesAsRead(newMarker) - : d->setLastReadEvent(localUser(), readEventId); + ? d->markMessagesAsRead(newMarker) + : d->setLastReadEvent(localUser(), readEventId); } // For all account data events auto& currentData = d->accountData[event->matrixType()]; // A polymorphic event-specific comparison might be a bit more // efficient; maaybe do it another day - if (!currentData || currentData->contentJson() != event->contentJson()) - { + if (!currentData || currentData->contentJson() != event->contentJson()) { emit accountDataAboutToChange(event->matrixType()); currentData = move(event); - qCDebug(MAIN) << "Updated account data of type" - << currentData->matrixType(); + qCDebug(STATE) << "Updated account data of type" + << currentData->matrixType(); emit accountDataChanged(currentData->matrixType()); return Change::AccountDataChange; } @@ -2600,15 +2697,14 @@ Room::Private::buildShortlist(const ContT& users) const // display names of two topmost users excluding the current one to render // the name of the room. The below code selects 3 topmost users, // slightly extending the spec. - users_shortlist_t shortlist { }; // Prefill with nullptrs + users_shortlist_t shortlist {}; // Prefill with nullptrs std::partial_sort_copy( - users.begin(), users.end(), - shortlist.begin(), shortlist.end(), - [this] (const User* u1, const User* u2) { - // localUser(), if it's in the list, is sorted below all others + users.begin(), users.end(), shortlist.begin(), shortlist.end(), + [this](const User* u1, const User* u2) { + // localUser(), if it's in the list, is sorted + // below all others return isLocalUser(u2) || (!isLocalUser(u1) && u1->id() < u2->id()); - } - ); + }); return shortlist; } @@ -2617,14 +2713,14 @@ Room::Private::buildShortlist(const QStringList& userIds) const { QList<User*> users; users.reserve(userIds.size()); - for (const auto& h: userIds) + for (const auto& h : userIds) users.push_back(q->user(h)); return buildShortlist(users); } QString Room::Private::calculateDisplayname() const { - // CS spec, section 11.2.2.5 Calculating the display name for a room + // CS spec, section 13.2.2.5 Calculating the display name for a room // Numbers below refer to respective parts in the spec. // 1. Name (from m.room.name) @@ -2638,35 +2734,38 @@ QString Room::Private::calculateDisplayname() const if (!dispName.isEmpty()) return dispName; - // Using m.room.aliases in naming is explicitly discouraged by the spec + // 3. m.room.aliases - only local aliases, subject for further removal + const auto aliases = q->aliases(); + if (!aliases.isEmpty()) + return aliases.front(); + + // 4. m.heroes and m.room.member + // From here on, we use a more general algorithm than the spec describes + // in order to provide back-compatibility with pre-MSC688 servers. - // Supplementary code for 3 and 4: build the shortlist of users whose names + // Supplementary code: build the shortlist of users whose names // will be used to construct the room name. Takes into account MSC688's // "heroes" if available. - const bool localUserIsIn = joinState == JoinState::Join; - const bool emptyRoom = membersMap.isEmpty() || - (membersMap.size() == 1 && isLocalUser(*membersMap.begin())); - const bool nonEmptySummary = - !summary.heroes.omitted() && !summary.heroes->empty(); - auto shortlist = nonEmptySummary ? buildShortlist(summary.heroes.value()) : - !emptyRoom ? buildShortlist(membersMap) : - users_shortlist_t { }; - - // When lazy-loading is on, we can rely on the heroes list. - // If it's off, the below code gathers invited and left members. - // NB: including invitations, if any, into naming is a spec extension. - // This kicks in when there's no lazy loading and it's a room with - // the local user as the only member, with more users invited. + const bool emptyRoom = + membersMap.isEmpty() + || (membersMap.size() == 1 && isLocalUser(*membersMap.cbegin())); + const bool nonEmptySummary = summary.heroes && !summary.heroes->empty(); + auto shortlist = nonEmptySummary ? buildShortlist(*summary.heroes) + : !emptyRoom ? buildShortlist(membersMap) + : users_shortlist_t {}; + + // When the heroes list is there, we can rely on it. If the heroes list is + // missing, the below code gathers invited, or, if there are no invitees, + // left members. if (!shortlist.front() && localUserIsIn) shortlist = buildShortlist(usersInvited); - if (!shortlist.front()) // Still empty shortlist; use left members + if (!shortlist.front()) shortlist = buildShortlist(membersLeft); QStringList names; - for (auto u: shortlist) - { + for (auto u : shortlist) { if (u == nullptr || isLocalUser(u)) break; // Only disambiguate if the room is not empty @@ -2674,17 +2773,19 @@ QString Room::Private::calculateDisplayname() const } const auto usersCountExceptLocal = - !emptyRoom ? q->joinedCount() - int(joinState == JoinState::Join) : - !usersInvited.empty() ? usersInvited.count() : - membersLeft.size() - int(joinState == JoinState::Leave); + !emptyRoom + ? q->joinedCount() - int(joinState == JoinState::Join) + : !usersInvited.empty() + ? usersInvited.count() + : membersLeft.size() - int(joinState == JoinState::Leave); if (usersCountExceptLocal > int(shortlist.size())) - names << - tr("%Ln other(s)", - "Used to make a room name from user names: A, B and _N others_", - usersCountExceptLocal - int(shortlist.size())); + names << tr( + "%Ln other(s)", + "Used to make a room name from user names: A, B and _N others_", + usersCountExceptLocal - int(shortlist.size())); const auto namesList = QLocale().createSeparatedList(names); - // 3. Room members + // Room members if (!emptyRoom) return namesList; @@ -2692,22 +2793,21 @@ QString Room::Private::calculateDisplayname() const if (!usersInvited.empty()) return tr("Empty room (invited: %1)").arg(namesList); - // 4. Users that previously left the room - if (membersLeft.size() > 0) + // Users that previously left the room + if (!membersLeft.isEmpty()) return tr("Empty room (was: %1)").arg(namesList); - // 5. Fail miserably + // Fail miserably return tr("Empty room (%1)").arg(id); } void Room::Private::updateDisplayname() { auto swappedName = calculateDisplayname(); - if (swappedName != displayname) - { + if (swappedName != displayname) { emit q->displaynameAboutToChange(q); swap(displayname, swappedName); - qDebug(MAIN) << q->objectName() << "has changed display name from" + qCDebug(MAIN) << q->objectName() << "has changed display name from" << swappedName << "to" << displayname; emit q->displaynameChanged(q, swappedName); } @@ -2715,17 +2815,17 @@ void Room::Private::updateDisplayname() QJsonObject Room::Private::toJson() const { - QElapsedTimer et; et.start(); + QElapsedTimer et; + et.start(); QJsonObject result; addParam<IfNotEmpty>(result, QStringLiteral("summary"), summary); { QJsonArray stateEvents; - for (const auto* evt: currentState) - { + for (const auto* evt : currentState) { Q_ASSERT(evt->isStateEvent()); - if ((evt->isRedacted() && !is<RoomMemberEvent>(*evt)) || - evt->contentJson().isEmpty()) + if ((evt->isRedacted() && !is<RoomMemberEvent>(*evt)) + || evt->contentJson().isEmpty()) continue; auto json = evt->fullJson(); @@ -2735,31 +2835,32 @@ QJsonObject Room::Private::toJson() const stateEvents.append(json); } - const auto stateObjName = joinState == JoinState::Invite ? - QStringLiteral("invite_state") : QStringLiteral("state"); + const auto stateObjName = joinState == JoinState::Invite + ? QStringLiteral("invite_state") + : QStringLiteral("state"); result.insert(stateObjName, - QJsonObject {{ QStringLiteral("events"), stateEvents }}); + QJsonObject { { QStringLiteral("events"), stateEvents } }); } - if (!accountData.empty()) - { + if (!accountData.empty()) { QJsonArray accountDataEvents; - for (const auto& e: accountData) - { + for (const auto& e : accountData) { if (!e.second->contentJson().isEmpty()) accountDataEvents.append(e.second->fullJson()); } result.insert(QStringLiteral("account_data"), - QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); + QJsonObject { + { QStringLiteral("events"), accountDataEvents } }); } - QJsonObject unreadNotifObj - { { SyncRoomData::UnreadCountKey, unreadMessages } }; + QJsonObject unreadNotifObj { { SyncRoomData::UnreadCountKey, + unreadMessages } }; if (highlightCount > 0) unreadNotifObj.insert(QStringLiteral("highlight_count"), highlightCount); if (notificationCount > 0) - unreadNotifObj.insert(QStringLiteral("notification_count"), notificationCount); + unreadNotifObj.insert(QStringLiteral("notification_count"), + notificationCount); result.insert(QStringLiteral("unread_notifications"), unreadNotifObj); @@ -2769,22 +2870,16 @@ QJsonObject Room::Private::toJson() const return result; } -QJsonObject Room::toJson() const -{ - return d->toJson(); -} +QJsonObject Room::toJson() const { return d->toJson(); } -MemberSorter Room::memberSorter() const -{ - return MemberSorter(this); -} +MemberSorter Room::memberSorter() const { return MemberSorter(this); } -bool MemberSorter::operator()(User *u1, User *u2) const +bool MemberSorter::operator()(User* u1, User* u2) const { return operator()(u1, room->roomMembername(u2)); } -bool MemberSorter::operator ()(User* u1, const QString& u2name) const +bool MemberSorter::operator()(User* u1, const QString& u2name) const { auto n1 = room->roomMembername(u1); if (n1.startsWith('@')) @@ -13,626 +13,742 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once -#include "csapi/message_pagination.h" -#include "events/roommessageevent.h" -#include "events/accountdataevents.h" +#include "connection.h" #include "eventitem.h" #include "joinstate.h" +#include "csapi/message_pagination.h" + +#include "events/accountdataevents.h" +#include "events/encryptedevent.h" +#include "events/roomkeyevent.h" +#include "events/roommessageevent.h" +#include "events/roomcreateevent.h" +#include "events/roomtombstoneevent.h" + +#include <QtCore/QJsonObject> #include <QtGui/QImage> -#include <memory> #include <deque> +#include <memory> #include <utility> -namespace QMatrixClient -{ - class Event; - class Avatar; - class SyncRoomData; - class RoomMemberEvent; - class Connection; - class User; - class MemberSorter; - class LeaveRoomJob; - class SetRoomStateWithKeyJob; - class RedactEventJob; - - /** The data structure used to expose file transfer information to views +namespace Quotient { +class Event; +class Avatar; +class SyncRoomData; +class RoomMemberEvent; +class User; +class MemberSorter; +class LeaveRoomJob; +class SetRoomStateWithKeyJob; +class RedactEventJob; + +/** The data structure used to expose file transfer information to views + * + * This is specifically tuned to work with QML exposing all traits as + * Q_PROPERTY values. + */ +class FileTransferInfo { + Q_GADGET + Q_PROPERTY(bool isUpload MEMBER isUpload CONSTANT) + Q_PROPERTY(bool active READ active CONSTANT) + Q_PROPERTY(bool started READ started CONSTANT) + Q_PROPERTY(bool completed READ completed CONSTANT) + Q_PROPERTY(bool failed READ failed CONSTANT) + Q_PROPERTY(int progress MEMBER progress CONSTANT) + Q_PROPERTY(int total MEMBER total CONSTANT) + Q_PROPERTY(QUrl localDir MEMBER localDir CONSTANT) + Q_PROPERTY(QUrl localPath MEMBER localPath CONSTANT) +public: + enum Status { None, Started, Completed, Failed, Cancelled }; + Status status = None; + bool isUpload = false; + int progress = 0; + int total = -1; + QUrl localDir {}; + QUrl localPath {}; + + bool started() const { return status == Started; } + bool completed() const { return status == Completed; } + bool active() const { return started() || completed(); } + bool failed() const { return status == Failed; } +}; + +class Room : public QObject { + Q_OBJECT + Q_PROPERTY(Connection* connection READ connection CONSTANT) + Q_PROPERTY(User* localUser READ localUser CONSTANT) + Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(QString version READ version NOTIFY baseStateLoaded) + Q_PROPERTY(bool isUnstable READ isUnstable NOTIFY stabilityUpdated) + Q_PROPERTY(QString predecessorId READ predecessorId NOTIFY baseStateLoaded) + Q_PROPERTY(QString successorId READ successorId NOTIFY upgraded) + Q_PROPERTY(QString name READ name NOTIFY namesChanged) + Q_PROPERTY(QStringList aliases READ aliases NOTIFY namesChanged) + Q_PROPERTY(QStringList altAliases READ altAliases NOTIFY namesChanged) + Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) + Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged) + Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) + Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged + STORED false) + Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) + Q_PROPERTY(bool usesEncryption READ usesEncryption NOTIFY encryption) + + Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages) + Q_PROPERTY(QStringList memberNames READ memberNames NOTIFY memberListChanged) + Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged) + Q_PROPERTY(int joinedCount READ joinedCount NOTIFY memberListChanged) + Q_PROPERTY(int invitedCount READ invitedCount NOTIFY memberListChanged) + Q_PROPERTY(int totalMemberCount READ totalMemberCount NOTIFY memberListChanged) + + Q_PROPERTY(bool displayed READ displayed WRITE setDisplayed NOTIFY + displayedChanged) + Q_PROPERTY(QString firstDisplayedEventId READ firstDisplayedEventId WRITE + setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged) + Q_PROPERTY(QString lastDisplayedEventId READ lastDisplayedEventId WRITE + setLastDisplayedEventId NOTIFY lastDisplayedEventChanged) + + Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE + markMessagesAsRead NOTIFY readMarkerMoved) + Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY + unreadMessagesChanged) + Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadMessagesChanged) + Q_PROPERTY(int highlightCount READ highlightCount NOTIFY + highlightCountChanged RESET resetHighlightCount) + Q_PROPERTY(int notificationCount READ notificationCount NOTIFY + notificationCountChanged RESET resetNotificationCount) + Q_PROPERTY(bool allHistoryLoaded READ allHistoryLoaded NOTIFY addedMessages + STORED false) + Q_PROPERTY(QStringList tagNames READ tagNames NOTIFY tagsChanged) + Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged) + Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged) + + Q_PROPERTY(GetRoomEventsJob* eventsHistoryJob READ eventsHistoryJob NOTIFY + eventsHistoryJobChanged) + +public: + using Timeline = std::deque<TimelineItem>; + using PendingEvents = std::vector<PendingEventItem>; + using RelatedEvents = QVector<const RoomEvent*>; + using rev_iter_t = Timeline::const_reverse_iterator; + using timeline_iter_t = Timeline::const_iterator; + + enum Change : uint { + NoChange = 0x0, + NameChange = 0x1, + AliasesChange = 0x2, + CanonicalAliasChange = AliasesChange, + TopicChange = 0x4, + UnreadNotifsChange = 0x8, + AvatarChange = 0x10, + JoinStateChange = 0x20, + TagsChange = 0x40, + MembersChange = 0x80, + /* = 0x100, */ + AccountDataChange = 0x200, + SummaryChange = 0x400, + ReadMarkerChange = 0x800, + OtherChange = 0x8000, + AnyChange = 0xFFFF + }; + Q_DECLARE_FLAGS(Changes, Change) + Q_FLAG(Changes) + + Room(Connection* connection, QString id, JoinState initialJoinState); + ~Room() override; + + // Property accessors + + Connection* connection() const; + User* localUser() const; + const QString& id() const; + QString version() const; + bool isUnstable() const; + QString predecessorId() const; + /// Room predecessor + /** This function validates that the predecessor has a tombstone and + * the tombstone refers to the current room. If that's not the case, + * or if the predecessor is in a join state not matching \p stateFilter, + * the function returns nullptr. + */ + Room* predecessor(JoinStates statesFilter = JoinState::Invite + | JoinState::Join) const; + QString successorId() const; + /// Room successor + /** This function validates that the successor room's creation event + * refers to the current room. If that's not the case, or if the successor + * is in a join state not matching \p stateFilter, it returns nullptr. + */ + Room* successor(JoinStates statesFilter = JoinState::Invite + | JoinState::Join) const; + QString name() const; + /// Room aliases defined on the current user's server + /// \sa remoteAliases, setLocalAliases + [[deprecated("Use aliases()")]] + QStringList localAliases() const; + /// Room aliases defined on other servers + /// \sa localAliases + [[deprecated("Use aliases()")]] + QStringList remoteAliases() const; + QString canonicalAlias() const; + QStringList altAliases() const; + QStringList aliases() const; + QString displayName() const; + QString topic() const; + QString avatarMediaId() const; + QUrl avatarUrl() const; + const Avatar& avatarObject() const; + Q_INVOKABLE JoinState joinState() const; + Q_INVOKABLE QList<Quotient::User*> usersTyping() const; + QList<User*> membersLeft() const; + + Q_INVOKABLE QList<Quotient::User*> users() const; + QStringList memberNames() const; + [[deprecated("Use joinedCount(), invitedCount(), totalMemberCount()")]] + int memberCount() const; + int timelineSize() const; + bool usesEncryption() const; + RoomEventPtr decryptMessage(const EncryptedEvent& encryptedEvent); + void handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, const QString& senderKey); + int joinedCount() const; + int invitedCount() const; + int totalMemberCount() const; + + GetRoomEventsJob* eventsHistoryJob() const; + + /** + * Returns a square room avatar with the given size and requests it + * from the network if needed + * \return a pixmap with the avatar or a placeholder if there's none + * available yet + */ + Q_INVOKABLE QImage avatar(int dimension); + /** + * Returns a room avatar with the given dimensions and requests it + * from the network if needed + * \return a pixmap with the avatar or a placeholder if there's none + * available yet + */ + Q_INVOKABLE QImage avatar(int width, int height); + + /** + * \brief Get a user object for a given user id + * This is the recommended way to get a user object in a room + * context. The actual object type may be changed in further + * versions to provide room-specific user information (display name, + * avatar etc.). + * \note The method will return a valid user regardless of + * the membership. + */ + Q_INVOKABLE Quotient::User* user(const QString& userId) const; + + /** + * \brief Check the join state of a given user in this room * - * This is specifically tuned to work with QML exposing all traits as - * Q_PROPERTY values. + * \note Banned and invited users are not tracked for now (Leave + * will be returned for them). + * + * \return Join if the user is a room member; Leave otherwise */ - class FileTransferInfo - { - Q_GADGET - Q_PROPERTY(bool isUpload MEMBER isUpload CONSTANT) - Q_PROPERTY(bool active READ active CONSTANT) - Q_PROPERTY(bool started READ started CONSTANT) - Q_PROPERTY(bool completed READ completed CONSTANT) - Q_PROPERTY(bool failed READ failed CONSTANT) - Q_PROPERTY(int progress MEMBER progress CONSTANT) - Q_PROPERTY(int total MEMBER total CONSTANT) - Q_PROPERTY(QUrl localDir MEMBER localDir CONSTANT) - Q_PROPERTY(QUrl localPath MEMBER localPath CONSTANT) - public: - enum Status { None, Started, Completed, Failed, Cancelled }; - Status status = None; - bool isUpload = false; - int progress = 0; - int total = -1; - QUrl localDir { }; - QUrl localPath { }; - - bool started() const { return status == Started; } - bool completed() const { return status == Completed; } - bool active() const { return started() || completed(); } - bool failed() const { return status == Failed; } - }; + Q_INVOKABLE Quotient::JoinState memberJoinState(Quotient::User* user) const; - class Room: public QObject - { - Q_OBJECT - Q_PROPERTY(Connection* connection READ connection CONSTANT) - Q_PROPERTY(User* localUser READ localUser CONSTANT) - Q_PROPERTY(QString id READ id CONSTANT) - Q_PROPERTY(QString version READ version NOTIFY baseStateLoaded) - Q_PROPERTY(bool isUnstable READ isUnstable NOTIFY stabilityUpdated) - Q_PROPERTY(QString predecessorId READ predecessorId NOTIFY baseStateLoaded) - Q_PROPERTY(QString successorId READ successorId NOTIFY upgraded) - Q_PROPERTY(QString name READ name NOTIFY namesChanged) - Q_PROPERTY(QStringList aliases READ aliases NOTIFY namesChanged) - Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) - Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged) - Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) - Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) - Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) - Q_PROPERTY(bool usesEncryption READ usesEncryption NOTIFY encryption) - - Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages) - Q_PROPERTY(QStringList memberNames READ memberNames NOTIFY memberListChanged) - Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged) - Q_PROPERTY(int joinedCount READ joinedCount NOTIFY memberListChanged) - Q_PROPERTY(int invitedCount READ invitedCount NOTIFY memberListChanged) - Q_PROPERTY(int totalMemberCount READ totalMemberCount NOTIFY memberListChanged) - - Q_PROPERTY(bool displayed READ displayed WRITE setDisplayed NOTIFY displayedChanged) - Q_PROPERTY(QString firstDisplayedEventId READ firstDisplayedEventId WRITE setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged) - Q_PROPERTY(QString lastDisplayedEventId READ lastDisplayedEventId WRITE setLastDisplayedEventId NOTIFY lastDisplayedEventChanged) - - Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerMoved) - Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY unreadMessagesChanged) - Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadMessagesChanged) - Q_PROPERTY(QStringList tagNames READ tagNames NOTIFY tagsChanged) - Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged) - Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged) - - Q_PROPERTY(GetRoomEventsJob* eventsHistoryJob READ eventsHistoryJob NOTIFY eventsHistoryJobChanged) - - public: - using Timeline = std::deque<TimelineItem>; - using PendingEvents = std::vector<PendingEventItem>; - using RelatedEvents = QVector<const RoomEvent*>; - using rev_iter_t = Timeline::const_reverse_iterator; - using timeline_iter_t = Timeline::const_iterator; - - enum Change : uint { - NoChange = 0x0, - NameChange = 0x1, - CanonicalAliasChange = 0x2, - TopicChange = 0x4, - UnreadNotifsChange = 0x8, - AvatarChange = 0x10, - JoinStateChange = 0x20, - TagsChange = 0x40, - MembersChange = 0x80, - /* = 0x100, */ - AccountDataChange = 0x200, - SummaryChange = 0x400, - ReadMarkerChange = 0x800, - OtherChange = 0x8000, - AnyChange = 0xFFFF - }; - Q_DECLARE_FLAGS(Changes, Change) - Q_FLAG(Changes) - - Room(Connection* connection, QString id, JoinState initialJoinState); - ~Room() override; - - // Property accessors - - Connection* connection() const; - User* localUser() const; - const QString& id() const; - QString version() const; - bool isUnstable() const; - QString predecessorId() const; - QString successorId() const; - QString name() const; - QStringList aliases() const; - QStringList altAliases() const; - QString canonicalAlias() const; - QString displayName() const; - QString topic() const; - QString avatarMediaId() const; - QUrl avatarUrl() const; - const Avatar& avatarObject() const; - Q_INVOKABLE JoinState joinState() const; - Q_INVOKABLE QList<User*> usersTyping() const; - QList<User*> membersLeft() const; - - Q_INVOKABLE QList<User*> users() const; - QStringList memberNames() const; - [[deprecated("Use joinedCount(), invitedCount(), totalMemberCount()")]] - int memberCount() const; - int timelineSize() const; - bool usesEncryption() const; - int joinedCount() const; - int invitedCount() const; - int totalMemberCount() const; - - GetRoomEventsJob* eventsHistoryJob() const; - - /** - * Returns a square room avatar with the given size and requests it - * from the network if needed - * \return a pixmap with the avatar or a placeholder if there's none - * available yet - */ - Q_INVOKABLE QImage avatar(int dimension); - /** - * Returns a room avatar with the given dimensions and requests it - * from the network if needed - * \return a pixmap with the avatar or a placeholder if there's none - * available yet - */ - Q_INVOKABLE QImage avatar(int width, int height); - - /** - * \brief Get a user object for a given user id - * This is the recommended way to get a user object in a room - * context. The actual object type may be changed in further - * versions to provide room-specific user information (display name, - * avatar etc.). - * \note The method will return a valid user regardless of - * the membership. - */ - Q_INVOKABLE User* user(const QString& userId) const; - - /** - * \brief Check the join state of a given user in this room - * - * \note Banned and invited users are not tracked for now (Leave - * will be returned for them). - * - * \return either of Join, Leave, depending on the given - * user's state in the room - */ - Q_INVOKABLE JoinState memberJoinState(User* user) const; - - /** - * Get a disambiguated name for a given user in - * the context of the room - */ - Q_INVOKABLE QString roomMembername(const User* u) const; - /** - * Get a disambiguated name for a user with this id in - * the context of the room - */ - Q_INVOKABLE QString roomMembername(const QString& userId) const; - - const Timeline& messageEvents() const; - const PendingEvents& pendingEvents() const; - /** - * A convenience method returning the read marker to the position - * before the "oldest" event; same as messageEvents().crend() - */ - rev_iter_t historyEdge() const; - /** - * A convenience method returning the iterator beyond the latest - * arrived event; same as messageEvents().cend() - */ - Timeline::const_iterator syncEdge() const; - /// \deprecated Use historyEdge instead - rev_iter_t timelineEdge() const; - Q_INVOKABLE TimelineItem::index_t minTimelineIndex() const; - Q_INVOKABLE TimelineItem::index_t maxTimelineIndex() const; - Q_INVOKABLE bool isValidIndex(TimelineItem::index_t timelineIndex) const; - - rev_iter_t findInTimeline(TimelineItem::index_t index) const; - rev_iter_t findInTimeline(const QString& evtId) const; - PendingEvents::iterator findPendingEvent(const QString & txnId); - PendingEvents::const_iterator findPendingEvent(const QString & txnId) const; - - const RelatedEvents relatedEvents(const QString& evtId, - const char* relType) const; - const RelatedEvents relatedEvents(const RoomEvent& evt, - const char* relType) const; - - bool displayed() const; - /// Mark the room as currently displayed to the user - /** - * Marking the room displayed causes the room to obtain the full - * list of members if it's been lazy-loaded before; in the future - * it may do more things bound to "screen time" of the room, e.g. - * measure that "screen time". - */ - void setDisplayed(bool displayed = true); - QString firstDisplayedEventId() const; - rev_iter_t firstDisplayedMarker() const; - void setFirstDisplayedEventId(const QString& eventId); - void setFirstDisplayedEvent(TimelineItem::index_t index); - QString lastDisplayedEventId() const; - rev_iter_t lastDisplayedMarker() const; - void setLastDisplayedEventId(const QString& eventId); - void setLastDisplayedEvent(TimelineItem::index_t index); - - rev_iter_t readMarker(const User* user) const; - rev_iter_t readMarker() const; - QString readMarkerEventId() const; - QList<User*> usersAtEventId(const QString& eventId); - /** - * \brief Mark the event with uptoEventId as read - * - * Finds in the timeline and marks as read the event with - * the specified id; also posts a read receipt to the server either - * for this message or, if it's from the local user, for - * the nearest non-local message before. uptoEventId must be non-empty. - */ - void markMessagesAsRead(QString uptoEventId); - - /// Check whether there are unread messages in the room - bool hasUnreadMessages() const; - - /** Get the number of unread messages in the room - * Depending on the read marker state, this call may return either - * a precise or an estimate number of unread events. Only "notable" - * events (non-redacted message events from users other than local) - * are counted. - * - * In a case when readMarker() == timelineEdge() (the local read - * marker is beyond the local timeline) only the bottom limit of - * the unread messages number can be estimated (and even that may - * be slightly off due to, e.g., redactions of events not loaded - * to the local timeline). - * - * If all messages are read, this function will return -1 (_not_ 0, - * as zero may mean "zero or more unread messages" in a situation - * when the read marker is outside the local timeline. - */ - int unreadCount() const; - - Q_INVOKABLE int notificationCount() const; - Q_INVOKABLE void resetNotificationCount(); - Q_INVOKABLE int highlightCount() const; - Q_INVOKABLE void resetHighlightCount(); - - /** Check whether the room has account data of the given type - * Tags and read markers are not supported by this method _yet_. - */ - bool hasAccountData(const QString& type) const; - - /** Get a generic account data event of the given type - * This returns a generic hashmap for any room account data event - * stored on the server. Tags and read markers cannot be retrieved - * using this method _yet_. - */ - const EventPtr& accountData(const QString& type) const; - - QStringList tagNames() const; - TagsMap tags() const; - TagRecord tag(const QString& name) const; - - /** Add a new tag to this room - * If this room already has this tag, nothing happens. If it's a new - * tag for the room, the respective tag record is added to the set - * of tags and the new set is sent to the server to update other - * clients. - */ - void addTag(const QString& name, const TagRecord& record = {}); - Q_INVOKABLE void addTag(const QString& name, float order); - - /// Remove a tag from the room - Q_INVOKABLE void removeTag(const QString& name); - - /** Overwrite the room's tags - * This completely replaces the existing room's tags with a set - * of new ones and updates the new set on the server. Unlike - * most other methods in Room, this one sends a signal about changes - * immediately, not waiting for confirmation from the server - * (because tags are saved in account data rather than in shared - * room state). - */ - void setTags(TagsMap newTags); - - /// Check whether the list of tags has m.favourite - bool isFavourite() const; - /// Check whether the list of tags has m.lowpriority - bool isLowPriority() const; - /// Check whether this room is for server notices (MSC1452) - bool isServerNoticeRoom() const; - - /// Check whether this room is a direct chat - Q_INVOKABLE bool isDirectChat() const; - - /// Get the list of users this room is a direct chat with - QList<User*> directChatUsers() const; - - Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId) const; - Q_INVOKABLE QUrl urlToDownload(const QString& eventId) const; - - /// Get a file name for downloading for a given event id - /*! - * The event MUST be RoomMessageEvent and have content - * for downloading. \sa RoomMessageEvent::hasContent - */ - Q_INVOKABLE QString fileNameToDownload(const QString& eventId) const; - - /// Get information on file upload/download - /*! - * \param id uploads are identified by the corresponding event's - * transactionId (because uploads are done before - * the event is even sent), while downloads are using - * the normal event id for identifier. - */ - Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const; - - /// Get the URL to the actual file source in a unified way - /*! - * For uploads it will return a URL to a local file; for downloads - * the URL will be taken from the corresponding room event. - */ - Q_INVOKABLE QUrl fileSource(const QString& id) const; - - /** Pretty-prints plain text into HTML - * As of now, it's exactly the same as QMatrixClient::prettyPrint(); - * in the future, it will also linkify room aliases, mxids etc. - * using the room context. - */ - QString prettyPrint(const QString& plainText) const; - - MemberSorter memberSorter() const; - - Q_INVOKABLE void inviteCall(const QString& callId, - const int lifetime, const QString& sdp); - Q_INVOKABLE void sendCallCandidates(const QString& callId, - const QJsonArray& candidates); - Q_INVOKABLE void answerCall(const QString& callId, const int lifetime, - const QString& sdp); - Q_INVOKABLE void answerCall(const QString& callId, - const QString& sdp); - Q_INVOKABLE void hangupCall(const QString& callId); - Q_INVOKABLE bool supportsCalls() const; - - public slots: - /** Check whether the room should be upgraded */ - void checkVersion(); - - QString postMessage(const QString& plainText, MessageEventType type); - QString postPlainText(const QString& plainText); - QString postHtmlMessage(const QString& plainText, - const QString& html, - MessageEventType type = MessageEventType::Text); - QString postHtmlText(const QString& plainText, const QString& html); - /** Send a reaction on a given event with a given key */ - QString postReaction(const QString& eventId, const QString& key); - - QString postFile(const QString& plainText, const QUrl& localPath, - bool asGenericFile = false); - /** Post a pre-created room message event - * - * Takes ownership of the event, deleting it once the matching one - * arrives with the sync - * \return transaction id associated with the event. - */ - QString postEvent(RoomEvent* event); - QString postJson(const QString& matrixType, - const QJsonObject& eventContent); - QString retryMessage(const QString& txnId); - void discardMessage(const QString& txnId); - void setName(const QString& newName); - void setCanonicalAlias(const QString& newAlias); - void setAliases(const QStringList& aliases); - void setTopic(const QString& newTopic); - - /// You shouldn't normally call this method; it's here for debugging - void refreshDisplayName(); - - void getPreviousContent(int limit = 10); - - void inviteToRoom(const QString& memberId); - LeaveRoomJob* leaveRoom(); - SetRoomStateWithKeyJob* setMemberState( - const QString& memberId, const RoomMemberEvent& event) const; - void kickMember(const QString& memberId, const QString& reason = {}); - void ban(const QString& userId, const QString& reason = {}); - void unban(const QString& userId); - void redactEvent(const QString& eventId, - const QString& reason = {}); - - void uploadFile(const QString& id, const QUrl& localFilename, - const QString& overrideContentType = {}); - // If localFilename is empty a temporary file is created - void downloadFile(const QString& eventId, - const QUrl& localFilename = {}); - void cancelFileTransfer(const QString& id); - - /// Mark all messages in the room as read - void markAllMessagesAsRead(); - - /// Whether the current user is allowed to upgrade the room - bool canSwitchVersions() const; - - /// Switch the room's version (aka upgrade) - void switchVersion(QString newVersion); - - signals: - /// Initial set of state events has been loaded - /** - * The initial set is what comes from the initial sync for the room. - * This includes all basic things like RoomCreateEvent, - * RoomNameEvent, a (lazy-loaded, not full) set of RoomMemberEvents - * etc. This is a per-room reflection of Connection::loadedRoomState - * \sa Connection::loadedRoomState - */ - void baseStateLoaded(); - void eventsHistoryJobChanged(); - void aboutToAddHistoricalMessages(RoomEventsRange events); - void aboutToAddNewMessages(RoomEventsRange events); - void addedMessages(int fromIndex, int toIndex); - /// The event is about to be appended to the list of pending events - void pendingEventAboutToAdd(RoomEvent* event); - /// An event has been appended to the list of pending events - void pendingEventAdded(); - /// The remote echo has arrived with the sync and will be merged - /// with its local counterpart - /** NB: Requires a sync loop to be emitted */ - void pendingEventAboutToMerge(RoomEvent* serverEvent, - int pendingEventIndex); - /// The remote and local copies of the event have been merged - /** NB: Requires a sync loop to be emitted */ - void pendingEventMerged(); - /// An event will be removed from the list of pending events - void pendingEventAboutToDiscard(int pendingEventIndex); - /// An event has just been removed from the list of pending events - void pendingEventDiscarded(); - /// The status of a pending event has changed - /** \sa PendingEventItem::deliveryStatus */ - void pendingEventChanged(int pendingEventIndex); - /// The server accepted the message - /** This is emitted when an event sending request has successfully - * completed. This does not mean that the event is already in the - * local timeline, only that the server has accepted it. - * \param txnId transaction id assigned by the client during sending - * \param eventId event id assigned by the server upon acceptance - * \sa postEvent, postPlainText, postMessage, postHtmlMessage - * \sa pendingEventMerged, aboutToAddNewMessages - */ - void messageSent(QString txnId, QString eventId); - - /** A common signal for various kinds of changes in the room - * Aside from all changes in the room state - * @param changes a set of flags describing what changes occured - * upon the last sync - * \sa StateChange - */ - void changed(Changes changes); - /** - * \brief The room name, the canonical alias or other aliases changed - * - * Not triggered when displayname changes. - */ - void namesChanged(Room* room); - void displaynameAboutToChange(Room* room); - void displaynameChanged(Room* room, QString oldName); - void topicChanged(); - void avatarChanged(); - void userAdded(User* user); - void userRemoved(User* user); - void memberAboutToRename(User* user, QString newName); - void memberRenamed(User* user); - /// The list of members has changed - /** Emitted no more than once per sync, this is a good signal to - * for cases when some action should be done upon any change in - * the member list. If you need per-item granularity you should use - * userAdded, userRemoved and memberAboutToRename / memberRenamed - * instead. - */ - void memberListChanged(); - /// The previously lazy-loaded members list is now loaded entirely - /// \sa setDisplayed - void allMembersLoaded(); - void encryption(); - - void joinStateChanged(JoinState oldState, JoinState newState); - void typingChanged(); - - void highlightCountChanged(Room* room); - void notificationCountChanged(Room* room); - - void displayedChanged(bool displayed); - void firstDisplayedEventChanged(); - void lastDisplayedEventChanged(); - void lastReadEventChanged(User* user); - void readMarkerMoved(QString fromEventId, QString toEventId); - void readMarkerForUserMoved(User* user, QString fromEventId, QString toEventId); - void unreadMessagesChanged(Room* room); - - void accountDataAboutToChange(QString type); - void accountDataChanged(QString type); - void tagsAboutToChange(); - void tagsChanged(); - - void updatedEvent(QString eventId); - void replacedEvent(const RoomEvent* newEvent, - const RoomEvent* oldEvent); - - void newFileTransfer(QString id, QUrl localFile); - void fileTransferProgress(QString id, qint64 progress, qint64 total); - void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl); - void fileTransferFailed(QString id, QString errorMessage = {}); - void fileTransferCancelled(QString id); - - void callEvent(Room* room, const RoomEvent* event); - - /// The room's version stability may have changed - void stabilityUpdated(QString recommendedDefault, - QStringList stableVersions); - /// This room has been upgraded and won't receive updates anymore - void upgraded(QString serverMessage, Room* successor); - /// An attempted room upgrade has failed - void upgradeFailed(QString errorMessage); - - /// The room is about to be deleted - void beforeDestruction(Room*); - - protected: - /// Returns true if any of room names/aliases has changed - virtual Changes processStateEvent(const RoomEvent& e); - virtual Changes processEphemeralEvent(EventPtr&& event); - virtual Changes processAccountDataEvent(EventPtr&& event); - virtual void onAddNewTimelineEvents(timeline_iter_t /*from*/) { } - virtual void onAddHistoricalTimelineEvents(rev_iter_t /*from*/) { } - virtual void onRedaction(const RoomEvent& /*prevEvent*/, - const RoomEvent& /*after*/) { } - virtual QJsonObject toJson() const; - virtual void updateData(SyncRoomData&& data, bool fromCache = false); - - private: - friend class Connection; - - class Private; - Private* d; - - // This is called from Connection, reflecting a state change that - // arrived from the server. Clients should use - // Connection::joinRoom() and Room::leaveRoom() to change the state. - void setJoinState(JoinState state); - }; + /** + * Get a disambiguated name for a given user in + * the context of the room + */ + Q_INVOKABLE QString roomMembername(const Quotient::User* u) const; + /** + * Get a disambiguated name for a user with this id in + * the context of the room + */ + Q_INVOKABLE QString roomMembername(const QString& userId) const; - class MemberSorter - { - public: - explicit MemberSorter(const Room* r) : room(r) { } + /** Get a display-safe member name in the context of this room + * + * Display-safe means HTML-safe + without RLO/LRO markers + * (see https://github.com/quotient-im/Quaternion/issues/545). + */ + Q_INVOKABLE QString safeMemberName(const QString& userId) const; - bool operator()(User* u1, User* u2) const; - bool operator()(User* u1, const QString& u2name) const; + const Timeline& messageEvents() const; + const PendingEvents& pendingEvents() const; - template <typename ContT, typename ValT> - typename ContT::size_type lowerBoundIndex(const ContT& c, - const ValT& v) const - { - return std::lower_bound(c.begin(), c.end(), v, *this) - c.begin(); - } + /// Check whether all historical messages are already loaded + /** + * \return true if the "oldest" event in the timeline is + * a room creation event and there's no further history + * to load; false otherwise + */ + bool allHistoryLoaded() const; + /** + * A convenience method returning the read marker to the position + * before the "oldest" event; same as messageEvents().crend() + */ + rev_iter_t historyEdge() const; + /** + * A convenience method returning the iterator beyond the latest + * arrived event; same as messageEvents().cend() + */ + Timeline::const_iterator syncEdge() const; + /// \deprecated Use historyEdge instead + rev_iter_t timelineEdge() const; + Q_INVOKABLE Quotient::TimelineItem::index_t minTimelineIndex() const; + Q_INVOKABLE Quotient::TimelineItem::index_t maxTimelineIndex() const; + Q_INVOKABLE bool + isValidIndex(Quotient::TimelineItem::index_t timelineIndex) const; + + rev_iter_t findInTimeline(TimelineItem::index_t index) const; + rev_iter_t findInTimeline(const QString& evtId) const; + PendingEvents::iterator findPendingEvent(const QString& txnId); + PendingEvents::const_iterator findPendingEvent(const QString& txnId) const; + + const RelatedEvents relatedEvents(const QString& evtId, + const char* relType) const; + const RelatedEvents relatedEvents(const RoomEvent& evt, + const char* relType) const; + + const RoomCreateEvent* creation() const + { return getCurrentState<RoomCreateEvent>(); } + const RoomTombstoneEvent* tombstone() const + { return getCurrentState<RoomTombstoneEvent>(); } + + bool displayed() const; + /// Mark the room as currently displayed to the user + /** + * Marking the room displayed causes the room to obtain the full + * list of members if it's been lazy-loaded before; in the future + * it may do more things bound to "screen time" of the room, e.g. + * measure that "screen time". + */ + void setDisplayed(bool displayed = true); + QString firstDisplayedEventId() const; + rev_iter_t firstDisplayedMarker() const; + void setFirstDisplayedEventId(const QString& eventId); + void setFirstDisplayedEvent(TimelineItem::index_t index); + QString lastDisplayedEventId() const; + rev_iter_t lastDisplayedMarker() const; + void setLastDisplayedEventId(const QString& eventId); + void setLastDisplayedEvent(TimelineItem::index_t index); + + rev_iter_t readMarker(const User* user) const; + rev_iter_t readMarker() const; + QString readMarkerEventId() const; + QList<User*> usersAtEventId(const QString& eventId); + /** + * \brief Mark the event with uptoEventId as read + * + * Finds in the timeline and marks as read the event with + * the specified id; also posts a read receipt to the server either + * for this message or, if it's from the local user, for + * the nearest non-local message before. uptoEventId must be non-empty. + */ + void markMessagesAsRead(QString uptoEventId); + + /// Check whether there are unread messages in the room + bool hasUnreadMessages() const; + + /** Get the number of unread messages in the room + * Depending on the read marker state, this call may return either + * a precise or an estimate number of unread events. Only "notable" + * events (non-redacted message events from users other than local) + * are counted. + * + * In a case when readMarker() == timelineEdge() (the local read + * marker is beyond the local timeline) only the bottom limit of + * the unread messages number can be estimated (and even that may + * be slightly off due to, e.g., redactions of events not loaded + * to the local timeline). + * + * If all messages are read, this function will return -1 (_not_ 0, + * as zero may mean "zero or more unread messages" in a situation + * when the read marker is outside the local timeline. + */ + int unreadCount() const; - private: - const Room* room; + Q_INVOKABLE int notificationCount() const; + Q_INVOKABLE void resetNotificationCount(); + Q_INVOKABLE int highlightCount() const; + Q_INVOKABLE void resetHighlightCount(); + + /** Check whether the room has account data of the given type + * Tags and read markers are not supported by this method _yet_. + */ + bool hasAccountData(const QString& type) const; + + /** Get a generic account data event of the given type + * This returns a generic hash map for any room account data event + * stored on the server. Tags and read markers cannot be retrieved + * using this method _yet_. + */ + const EventPtr& accountData(const QString& type) const; + + QStringList tagNames() const; + TagsMap tags() const; + TagRecord tag(const QString& name) const; + + /** Add a new tag to this room + * If this room already has this tag, nothing happens. If it's a new + * tag for the room, the respective tag record is added to the set + * of tags and the new set is sent to the server to update other + * clients. + */ + void addTag(const QString& name, const TagRecord& record = {}); + Q_INVOKABLE void addTag(const QString& name, float order); + + /// Remove a tag from the room + Q_INVOKABLE void removeTag(const QString& name); + + /// The scope to apply an action on + /*! This enumeration is used to pick a strategy to propagate certain + * actions on the room to its predecessors and successors. + */ + enum ActionScope { + ThisRoomOnly, //< Do not apply to predecessors and successors + WithinSameState, //< Apply to predecessors and successors in the same + //< state as the current one + OmitLeftState, //< Apply to all reachable predecessors and successors + //< except those in Leave state + WholeSequence //< Apply to all reachable predecessors and successors }; -} // namespace QMatrixClient -Q_DECLARE_METATYPE(QMatrixClient::FileTransferInfo) -Q_DECLARE_OPERATORS_FOR_FLAGS(QMatrixClient::Room::Changes) + + /** Overwrite the room's tags + * This completely replaces the existing room's tags with a set + * of new ones and updates the new set on the server. Unlike + * most other methods in Room, this one sends a signal about changes + * immediately, not waiting for confirmation from the server + * (because tags are saved in account data rather than in shared + * room state). + * \param applyOn setting this to Room::OnAllConversations will set tags + * on this and all _known_ predecessors and successors; + * by default only the current room is changed + */ + void setTags(TagsMap newTags, ActionScope applyOn = ThisRoomOnly); + + /// Check whether the list of tags has m.favourite + bool isFavourite() const; + /// Check whether the list of tags has m.lowpriority + bool isLowPriority() const; + /// Check whether this room is for server notices (MSC1452) + bool isServerNoticeRoom() const; + + /// Check whether this room is a direct chat + Q_INVOKABLE bool isDirectChat() const; + + /// Get the list of users this room is a direct chat with + QList<User*> directChatUsers() const; + + Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId) const; + Q_INVOKABLE QUrl urlToDownload(const QString& eventId) const; + + /// Get a file name for downloading for a given event id + /*! + * The event MUST be RoomMessageEvent and have content + * for downloading. \sa RoomMessageEvent::hasContent + */ + Q_INVOKABLE QString fileNameToDownload(const QString& eventId) const; + + /// Get information on file upload/download + /*! + * \param id uploads are identified by the corresponding event's + * transactionId (because uploads are done before + * the event is even sent), while downloads are using + * the normal event id for identifier. + */ + Q_INVOKABLE Quotient::FileTransferInfo + fileTransferInfo(const QString& id) const; + + /// Get the URL to the actual file source in a unified way + /*! + * For uploads it will return a URL to a local file; for downloads + * the URL will be taken from the corresponding room event. + */ + Q_INVOKABLE QUrl fileSource(const QString& id) const; + + /** Pretty-prints plain text into HTML + * As of now, it's exactly the same as Quotient::prettyPrint(); + * in the future, it will also linkify room aliases, mxids etc. + * using the room context. + */ + Q_INVOKABLE QString prettyPrint(const QString& plainText) const; + + MemberSorter memberSorter() const; + + Q_INVOKABLE bool supportsCalls() const; + + /// Whether the current user is allowed to upgrade the room + Q_INVOKABLE bool canSwitchVersions() const; + + /// Get a state event with the given event type and state key + /*! This method returns a (potentially empty) state event corresponding + * to the pair of event type \p evtType and state key \p stateKey. + */ + Q_INVOKABLE const Quotient::StateEventBase* + getCurrentState(const QString& evtType, const QString& stateKey = {}) const; + + /// Get a state event with the given event type and state key + /*! This is a typesafe overload that accepts a C++ event type instead of + * its Matrix name. + */ + template <typename EvT> + const EvT* getCurrentState(const QString& stateKey = {}) const + { + const auto* evt = + eventCast<const EvT>(getCurrentState(EvT::matrixTypeId(), stateKey)); + Q_ASSERT(evt); + Q_ASSERT(evt->matrixTypeId() == EvT::matrixTypeId() + && evt->stateKey() == stateKey); + return evt; + } + + /// Set a state event of the given type with the given arguments + /*! This typesafe overload attempts to send a state event with the type + * \p EvT and the content defined by \p args. Specifically, the function + * creates a temporary object of type \p EvT passing \p args to + * the constructor, and sends a request to the homeserver using + * the Matrix event type defined by \p EvT and the event content produced + * via EvT::contentJson(). + */ + template <typename EvT, typename... ArgTs> + auto setState(ArgTs&&... args) const + { + return setState(EvT(std::forward<ArgTs>(args)...)); + } + +public slots: + /** Check whether the room should be upgraded */ + void checkVersion(); + + QString postMessage(const QString& plainText, MessageEventType type); + QString postPlainText(const QString& plainText); + QString postHtmlMessage(const QString& plainText, const QString& html, + MessageEventType type = MessageEventType::Text); + QString postHtmlText(const QString& plainText, const QString& html); + /// Send a reaction on a given event with a given key + QString postReaction(const QString& eventId, const QString& key); + QString postFile(const QString& plainText, const QUrl& localPath, + bool asGenericFile = false); + /** Post a pre-created room message event + * + * Takes ownership of the event, deleting it once the matching one + * arrives with the sync + * \return transaction id associated with the event. + */ + QString postEvent(RoomEvent* event); + QString postJson(const QString& matrixType, const QJsonObject& eventContent); + QString retryMessage(const QString& txnId); + void discardMessage(const QString& txnId); + + /// Send a request to update the room state with the given event + SetRoomStateWithKeyJob* setState(const StateEventBase& evt) const; + void setName(const QString& newName); + void setCanonicalAlias(const QString& newAlias); + /// Set room aliases on the user's current server + void setLocalAliases(const QStringList& aliases); + void setTopic(const QString& newTopic); + + /// You shouldn't normally call this method; it's here for debugging + void refreshDisplayName(); + + void getPreviousContent(int limit = 10); + + void inviteToRoom(const QString& memberId); + LeaveRoomJob* leaveRoom(); + /// \deprecated - use setState() instead") + SetRoomStateWithKeyJob* setMemberState(const QString& memberId, + const RoomMemberEvent& event) const; + void kickMember(const QString& memberId, const QString& reason = {}); + void ban(const QString& userId, const QString& reason = {}); + void unban(const QString& userId); + void redactEvent(const QString& eventId, const QString& reason = {}); + + void uploadFile(const QString& id, const QUrl& localFilename, + const QString& overrideContentType = {}); + // If localFilename is empty a temporary file is created + void downloadFile(const QString& eventId, const QUrl& localFilename = {}); + void cancelFileTransfer(const QString& id); + + /// Mark all messages in the room as read + void markAllMessagesAsRead(); + + /// Switch the room's version (aka upgrade) + void switchVersion(QString newVersion); + + void inviteCall(const QString& callId, const int lifetime, + const QString& sdp); + void sendCallCandidates(const QString& callId, const QJsonArray& candidates); + void answerCall(const QString& callId, const int lifetime, + const QString& sdp); + void answerCall(const QString& callId, const QString& sdp); + void hangupCall(const QString& callId); + +signals: + /// Initial set of state events has been loaded + /** + * The initial set is what comes from the initial sync for the room. + * This includes all basic things like RoomCreateEvent, + * RoomNameEvent, a (lazy-loaded, not full) set of RoomMemberEvents + * etc. This is a per-room reflection of Connection::loadedRoomState + * \sa Connection::loadedRoomState + */ + void baseStateLoaded(); + void eventsHistoryJobChanged(); + void aboutToAddHistoricalMessages(RoomEventsRange events); + void aboutToAddNewMessages(RoomEventsRange events); + void addedMessages(int fromIndex, int toIndex); + /// The event is about to be appended to the list of pending events + void pendingEventAboutToAdd(RoomEvent* event); + /// An event has been appended to the list of pending events + void pendingEventAdded(); + /// The remote echo has arrived with the sync and will be merged + /// with its local counterpart + /** NB: Requires a sync loop to be emitted */ + void pendingEventAboutToMerge(Quotient::RoomEvent* serverEvent, + int pendingEventIndex); + /// The remote and local copies of the event have been merged + /** NB: Requires a sync loop to be emitted */ + void pendingEventMerged(); + /// An event will be removed from the list of pending events + void pendingEventAboutToDiscard(int pendingEventIndex); + /// An event has just been removed from the list of pending events + void pendingEventDiscarded(); + /// The status of a pending event has changed + /** \sa PendingEventItem::deliveryStatus */ + void pendingEventChanged(int pendingEventIndex); + /// The server accepted the message + /** This is emitted when an event sending request has successfully + * completed. This does not mean that the event is already in the + * local timeline, only that the server has accepted it. + * \param txnId transaction id assigned by the client during sending + * \param eventId event id assigned by the server upon acceptance + * \sa postEvent, postPlainText, postMessage, postHtmlMessage + * \sa pendingEventMerged, aboutToAddNewMessages + */ + void messageSent(QString txnId, QString eventId); + + /** A common signal for various kinds of changes in the room + * Aside from all changes in the room state + * @param changes a set of flags describing what changes occurred + * upon the last sync + * \sa Changes + */ + void changed(Quotient::Room::Changes changes); + /** + * \brief The room name, the canonical alias or other aliases changed + * + * Not triggered when display name changes. + */ + void namesChanged(Quotient::Room* room); + void displaynameAboutToChange(Quotient::Room* room); + void displaynameChanged(Quotient::Room* room, QString oldName); + void topicChanged(); + void avatarChanged(); + void userAdded(Quotient::User* user); + void userRemoved(Quotient::User* user); + void memberAboutToRename(Quotient::User* user, QString newName); + void memberRenamed(Quotient::User* user); + /// The list of members has changed + /** Emitted no more than once per sync, this is a good signal to + * for cases when some action should be done upon any change in + * the member list. If you need per-item granularity you should use + * userAdded, userRemoved and memberAboutToRename / memberRenamed + * instead. + */ + void memberListChanged(); + /// The previously lazy-loaded members list is now loaded entirely + /// \sa setDisplayed + void allMembersLoaded(); + void encryption(); + + void joinStateChanged(Quotient::JoinState oldState, + Quotient::JoinState newState); + void typingChanged(); + + void highlightCountChanged(); + void notificationCountChanged(); + + void displayedChanged(bool displayed); + void firstDisplayedEventChanged(); + void lastDisplayedEventChanged(); + void lastReadEventChanged(Quotient::User* user); + void readMarkerMoved(QString fromEventId, QString toEventId); + void readMarkerForUserMoved(Quotient::User* user, QString fromEventId, + QString toEventId); + void unreadMessagesChanged(Quotient::Room* room); + + void accountDataAboutToChange(QString type); + void accountDataChanged(QString type); + void tagsAboutToChange(); + void tagsChanged(); + + void updatedEvent(QString eventId); + void replacedEvent(const Quotient::RoomEvent* newEvent, + const Quotient::RoomEvent* oldEvent); + + void newFileTransfer(QString id, QUrl localFile); + void fileTransferProgress(QString id, qint64 progress, qint64 total); + void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl); + void fileTransferFailed(QString id, QString errorMessage = {}); + void fileTransferCancelled(QString id); + + void callEvent(Quotient::Room* room, const Quotient::RoomEvent* event); + + /// The room's version stability may have changed + void stabilityUpdated(QString recommendedDefault, + QStringList stableVersions); + /// This room has been upgraded and won't receive updates any more + void upgraded(QString serverMessage, Quotient::Room* successor); + /// An attempted room upgrade has failed + void upgradeFailed(QString errorMessage); + + /// The room is about to be deleted + void beforeDestruction(Quotient::Room*); + +protected: + virtual Changes processStateEvent(const RoomEvent& e); + virtual Changes processEphemeralEvent(EventPtr&& event); + virtual Changes processAccountDataEvent(EventPtr&& event); + virtual void onAddNewTimelineEvents(timeline_iter_t /*from*/) {} + virtual void onAddHistoricalTimelineEvents(rev_iter_t /*from*/) {} + virtual void onRedaction(const RoomEvent& /*prevEvent*/, + const RoomEvent& /*after*/) + {} + virtual QJsonObject toJson() const; + virtual void updateData(SyncRoomData&& data, bool fromCache = false); + +private: + friend class Connection; + + class Private; + Private* d; + + // This is called from Connection, reflecting a state change that + // arrived from the server. Clients should use + // Connection::joinRoom() and Room::leaveRoom() to change the state. + void setJoinState(JoinState state); +}; + +class MemberSorter { +public: + explicit MemberSorter(const Room* r) : room(r) {} + + bool operator()(User* u1, User* u2) const; + bool operator()(User* u1, const QString& u2name) const; + + template <typename ContT, typename ValT> + typename ContT::size_type lowerBoundIndex(const ContT& c, const ValT& v) const + { + return std::lower_bound(c.begin(), c.end(), v, *this) - c.begin(); + } + +private: + const Room* room; +}; +} // namespace Quotient +Q_DECLARE_METATYPE(Quotient::FileTransferInfo) +Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::Room::Changes) diff --git a/lib/settings.cpp b/lib/settings.cpp index 124d7042..dd086d9c 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -4,7 +4,7 @@ #include <QtCore/QUrl> -using namespace QMatrixClient; +using namespace Quotient; QString Settings::legacyOrganizationName {}; QString Settings::legacyApplicationName {}; @@ -16,14 +16,25 @@ void Settings::setLegacyNames(const QString& organizationName, legacyApplicationName = applicationName; } +Settings::Settings(QObject* parent) : QSettings(parent) +{ + setIniCodec("UTF-8"); +} + void Settings::setValue(const QString& key, const QVariant& value) { -// qCDebug() << "Setting" << key << "to" << value; QSettings::setValue(key, value); if (legacySettings.contains(key)) legacySettings.remove(key); } +void Settings::remove(const QString& key) +{ + QSettings::remove(key); + if (legacySettings.contains(key)) + legacySettings.remove(key); +} + QVariant Settings::value(const QString& key, const QVariant& defaultValue) const { auto value = QSettings::value(key, legacySettings.value(key, defaultValue)); @@ -42,8 +53,12 @@ bool Settings::contains(const QString& key) const QStringList Settings::childGroups() const { - auto l = QSettings::childGroups(); - return !l.isEmpty() ? l : legacySettings.childGroups(); + auto groups = QSettings::childGroups(); + const auto& legacyGroups = legacySettings.childGroups(); + for (const auto& g: legacyGroups) + if (!groups.contains(g)) + groups.push_back(g); + return groups; } void SettingsGroup::setValue(const QString& key, const QVariant& value) @@ -56,15 +71,13 @@ bool SettingsGroup::contains(const QString& key) const return Settings::contains(groupPath + '/' + key); } -QVariant SettingsGroup::value(const QString& key, const QVariant& defaultValue) const +QVariant SettingsGroup::value(const QString& key, + const QVariant& defaultValue) const { return Settings::value(groupPath + '/' + key, defaultValue); } -QString SettingsGroup::group() const -{ - return groupPath; -} +QString SettingsGroup::group() const { return groupPath; } QStringList SettingsGroup::childGroups() const { @@ -84,12 +97,17 @@ void SettingsGroup::remove(const QString& key) Settings::remove(fullKey); } -QMC_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {}, setDeviceId) -QMC_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, setDeviceName) -QMC_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, setKeepLoggedIn) +QTNT_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {}, + setDeviceId) +QTNT_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, + setDeviceName) +QTNT_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, + setKeepLoggedIn) static const auto HomeserverKey = QStringLiteral("homeserver"); static const auto AccessTokenKey = QStringLiteral("access_token"); +static const auto EncryptionAccountPickleKey = + QStringLiteral("encryption_account_pickle"); QUrl AccountSettings::homeserver() const { @@ -101,10 +119,7 @@ void AccountSettings::setHomeserver(const QUrl& url) setValue(HomeserverKey, url.toString()); } -QString AccountSettings::userId() const -{ - return group().section('/', -1); -} +QString AccountSettings::userId() const { return group().section('/', -1); } QString AccountSettings::accessToken() const { @@ -114,13 +129,37 @@ QString AccountSettings::accessToken() const void AccountSettings::setAccessToken(const QString& accessToken) { qCWarning(MAIN) << "Saving access_token to QSettings is insecure." - " Developers, please save access_token separately."; + " Developers, do it manually or contribute to share " + "QtKeychain logic to libQuotient."; setValue(AccessTokenKey, accessToken); } void AccountSettings::clearAccessToken() { legacySettings.remove(AccessTokenKey); - legacySettings.remove(QStringLiteral("device_id")); // Force the server to re-issue it + legacySettings.remove(QStringLiteral("device_id")); // Force the server to + // re-issue it remove(AccessTokenKey); } + +QByteArray AccountSettings::encryptionAccountPickle() +{ + QString passphrase = ""; // FIXME: add QtKeychain + return value("encryption_account_pickle", "").toByteArray(); +} + +void AccountSettings::setEncryptionAccountPickle( + const QByteArray& encryptionAccountPickle) +{ + qCWarning(MAIN) + << "Saving encryption_account_pickle to QSettings is insecure." + " Developers, do it manually or contribute to share QtKeychain " + "logic to libQuotient."; + QString passphrase = ""; // FIXME: add QtKeychain + setValue("encryption_account_pickle", encryptionAccountPickle); +} + +void AccountSettings::clearEncryptionAccountPickle() +{ + remove(EncryptionAccountPickleKey); // TODO: Force to re-issue it? +} diff --git a/lib/settings.h b/lib/settings.h index 759bda35..c45764a6 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -13,140 +13,164 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include <QtCore/QSettings> -#include <QtCore/QVector> #include <QtCore/QUrl> +#include <QtCore/QVector> class QVariant; -namespace QMatrixClient -{ - class Settings: public QSettings - { - Q_OBJECT - public: - /** - * Use this function before creating any Settings objects in order - * to setup a read-only location where configuration has previously - * been stored. This will provide an additional fallback in case of - * renaming the organisation/application. - */ - static void setLegacyNames(const QString& organizationName, - const QString& applicationName = {}); - -#if defined(_MSC_VER) && _MSC_VER < 1900 - // VS 2013 (and probably older) aren't friends with 'using' statements - // that involve private constructors - explicit Settings(QObject* parent = 0) : QSettings(parent) { } -#else - using QSettings::QSettings; -#endif - - Q_INVOKABLE void setValue(const QString &key, - const QVariant &value); - Q_INVOKABLE QVariant value(const QString &key, - const QVariant &defaultValue = {}) const; - - template <typename T> - T get(const QString& key, const T& defaultValue = {}) const - { - const auto qv = value(key, QVariant()); - return qv.isValid() && qv.canConvert<T>() ? qv.value<T>() - : defaultValue; - } - - Q_INVOKABLE bool contains(const QString& key) const; - Q_INVOKABLE QStringList childGroups() const; - - private: - static QString legacyOrganizationName; - static QString legacyApplicationName; - - protected: - QSettings legacySettings { legacyOrganizationName, - legacyApplicationName }; - }; - - class SettingsGroup: public Settings +namespace Quotient { + +class Settings : public QSettings { + Q_OBJECT +public: + /// Add a legacy organisation/application name to migrate settings from + /*! + * Use this function before creating any Settings objects in order + * to set a legacy location where configuration has previously been stored. + * This will provide an additional fallback in case of renaming + * the organisation/application. Values in legacy locations are _removed_ + * when setValue() or remove() is called. + */ + static void setLegacyNames(const QString& organizationName, + const QString& applicationName = {}); + + explicit Settings(QObject* parent = nullptr); + + /// Set the value for a given key + /*! If the key exists in the legacy location, it is removed. */ + Q_INVOKABLE void setValue(const QString& key, const QVariant& value); + + /// Remove the value from both the primary and legacy locations + Q_INVOKABLE void remove(const QString& key); + + /// Obtain a value for a given key + /*! + * If the key doesn't exist in the primary settings location, the legacy + * location is checked. If neither location has the key, + * \p defaultValue is returned. + * + * This function returns a QVariant; use get<>() to get the unwrapped + * value if you know the type upfront. + * + * \sa setLegacyNames, get + */ + Q_INVOKABLE QVariant value(const QString& key, + const QVariant& defaultValue = {}) const; + + /// Obtain a value for a given key, coerced to the given type + /*! + * On top of value(), this function unwraps the QVariant and returns + * its contents assuming the type passed as the template parameter. + * If the type is different from the one stored inside the QVariant, + * \p defaultValue is returned. In presence of legacy settings, + * only the first found value is checked; if its type does not match, + * further checks through legacy settings are not performed and + * \p defaultValue is returned. + */ + template <typename T> + T get(const QString& key, const T& defaultValue = {}) const { - public: - template <typename... ArgTs> - explicit SettingsGroup(QString path, ArgTs&&... qsettingsArgs) - : Settings(std::forward<ArgTs>(qsettingsArgs)...) - , groupPath(std::move(path)) - { } - - Q_INVOKABLE bool contains(const QString& key) const; - Q_INVOKABLE QVariant value(const QString &key, - const QVariant &defaultValue = {}) const; - - template <typename T> - T get(const QString& key, const T& defaultValue = {}) const - { - const auto qv = value(key, QVariant()); - return qv.isValid() && qv.canConvert<T>() ? qv.value<T>() - : defaultValue; - } - - Q_INVOKABLE QString group() const; - Q_INVOKABLE QStringList childGroups() const; - Q_INVOKABLE void setValue(const QString &key, - const QVariant &value); - - Q_INVOKABLE void remove(const QString& key); - - private: - QString groupPath; - }; - -#define QMC_DECLARE_SETTING(type, propname, setter) \ - Q_PROPERTY(type propname READ propname WRITE setter) \ - public: \ - type propname() const; \ - void setter(type newValue); \ - private: - -#define QMC_DEFINE_SETTING(classname, type, propname, qsettingname, defaultValue, setter) \ -type classname::propname() const \ -{ \ - return get<type>(QStringLiteral(qsettingname), defaultValue); \ -} \ -\ -void classname::setter(type newValue) \ -{ \ - setValue(QStringLiteral(qsettingname), std::move(newValue)); \ -} \ - - class AccountSettings: public SettingsGroup + const auto qv = value(key, QVariant()); + return qv.isValid() && qv.canConvert<T>() ? qv.value<T>() : defaultValue; + } + + Q_INVOKABLE bool contains(const QString& key) const; + Q_INVOKABLE QStringList childGroups() const; + +private: + static QString legacyOrganizationName; + static QString legacyApplicationName; + +protected: + QSettings legacySettings { legacyOrganizationName, legacyApplicationName }; +}; + +class SettingsGroup : public Settings { +public: + template <typename... ArgTs> + explicit SettingsGroup(QString path, ArgTs&&... qsettingsArgs) + : Settings(std::forward<ArgTs>(qsettingsArgs)...) + , groupPath(std::move(path)) + {} + + Q_INVOKABLE bool contains(const QString& key) const; + Q_INVOKABLE QVariant value(const QString& key, + const QVariant& defaultValue = {}) const; + + template <typename T> + T get(const QString& key, const T& defaultValue = {}) const { - Q_OBJECT - Q_PROPERTY(QString userId READ userId CONSTANT) - QMC_DECLARE_SETTING(QString, deviceId, setDeviceId) - QMC_DECLARE_SETTING(QString, deviceName, setDeviceName) - QMC_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn) - /** \deprecated \sa setAccessToken */ - Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken) - public: - template <typename... ArgTs> - explicit AccountSettings(const QString& accountId, ArgTs... qsettingsArgs) - : SettingsGroup("Accounts/" + accountId, qsettingsArgs...) - { } - - QString userId() const; - - QUrl homeserver() const; - void setHomeserver(const QUrl& url); - - /** \deprecated \sa setToken */ - QString accessToken() const; - /** \deprecated Storing accessToken in QSettings is unsafe, - * see QMatrixClient/Quaternion#181 */ - void setAccessToken(const QString& accessToken); - Q_INVOKABLE void clearAccessToken(); - }; -} // namespace QMatrixClient + const auto qv = value(key, QVariant()); + return qv.isValid() && qv.canConvert<T>() ? qv.value<T>() : defaultValue; + } + + Q_INVOKABLE QString group() const; + Q_INVOKABLE QStringList childGroups() const; + Q_INVOKABLE void setValue(const QString& key, const QVariant& value); + + Q_INVOKABLE void remove(const QString& key); + +private: + QString groupPath; +}; + +#define QTNT_DECLARE_SETTING(type, propname, setter) \ + Q_PROPERTY(type propname READ propname WRITE setter) \ +public: \ + type propname() const; \ + void setter(type newValue); \ + \ +private: + +#define QTNT_DEFINE_SETTING(classname, type, propname, qsettingname, \ + defaultValue, setter) \ + type classname::propname() const \ + { \ + return get<type>(QStringLiteral(qsettingname), defaultValue); \ + } \ + \ + void classname::setter(type newValue) \ + { \ + setValue(QStringLiteral(qsettingname), std::move(newValue)); \ + } + +class AccountSettings : public SettingsGroup { + Q_OBJECT + Q_PROPERTY(QString userId READ userId CONSTANT) + QTNT_DECLARE_SETTING(QString, deviceId, setDeviceId) + QTNT_DECLARE_SETTING(QString, deviceName, setDeviceName) + QTNT_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn) + /** \deprecated \sa setAccessToken */ + Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken) + Q_PROPERTY(QByteArray encryptionAccountPickle READ encryptionAccountPickle + WRITE setEncryptionAccountPickle) +public: + template <typename... ArgTs> + explicit AccountSettings(const QString& accountId, ArgTs&&... qsettingsArgs) + : SettingsGroup("Accounts/" + accountId, + std::forward<ArgTs>(qsettingsArgs)...) + {} + + QString userId() const; + + QUrl homeserver() const; + void setHomeserver(const QUrl& url); + + /** \deprecated \sa setToken */ + QString accessToken() const; + /** \deprecated Storing accessToken in QSettings is unsafe, + * see quotient-im/Quaternion#181 */ + void setAccessToken(const QString& accessToken); + Q_INVOKABLE void clearAccessToken(); + + QByteArray encryptionAccountPickle(); + void setEncryptionAccountPickle(const QByteArray& encryptionAccountPickle); + Q_INVOKABLE void clearEncryptionAccountPickle(); +}; +} // namespace Quotient diff --git a/lib/ssosession.cpp b/lib/ssosession.cpp index 6ea4a3f5..be701204 100644 --- a/lib/ssosession.cpp +++ b/lib/ssosession.cpp @@ -8,9 +8,10 @@ #include <QtCore/QCoreApplication> #include <QtCore/QStringBuilder> -using namespace QMatrixClient; +using namespace Quotient; -struct SsoSession::Private { +class SsoSession::Private { +public: Private(SsoSession* q, const QString& initialDeviceName = {}, const QString& deviceId = {}, Connection* connection = nullptr) : initialDeviceName(initialDeviceName) @@ -38,7 +39,7 @@ struct SsoSession::Private { processCallback(); }); QObject::connect(socket, &QTcpSocket::disconnected, socket, - [this] { socket->deleteLater(); }); + &QTcpSocket::deleteLater); }); } void processCallback(); diff --git a/lib/ssosession.h b/lib/ssosession.h index af20c075..5845cd4d 100644 --- a/lib/ssosession.h +++ b/lib/ssosession.h @@ -8,7 +8,7 @@ class QTcpServer; class QTcpSocket; -namespace QMatrixClient { +namespace Quotient { class Connection; /*! Single sign-on (SSO) session encapsulation @@ -41,4 +41,4 @@ private: class Private; std::unique_ptr<Private> d; }; -} // namespace QMatrixClient +} // namespace Quotient diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index 21517884..e6472e18 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -13,7 +13,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "syncdata.h" @@ -23,36 +23,34 @@ #include <QtCore/QFile> #include <QtCore/QFileInfo> -using namespace QMatrixClient; +using namespace Quotient; const QString SyncRoomData::UnreadCountKey = - QStringLiteral("x-qmatrixclient.unread_count"); + QStringLiteral("x-quotient.unread_count"); bool RoomSummary::isEmpty() const { - return joinedMemberCount.omitted() && invitedMemberCount.omitted() && - heroes.omitted(); + return !joinedMemberCount && !invitedMemberCount && !heroes; } bool RoomSummary::merge(const RoomSummary& other) { // Using bitwise OR to prevent computation shortcut. - return - joinedMemberCount.merge(other.joinedMemberCount) | - invitedMemberCount.merge(other.invitedMemberCount) | - heroes.merge(other.heroes); + return joinedMemberCount.merge(other.joinedMemberCount) + | invitedMemberCount.merge(other.invitedMemberCount) + | heroes.merge(other.heroes); } -QDebug QMatrixClient::operator<<(QDebug dbg, const RoomSummary& rs) +QDebug Quotient::operator<<(QDebug dbg, const RoomSummary& rs) { QDebugStateSaver _(dbg); QStringList sl; - if (!rs.joinedMemberCount.omitted()) - sl << QStringLiteral("joined: %1").arg(rs.joinedMemberCount.value()); - if (!rs.invitedMemberCount.omitted()) - sl << QStringLiteral("invited: %1").arg(rs.invitedMemberCount.value()); - if (!rs.heroes.omitted()) - sl << QStringLiteral("heroes: [%1]").arg(rs.heroes.value().join(',')); + if (rs.joinedMemberCount) + sl << QStringLiteral("joined: %1").arg(*rs.joinedMemberCount); + if (rs.invitedMemberCount) + sl << QStringLiteral("invited: %1").arg(*rs.invitedMemberCount); + if (rs.heroes) + sl << QStringLiteral("heroes: [%1]").arg(rs.heroes->join(',')); dbg.nospace().noquote() << sl.join(QStringLiteral("; ")); return dbg; } @@ -87,23 +85,23 @@ SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, , joinState(joinState_) , summary(fromJson<RoomSummary>(room_["summary"_ls])) , state(load<StateEvents>(room_, joinState == JoinState::Invite - ? "invite_state"_ls : "state"_ls)) + ? "invite_state"_ls + : "state"_ls)) { switch (joinState) { - case JoinState::Join: - ephemeral = load<Events>(room_, "ephemeral"_ls); - FALLTHROUGH; - case JoinState::Leave: - { - accountData = load<Events>(room_, "account_data"_ls); - timeline = load<RoomEvents>(room_, "timeline"_ls); - const auto timelineJson = room_.value("timeline"_ls).toObject(); - timelineLimited = timelineJson.value("limited"_ls).toBool(); - timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString(); - - break; - } - default: /* nothing on top of state */; + case JoinState::Join: + ephemeral = load<Events>(room_, "ephemeral"_ls); + [[fallthrough]]; + case JoinState::Leave: { + accountData = load<Events>(room_, "account_data"_ls); + timeline = load<RoomEvents>(room_, "timeline"_ls); + const auto timelineJson = room_.value("timeline"_ls).toObject(); + timelineLimited = timelineJson.value("limited"_ls).toBool(); + timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString(); + + break; + } + default: /* nothing on top of state */; } const auto unreadJson = room_.value("unread_notifications"_ls).toObject(); @@ -121,20 +119,17 @@ SyncData::SyncData(const QString& cacheFileName) QFileInfo cacheFileInfo { cacheFileName }; auto json = loadJson(cacheFileName); auto requiredVersion = std::get<0>(cacheVersion()); - auto actualVersion = json.value("cache_version"_ls).toObject() - .value("major"_ls).toInt(); + auto actualVersion = + json.value("cache_version"_ls).toObject().value("major"_ls).toInt(); if (actualVersion == requiredVersion) parseJson(json, cacheFileInfo.absolutePath() + '/'); else - qCWarning(MAIN) - << "Major version of the cache file is" << actualVersion << "but" - << requiredVersion << "is required; discarding the cache"; + qCWarning(MAIN) << "Major version of the cache file is" << actualVersion + << "but" << requiredVersion + << "is required; discarding the cache"; } -SyncDataList&& SyncData::takeRoomData() -{ - return move(roomData); -} +SyncDataList&& SyncData::takeRoomData() { return move(roomData); } QString SyncData::fileNameForRoom(QString roomId) { @@ -142,42 +137,35 @@ QString SyncData::fileNameForRoom(QString roomId) return roomId + ".json"; } -Events&& SyncData::takePresenceData() -{ - return std::move(presenceData); -} +Events&& SyncData::takePresenceData() { return std::move(presenceData); } -Events&& SyncData::takeAccountData() -{ - return std::move(accountData); -} +Events&& SyncData::takeAccountData() { return std::move(accountData); } -Events&& SyncData::takeToDeviceEvents() -{ - return std::move(toDeviceEvents); -} +Events&& SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); } QJsonObject SyncData::loadJson(const QString& fileName) { QFile roomFile { fileName }; - if (!roomFile.exists()) - { + if (!roomFile.exists()) { qCWarning(MAIN) << "No state cache file" << fileName; return {}; } - if(!roomFile.open(QIODevice::ReadOnly)) - { + if (!roomFile.open(QIODevice::ReadOnly)) { qCWarning(MAIN) << "Failed to open state cache file" << roomFile.fileName(); return {}; } auto data = roomFile.readAll(); - const auto json = - (data.startsWith('{') ? QJsonDocument::fromJson(data) - : QJsonDocument::fromBinaryData(data)).object(); - if (json.isEmpty()) - { + const auto json = data.startsWith('{') + ? QJsonDocument::fromJson(data).object() +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + : QCborValue::fromCbor(data).toJsonValue().toObject() +#else + : QJsonDocument::fromBinaryData(data).object() +#endif + ; + if (json.isEmpty()) { qCWarning(MAIN) << "State cache in" << fileName << "is broken or empty, discarding"; } @@ -186,36 +174,38 @@ QJsonObject SyncData::loadJson(const QString& fileName) void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) { - QElapsedTimer et; et.start(); + QElapsedTimer et; + et.start(); nextBatch_ = json.value("next_batch"_ls).toString(); presenceData = load<Events>(json, "presence"_ls); accountData = load<Events>(json, "account_data"_ls); toDeviceEvents = load<Events>(json, "to_device"_ls); + fromJson(json.value("device_one_time_keys_count"_ls), + deviceOneTimeKeysCount_); + auto rooms = json.value("rooms"_ls).toObject(); JoinStates::Int ii = 1; // ii is used to make a JoinState value auto totalRooms = 0; auto totalEvents = 0; - for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1) - { + for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1) { const auto rs = rooms.value(JoinStateStrings[i]).toObject(); // We have a Qt container on the right and an STL one on the left roomData.reserve(static_cast<size_t>(rs.size())); - for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) - { - auto roomJson = roomIt->isObject() - ? roomIt->toObject() - : loadJson(baseDir + fileNameForRoom(roomIt.key())); - if (roomJson.isEmpty()) - { + for (auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) { + auto roomJson = + roomIt->isObject() + ? roomIt->toObject() + : loadJson(baseDir + fileNameForRoom(roomIt.key())); + if (roomJson.isEmpty()) { unresolvedRoomIds.push_back(roomIt.key()); continue; } roomData.emplace_back(roomIt.key(), JoinState(ii), roomJson); const auto& r = roomData.back(); - totalEvents += r.state.size() + r.ephemeral.size() + - r.accountData.size() + r.timeline.size(); + totalEvents += r.state.size() + r.ephemeral.size() + + r.accountData.size() + r.timeline.size(); } totalRooms += rs.size(); } @@ -223,6 +213,6 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) qCWarning(MAIN) << "Unresolved rooms:" << unresolvedRoomIds.join(','); if (totalRooms > 9 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with" - << totalRooms << "room(s)," - << totalEvents << "event(s) in" << et; + << totalRooms << "room(s)," << totalEvents + << "event(s) in" << et; } diff --git a/lib/syncdata.h b/lib/syncdata.h index 8694626e..67d04557 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -13,104 +13,107 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "joinstate.h" + #include "events/stateevent.h" -namespace QMatrixClient { - /// Room summary, as defined in MSC688 - /** - * Every member of this structure is an Omittable; as per the MSC, only - * changed values are sent from the server so if nothing is in the payload - * the respective member will be omitted. In particular, `heroes.omitted()` - * means that nothing has come from the server; heroes.value().isEmpty() - * means a peculiar case of a room with the only member - the current user. +namespace Quotient { +/// Room summary, as defined in MSC688 +/** + * Every member of this structure is an Omittable; as per the MSC, only + * changed values are sent from the server so if nothing is in the payload + * the respective member will be omitted. In particular, `heroes.omitted()` + * means that nothing has come from the server; heroes.value().isEmpty() + * means a peculiar case of a room with the only member - the current user. + */ +struct RoomSummary { + Omittable<int> joinedMemberCount; + Omittable<int> invitedMemberCount; + Omittable<QStringList> heroes; //< mxids of users to take part in the room + // name + + bool isEmpty() const; + /// Merge the contents of another RoomSummary object into this one + /// \return true, if the current object has changed; false otherwise + bool merge(const RoomSummary& other); +}; +QDebug operator<<(QDebug dbg, const RoomSummary& rs); + + +template <> +struct JsonObjectConverter<RoomSummary> { + static void dumpTo(QJsonObject& jo, const RoomSummary& rs); + static void fillFrom(const QJsonObject& jo, RoomSummary& rs); +}; + +class SyncRoomData { +public: + QString roomId; + JoinState joinState; + RoomSummary summary; + StateEvents state; + RoomEvents timeline; + Events ephemeral; + Events accountData; + + bool timelineLimited; + QString timelinePrevBatch; + int unreadCount; + int highlightCount; + int notificationCount; + + SyncRoomData(const QString& roomId, JoinState joinState_, + const QJsonObject& room_); + SyncRoomData(SyncRoomData&&) = default; + SyncRoomData& operator=(SyncRoomData&&) = default; + + static const QString UnreadCountKey; +}; + +// QVector cannot work with non-copyable objects, std::vector can. +using SyncDataList = std::vector<SyncRoomData>; + +class SyncData { +public: + SyncData() = default; + explicit SyncData(const QString& cacheFileName); + /** Parse sync response into room events + * \param json response from /sync or a room state cache + * \return the list of rooms with missing cache files; always + * empty when parsing response from /sync */ - struct RoomSummary + void parseJson(const QJsonObject& json, const QString& baseDir = {}); + + Events&& takePresenceData(); + Events&& takeAccountData(); + Events&& takeToDeviceEvents(); + const QHash<QString, int>& deviceOneTimeKeysCount() const { - Omittable<int> joinedMemberCount; - Omittable<int> invitedMemberCount; - Omittable<QStringList> heroes; //< mxids of users to take part in the room name + return deviceOneTimeKeysCount_; + } + SyncDataList&& takeRoomData(); - bool isEmpty() const; - /// Merge the contents of another RoomSummary object into this one - /// \return true, if the current object has changed; false otherwise - bool merge(const RoomSummary& other); + QString nextBatch() const { return nextBatch_; } - friend QDebug operator<<(QDebug dbg, const RoomSummary& rs); - }; + QStringList unresolvedRooms() const { return unresolvedRoomIds; } - template <> - struct JsonObjectConverter<RoomSummary> - { - static void dumpTo(QJsonObject& jo, const RoomSummary& rs); - static void fillFrom(const QJsonObject& jo, RoomSummary& rs); - }; + static std::pair<int, int> cacheVersion() { return { 11, 0 }; } + static QString fileNameForRoom(QString roomId); - class SyncRoomData - { - public: - QString roomId; - JoinState joinState; - RoomSummary summary; - StateEvents state; - RoomEvents timeline; - Events ephemeral; - Events accountData; - - bool timelineLimited; - QString timelinePrevBatch; - int unreadCount; - int highlightCount; - int notificationCount; - - SyncRoomData(const QString& roomId, JoinState joinState_, - const QJsonObject& room_); - SyncRoomData(SyncRoomData&&) = default; - SyncRoomData& operator=(SyncRoomData&&) = default; - - static const QString UnreadCountKey; - }; - - // QVector cannot work with non-copiable objects, std::vector can. - using SyncDataList = std::vector<SyncRoomData>; - - class SyncData - { - public: - SyncData() = default; - explicit SyncData(const QString& cacheFileName); - /** Parse sync response into room events - * \param json response from /sync or a room state cache - * \return the list of rooms with missing cache files; always - * empty when parsing response from /sync - */ - void parseJson(const QJsonObject& json, const QString& baseDir = {}); - - Events&& takePresenceData(); - Events&& takeAccountData(); - Events&& takeToDeviceEvents(); - SyncDataList&& takeRoomData(); - - QString nextBatch() const { return nextBatch_; } - - QStringList unresolvedRooms() const { return unresolvedRoomIds; } - - static std::pair<int, int> cacheVersion() { return { 10, 0 }; } - static QString fileNameForRoom(QString roomId); - - private: - QString nextBatch_; - Events presenceData; - Events accountData; - Events toDeviceEvents; - SyncDataList roomData; - QStringList unresolvedRoomIds; - - static QJsonObject loadJson(const QString& fileName); - }; -} // namespace QMatrixClient +private: + QString nextBatch_; + Events presenceData; + Events accountData; + Events toDeviceEvents; + SyncDataList roomData; + QStringList unresolvedRoomIds; + QHash<QString, int> deviceOneTimeKeysCount_; + + static QJsonObject loadJson(const QString& fileName); +}; +} // namespace Quotient diff --git a/lib/uri.cpp b/lib/uri.cpp new file mode 100644 index 00000000..e0912eb6 --- /dev/null +++ b/lib/uri.cpp @@ -0,0 +1,219 @@ +#include "uri.h" + +#include "logging.h" + +#include <QtCore/QRegularExpression> + +using namespace Quotient; + +struct ReplacePair { QByteArray uriString; char sigil; }; +/// Defines bi-directional mapping of path prefixes and sigils +static const auto replacePairs = { + ReplacePair { "user/", '@' }, + { "roomid/", '!' }, + { "room/", '#' }, + // The notation for bare event ids is not proposed in MSC2312 but there's + // https://github.com/matrix-org/matrix-doc/pull/2644 + { "event/", '$' } +}; + +Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query) +{ + if (primaryId.isEmpty()) + primaryType_ = Empty; + else { + setScheme("matrix"); + QString pathToBe; + primaryType_ = Invalid; + if (primaryId.size() < 2) // There should be something after sigil + return; + for (const auto& p: replacePairs) + if (primaryId[0] == p.sigil) { + primaryType_ = Type(p.sigil); + auto safePrimaryId = primaryId.mid(1); + safePrimaryId.replace('/', "%2F"); + pathToBe = p.uriString + safePrimaryId; + break; + } + if (!secondaryId.isEmpty()) { + if (secondaryId.size() < 2) { + primaryType_ = Invalid; + return; + } + auto safeSecondaryId = secondaryId.mid(1); + safeSecondaryId.replace('/', "%2F"); + pathToBe += "/event/" + safeSecondaryId; + } + setPath(pathToBe, QUrl::TolerantMode); + } + if (!query.isEmpty()) + setQuery(query); +} + +static inline auto encodedPath(const QUrl& url) +{ + return url.path(QUrl::EncodeDelimiters | QUrl::EncodeUnicode); +} + +static QString pathSegment(const QUrl& url, int which) +{ + return QUrl::fromPercentEncoding( + encodedPath(url).section('/', which, which).toUtf8()); +} + +static auto decodeFragmentPart(const QStringRef& part) +{ + return QUrl::fromPercentEncoding(part.toLatin1()).toUtf8(); +} + +static auto matrixToUrlRegexInit() +{ + // See https://matrix.org/docs/spec/appendices#matrix-to-navigation + const QRegularExpression MatrixToUrlRE { + "^/(?<main>[^:]+:[^/?]+)(/(?<sec>(\\$|%24)[^?]+))?(\\?(?<query>.+))?$" + }; + Q_ASSERT(MatrixToUrlRE.isValid()); + return MatrixToUrlRE; +} + +Uri::Uri(QUrl url) : QUrl(std::move(url)) +{ + // NB: don't try to use `url` from here on, it's moved-from and empty + if (isEmpty()) + return; // primaryType_ == Empty + + primaryType_ = Invalid; + if (!QUrl::isValid()) // MatrixUri::isValid() checks primaryType_ + return; + + if (scheme() == "matrix") { + // Check sanity as per https://github.com/matrix-org/matrix-doc/pull/2312 + const auto& urlPath = encodedPath(*this); + const auto& splitPath = urlPath.splitRef('/'); + switch (splitPath.size()) { + case 2: + break; + case 4: + if (splitPath[2] == "event") + break; + [[fallthrough]]; + default: + return; // Invalid + } + + for (const auto& p: replacePairs) + if (urlPath.startsWith(p.uriString)) { + primaryType_ = Type(p.sigil); + return; // The only valid return path for matrix: URIs + } + qCDebug(MAIN) << "The matrix: URI is not recognised:" + << toDisplayString(); + return; + } + + primaryType_ = NonMatrix; // Default, unless overridden by the code below + if (scheme() == "https" && authority() == "matrix.to") { + static const auto MatrixToUrlRE = matrixToUrlRegexInit(); + // matrix.to accepts both literal sigils (as well as & and ? used in + // its "query" substitute) and their %-encoded forms; + // so force QUrl to decode everything. + auto f = fragment(QUrl::EncodeUnicode); + if (auto&& m = MatrixToUrlRE.match(f); m.hasMatch()) + *this = Uri { decodeFragmentPart(m.capturedRef("main")), + decodeFragmentPart(m.capturedRef("sec")), + decodeFragmentPart(m.capturedRef("query")) }; + } +} + +Uri::Uri(const QString& uriOrId) : Uri(fromUserInput(uriOrId)) {} + +Uri Uri::fromUserInput(const QString& uriOrId) +{ + if (uriOrId.isEmpty()) + return {}; // type() == None + + // A quick check if uriOrId is a plain Matrix id + // Bare event ids cannot be resolved without a room scope as per the current + // spec but there's a movement towards making them navigable (see, e.g., + // https://github.com/matrix-org/matrix-doc/pull/2644) - so treat them + // as valid + if (QStringLiteral("!@#+$").contains(uriOrId[0])) + return Uri { uriOrId.toUtf8() }; + + return Uri { QUrl::fromUserInput(uriOrId) }; +} + +Uri::Type Uri::type() const { return primaryType_; } + +Uri::SecondaryType Uri::secondaryType() const +{ + return pathSegment(*this, 2) == "event" ? EventId : NoSecondaryId; +} + +QUrl Uri::toUrl(UriForm form) const +{ + if (!isValid()) + return {}; + + if (form == CanonicalUri || type() == NonMatrix) + return *this; // NOLINT(cppcoreguidelines-slicing): It's intentional + + QUrl url; + url.setScheme("https"); + url.setHost("matrix.to"); + url.setPath("/"); + auto fragment = '/' + primaryId(); + if (const auto& secId = secondaryId(); !secId.isEmpty()) + fragment += '/' + secId; + if (const auto& q = query(); !q.isEmpty()) + fragment += '?' + q; + url.setFragment(fragment); + return url; +} + +QString Uri::primaryId() const +{ + if (primaryType_ == Empty || primaryType_ == Invalid) + return {}; + + const auto& idStem = pathSegment(*this, 1); + return idStem.isEmpty() ? idStem : primaryType_ + idStem; +} + +QString Uri::secondaryId() const +{ + const auto& idStem = pathSegment(*this, 3); + return idStem.isEmpty() ? idStem : secondaryType() + idStem; +} + +static const auto ActionKey = QStringLiteral("action"); + +QString Uri::action() const +{ + return type() == NonMatrix || !isValid() + ? QString() + : QUrlQuery { query() }.queryItemValue(ActionKey); +} + +void Uri::setAction(const QString& newAction) +{ + if (!isValid()) { + qCWarning(MAIN) << "Cannot set an action on an invalid Quotient::Uri"; + return; + } + QUrlQuery q { query() }; + q.removeQueryItem(ActionKey); + q.addQueryItem(ActionKey, newAction); + setQuery(q); +} + +QStringList Uri::viaServers() const +{ + return QUrlQuery { query() }.allQueryItemValues(QStringLiteral("via"), + QUrl::EncodeReserved); +} + +bool Uri::isValid() const +{ + return primaryType_ != Empty && primaryType_ != Invalid; +} diff --git a/lib/uri.h b/lib/uri.h new file mode 100644 index 00000000..270766dd --- /dev/null +++ b/lib/uri.h @@ -0,0 +1,85 @@ +#pragma once + +#include "quotient_common.h" + +#include <QtCore/QUrl> +#include <QtCore/QUrlQuery> + +namespace Quotient { + +/*! \brief A wrapper around a Matrix URI or identifier + * + * This class encapsulates a Matrix resource identifier, passed in either of + * 3 forms: a plain Matrix identifier (sigil, localpart, serverpart or, for + * modern event ids, sigil and base64 hash); an MSC2312 URI (aka matrix: URI); + * or a matrix.to URL. The input can be either encoded (serverparts with + * punycode, the rest with percent-encoding) or unencoded (in this case it is + * the caller's responsibility to resolve all possible ambiguities). + * + * The class provides functions to check the validity of the identifier, + * its type, and obtain components, also in either unencoded (for displaying) + * or encoded (for APIs) form. + */ +class Uri : private QUrl { + Q_GADGET +public: + enum Type : char { + Invalid = char(-1), + Empty = 0x0, + UserId = '@', + RoomId = '!', + RoomAlias = '#', + Group = '+', + BareEventId = '$', // https://github.com/matrix-org/matrix-doc/pull/2644 + NonMatrix = ':' + }; + Q_ENUM(Type) + enum SecondaryType : char { NoSecondaryId = 0x0, EventId = '$' }; + Q_ENUM(SecondaryType) + + enum UriForm : short { CanonicalUri, MatrixToUri }; + Q_ENUM(UriForm) + + /// Construct an empty Matrix URI + Uri() = default; + /*! \brief Decode a user input string to a Matrix identifier + * + * Accepts plain Matrix ids, MSC2312 URIs (aka matrix: URIs) and + * matrix.to URLs. In case of URIs/URLs, it uses QUrl's TolerantMode + * parser to decode common mistakes/irregularities (see QUrl documentation + * for more details). + */ + Uri(const QString& uriOrId); + + /// Construct a Matrix URI from components + explicit Uri(QByteArray primaryId, QByteArray secondaryId = {}, + QString query = {}); + /// Construct a Matrix URI from matrix.to or MSC2312 (matrix:) URI + explicit Uri(QUrl url); + + static Uri fromUserInput(const QString& uriOrId); + static Uri fromUrl(QUrl url); + + /// Get the primary type of the Matrix URI (user id, room id or alias) + /*! Note that this does not include an event as a separate type, since + * events can only be addressed inside of rooms, which, in turn, are + * addressed either by id or alias. If you need to check whether the URI + * is specifically an event URI, use secondaryType() instead. + */ + Q_INVOKABLE Type type() const; + Q_INVOKABLE SecondaryType secondaryType() const; + Q_INVOKABLE QUrl toUrl(UriForm form = CanonicalUri) const; + Q_INVOKABLE QString primaryId() const; + Q_INVOKABLE QString secondaryId() const; + Q_INVOKABLE QString action() const; + Q_INVOKABLE void setAction(const QString& newAction); + Q_INVOKABLE QStringList viaServers() const; + Q_INVOKABLE bool isValid() const; + using QUrl::path, QUrl::query, QUrl::fragment; + using QUrl::isEmpty, QUrl::toDisplayString; + +private: + Type primaryType_ = Empty; +}; + +} diff --git a/lib/uriresolver.cpp b/lib/uriresolver.cpp new file mode 100644 index 00000000..27360bcc --- /dev/null +++ b/lib/uriresolver.cpp @@ -0,0 +1,114 @@ +#include "uriresolver.h" + +#include "connection.h" +#include "user.h" + +using namespace Quotient; + +UriResolveResult UriResolverBase::visitResource(Connection* account, + const Uri& uri) +{ + switch (uri.type()) { + case Uri::NonMatrix: + return visitNonMatrix(uri.toUrl()) ? UriResolved : CouldNotResolve; + case Uri::Invalid: + case Uri::Empty: + return InvalidUri; + default:; + } + + if (!account) + return NoAccount; + + switch (uri.type()) { + case Uri::UserId: { + if (uri.action() == "join") + return IncorrectAction; + auto* user = account->user(uri.primaryId()); + Q_ASSERT(user != nullptr); + return visitUser(user, uri.action()); + } + case Uri::RoomId: + case Uri::RoomAlias: { + auto* room = uri.type() == Uri::RoomId + ? account->room(uri.primaryId()) + : account->roomByAlias(uri.primaryId()); + if (room != nullptr) { + visitRoom(room, uri.secondaryId()); + return UriResolved; + } + if (uri.action() == "join") { + joinRoom(account, uri.primaryId(), uri.viaServers()); + return UriResolved; + } + [[fallthrough]]; + } + default: + return CouldNotResolve; + } +} + +// This template is only instantiated once, for Quotient::visitResource() +template <typename... FnTs> +class StaticUriDispatcher : public UriResolverBase { +public: + StaticUriDispatcher(const FnTs&... fns) : fns_(fns...) {} + +private: + UriResolveResult visitUser(User* user, const QString& action) override + { + return std::get<0>(fns_)(user, action); + } + void visitRoom(Room* room, const QString& eventId) override + { + std::get<1>(fns_)(room, eventId); + } + void joinRoom(Connection* account, const QString& roomAliasOrId, + const QStringList& viaServers = {}) override + { + std::get<2>(fns_)(account, roomAliasOrId, viaServers); + } + bool visitNonMatrix(const QUrl& url) override + { + return std::get<3>(fns_)(url); + } + + std::tuple<FnTs...> fns_; +}; +template <typename... FnTs> +StaticUriDispatcher(FnTs&&... fns) -> StaticUriDispatcher<FnTs...>; + +UriResolveResult Quotient::visitResource( + Connection* account, const Uri& uri, + std::function<UriResolveResult(User*, QString)> userHandler, + std::function<void(Room*, QString)> roomEventHandler, + std::function<void(Connection*, QString, QStringList)> joinHandler, + std::function<bool(const QUrl&)> nonMatrixHandler) +{ + return StaticUriDispatcher(userHandler, roomEventHandler, joinHandler, + nonMatrixHandler) + .visitResource(account, uri); +} + +UriResolveResult UriDispatcher::visitUser(User *user, const QString &action) +{ + emit userAction(user, action); + return UriResolved; +} + +void UriDispatcher::visitRoom(Room *room, const QString &eventId) +{ + emit roomAction(room, eventId); +} + +void UriDispatcher::joinRoom(Connection* account, const QString& roomAliasOrId, + const QStringList& viaServers) +{ + emit joinAction(account, roomAliasOrId, viaServers); +} + +bool UriDispatcher::visitNonMatrix(const QUrl &url) +{ + emit nonMatrixAction(url); + return true; +} diff --git a/lib/uriresolver.h b/lib/uriresolver.h new file mode 100644 index 00000000..9b2ced9d --- /dev/null +++ b/lib/uriresolver.h @@ -0,0 +1,168 @@ +#pragma once + +#include "uri.h" + +#include <QtCore/QObject> + +#include <functional> + +namespace Quotient { +class Connection; +class Room; +class User; + +/*! \brief Abstract class to resolve the resource and act on it + * + * This class encapsulates the logic of resolving a Matrix identifier or URI + * into a Quotient object (or objects) and calling an appropriate handler on it. + * It is a type-safe way of handling a URI with no prior context on its type + * in cases like, e.g., when a user clicks on a URI in the application. + * + * This class provides empty "handlers" for each type of URI to facilitate + * gradual implementation. Derived classes are encouraged to override as many + * of them as possible. + */ +class UriResolverBase { +public: + /*! \brief Resolve the resource and dispatch an action depending on its type + * + * This method: + * 1. Resolves \p uri into an actual object (e.g., Room or User), + * with possible additional data such as event id, in the context of + * \p account. + * 2. If the resolving is successful, depending on the type of the object, + * calls the appropriate virtual function (defined in a derived + * concrete class) to perform an action on the resource (open a room, + * mention a user etc.). + * 3. Returns the result of resolving the resource. + */ + UriResolveResult visitResource(Connection* account, const Uri& uri); + +protected: + /// Called by visitResource() when the passed URI identifies a Matrix user + /*! + * \return IncorrectAction if the action is not correct or not supported; + * UriResolved if it is accepted; other values are disallowed + */ + virtual UriResolveResult visitUser(User* user, const QString& action) + { + return IncorrectAction; + } + /// Called by visitResource() when the passed URI identifies a room or + /// an event in a room + virtual void visitRoom(Room* room, const QString& eventId) {} + /// Called by visitResource() when the passed URI has `action() == "join"` + /// and identifies a room that the user defined by the Connection argument + /// is not a member of + virtual void joinRoom(Connection* account, const QString& roomAliasOrId, + const QStringList& viaServers = {}) + {} + /// Called by visitResource() when the passed URI has `type() == NonMatrix` + /*! + * Should return true if the URI is considered resolved, false otherwise. + * A basic implementation in a graphical client can look like + * `return QDesktopServices::openUrl(url);` but it's strongly advised to + * ask for a user confirmation beforehand. + */ + virtual bool visitNonMatrix(const QUrl& url) { return false; } +}; + +/*! \brief Resolve the resource and invoke an action on it, via function objects + * + * This function encapsulates the logic of resolving a Matrix identifier or URI + * into a Quotient object (or objects) and calling an appropriate handler on it. + * Unlike UriResolverBase it accepts the list of handlers from + * the caller; internally it's uses a minimal UriResolverBase class + * + * \param account The connection used as a context to resolve the identifier + * + * \param uri A URI that can represent a Matrix entity + * + * \param userHandler Called when the passed URI identifies a Matrix user + * + * \param roomEventHandler Called when the passed URI identifies a room or + * an event in a room + * + * \param joinHandler Called when the passed URI has `action() == "join"` and + * identifies a room that the user defined by + * the Connection argument is not a member of + * + * \param nonMatrixHandler Called when the passed URI has `type() == NonMatrix`; + * should return true if the URI is considered resolved, + * false otherwise + * + * \sa UriResolverBase, UriDispatcher + */ +UriResolveResult +visitResource(Connection* account, const Uri& uri, + std::function<UriResolveResult(User*, QString)> userHandler, + std::function<void(Room*, QString)> roomEventHandler, + std::function<void(Connection*, QString, QStringList)> joinHandler, + std::function<bool(const QUrl&)> nonMatrixHandler); + +/*! \brief Check that the resource is resolvable with no action on it */ +inline UriResolveResult checkResource(Connection* account, const Uri& uri) +{ + return visitResource( + account, uri, [](auto, auto) { return UriResolved; }, [](auto, auto) {}, + [](auto, auto, auto) {}, [](auto) { return false; }); +} + +/*! \brief Resolve the resource and invoke an action on it, via Qt signals + * + * This is an implementation of UriResolverBase that is based on + * QObject and uses Qt signals instead of virtual functions to provide an + * open-ended interface for visitors. + * + * This class is aimed primarily at clients where invoking the resolving/action + * and handling the action are happening in decoupled parts of the code; it's + * also useful to operate on Matrix identifiers and URIs from QML/JS code + * that cannot call resolveResource() due to QML/C++ interface limitations. + * + * This class does not restrain the client code to a certain type of + * connections: both direct and queued (or a mix) will work fine. One limitation + * caused by that is there's no way to indicate if a non-Matrix URI has been + * successfully resolved - a signal always returns void. + * + * Note that in case of using (non-blocking) queued connections the code that + * calls resolveResource() should not expect the action to be performed + * synchronously - the returned value is the result of resolving the URI, + * not acting on it. + */ +class UriDispatcher : public QObject, public UriResolverBase { + Q_OBJECT +public: + explicit UriDispatcher(QObject* parent = nullptr) : QObject(parent) {} + + // It's actually UriResolverBase::visitResource() but with Q_INVOKABLE + Q_INVOKABLE UriResolveResult resolveResource(Connection* account, + const Uri& uri) + { + return UriResolverBase::visitResource(account, uri); + } + +signals: + /// An action on a user has been requested + void userAction(Quotient::User* user, QString action); + + /// An action on a room has been requested, with optional event id + void roomAction(Quotient::Room* room, QString eventId); + + /// A join action has been requested, with optional 'via' servers + void joinAction(Quotient::Connection* account, QString roomAliasOrId, + QStringList viaServers); + + /// An action on a non-Matrix URL has been requested + void nonMatrixAction(QUrl url); + +private: + UriResolveResult visitUser(User* user, const QString& action) override; + void visitRoom(Room* room, const QString& eventId) override; + void joinRoom(Connection* account, const QString& roomAliasOrId, + const QStringList& viaServers = {}) override; + bool visitNonMatrix(const QUrl& url) override; +}; + +} // namespace Quotient + + diff --git a/lib/user.cpp b/lib/user.cpp index c51354a0..85f9d9a7 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -13,274 +13,159 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "user.h" +#include "avatar.h" #include "connection.h" #include "room.h" -#include "avatar.h" + +#include "csapi/content-repo.h" +#include "csapi/profile.h" +#include "csapi/room_state.h" + #include "events/event.h" #include "events/roommemberevent.h" -#include "csapi/room_state.h" -#include "csapi/profile.h" -#include "csapi/content-repo.h" -#include <QtCore/QTimer> -#include <QtCore/QRegularExpression> +#include <QtCore/QElapsedTimer> #include <QtCore/QPointer> +#include <QtCore/QRegularExpression> #include <QtCore/QStringBuilder> -#include <QtCore/QElapsedTimer> +#include <QtCore/QTimer> #include <functional> -using namespace QMatrixClient; -using namespace std::placeholders; +using namespace Quotient; using std::move; -class User::Private -{ - public: - static Avatar makeAvatar(QUrl url) - { - return Avatar(move(url)); - } +class User::Private { +public: + Private(QString userId) : id(move(userId)), hueF(stringToHueF(id)) { } - Private(QString userId, Connection* connection) - : userId(move(userId)), connection(connection) - { } - - QString userId; - Connection* connection; - - QString bridged; - QString mostUsedName; - QMultiHash<QString, const Room*> otherNames; - Avatar mostUsedAvatar { makeAvatar({}) }; - std::vector<Avatar> otherAvatars; - auto otherAvatar(const QUrl& url) - { - return std::find_if(otherAvatars.begin(), otherAvatars.end(), - [&url] (const auto& av) { return av.url() == url; }); - } - QMultiHash<QUrl, const Room*> avatarsToRooms; + QString id; + qreal hueF; - mutable int totalRooms = 0; + // In the following two, isNull/nullopt mean they are uninitialised; + // isEmpty/Avatar::url().isEmpty() mean they are initialised but empty. + QString defaultName; + std::optional<Avatar> defaultAvatar; - QString nameForRoom(const Room* r, const QString& hint = {}) const; - void setNameForRoom(const Room* r, QString newName, const QString& oldName); - QUrl avatarUrlForRoom(const Room* r, const QUrl& hint = {}) const; - void setAvatarForRoom(const Room* r, const QUrl& newUrl, - const QUrl& oldUrl); + // NB: This container is ever-growing. Even if the user no more scrolls + // the timeline that far back, historical avatars are still kept around. + // This is consistent with the rest of Quotient, as room timelines + // are never rotated either. This will probably change in the future. + /// Map of mediaId to Avatar objects + static UnorderedMap<QString, Avatar> otherAvatars; - void setAvatarOnServer(QString contentUri, User* q); + void fetchProfile(const User* q); + template <typename SourceT> + bool doSetAvatar(SourceT&& source, User* q); }; +decltype(User::Private::otherAvatars) User::Private::otherAvatars {}; -QString User::Private::nameForRoom(const Room* r, const QString& hint) const +void User::Private::fetchProfile(const User* q) { - // If the hint is accurate, this function is O(1) instead of O(n) - if (!hint.isNull() - && (hint == mostUsedName || otherNames.contains(hint, r))) - return hint; - return otherNames.key(r, mostUsedName); -} - -static constexpr int MIN_JOINED_ROOMS_TO_LOG = 20; - -void User::Private::setNameForRoom(const Room* r, QString newName, - const QString& oldName) -{ - Q_ASSERT(oldName != newName); - Q_ASSERT(oldName == mostUsedName || otherNames.contains(oldName, r)); - if (totalRooms < 2) - { - Q_ASSERT_X(totalRooms > 0 && otherNames.empty(), __FUNCTION__, - "Internal structures inconsistency"); - mostUsedName = move(newName); - return; - } - otherNames.remove(oldName, r); - if (newName != mostUsedName) - { - // Check if the newName is about to become most used. - if (otherNames.count(newName) >= totalRooms - otherNames.size()) - { - Q_ASSERT(totalRooms > 1); - QElapsedTimer et; - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) - { - qCDebug(MAIN) << "Switching the most used name of user" << userId - << "from" << mostUsedName << "to" << newName; - qCDebug(MAIN) << "The user is in" << totalRooms << "rooms"; - et.start(); - } - - for (auto* r1: connection->allRooms()) - if (nameForRoom(r1) == mostUsedName) - otherNames.insert(mostUsedName, r1); - - mostUsedName = newName; - otherNames.remove(newName); - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) - qCDebug(PROFILER) << et << "to switch the most used name"; - } - else - otherNames.insert(newName, r); - } -} - -QUrl User::Private::avatarUrlForRoom(const Room* r, const QUrl& hint) const -{ - // If the hint is accurate, this function is O(1) instead of O(n) - if (hint == mostUsedAvatar.url() || avatarsToRooms.contains(hint, r)) - return hint; - auto it = std::find(avatarsToRooms.begin(), avatarsToRooms.end(), r); - return it == avatarsToRooms.end() ? mostUsedAvatar.url() : it.key(); -} - -void User::Private::setAvatarForRoom(const Room* r, const QUrl& newUrl, - const QUrl& oldUrl) -{ - Q_ASSERT(oldUrl != newUrl); - Q_ASSERT(oldUrl == mostUsedAvatar.url() || - avatarsToRooms.contains(oldUrl, r)); - if (totalRooms < 2) - { - Q_ASSERT_X(totalRooms > 0 && otherAvatars.empty(), __FUNCTION__, - "Internal structures inconsistency"); - mostUsedAvatar.updateUrl(newUrl); - return; - } - avatarsToRooms.remove(oldUrl, r); - if (!avatarsToRooms.contains(oldUrl)) - { - auto it = otherAvatar(oldUrl); - if (it != otherAvatars.end()) - otherAvatars.erase(it); - } - if (newUrl != mostUsedAvatar.url()) - { - // Check if the new avatar is about to become most used. - const auto newUrlUsage = avatarsToRooms.count(newUrl); - if (newUrlUsage >= totalRooms - avatarsToRooms.size()) { - QElapsedTimer et; - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) { - qCInfo(MAIN) << "Switching the most used avatar of user" << userId - << "from" << mostUsedAvatar.url().toDisplayString() - << "to" << newUrl.toDisplayString(); - et.start(); - } - avatarsToRooms.remove(newUrl); - auto nextMostUsedIt = otherAvatar(newUrl); - if (nextMostUsedIt == otherAvatars.end()) { - qCCritical(MAIN) - << userId << "doesn't have" << newUrl.toDisplayString() - << "in otherAvatars though it seems to be used in" - << newUrlUsage << "rooms"; - Q_ASSERT(false); - otherAvatars.emplace_back(makeAvatar(newUrl)); - nextMostUsedIt = otherAvatars.end() - 1; - } - std::swap(mostUsedAvatar, *nextMostUsedIt); - for (const auto* r1: connection->allRooms()) - if (avatarUrlForRoom(r1) == nextMostUsedIt->url()) - avatarsToRooms.insert(nextMostUsedIt->url(), r1); - - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) - qCDebug(PROFILER) << et << "to switch the most used avatar"; - } else { - if (otherAvatar(newUrl) == otherAvatars.end()) - otherAvatars.emplace_back(makeAvatar(newUrl)); - avatarsToRooms.insert(newUrl, r); - } - } + defaultAvatar.emplace(Avatar {}); + defaultName = ""; + auto* j = q->connection()->callApi<GetUserProfileJob>(BackgroundRequest, id); + // FIXME: accepting const User* and const_cast'ing it here is only + // until we get a better User API in 0.7 + QObject::connect(j, &BaseJob::success, q, + [this, q = const_cast<User*>(q), j] { + q->updateName(j->displayname()); + defaultAvatar->updateUrl(j->avatarUrl()); + emit q->avatarChanged(q, nullptr); + }); } User::User(QString userId, Connection* connection) - : QObject(connection), d(new Private(move(userId), connection)) + : QObject(connection), d(new Private(move(userId))) { - setObjectName(userId); + setObjectName(id()); } Connection* User::connection() const { - Q_ASSERT(d->connection); - return d->connection; + Q_ASSERT(parent()); + return static_cast<Connection*>(parent()); } User::~User() = default; -QString User::id() const -{ - return d->userId; -} +QString User::id() const { return d->id; } bool User::isGuest() const { - Q_ASSERT(!d->userId.isEmpty() && d->userId.startsWith('@')); - auto it = std::find_if_not(d->userId.begin() + 1, d->userId.end(), - [] (QChar c) { return c.isDigit(); }); - Q_ASSERT(it != d->userId.end()); + Q_ASSERT(!d->id.isEmpty() && d->id.startsWith('@')); + auto it = std::find_if_not(d->id.cbegin() + 1, d->id.cend(), + [](QChar c) { return c.isDigit(); }); + Q_ASSERT(it != d->id.end()); return *it == ':'; } -QString User::name(const Room* room) const -{ - return d->nameForRoom(room); -} +int User::hue() const { return int(hueF() * 359); } -QString User::rawName(const Room* room) const +/// \sa https://github.com/matrix-org/matrix-doc/issues/1375 +/// +/// Relies on untrusted prevContent so can't be put to RoomMemberEvent and +/// in general should rather be remade in terms of the room's eventual "state +/// time machine" +QString getBestKnownName(const RoomMemberEvent* event) { - return d->bridged.isEmpty() ? name(room) : - name(room) % " (" % d->bridged % ')'; + const auto& jv = event->contentJson().value("displayname"_ls); + return !jv.isUndefined() + ? jv.toString() + : event->prevContent() ? event->prevContent()->displayName + : QString(); } -void User::updateName(const QString& newName, const Room* room) +QString User::name(const Room* room) const { - updateName(newName, d->nameForRoom(room), room); -} + if (room) + return getBestKnownName(room->getCurrentState<RoomMemberEvent>(id())); -void User::updateName(const QString& newName, const QString& oldName, - const Room* room) -{ - Q_ASSERT(oldName == d->mostUsedName || d->otherNames.contains(oldName, room)); - if (newName != oldName) - { - emit nameAboutToChange(newName, oldName, room); - d->setNameForRoom(room, newName, oldName); - setObjectName(displayname()); - emit nameChanged(newName, oldName, room); - } + if (d->defaultName.isNull()) + d->fetchProfile(this); + + return d->defaultName; } -void User::updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl, - const Room* room) +QString User::rawName(const Room* room) const { return name(room); } + +void User::updateName(const QString& newName, const Room* r) { - Q_ASSERT(oldUrl == d->mostUsedAvatar.url() || - d->avatarsToRooms.contains(oldUrl, room)); - if (newUrl != oldUrl) - { - d->setAvatarForRoom(room, newUrl, oldUrl); - setObjectName(displayname()); - emit avatarChanged(this, room); - } + Q_ASSERT(r == nullptr); + if (newName == d->defaultName) + return; + emit nameAboutToChange(newName, d->defaultName, nullptr); + const auto& oldName = + std::exchange(d->defaultName, newName); + emit nameChanged(d->defaultName, oldName, nullptr); } +void User::updateName(const QString&, const QString&, const Room*) {} +void User::updateAvatarUrl(const QUrl&, const QUrl&, const Room*) {} void User::rename(const QString& newName) { const auto actualNewName = sanitized(newName); + if (actualNewName == d->defaultName) + return; // Nothing to do + connect(connection()->callApi<SetDisplayNameJob>(id(), actualNewName), - &BaseJob::success, this, [=] { updateName(actualNewName); }); + &BaseJob::success, this, [this, actualNewName] { + d->fetchProfile(this); + updateName(actualNewName); + }); } void User::rename(const QString& newName, const Room* r) { - if (!r) - { + if (!r) { qCWarning(MAIN) << "Passing a null room to two-argument User::rename()" "is incorrect; client developer, please fix it"; rename(newName); @@ -291,73 +176,99 @@ void User::rename(const QString& newName, const Room* r) const auto actualNewName = sanitized(newName); MemberEventContent evtC; evtC.displayName = actualNewName; - connect(r->setMemberState(id(), RoomMemberEvent(move(evtC))), - &BaseJob::success, this, [=] { updateName(actualNewName, r); }); + r->setState<RoomMemberEvent>(id(), move(evtC)); + // The state will be updated locally after it arrives with sync } -bool User::setAvatar(const QString& fileName) +template <typename SourceT> +bool User::Private::doSetAvatar(SourceT&& source, User* q) { - return avatarObject().upload(connection(), fileName, - std::bind(&Private::setAvatarOnServer, d.data(), _1, this)); + if (!defaultAvatar) { + defaultName = ""; + defaultAvatar.emplace(Avatar {}); + } + return defaultAvatar->upload( + q->connection(), source, [this, q](const QString& contentUri) { + auto* j = + q->connection()->callApi<SetAvatarUrlJob>(id, contentUri); + QObject::connect(j, &BaseJob::success, q, + [this, q, newUrl = QUrl(contentUri)] { + // Fetch displayname to complete the profile + fetchProfile(q); + if (newUrl == defaultAvatar->url()) { + qCWarning(MAIN) + << "User" << id + << "already has avatar URL set to" + << newUrl.toDisplayString(); + return; + } + + defaultAvatar->updateUrl(newUrl); + emit q->avatarChanged(q, nullptr); + }); + }); } -bool User::setAvatar(QIODevice* source) +bool User::setAvatar(const QString& fileName) { - return avatarObject().upload(connection(), source, - std::bind(&Private::setAvatarOnServer, d.data(), _1, this)); + return d->doSetAvatar(fileName, this); } -void User::requestDirectChat() +bool User::setAvatar(QIODevice* source) { - connection()->requestDirectChat(this); + return d->doSetAvatar(source, this); } -void User::ignore() -{ - connection()->addToIgnoredUsers(this); -} +void User::requestDirectChat() { connection()->requestDirectChat(this); } -void User::unmarkIgnore() -{ - connection()->removeFromIgnoredUsers(this); -} +void User::ignore() { connection()->addToIgnoredUsers(this); } -bool User::isIgnored() const -{ - return connection()->isIgnored(this); -} +void User::unmarkIgnore() { connection()->removeFromIgnoredUsers(this); } -void User::Private::setAvatarOnServer(QString contentUri, User* q) -{ - auto* j = connection->callApi<SetAvatarUrlJob>(userId, contentUri); - connect(j, &BaseJob::success, q, - [=] { q->updateAvatarUrl(contentUri, avatarUrlForRoom(nullptr)); }); -} +bool User::isIgnored() const { return connection()->isIgnored(this); } QString User::displayname(const Room* room) const { if (room) return room->roomMembername(this); - const auto name = d->nameForRoom(nullptr); - return name.isEmpty() ? d->userId : name; + if (auto n = name(); !n.isEmpty()) + return n; + + return d->id; } QString User::fullName(const Room* room) const { - const auto name = d->nameForRoom(room); - return name.isEmpty() ? d->userId : name % " (" % d->userId % ')'; + const auto displayName = name(room); + return displayName.isEmpty() ? id() : (displayName % " (" % id() % ')'); } -QString User::bridged() const +QString User::bridged() const { return {}; } + +/// \sa getBestKnownName, https://github.com/matrix-org/matrix-doc/issues/1375 +QUrl getBestKnownAvatarUrl(const RoomMemberEvent* event) { - return d->bridged; + const auto& jv = event->contentJson().value("avatar_url"_ls); + return !jv.isUndefined() + ? jv.toString() + : event->prevContent() ? event->prevContent()->avatarUrl + : QUrl(); } const Avatar& User::avatarObject(const Room* room) const { - auto it = d->otherAvatar(d->avatarUrlForRoom(room)); - return it != d->otherAvatars.end() ? *it : d->mostUsedAvatar; + if (!room) { + if (!d->defaultAvatar) { + d->fetchProfile(this); + } + return *d->defaultAvatar; + } + + const auto& url = + getBestKnownAvatarUrl(room->getCurrentState<RoomMemberEvent>(id())); + const auto& mediaId = url.authority() + url.path(); + return d->otherAvatars.try_emplace(mediaId, url).first->second; } QImage User::avatar(int dimension, const Room* room) @@ -367,14 +278,16 @@ QImage User::avatar(int dimension, const Room* room) QImage User::avatar(int width, int height, const Room* room) { - return avatar(width, height, room, []{}); + return avatar(width, height, room, [] {}); } QImage User::avatar(int width, int height, const Room* room, const Avatar::get_callback_t& callback) { - return avatarObject(room).get(d->connection, width, height, - [=] { emit avatarChanged(this, room); callback(); }); + return avatarObject(room).get(connection(), width, height, [=] { + emit avatarChanged(this, room); + callback(); + }); } QString User::avatarMediaId(const Room* room) const @@ -392,44 +305,27 @@ void User::processEvent(const RoomMemberEvent& event, const Room* room, { Q_ASSERT(room); - if (firstMention) - ++d->totalRooms; - - if (event.membership() != MembershipType::Invite && - event.membership() != MembershipType::Join) - return; - - auto newName = event.displayName(); - // `bridged` value uses the same notification signal as the name; - // it is assumed that first setting of the bridge occurs together with - // the first setting of the name, and further bridge updates are - // exceptionally rare (the only reasonable case being that the bridge - // changes the naming convention). For the same reason room-specific - // bridge tags are not supported at all. - QRegularExpression reSuffix(QStringLiteral(" \\((IRC|Gitter|Telegram)\\)$")); - auto match = reSuffix.match(newName); - if (match.hasMatch()) - { - if (d->bridged != match.captured(1)) - { - if (!d->bridged.isEmpty()) - qCWarning(MAIN) << "Bridge for user" << id() << "changed:" - << d->bridged << "->" << match.captured(1); - d->bridged = match.captured(1); - } - newName.truncate(match.capturedStart(0)); - } - if (event.prevContent()) - { - // FIXME: the hint doesn't work for bridged users - auto oldNameHint = - d->nameForRoom(room, event.prevContent()->displayName); - updateName(newName, oldNameHint, room); - updateAvatarUrl(event.avatarUrl(), - d->avatarUrlForRoom(room, event.prevContent()->avatarUrl), - room); - } else { - updateName(newName, room); - updateAvatarUrl(event.avatarUrl(), d->avatarUrlForRoom(room), room); + // This is prone to abuse if prevContent is forged; only here until 0.7 + // (and the whole method, actually). + const auto& oldName = event.prevContent() ? event.prevContent()->displayName + : QString(); + const auto& newName = getBestKnownName(&event); + // A hacky way to find out if it's about to change or already changed; + // making it a lambda allows to omit stub event creation when unneeded + const auto& isAboutToChange = [&event, room, this] { + return room->getCurrentState<RoomMemberEvent>(id()) != &event; + }; + if (firstMention || newName != oldName) { + if (isAboutToChange()) + emit nameAboutToChange(newName, oldName, room); + else + emit nameChanged(newName, oldName, room); } + const auto& oldAvatarUrl = + event.prevContent() ? event.prevContent()->avatarUrl : QUrl(); + const auto& newAvatarUrl = getBestKnownAvatarUrl(&event); + if ((firstMention || newAvatarUrl != oldAvatarUrl) && !isAboutToChange()) + emit avatarChanged(this, room); } + +qreal User::hueF() const { return d->hueF; } @@ -13,142 +13,164 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once -#include <QtCore/QString> -#include <QtCore/QObject> #include "avatar.h" -namespace QMatrixClient -{ - class Connection; - class Room; - class RoomMemberEvent; - - class User: public QObject - { - Q_OBJECT - Q_PROPERTY(QString id READ id CONSTANT) - Q_PROPERTY(bool isGuest READ isGuest CONSTANT) - Q_PROPERTY(QString name READ name NOTIFY nameChanged) - Q_PROPERTY(QString displayName READ displayname NOTIFY nameChanged STORED false) - Q_PROPERTY(QString fullName READ fullName NOTIFY nameChanged STORED false) - Q_PROPERTY(QString bridgeName READ bridged NOTIFY nameChanged STORED false) - Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) - Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) - public: - User(QString userId, Connection* connection); - ~User() override; - - Connection* connection() const; - - /** Get unique stable user id - * User id is generated by the server and is not changed ever. - */ - QString id() const; - - /** Get the name chosen by the user - * This may be empty if the user didn't choose the name or cleared - * it. If the user is bridged, the bridge postfix (such as '(IRC)') - * is stripped out. No disambiguation for the room is done. - * \sa displayName, rawName - */ - QString name(const Room* room = nullptr) const; - - /** Get the user name along with the bridge postfix - * This function is similar to name() but appends the bridge postfix - * (such as '(IRC)') to the user name. No disambiguation is done. - * \sa name, displayName - */ - QString rawName(const Room* room = nullptr) const; - - /** Get the displayed user name - * When \p room is null, this method returns result of name() if - * the name is non-empty; otherwise it returns user id. - * When \p room is non-null, this call is equivalent to - * Room::roomMembername invocation for the user (i.e. the user's - * disambiguated room-specific name is returned). - * \sa name, id, fullName, Room::roomMembername - */ - QString displayname(const Room* room = nullptr) const; - - /** Get user name and id in one string - * The constructed string follows the format 'name (id)' - * which the spec recommends for users disambiguation in - * a room context and in other places. - * \sa displayName, Room::roomMembername - */ - QString fullName(const Room* room = nullptr) const; - - /** - * Returns the name of bridge the user is connected from or empty. - */ - QString bridged() const; - - /** Whether the user is a guest - * As of now, the function relies on the convention used in Synapse - * that guests and only guests have all-numeric IDs. This may or - * may not work with non-Synapse servers. - */ - bool isGuest() const; - - const Avatar& avatarObject(const Room* room = nullptr) const; - Q_INVOKABLE QImage avatar(int dimension, const Room* room = nullptr); - Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight, - const Room* room = nullptr); - QImage avatar(int width, int height, const Room* room, - const Avatar::get_callback_t& callback); - - QString avatarMediaId(const Room* room = nullptr) const; - QUrl avatarUrl(const Room* room = nullptr) const; - - /// This method is for internal use and should not be called - /// from client code - // FIXME: Move it away to private in lib 0.6 - void processEvent(const RoomMemberEvent& event, const Room* r, - bool firstMention); - - public slots: - /** Set a new name in the global user profile */ - void rename(const QString& newName); - /** Set a new name for the user in one room */ - void rename(const QString& newName, const Room* r); - /** Upload the file and use it as an avatar */ - bool setAvatar(const QString& fileName); - /** Upload contents of the QIODevice and set that as an avatar */ - bool setAvatar(QIODevice* source); - /** Create or find a direct chat with this user - * The resulting chat is returned asynchronously via - * Connection::directChatAvailable() - */ - void requestDirectChat(); - /** Add the user to the ignore list */ - void ignore(); - /** Remove the user from the ignore list */ - void unmarkIgnore(); - /** Check whether the user is in ignore list */ - bool isIgnored() const; - - signals: - void nameAboutToChange(QString newName, QString oldName, - const Room* roomContext); - void nameChanged(QString newName, QString oldName, - const Room* roomContext); - void avatarChanged(User* user, const Room* roomContext); - - private slots: - void updateName(const QString& newName, const Room* room = nullptr); - void updateName(const QString& newName, const QString& oldName, - const Room* room = nullptr); - void updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl, - const Room* room = nullptr); - - private: - class Private; - QScopedPointer<Private> d; - }; -} -Q_DECLARE_METATYPE(QMatrixClient::User*) +#include <QtCore/QObject> + +namespace Quotient { +class Connection; +class Room; +class RoomMemberEvent; + +class User : public QObject { + Q_OBJECT + Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(bool isGuest READ isGuest CONSTANT) + Q_PROPERTY(int hue READ hue CONSTANT) + Q_PROPERTY(qreal hueF READ hueF CONSTANT) + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString displayName READ displayname NOTIFY nameChanged STORED false) + Q_PROPERTY(QString fullName READ fullName NOTIFY nameChanged STORED false) + Q_PROPERTY(QString bridgeName READ bridged NOTIFY nameChanged STORED false) + Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged + STORED false) + Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) +public: + User(QString userId, Connection* connection); + ~User() override; + + Connection* connection() const; + + /** Get unique stable user id + * User id is generated by the server and is not changed ever. + */ + QString id() const; + + /** Get the name chosen by the user + * This may be empty if the user didn't choose the name or cleared + * it. If the user is bridged, the bridge postfix (such as '(IRC)') + * is stripped out. No disambiguation for the room is done. + * \sa displayName, rawName + */ + QString name(const Room* room = nullptr) const; + + /** Get the user name along with the bridge postfix + * This function is similar to name() but appends the bridge postfix + * (such as '(IRC)') to the user name. No disambiguation is done. + * \sa name, displayName + */ + [[deprecated("Bridge postfixes exist no more, use name() instead")]] + QString rawName(const Room* room = nullptr) const; + + /** Get the displayed user name + * When \p room is null, this method returns result of name() if + * the name is non-empty; otherwise it returns user id. + * When \p room is non-null, this call is equivalent to + * Room::roomMembername invocation for the user (i.e. the user's + * disambiguated room-specific name is returned). + * \sa name, id, fullName, Room::roomMembername + */ + QString displayname(const Room* room = nullptr) const; + + /** Get user name and id in one string + * The constructed string follows the format 'name (id)' + * which the spec recommends for users disambiguation in + * a room context and in other places. + * \sa displayName, Room::roomMembername + */ + QString fullName(const Room* room = nullptr) const; + + /** + * Returns the name of bridge the user is connected from or empty. + */ + [[deprecated("Bridged status is no more supported; this always returns" + " an empty string")]] + QString bridged() const; + + /** Whether the user is a guest + * As of now, the function relies on the convention used in Synapse + * that guests and only guests have all-numeric IDs. This may or + * may not work with non-Synapse servers. + */ + bool isGuest() const; + + /** Hue color component of this user based on id. + * The implementation is based on XEP-0392: + * https://xmpp.org/extensions/xep-0392.html + * Naming and ranges are the same as QColor's hue methods: + * https://doc.qt.io/qt-5/qcolor.html#integer-vs-floating-point-precision + */ + int hue() const; + qreal hueF() const; + + /// Get a reference to a user avatar object for a given room + /*! This reference should be considered short-lived: processing the next + * room member event for this user may (or may not) invalidate it. + */ + const Avatar& avatarObject(const Room* room = nullptr) const; + Q_INVOKABLE QImage avatar(int dimension, + const Quotient::Room* room = nullptr); + Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight, + const Quotient::Room* room = nullptr); + QImage avatar(int width, int height, const Room* room, + const Avatar::get_callback_t& callback); + + QString avatarMediaId(const Room* room = nullptr) const; + QUrl avatarUrl(const Room* room = nullptr) const; + + // TODO: This method is only there to emit obsolete signals: + // nameAboutToChange(), nameChanged() and avatarChanged() - all of these + // to be removed in 0.7 + /// \deprecated + void processEvent(const RoomMemberEvent& event, const Room* r, + bool firstMention); + +public slots: + /// Set a new name in the global user profile + void rename(const QString& newName); + /// Set a new name for the user in one room + void rename(const QString& newName, const Room* r); + /// Upload the file and use it as an avatar + bool setAvatar(const QString& fileName); + /// Upload contents of the QIODevice and set that as an avatar + bool setAvatar(QIODevice* source); + /// Create or find a direct chat with this user + /*! The resulting chat is returned asynchronously via + * Connection::directChatAvailable() + */ + void requestDirectChat(); + /// Add the user to the ignore list + void ignore(); + /// Remove the user from the ignore list + void unmarkIgnore(); + /// Check whether the user is in ignore list + bool isIgnored() const; + +signals: + /// \deprecated Use Room::memberListChanged() for member changes + void nameAboutToChange(QString newName, QString oldName, + const Quotient::Room* roomContext); + /// \deprecated Use Room::memberListChanged() for member changes + void nameChanged(QString newName, QString oldName, + const Quotient::Room* roomContext); + /// \deprecated Use Room::memberListChanged() for member changes + void avatarChanged(Quotient::User* user, const Quotient::Room* roomContext); + +private slots: // TODO: remove in 0.7 + /// \deprecated + void updateName(const QString& newName, const Room* r = nullptr); + /// \deprecated + void updateName(const QString&, const QString&, const Room* = nullptr); + /// \deprecated + void updateAvatarUrl(const QUrl&, const QUrl&, const Room* = nullptr); + +private: + class Private; + QScopedPointer<Private> d; +}; +} // namespace Quotient diff --git a/lib/util.cpp b/lib/util.cpp index 81862ab6..875d7522 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -13,153 +13,154 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "util.h" +#include <QtCore/QCryptographicHash> +#include <QtCore/QDataStream> +#include <QtCore/QDir> #include <QtCore/QRegularExpression> #include <QtCore/QStandardPaths> -#include <QtCore/QDir> #include <QtCore/QStringBuilder> +#include <QtCore/QtEndian> static const auto RegExpOptions = QRegularExpression::CaseInsensitiveOption - | QRegularExpression::OptimizeOnFirstUsageOption +#if QT_VERSION < QT_VERSION_CHECK(5, 12, 0) + | QRegularExpression::OptimizeOnFirstUsageOption // Default since 5.12 +#endif | QRegularExpression::UseUnicodePropertiesOption; // Converts all that looks like a URL into HTML links -static void linkifyUrls(QString& htmlEscapedText) +void Quotient::linkifyUrls(QString& htmlEscapedText) { + // Note: outer parentheses are a part of C++ raw string delimiters, not of + // the regex (see http://en.cppreference.com/w/cpp/language/string_literal). + // Note2: the next-outer parentheses are \N in the replacement. + + // generic url: // regexp is originally taken from Konsole (https://github.com/KDE/konsole) - // full url: // protocolname:// or www. followed by anything other than whitespaces, // <, >, ' or ", and ends before whitespaces, <, >, ', ", ], !, ), :, // comma or dot - // Note: outer parentheses are a part of C++ raw string delimiters, not of - // the regex (see http://en.cppreference.com/w/cpp/language/string_literal). - // Note2: the next-outer parentheses are \N in the replacement. - static const QRegularExpression FullUrlRegExp(QStringLiteral( - R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet)://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))" - ), RegExpOptions); + static const QRegularExpression FullUrlRegExp( + QStringLiteral( + R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet|matrix):(//)?)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"), + RegExpOptions); // email address: // [word chars, dots or dashes]@[word chars, dots or dashes].[word chars] - static const QRegularExpression EmailAddressRegExp(QStringLiteral( - R"(\b(mailto:)?((\w|\.|-)+@(\w|\.|-)+\.\w+\b))" - ), RegExpOptions); + static const QRegularExpression EmailAddressRegExp( + QStringLiteral(R"(\b(mailto:)?((\w|\.|-)+@(\w|\.|-)+\.\w+\b))"), + RegExpOptions); // An interim liberal implementation of // https://matrix.org/docs/spec/appendices.html#identifier-grammar - static const QRegularExpression MxIdRegExp(QStringLiteral( - R"((^|[^<>/])([!#@][-a-z0-9_=#/.]{1,252}:(?:\w|\.|-)+\.\w+(?::\d{1,5})?))" - ), RegExpOptions); + static const QRegularExpression MxIdRegExp( + QStringLiteral( + R"((^|[^<>/])([!#@][-a-z0-9_=#/.]{1,252}:(?:\w|\.|-)+\.\w+(?::\d{1,5})?))"), + RegExpOptions); + Q_ASSERT(FullUrlRegExp.isValid() && EmailAddressRegExp.isValid() + && MxIdRegExp.isValid()); // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,&," htmlEscapedText.replace(EmailAddressRegExp, - QStringLiteral(R"(<a href="mailto:\2">\1\2</a>)")); + QStringLiteral(R"(<a href="mailto:\2">\1\2</a>)")); htmlEscapedText.replace(FullUrlRegExp, - QStringLiteral(R"(<a href="\1">\1</a>)")); - htmlEscapedText.replace(MxIdRegExp, - QStringLiteral(R"(\1<a href="https://matrix.to/#/\2">\2</a>)")); + QStringLiteral(R"(<a href="\1">\1</a>)")); + htmlEscapedText.replace( + MxIdRegExp, + QStringLiteral(R"(\1<a href="https://matrix.to/#/\2">\2</a>)")); } -QString QMatrixClient::sanitized(const QString& plainText) +QString Quotient::sanitized(const QString& plainText) { auto text = plainText; - text.remove(QChar(0x202e)); - text.remove(QChar(0x202d)); + text.remove(QChar(0x202e)); // RLO + text.remove(QChar(0x202d)); // LRO + text.remove(QChar(0xfffc)); // Object replacement character return text; } -QString QMatrixClient::prettyPrint(const QString& plainText) +QString Quotient::prettyPrint(const QString& plainText) { - auto pt = QStringLiteral("<span style='white-space:pre-wrap'>") + - plainText.toHtmlEscaped() + QStringLiteral("</span>"); - pt.replace('\n', QStringLiteral("<br/>")); - + auto pt = plainText.toHtmlEscaped(); linkifyUrls(pt); - return pt; + pt.replace('\n', QStringLiteral("<br/>")); + return QStringLiteral("<span style='white-space:pre-wrap'>") + pt + + QStringLiteral("</span>"); } -QString QMatrixClient::cacheLocation(const QString& dirName) +QString Quotient::cacheLocation(const QString& dirName) { const QString cachePath = - QStandardPaths::writableLocation(QStandardPaths::CacheLocation) - % '/' % dirName % '/'; + QStandardPaths::writableLocation(QStandardPaths::CacheLocation) % '/' + % dirName % '/'; QDir dir; if (!dir.exists(cachePath)) dir.mkpath(cachePath); return cachePath; } +qreal Quotient::stringToHueF(const QString& s) +{ + Q_ASSERT(!s.isEmpty()); + QByteArray hash = QCryptographicHash::hash(s.toUtf8(), + QCryptographicHash::Sha1); + QDataStream dataStream(hash.left(2)); + dataStream.setByteOrder(QDataStream::LittleEndian); + quint16 hashValue; + dataStream >> hashValue; + const auto hueF = qreal(hashValue) / std::numeric_limits<quint16>::max(); + Q_ASSERT((0 <= hueF) && (hueF <= 1)); + return hueF; +} + +static const auto ServerPartRegEx = QStringLiteral( + "(\\[[^][:blank:]]+\\]|[-[:alnum:].]+)" // Either IPv6 address or hostname/IPv4 address + "(?::(\\d{1,5}))?" // Optional port +); + +QString Quotient::serverPart(const QString& mxId) +{ + static QString re = "^[@!#$+].*?:(" // Localpart and colon + % ServerPartRegEx % ")$"; + static QRegularExpression parser( + re, + QRegularExpression::UseUnicodePropertiesOption); // Because Asian digits + Q_ASSERT(parser.isValid()); + return parser.match(mxId).captured(1); +} + // Tests for function_traits<> -#ifdef Q_CC_CLANG -#pragma clang diagnostic push -#pragma ide diagnostic ignored "OCSimplifyInspection" -#endif -using namespace QMatrixClient; +using namespace Quotient; -int f(); -static_assert(std::is_same<fn_return_t<decltype(f)>, int>::value, +int f_(); +static_assert(std::is_same<fn_return_t<decltype(f_)>, int>::value, "Test fn_return_t<>"); -void f1(int); -static_assert(function_traits<decltype(f1)>::arg_number == 1, - "Test fn_arg_number"); - -void f2(int, QString); -static_assert(std::is_same<fn_arg_t<decltype(f2), 1>, QString>::value, +void f1_(int, QString); +static_assert(std::is_same<fn_arg_t<decltype(f1_), 1>, QString>::value, "Test fn_arg_t<>"); -struct S { int mf(); }; -static_assert(is_callable_v<decltype(&S::mf)>, "Test member function"); -static_assert(returns<int, decltype(&S::mf)>(), "Test returns<> with member function"); - -struct Fo { int operator()(); }; -static_assert(is_callable_v<Fo>, "Test is_callable<> with function object"); -static_assert(function_traits<Fo>::arg_number == 0, "Test function object"); +struct Fo { + int operator()(); + static constexpr auto l = [] { return 0.0f; }; +}; static_assert(std::is_same<fn_return_t<Fo>, int>::value, "Test return type of function object"); +static_assert(std::is_same<fn_return_t<decltype(Fo::l)>, float>::value, + "Test return type of lambda"); -struct Fo1 { void operator()(int); }; -static_assert(function_traits<Fo1>::arg_number == 1, "Test function object 1"); -static_assert(is_callable_v<Fo1>, "Test is_callable<> with function object 1"); +struct Fo1 { + void operator()(int); +}; static_assert(std::is_same<fn_arg_t<Fo1>, int>(), "Test fn_arg_t defaulting to first argument"); -#if (!defined(_MSC_VER) || _MSC_VER >= 1910) -static auto l = [] { return 1; }; -static_assert(is_callable_v<decltype(l)>, "Test is_callable_v<> with lambda"); -static_assert(std::is_same<fn_return_t<decltype(l)>, int>::value, - "Test fn_return_t<> with lambda"); -#endif - template <typename T> -struct fn_object -{ - static int smf(double) { return 0; } -}; -template <> -struct fn_object<QString> -{ - void operator()(QString); -}; -static_assert(is_callable_v<fn_object<QString>>, "Test function object"); -static_assert(returns<void, fn_object<QString>>(), - "Test returns<> with function object"); -static_assert(!is_callable_v<fn_object<int>>, "Test non-function object"); -// FIXME: These two don't work -//static_assert(is_callable_v<decltype(&fn_object<int>::smf)>, -// "Test static member function"); -//static_assert(returns<int, decltype(&fn_object<int>::smf)>(), -// "Test returns<> with static member function"); - -template <typename T> -QString ft(T&&) { return {}; } +static QString ft(T&&); static_assert(std::is_same<fn_arg_t<decltype(ft<QString>)>, QString&&>(), "Test function templates"); - -#ifdef Q_CC_CLANG -#pragma clang diagnostic pop -#endif @@ -13,303 +13,288 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include <QtCore/QLatin1String> - -#if QT_VERSION < QT_VERSION_CHECK(5, 5, 0) -#include <QtCore/QMetaEnum> -#include <QtCore/QDebug> -#endif +#include <QtCore/QHashFunctions> #include <functional> #include <memory> +#include <unordered_map> +#include <optional> -#if __has_cpp_attribute(fallthrough) -#define FALLTHROUGH [[fallthrough]] -#elif __has_cpp_attribute(clang::fallthrough) -#define FALLTHROUGH [[clang::fallthrough]] -#elif __has_cpp_attribute(gnu::fallthrough) -#define FALLTHROUGH [[gnu::fallthrough]] -#else -#define FALLTHROUGH // -fallthrough -#endif - -// Along the lines of Q_DISABLE_COPY -#define DISABLE_MOVE(_ClassName) \ +// Along the lines of Q_DISABLE_COPY - the upstream version comes in Qt 5.13 +#define DISABLE_MOVE(_ClassName) \ _ClassName(_ClassName&&) Q_DECL_EQ_DELETE; \ _ClassName& operator=(_ClassName&&) Q_DECL_EQ_DELETE; -#if QT_VERSION < QT_VERSION_CHECK(5, 7, 0) -// Copy-pasted from Qt 5.10 -template <typename T> -Q_DECL_CONSTEXPR typename std::add_const<T>::type &qAsConst(T &t) Q_DECL_NOTHROW { return t; } -// prevent rvalue arguments: +namespace Quotient { +/// An equivalent of std::hash for QTypes to enable std::unordered_map<QType, ...> template <typename T> -static void qAsConst(const T &&) Q_DECL_EQ_DELETE; -#endif - -// MSVC 2015 and older GCC's don't handle initialisation from initializer lists -// right in the absense of a constructor; MSVC 2015, notably, fails with -// "error C2440: 'return': cannot convert from 'initializer list' to '<type>'" -#if (defined(_MSC_VER) && _MSC_VER < 1910) || \ - (defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 4) -# define BROKEN_INITIALIZER_LISTS -#endif - -namespace QMatrixClient -{ - // The below enables pretty-printing of enums in logs -#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) -#define REGISTER_ENUM(EnumName) Q_ENUM(EnumName) -#else - // Thanks to Olivier for spelling it and for making Q_ENUM to replace it: - // https://woboq.com/blog/q_enum.html -#define REGISTER_ENUM(EnumName) \ - Q_ENUMS(EnumName) \ - friend QDebug operator<<(QDebug dbg, EnumName val) \ - { \ - static int enumIdx = staticMetaObject.indexOfEnumerator(#EnumName); \ - return dbg << Event::staticMetaObject.enumerator(enumIdx).valueToKey(int(val)); \ - } -#endif - - /** static_cast<> for unique_ptr's */ - template <typename T1, typename PtrT2> - inline auto unique_ptr_cast(PtrT2&& p) +struct HashQ { + size_t operator()(const T& s) const Q_DECL_NOEXCEPT { - return std::unique_ptr<T1>(static_cast<T1*>(p.release())); + return qHash(s, uint(qGlobalQHashSeed())); } +}; +/// A wrapper around std::unordered_map compatible with types that have qHash +template <typename KeyT, typename ValT> +using UnorderedMap = std::unordered_map<KeyT, ValT, HashQ<KeyT>>; - struct NoneTag {}; - constexpr NoneTag none {}; +constexpr auto none = std::nullopt; - /** A crude substitute for `optional` while we're not C++17 - * - * Only works with default-constructible types. - */ - template <typename T> - class Omittable +/** `std::optional` with tweaks + * + * The tweaks are: + * - streamlined assignment (operator=)/emplace()ment of values that can be + * used to implicitly construct the underlying type, including + * direct-list-initialisation, e.g.: + * \code + * struct S { int a; char b; } + * Omittable<S> o; + * o = { 1, 'a' }; // std::optional would require o = S { 1, 'a' } + * \endcode + * - entirely deleted value(). The technical reason is that Xcode 10 doesn't + * have it; but besides that, value_or() or (after explicit checking) + * `operator*()`/`operator->()` are better alternatives within Quotient + * that doesn't practice throwing exceptions (as doesn't most of Qt). + * - disabled non-const lvalue operator*() and operator->(), as it's too easy + * to inadvertently cause a value change through them. + * - edit() to provide a safe and explicit lvalue accessor instead of those + * above. Requires the underlying type to be default-constructible. + * Allows chained initialisation of nested Omittables: + * \code + * struct Inner { int member = 10; Omittable<int> innermost; }; + * struct Outer { int anotherMember = 10; Omittable<Inner> inner; }; + * Omittable<Outer> o; // = { 10, std::nullopt }; + * o.edit().inner.edit().innermost.emplace(42); + * \endcode + * - merge() - a soft version of operator= that only overwrites its first + * operand with the second one if the second one is not empty. + */ +template <typename T> +class Omittable : public std::optional<T> { +public: + using base_type = std::optional<T>; + using value_type = std::decay_t<T>; + + using std::optional<T>::optional; + + // Overload emplace() and operator=() to allow passing braced-init-lists + // (the standard emplace() does direct-initialisation but + // not direct-list-initialisation). + using base_type::operator=; + Omittable& operator=(const value_type& v) { - static_assert(!std::is_reference<T>::value, - "You cannot make an Omittable<> with a reference type"); - public: - using value_type = std::decay_t<T>; - - explicit Omittable() : Omittable(none) { } - Omittable(NoneTag) : _value(value_type()), _omitted(true) { } - Omittable(const value_type& val) : _value(val) { } - Omittable(value_type&& val) : _value(std::move(val)) { } - Omittable<T>& operator=(const value_type& val) - { - _value = val; - _omitted = false; - return *this; - } - Omittable<T>& operator=(value_type&& val) - { - // For some reason GCC complains about -Wmaybe-uninitialized - // in the context of using Omittable<bool> with converters.h; - // though the logic looks very much benign (GCC bug???) - _value = std::move(val); - _omitted = false; - return *this; - } - - bool operator==(const value_type& rhs) const - { - return !omitted() && value() == rhs; - } - friend bool operator==(const value_type& lhs, - const Omittable<value_type>& rhs) - { - return rhs == lhs; - } - bool operator!=(const value_type& rhs) const - { - return !operator==(rhs); - } - friend bool operator!=(const value_type& lhs, - const Omittable<value_type>& rhs) - { - return !(rhs == lhs); - } - - bool omitted() const { return _omitted; } - const value_type& value() const - { - Q_ASSERT(!_omitted); - return _value; - } - value_type& editValue() - { - _omitted = false; - return _value; - } - /// Merge the value from another Omittable - /// \return true if \p other is not omitted and the value of - /// the current Omittable was different (or omitted); - /// in other words, if the current Omittable has changed; - /// false otherwise - template <typename T1> - auto merge(const Omittable<T1>& other) - -> std::enable_if_t<std::is_convertible<T1, T>::value, bool> - { - if (other.omitted() || - (!_omitted && _value == other.value())) - return false; - _omitted = false; - _value = other.value(); - return true; - } - value_type&& release() { _omitted = true; return std::move(_value); } - - const value_type* operator->() const & { return &value(); } - value_type* operator->() & { return &editValue(); } - const value_type& operator*() const & { return value(); } - value_type& operator*() & { return editValue(); } - - private: - T _value; - bool _omitted = false; - }; - - namespace _impl { - template <typename AlwaysVoid, typename> struct fn_traits; + base_type::operator=(v); + return *this; + } + Omittable& operator=(value_type&& v) + { + base_type::operator=(v); + return *this; + } + using base_type::emplace; + T& emplace(const T& val) { return base_type::emplace(val); } + T& emplace(T&& val) { return base_type::emplace(std::move(val)); } + + // use value_or() or check (with operator! or has_value) before accessing + // with operator-> or operator* + // The technical reason is that Xcode 10 has incomplete std::optional + // that has no value(); but using value() may also mean that you rely + // on the optional throwing an exception (which is not assumed practice + // throughout Quotient) or that you spend unnecessary CPU cycles on + // an extraneous has_value() check. + value_type& value() = delete; + const value_type& value() const = delete; + value_type& edit() + { + return this->has_value() ? base_type::operator*() : this->emplace(); } - /** Determine traits of an arbitrary function/lambda/functor - * Doesn't work with generic lambdas and function objects that have - * operator() overloaded. - * \sa https://stackoverflow.com/questions/7943525/is-it-possible-to-figure-out-the-parameter-type-and-return-type-of-a-lambda#7943765 - */ - template <typename T> - struct function_traits : public _impl::fn_traits<void, T> {}; - - // Specialisation for a function - template <typename ReturnT, typename... ArgTs> - struct function_traits<ReturnT(ArgTs...)> + [[deprecated("Use '!o' or '!o.has_value()' instead of 'o.omitted()'")]] + bool omitted() const { - static constexpr auto is_callable = true; - using return_type = ReturnT; - using arg_types = std::tuple<ArgTs...>; - using function_type = std::function<ReturnT(ArgTs...)>; - static constexpr auto arg_number = std::tuple_size<arg_types>::value; - }; - - namespace _impl { - template <typename AlwaysVoid, typename T> - struct fn_traits - { - static constexpr auto is_callable = false; - }; - - template <typename T> - struct fn_traits<decltype(void(&T::operator())), T> - : public fn_traits<void, decltype(&T::operator())> - { }; // A generic function object that has (non-overloaded) operator() - - // Specialisation for a member function - template <typename ReturnT, typename ClassT, typename... ArgTs> - struct fn_traits<void, ReturnT(ClassT::*)(ArgTs...)> - : function_traits<ReturnT(ArgTs...)> - { }; - - // Specialisation for a const member function - template <typename ReturnT, typename ClassT, typename... ArgTs> - struct fn_traits<void, ReturnT(ClassT::*)(ArgTs...) const> - : function_traits<ReturnT(ArgTs...)> - { }; - } // namespace _impl - - template <typename FnT> - using fn_return_t = typename function_traits<FnT>::return_type; - - template <typename FnT, int ArgN = 0> - using fn_arg_t = - std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>; - - template <typename R, typename FnT> - constexpr bool returns() + return !this->has_value(); + } + + /// Merge the value from another Omittable + /// \return true if \p other is not omitted and the value of + /// the current Omittable was different (or omitted); + /// in other words, if the current Omittable has changed; + /// false otherwise + template <typename T1> + auto merge(const Omittable<T1>& other) + -> std::enable_if_t<std::is_convertible<T1, T>::value, bool> { - return std::is_same<fn_return_t<FnT>, R>::value; + if (!other || (this->has_value() && **this == *other)) + return false; + *this = other; + return true; } - // Poor-man's is_invokable + // Hide non-const lvalue operator-> and operator* as these are + // a bit too surprising: value() & doesn't lazy-create an object; + // and it's too easy to inadvertently change the underlying value. + + const value_type* operator->() const& { return base_type::operator->(); } + value_type* operator->() && { return base_type::operator->(); } + const value_type& operator*() const& { return base_type::operator*(); } + value_type& operator*() && { return base_type::operator*(); } +}; + +namespace _impl { + template <typename AlwaysVoid, typename> + struct fn_traits; +} + +/// Determine traits of an arbitrary function/lambda/functor +/*! + * Doesn't work with generic lambdas and function objects that have + * operator() overloaded. + * \sa + * https://stackoverflow.com/questions/7943525/is-it-possible-to-figure-out-the-parameter-type-and-return-type-of-a-lambda#7943765 + */ +template <typename T> +struct function_traits + : public _impl::fn_traits<void, std::remove_reference_t<T>> {}; + +// Specialisation for a function +template <typename ReturnT, typename... ArgTs> +struct function_traits<ReturnT(ArgTs...)> { + using return_type = ReturnT; + using arg_types = std::tuple<ArgTs...>; + // Doesn't (and there's no plan to make it) work for "classic" + // member functions (i.e. outside of functors). + // See also the comment for wrap_in_function() below + using function_type = std::function<ReturnT(ArgTs...)>; +}; + +namespace _impl { + // Specialisation for function objects with (non-overloaded) operator() + // (this includes non-generic lambdas) template <typename T> - constexpr auto is_callable_v = function_traits<T>::is_callable; + struct fn_traits<decltype(void(&T::operator())), T> + : public fn_traits<void, decltype(&T::operator())> {}; + + // Specialisation for a member function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...)> + : function_traits<ReturnT(ArgTs...)> {}; + + // Specialisation for a const member function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...) const> + : function_traits<ReturnT(ArgTs...)> {}; +} // namespace _impl + +template <typename FnT> +using fn_return_t = typename function_traits<FnT>::return_type; + +template <typename FnT, int ArgN = 0> +using fn_arg_t = + std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>; + +// TODO: get rid of it as soon as Apple Clang gets proper deduction guides +// for std::function<> +// ...or consider using QtPrivate magic used by QObject::connect() +// since wrap_in_function() is actually made for qt_connection_util.h +// ...for inspiration, also check a possible std::not_fn implementation at +// https://en.cppreference.com/w/cpp/utility/functional/not_fn +template <typename FnT> +inline auto wrap_in_function(FnT&& f) +{ + return typename function_traits<FnT>::function_type(std::forward<FnT>(f)); +} + +inline auto operator"" _ls(const char* s, std::size_t size) +{ + return QLatin1String(s, int(size)); +} - inline auto operator"" _ls(const char* s, std::size_t size) +/** An abstraction over a pair of iterators + * This is a very basic range type over a container with iterators that + * are at least ForwardIterators. Inspired by Ranges TS. + */ +template <typename ArrayT> +class Range { + // Looking forward to C++20 ranges + using iterator = typename ArrayT::iterator; + using const_iterator = typename ArrayT::const_iterator; + using size_type = typename ArrayT::size_type; + +public: + constexpr Range(ArrayT& arr) : from(std::begin(arr)), to(std::end(arr)) {} + constexpr Range(iterator from, iterator to) : from(from), to(to) {} + + constexpr size_type size() const { - return QLatin1String(s, int(size)); + Q_ASSERT(std::distance(from, to) >= 0); + return size_type(std::distance(from, to)); } + constexpr bool empty() const { return from == to; } + constexpr const_iterator begin() const { return from; } + constexpr const_iterator end() const { return to; } + constexpr iterator begin() { return from; } + constexpr iterator end() { return to; } + +private: + iterator from; + iterator to; +}; + +/** A replica of std::find_first_of that returns a pair of iterators + * + * Convenient for cases when you need to know which particular "first of" + * [sFirst, sLast) has been found in [first, last). + */ +template <typename InputIt, typename ForwardIt, typename Pred> +inline std::pair<InputIt, ForwardIt> findFirstOf(InputIt first, InputIt last, + ForwardIt sFirst, + ForwardIt sLast, Pred pred) +{ + for (; first != last; ++first) + for (auto it = sFirst; it != sLast; ++it) + if (pred(*first, *it)) + return std::make_pair(first, it); - /** An abstraction over a pair of iterators - * This is a very basic range type over a container with iterators that - * are at least ForwardIterators. Inspired by Ranges TS. - */ - template <typename ArrayT> - class Range - { - // Looking forward for Ranges TS to produce something (in C++23?..) - using iterator = typename ArrayT::iterator; - using const_iterator = typename ArrayT::const_iterator; - using size_type = typename ArrayT::size_type; - public: - Range(ArrayT& arr) : from(std::begin(arr)), to(std::end(arr)) { } - Range(iterator from, iterator to) : from(from), to(to) { } - - size_type size() const - { - Q_ASSERT(std::distance(from, to) >= 0); - return size_type(std::distance(from, to)); - } - bool empty() const { return from == to; } - const_iterator begin() const { return from; } - const_iterator end() const { return to; } - iterator begin() { return from; } - iterator end() { return to; } - - private: - iterator from; - iterator to; - }; - - /** A replica of std::find_first_of that returns a pair of iterators - * - * Convenient for cases when you need to know which particular "first of" - * [sFirst, sLast) has been found in [first, last). - */ - template<typename InputIt, typename ForwardIt, typename Pred> - inline std::pair<InputIt, ForwardIt> findFirstOf( - InputIt first, InputIt last, ForwardIt sFirst, ForwardIt sLast, - Pred pred) - { - for (; first != last; ++first) - for (auto it = sFirst; it != sLast; ++it) - if (pred(*first, *it)) - return std::make_pair(first, it); + return std::make_pair(last, sLast); +} - return std::make_pair(last, sLast); - } +/** Convert what looks like a URL or a Matrix ID to an HTML hyperlink */ +void linkifyUrls(QString& htmlEscapedText); - /** Sanitize the text before showing in HTML - * This does toHtmlEscaped() and removes Unicode BiDi marks. - */ - QString sanitized(const QString& plainText); +/** Sanitize the text before showing in HTML + * + * This does toHtmlEscaped() and removes Unicode BiDi marks. + */ +QString sanitized(const QString& plainText); - /** Pretty-print plain text into HTML - * This includes HTML escaping of <,>,",& and URLs linkification. - */ - QString prettyPrint(const QString& plainText); +/** Pretty-print plain text into HTML + * + * This includes HTML escaping of <,>,",& and calling linkifyUrls() + */ +QString prettyPrint(const QString& plainText); - /** Return a path to cache directory after making sure that it exists - * The returned path has a trailing slash, clients don't need to append it. - * \param dir path to cache directory relative to the standard cache path - */ - QString cacheLocation(const QString& dirName); -} // namespace QMatrixClient +/** Return a path to cache directory after making sure that it exists + * + * The returned path has a trailing slash, clients don't need to append it. + * \param dir path to cache directory relative to the standard cache path + */ +QString cacheLocation(const QString& dirName); + +/** Hue color component of based of the hash of the string. + * + * The implementation is based on XEP-0392: + * https://xmpp.org/extensions/xep-0392.html + * Naming and range are the same as QColor's hueF method: + * https://doc.qt.io/qt-5/qcolor.html#integer-vs-floating-point-precision + */ +qreal stringToHueF(const QString& s); +/** Extract the serverpart from MXID */ +QString serverPart(const QString& mxId); +} // namespace Quotient |