diff options
author | arawaaa <77910862+arawaaa@users.noreply.github.com> | 2021-12-27 17:35:28 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-12-27 17:35:28 -0600 |
commit | 7ec3ba834dd313c4408622da30e04cdc6f4cf7c7 (patch) | |
tree | 679d7dc5939a229ad46676cd1d7aeaea7a25abce /lib | |
parent | 17bf4d180297c7e87363e179b8afa79ddb15dca7 (diff) | |
parent | 674e984e459375974f619d0e778d43a2cc928dc3 (diff) | |
download | libquotient-7ec3ba834dd313c4408622da30e04cdc6f4cf7c7.tar.gz libquotient-7ec3ba834dd313c4408622da30e04cdc6f4cf7c7.zip |
Merge branch 'dev' into pinned
Diffstat (limited to 'lib')
196 files changed, 4277 insertions, 2651 deletions
diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp new file mode 100644 index 00000000..a292ed45 --- /dev/null +++ b/lib/accountregistry.cpp @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "accountregistry.h" + +#include "connection.h" + +using namespace Quotient; + +void AccountRegistry::add(Connection* c) +{ + if (m_accounts.contains(c)) + return; + beginInsertRows(QModelIndex(), m_accounts.size(), m_accounts.size()); + m_accounts += c; + endInsertRows(); +} + +void AccountRegistry::drop(Connection* c) +{ + beginRemoveRows(QModelIndex(), m_accounts.indexOf(c), m_accounts.indexOf(c)); + m_accounts.removeOne(c); + endRemoveRows(); + Q_ASSERT(!m_accounts.contains(c)); +} + +bool AccountRegistry::isLoggedIn(const QString &userId) const +{ + return std::any_of(m_accounts.cbegin(), m_accounts.cend(), + [&userId](Connection* a) { return a->userId() == userId; }); +} + +bool AccountRegistry::contains(Connection *c) const +{ + return m_accounts.contains(c); +} + +AccountRegistry::AccountRegistry() = default; + +QVariant AccountRegistry::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + if (index.row() >= m_accounts.count()) { + return {}; + } + + auto account = m_accounts[index.row()]; + + if (role == ConnectionRole) { + return QVariant::fromValue(account); + } + + return {}; +} + +int AccountRegistry::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_accounts.count(); +} + +QHash<int, QByteArray> AccountRegistry::roleNames() const +{ + return {{ConnectionRole, "connection"}}; +} + +bool AccountRegistry::isEmpty() const +{ + return m_accounts.isEmpty(); +} + +int AccountRegistry::count() const +{ + return m_accounts.count(); +} + +const QVector<Connection*> AccountRegistry::accounts() const +{ + return m_accounts; +} + +Connection* AccountRegistry::get(const QString& userId) +{ + for (const auto &connection : m_accounts) { + if(connection->userId() == userId) { + return connection; + } + } + return nullptr; +} diff --git a/lib/accountregistry.h b/lib/accountregistry.h new file mode 100644 index 00000000..5efda459 --- /dev/null +++ b/lib/accountregistry.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <QtCore/QObject> +#include <QtCore/QList> +#include <QtCore/QAbstractListModel> + +namespace Quotient { +class Connection; + +class AccountRegistry : public QAbstractListModel { + Q_OBJECT +public: + enum EventRoles { + ConnectionRole = Qt::UserRole + 1, + }; + + static AccountRegistry &instance() { + static AccountRegistry _instance; + return _instance; + } + + const QVector<Connection*> accounts() const; + void add(Connection* a); + void drop(Connection* a); + bool isLoggedIn(const QString& userId) const; + bool isEmpty() const; + int count() const; + bool contains(Connection*) const; + Connection* get(const QString& userId); + + [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + [[nodiscard]] QHash<int, QByteArray> roleNames() const override; + +private: + AccountRegistry(); + + QVector<Connection *> m_accounts; +}; +} diff --git a/lib/application-service/definitions/protocol.h b/lib/application-service/definitions/protocol.h index 6aee9c57..213dbf19 100644 --- a/lib/application-service/definitions/protocol.h +++ b/lib/application-service/definitions/protocol.h @@ -40,7 +40,7 @@ struct ProtocolInstance { /// provided at the higher level Protocol object. QString icon; - /// Preset values for ``fields`` the client may use to search by. + /// Preset values for `fields` the client may use to search by. QJsonObject fields; /// A unique identifier across all instances. @@ -81,10 +81,9 @@ struct ThirdPartyProtocol { /// 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. + /// 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; diff --git a/lib/avatar.h b/lib/avatar.h index be125c17..d4634aea 100644 --- a/lib/avatar.h +++ b/lib/avatar.h @@ -21,7 +21,7 @@ public: Avatar& operator=(Avatar&&); using get_callback_t = std::function<void()>; - using upload_callback_t = std::function<void(QString)>; + using upload_callback_t = std::function<void(QUrl)>; QImage get(Connection* connection, int dimension, get_callback_t callback) const; @@ -42,5 +42,3 @@ private: std::unique_ptr<Private> d; }; } // namespace Quotient -/// \deprecated Use namespace Quotient instead -namespace QMatrixClient = Quotient; diff --git a/lib/connection.cpp b/lib/connection.cpp index a384783c..8d1c80f1 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -13,6 +13,10 @@ #include "room.h" #include "settings.h" #include "user.h" +#include "accountregistry.h" + +// NB: since Qt 6, moc_connection.cpp needs Room and User fully defined +#include "moc_connection.cpp" #include "csapi/account-data.h" #include "csapi/capabilities.h" @@ -55,7 +59,7 @@ 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 remove_if(HashT& hashMap, Pred pred) { HashT removals; for (auto it = hashMap.begin(); it != hashMap.end();) { @@ -73,8 +77,6 @@ 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; @@ -255,6 +257,7 @@ Connection::~Connection() { qCDebug(MAIN) << "deconstructing connection object for" << userId(); stopSync(); + AccountRegistry::instance().drop(this); } void Connection::resolveServer(const QString& mxid) @@ -283,7 +286,7 @@ void Connection::resolveServer(const QString& mxid) if (d->resolverJob->error() == BaseJob::Abandoned) return; - if (d->resolverJob->error() != BaseJob::NotFoundError) { + if (d->resolverJob->error() != BaseJob::NotFound) { if (!d->resolverJob->status().good()) { qCWarning(MAIN) << "Fetching .well-known file failed, FAIL_PROMPT"; @@ -311,12 +314,6 @@ void Connection::resolveServer(const QString& mxid) 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")); - }); }); } @@ -338,7 +335,7 @@ void Connection::loginWithPassword(const QString& userId, const QString& initialDeviceName, const QString& deviceId) { - d->checkAndConnect(userId, [=] { + d->checkAndConnect(userId, [=,this] { d->loginToServer(LoginFlows::Password.type, makeUserIdentifier(userId), password, /*token*/ "", deviceId, initialDeviceName); }, LoginFlows::Password); @@ -398,7 +395,7 @@ void Connection::reloadCapabilities() " disabling version upgrade recommendations to reduce noise"; }); connect(d->capabilitiesJob, &BaseJob::failure, this, [this] { - if (d->capabilitiesJob->error() == BaseJob::IncorrectRequestError) + if (d->capabilitiesJob->error() == BaseJob::IncorrectRequest) qCDebug(MAIN) << "Server doesn't support /capabilities;" " version upgrade recommendations won't be issued"; }); @@ -440,6 +437,7 @@ void Connection::Private::completeSetup(const QString& mxId) qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString() << "by user" << data->userId() << "from device" << data->deviceId(); + AccountRegistry::instance().add(q); #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED @@ -474,10 +472,11 @@ void Connection::Private::checkAndConnect(const QString& userId, connectFn(); else emit q->loginError( + tr("Unsupported login flow"), tr("The homeserver at %1 does not support" " the login flow '%2'") - .arg(data->baseUrl().toDisplayString()), - flow->type); + .arg(data->baseUrl().toDisplayString(), + flow->type)); }); else connectSingleShot(q, &Connection::homeserverChanged, q, connectFn); @@ -637,7 +636,7 @@ void Connection::Private::consumeRoomData(SyncDataList&& roomDataList, } qWarning(MAIN) << "Room" << roomData.roomId << "has just been forgotten but /sync returned it in" - << toCString(roomData.joinState) + << terse << roomData.joinState << "state - suspiciously fast turnaround"; } if (auto* r = q->provideRoom(roomData.roomId, roomData.joinState)) { @@ -660,21 +659,21 @@ void Connection::Private::consumeAccountData(Events&& accountDataEvents) // After running this loop, the account data events not saved in // accountData (see the end of the loop body) are auto-cleaned away for (auto&& eventPtr: accountDataEvents) { - visit(*eventPtr, + switchOnType(*eventPtr, [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) { + remove_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) { + remove_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) { + remove_if(dcLocalRemovals, [&remoteRemovals](auto it) { return remoteRemovals.contains(it.key(), it.value()); }); if (MAIN().isDebugEnabled()) @@ -702,7 +701,7 @@ void Connection::Private::consumeAccountData(Events&& accountDataEvents) << "Couldn't get a user object for" << it.key(); } // Remove from dcLocalAdditions what the server already has. - erase_if(dcLocalAdditions, [&remoteAdditions](auto it) { + remove_if(dcLocalAdditions, [&remoteAdditions](auto it) { return remoteAdditions.contains(it.key(), it.value()); }); if (!remoteAdditions.isEmpty() || !remoteRemovals.isEmpty()) @@ -761,7 +760,7 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) << ee.senderId(); // encryptionManager->updateDeviceKeys(); - visit(*sessionDecryptMessage(ee), + switchOnType(*sessionDecryptMessage(ee), [this, senderKey = ee.senderKey()](const RoomKeyEvent& roomKeyEvent) { if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); @@ -792,22 +791,17 @@ void Connection::stopSync() QString Connection::nextBatchToken() const { return d->data->lastEvent(); } -PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) -{ - return callApi<PostReceiptJob>(room->id(), "m.read", event->id()); -} - 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 - // (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(). + auto* const job = callApi<JoinRoomJob>(roomAlias, serverNames); + // Upon completion, ensure a room object is created in case it hasn't come + // with a sync yet. If the room object is not there, provideRoom() will + // create it in Join state. 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(), JoinState::Join); + provideRoom(job->roomId()); }); return job; } @@ -840,6 +834,15 @@ inline auto splitMediaId(const QString& mediaId) return idParts; } +QUrl Connection::makeMediaUrl(QUrl mxcUrl) const +{ + Q_ASSERT(mxcUrl.scheme() == "mxc"); + QUrlQuery q(mxcUrl.query()); + q.addQueryItem(QStringLiteral("user_id"), userId()); + mxcUrl.setQuery(q); + return mxcUrl; +} + MediaThumbnailJob* Connection::getThumbnail(const QString& mediaId, QSize requestedSize, RunningPolicy policy) @@ -1050,7 +1053,7 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id) connect(leaveJob, &BaseJob::result, this, [this, leaveJob, forgetJob, room] { if (leaveJob->error() == BaseJob::Success - || leaveJob->error() == BaseJob::NotFoundError) { + || leaveJob->error() == BaseJob::NotFound) { run(forgetJob); // If the matching /sync response hasn't arrived yet, // mark the room for explicit deletion @@ -1069,7 +1072,7 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id) 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) + || forgetJob->error() == BaseJob::NotFound) d->removeRoom(id); // Delete the room from roomMap else qCWarning(MAIN).nospace() << "Error forgetting room " << id << ": " @@ -1236,20 +1239,6 @@ int Connection::millisToReconnect() const return d->syncJob ? d->syncJob->millisToRetry() : 0; } -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();) { - if (it.value()->joinState() == JoinState::Leave) - it = roomMap.erase(it); - else - ++it; - } - return roomMap; -} - QVector<Room*> Connection::allRooms() const { QVector<Room*> result; @@ -1352,8 +1341,8 @@ 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"; + qCDebug(MAIN) << "Room" << r->objectName() << "in state" << terse + << r->joinState() << "will be deleted"; emit r->beforeDestruction(r); r->deleteLater(); } @@ -1385,7 +1374,7 @@ void Connection::removeFromDirectChats(const QString& roomId, User* user) removals.insert(user, roomId); d->dcLocalRemovals.insert(user, roomId); } else { - removals = erase_if(d->directChats, + removals = remove_if(d->directChats, [&roomId](auto it) { return it.value() == roomId; }); d->directChatUsers.remove(roomId); d->dcLocalRemovals += removals; @@ -1524,8 +1513,8 @@ room_factory_t Connection::roomFactory() { return _roomFactory; } user_factory_t Connection::userFactory() { return _userFactory; } -room_factory_t Connection::_roomFactory = defaultRoomFactory<>(); -user_factory_t Connection::_userFactory = defaultUserFactory<>(); +room_factory_t Connection::_roomFactory = defaultRoomFactory<>; +user_factory_t Connection::_userFactory = defaultUserFactory<>; QByteArray Connection::generateTxnId() const { @@ -1722,7 +1711,7 @@ void Connection::getTurnServers() { auto job = callApi<GetTurnServerJob>(); connect(job, &GetTurnServerJob::success, this, - [=] { emit turnServersChanged(job->data()); }); + [this,job] { emit turnServersChanged(job->data()); }); } const QString Connection::SupportedRoomVersion::StableTag = diff --git a/lib/connection.h b/lib/connection.h index 0d22d01f..0713af16 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -6,7 +6,6 @@ #pragma once #include "ssosession.h" -#include "joinstate.h" #include "qt_connection_util.h" #include "quotient_common.h" @@ -82,11 +81,9 @@ using user_factory_t = std::function<User*(Connection*, const QString&)>; * \sa Connection::setRoomFactory, Connection::setRoomType */ template <typename T = Room> -static inline room_factory_t defaultRoomFactory() +auto defaultRoomFactory(Connection* c, const QString& id, JoinState js) { - return [](Connection* c, const QString& id, JoinState js) { - return new T(c, id, js); - }; + return new T(c, id, js); } /** The default factory to create user objects @@ -95,9 +92,9 @@ static inline room_factory_t defaultRoomFactory() * \sa Connection::setUserFactory, Connection::setUserType */ template <typename T = User> -static inline user_factory_t defaultUserFactory() +auto defaultUserFactory(Connection* c, const QString& id) { - return [](Connection* c, const QString& id) { return new T(id, c); }; + return new T(id, c); } // Room ids, rather than room pointers, are used in the direct chat @@ -125,6 +122,7 @@ class Connection : public QObject { 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) + Q_PROPERTY(bool canChangePassword READ canChangePassword NOTIFY capabilitiesLoaded) public: using UsersToDevicesToEvents = @@ -139,15 +137,6 @@ public: 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. @@ -478,21 +467,34 @@ public: template <typename T> static void setRoomType() { - setRoomFactory(defaultRoomFactory<T>()); + setRoomFactory(defaultRoomFactory<T>); } /// Set the user factory to default with the overriden user type template <typename T> static void setUserType() { - setUserFactory(defaultUserFactory<T>()); + setUserFactory(defaultUserFactory<T>); } public Q_SLOTS: - /** Set the homeserver base URL */ + /// \brief Set the homeserver base URL and retrieve its login flows + /// + /// \sa LoginFlowsJob, loginFlows, loginFlowsChanged, homeserverChanged void setHomeserver(const QUrl& baseUrl); - /** Determine and set the homeserver from MXID */ + /// \brief Determine and set the homeserver from MXID + /// + /// This attempts to resolve the homeserver by requesting + /// .well-known/matrix/client record from the server taken from the MXID + /// serverpart. If there is no record found, the serverpart itself is + /// attempted as the homeserver base URL; if the record is there but + /// is malformed (e.g., the homeserver base URL cannot be found in it) + /// resolveError() is emitted and further processing stops. Otherwise, + /// setHomeserver is called, preparing the Connection object for the login + /// attempt. + /// \param mxid user Matrix ID, such as @someone:example.org + /// \sa setHomeserver, homeserverChanged, loginFlowsChanged, resolveError void resolveServer(const QString& mxid); /** \brief Log in using a username and password pair @@ -525,30 +527,12 @@ public Q_SLOTS: */ 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) - { - 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); @@ -557,6 +541,8 @@ public Q_SLOTS: void stopSync(); QString nextBatchToken() const; + Q_INVOKABLE QUrl makeMediaUrl(QUrl mxcUrl) const; + virtual MediaThumbnailJob* getThumbnail(const QString& mediaId, QSize requestedSize, RunningPolicy policy = BackgroundRequest); @@ -663,24 +649,12 @@ public Q_SLOTS: /** \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); - Q_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(); + /// \brief Initial server resolution has failed + /// + /// This signal is emitted when resolveServer() did not manage to resolve + /// the homeserver using its .well-known/client record or otherwise. + /// \sa resolveServer void resolveError(QString error); void homeserverChanged(QUrl baseUrl); @@ -688,7 +662,6 @@ Q_SIGNALS: void capabilitiesLoaded(); void connected(); - void reconnected(); //< \deprecated Use connected() instead void loggedOut(); /** Login data or state have changed * diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp index e54d909b..87ad4577 100644 --- a/lib/connectiondata.cpp +++ b/lib/connectiondata.cpp @@ -118,18 +118,6 @@ void ConnectionData::setToken(QByteArray token) d->accessToken = std::move(token); } -void ConnectionData::setHost(QString host) -{ - d->baseUrl.setHost(host); - qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; -} - -void ConnectionData::setPort(int port) -{ - d->baseUrl.setPort(port); - qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; -} - const QString& ConnectionData::deviceId() const { return d->deviceId; } const QString& ConnectionData::userId() const { return d->userId; } diff --git a/lib/connectiondata.h b/lib/connectiondata.h index 7dd96f26..e16a2dac 100644 --- a/lib/connectiondata.h +++ b/lib/connectiondata.h @@ -31,10 +31,6 @@ public: 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); diff --git a/lib/converters.cpp b/lib/converters.cpp index 4338e8ed..444ca4f6 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -5,24 +5,23 @@ #include <QtCore/QVariant> -using namespace Quotient; - -QJsonValue JsonConverter<QVariant>::dump(const QVariant& v) +QJsonValue Quotient::JsonConverter<QVariant>::dump(const QVariant& v) { return QJsonValue::fromVariant(v); } -QVariant JsonConverter<QVariant>::load(const QJsonValue& jv) +QVariant Quotient::JsonConverter<QVariant>::load(const QJsonValue& jv) { return jv.toVariant(); } -QJsonObject JsonConverter<QVariantHash>::dump(const QVariantHash& vh) +QJsonObject Quotient::toJson(const QVariantHash& vh) { return QJsonObject::fromVariantHash(vh); } -QVariantHash JsonConverter<QVariantHash>::load(const QJsonValue& jv) +template<> +QVariantHash Quotient::fromJson(const QJsonValue& jv) { return jv.toObject().toVariantHash(); } diff --git a/lib/converters.h b/lib/converters.h index e07b6ee4..9c3d5749 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -24,6 +24,21 @@ struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, T& pod) { pod = T(jo); } }; +//! \brief The switchboard for extra conversion algorithms behind from/toJson +//! +//! This template is mainly intended for partial conversion specialisations +//! since from/toJson are functions and cannot be partially specialised. +//! Another case for JsonConverter is to insulate types that can be constructed +//! from basic types - namely, QVariant and QUrl can be directly constructed +//! from QString and having an overload or specialisation for those leads to +//! ambiguity between these and QJsonValue. For trivial (converting +//! QJsonObject/QJsonValue) and most simple cases such as primitive types or +//! QString this class is not needed. +//! +//! Do NOT call the functions of this class directly unless you know what you're +//! doing; and do not try to specialise basic things unless you're really sure +//! that they are not supported and it's not feasible to support those by means +//! of overloading toJson() and specialising fromJson(). template <typename T> struct JsonConverter { static QJsonObject dump(const T& pod) @@ -42,13 +57,17 @@ struct JsonConverter { static T load(const QJsonDocument& jd) { return doLoad(jd.object()); } }; -template <typename T> +template <typename T, + typename = std::enable_if_t<!std::is_constructible_v<QJsonValue, T>>> inline auto toJson(const T& pod) +// -> can return anything from which QJsonValue or, in some cases, QJsonDocument +// is constructible { return JsonConverter<T>::dump(pod); } inline auto toJson(const QJsonObject& jo) { return jo; } +inline auto toJson(const QJsonValue& jv) { return jv; } template <typename T> inline void fillJson(QJsonObject& json, const T& data) @@ -62,6 +81,9 @@ inline T fromJson(const QJsonValue& jv) return JsonConverter<T>::load(jv); } +template<> +inline QJsonValue fromJson(const QJsonValue& jv) { return jv; } + template <typename T> inline T fromJson(const QJsonDocument& jd) { @@ -95,80 +117,68 @@ inline void fillFromJson(const QJsonValue& jv, T& pod) // 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<> +inline bool fromJson(const QJsonValue& jv) { return jv.toBool(); } template <> -struct JsonConverter<bool> : public TrivialJsonDumper<bool> { - static auto load(const QJsonValue& jv) { return jv.toBool(); } -}; +inline int fromJson(const QJsonValue& jv) { return jv.toInt(); } template <> -struct JsonConverter<int> : public TrivialJsonDumper<int> { - static auto load(const QJsonValue& jv) { return jv.toInt(); } -}; +inline double fromJson(const QJsonValue& jv) { return jv.toDouble(); } template <> -struct JsonConverter<double> : public TrivialJsonDumper<double> { - static auto load(const QJsonValue& jv) { return jv.toDouble(); } -}; +inline float fromJson(const QJsonValue& jv) { return float(jv.toDouble()); } template <> -struct JsonConverter<float> : public TrivialJsonDumper<float> { - static auto load(const QJsonValue& jv) { return float(jv.toDouble()); } -}; +inline qint64 fromJson(const QJsonValue& jv) { return qint64(jv.toDouble()); } template <> -struct JsonConverter<qint64> : public TrivialJsonDumper<qint64> { - static auto load(const QJsonValue& jv) { return qint64(jv.toDouble()); } -}; +inline QString fromJson(const QJsonValue& jv) { return jv.toString(); } template <> -struct JsonConverter<QString> : public TrivialJsonDumper<QString> { - static auto load(const QJsonValue& jv) { return jv.toString(); } -}; +inline QJsonArray fromJson(const QJsonValue& jv) { return jv.toArray(); } template <> -struct JsonConverter<QDateTime> { - static auto dump(const QDateTime& val) = delete; // not provided yet - static auto load(const QJsonValue& jv) - { - return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC); - } -}; +inline QJsonArray fromJson(const QJsonDocument& jd) { return jd.array(); } +inline QJsonValue toJson(const QDateTime& val) +{ + return val.isValid() ? val.toMSecsSinceEpoch() : QJsonValue(); +} template <> -struct JsonConverter<QDate> { - static auto dump(const QDate& val) = delete; // not provided yet - static auto load(const QJsonValue& jv) - { - return fromJson<QDateTime>(jv).date(); - } -}; +inline QDateTime fromJson(const QJsonValue& jv) +{ + return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC); +} +inline QJsonValue toJson(const QDate& val) { + return toJson( +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + QDateTime(val) +#else + val.startOfDay() +#endif + ); +} template <> -struct JsonConverter<QUrl> : JsonConverter<QString> { - static auto dump(const QUrl& url) // Override on top of that for QString - { - return JsonConverter<QString>::dump(url.toString(QUrl::FullyEncoded)); - } -}; +inline QDate fromJson(const QJsonValue& jv) +{ + return fromJson<QDateTime>(jv).date(); +} -template <> -struct JsonConverter<QJsonArray> : public TrivialJsonDumper<QJsonArray> { - static auto load(const QJsonValue& jv) { return jv.toArray(); } -}; +// Insulate QVariant and QUrl conversions into JsonConverter so that they don't +// interfere with toJson(const QJsonValue&) over QString, since both types are +// constructible from QString (even if QUrl requires explicit construction). template <> -struct JsonConverter<QByteArray> { - static QString dump(const QByteArray& ba) { return ba.constData(); } +struct JsonConverter<QUrl> { static auto load(const QJsonValue& jv) { - return fromJson<QString>(jv).toLatin1(); + return QUrl(jv.toString()); + } + static auto dump(const QUrl& url) + { + return url.toString(QUrl::FullyEncoded); } }; @@ -194,15 +204,11 @@ struct JsonConverter<Omittable<T>> { 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); + for (const auto& v : vals) + ja.push_back(toJson(v)); return ja; } static auto load(const QJsonArray& ja) @@ -221,14 +227,16 @@ template <typename T> struct JsonConverter<std::vector<T>> : public JsonArrayConverter<std::vector<T>> {}; +#if QT_VERSION_MAJOR < 6 // QVector is an alias of QList in Qt6 but not in Qt 5 template <typename T> struct JsonConverter<QVector<T>> : public JsonArrayConverter<QVector<T>> {}; +#endif template <typename T> struct JsonConverter<QList<T>> : public JsonArrayConverter<QList<T>> {}; template <> -struct JsonConverter<QStringList> : public JsonConverter<QList<QString>> { +struct JsonConverter<QStringList> : public JsonArrayConverter<QStringList> { static auto dump(const QStringList& sl) { return QJsonArray::fromStringList(sl); @@ -240,14 +248,13 @@ struct JsonObjectConverter<QSet<QString>> { static void dumpTo(QJsonObject& json, const QSet<QString>& s) { for (const auto& e : s) - json.insert(toJson(e), QJsonObject {}); + json.insert(e, QJsonObject {}); } - static auto fillFrom(const QJsonObject& json, QSet<QString>& s) + static void 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; } }; @@ -260,7 +267,7 @@ struct HashMapFromJson { } static void fillFrom(const QJsonObject& jo, HashMapT& h) { - h.reserve(jo.size()); + h.reserve(h.size() + jo.size()); for (auto it = jo.begin(); it != jo.end(); ++it) h[it.key()] = fromJson<typename HashMapT::mapped_type>(it.value()); } @@ -274,11 +281,9 @@ template <typename T> struct JsonObjectConverter<QHash<QString, T>> : public HashMapFromJson<QHash<QString, T>> {}; +QJsonObject toJson(const QVariantHash& vh); template <> -struct JsonConverter<QVariantHash> { - static QJsonObject dump(const QVariantHash& vh); - static QVariantHash load(const QJsonValue& jv); -}; +QVariantHash fromJson(const QJsonValue& jv); // Conditional insertion into a QJsonObject @@ -302,16 +307,15 @@ namespace _impl { q.addQueryItem(k, v ? QStringLiteral("true") : QStringLiteral("false")); } - inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals) + inline void addTo(QUrlQuery& q, const QString& k, const QUrl& v) { - for (const auto& v : vals) - q.addQueryItem(k, v); + q.addQueryItem(k, v.toEncoded()); } - inline void addTo(QUrlQuery& q, const QString&, const QJsonObject& vals) + inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals) { - for (auto it = vals.begin(); it != vals.end(); ++it) - q.addQueryItem(it.key(), it.value().toString()); + for (const auto& v : vals) + q.addQueryItem(k, v); } // This one is for types that don't have isEmpty() and for all types diff --git a/lib/csapi/account-data.cpp b/lib/csapi/account-data.cpp index 6a40e908..09fc8d40 100644 --- a/lib/csapi/account-data.cpp +++ b/lib/csapi/account-data.cpp @@ -4,31 +4,29 @@ #include "account-data.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; 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) + makePath("/_matrix/client/r0", "/user/", userId, "/account_data/", + type)) { - setRequestData(Data(toJson(content))); + setRequestData(RequestData(toJson(content))); } QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") % "/user/" - % userId % "/account_data/" % type); + makePath("/_matrix/client/r0", "/user/", + userId, "/account_data/", type)); } GetAccountDataJob::GetAccountDataJob(const QString& userId, const QString& type) : BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataJob"), - QStringLiteral("/_matrix/client/r0") % "/user/" % userId - % "/account_data/" % type) + makePath("/_matrix/client/r0", "/user/", userId, "/account_data/", + type)) {} SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, @@ -36,10 +34,10 @@ SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const QString& type, const QJsonObject& content) : BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataPerRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/user/" % userId - % "/rooms/" % roomId % "/account_data/" % type) + makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + roomId, "/account_data/", type)) { - setRequestData(Data(toJson(content))); + setRequestData(RequestData(toJson(content))); } QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, @@ -48,15 +46,15 @@ QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, const QString& type) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/user/" % userId % "/rooms/" % roomId - % "/account_data/" % type); + makePath("/_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) + makePath("/_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 9a31596f..0c760e80 100644 --- a/lib/csapi/account-data.h +++ b/lib/csapi/account-data.h @@ -12,7 +12,7 @@ namespace Quotient { * * 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``. + * top-level `account_data`. */ class SetAccountDataJob : public BaseJob { public: @@ -65,7 +65,7 @@ public: * * 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``. + * clients in the per-room `account_data`. */ class SetAccountDataPerRoomJob : public BaseJob { public: diff --git a/lib/csapi/admin.cpp b/lib/csapi/admin.cpp index 9619c441..81dd0624 100644 --- a/lib/csapi/admin.cpp +++ b/lib/csapi/admin.cpp @@ -4,18 +4,16 @@ #include "admin.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl GetWhoIsJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/admin/whois/" % userId); + makePath("/_matrix/client/r0", + "/admin/whois/", userId)); } GetWhoIsJob::GetWhoIsJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetWhoIsJob"), - QStringLiteral("/_matrix/client/r0") % "/admin/whois/" % userId) + makePath("/_matrix/client/r0", "/admin/whois/", userId)) {} diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp index fa4f475a..589c9fc1 100644 --- a/lib/csapi/administrative_contact.cpp +++ b/lib/csapi/administrative_contact.cpp @@ -4,25 +4,22 @@ #include "administrative_contact.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl GetAccount3PIDsJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/account/3pid"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/r0", "/account/3pid")); } GetAccount3PIDsJob::GetAccount3PIDsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetAccount3PIDsJob"), - QStringLiteral("/_matrix/client/r0") % "/account/3pid") + makePath("/_matrix/client/r0", "/account/3pid")) {} Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds) : BaseJob(HttpVerb::Post, QStringLiteral("Post3PIDsJob"), - QStringLiteral("/_matrix/client/r0") % "/account/3pid") + makePath("/_matrix/client/r0", "/account/3pid")) { QJsonObject _data; addParam<>(_data, QStringLiteral("three_pid_creds"), threePidCreds); @@ -32,7 +29,7 @@ Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds) Add3PIDJob::Add3PIDJob(const QString& clientSecret, const QString& sid, const Omittable<AuthenticationData>& auth) : BaseJob(HttpVerb::Post, QStringLiteral("Add3PIDJob"), - QStringLiteral("/_matrix/client/r0") % "/account/3pid/add") + makePath("/_matrix/client/r0", "/account/3pid/add")) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); @@ -44,7 +41,7 @@ Add3PIDJob::Add3PIDJob(const QString& clientSecret, const QString& sid, 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") + makePath("/_matrix/client/r0", "/account/3pid/bind")) { QJsonObject _data; addParam<>(_data, QStringLiteral("client_secret"), clientSecret); @@ -58,7 +55,7 @@ Delete3pidFromAccountJob::Delete3pidFromAccountJob(const QString& medium, const QString& address, const QString& idServer) : BaseJob(HttpVerb::Post, QStringLiteral("Delete3pidFromAccountJob"), - QStringLiteral("/_matrix/client/r0") % "/account/3pid/delete") + makePath("/_matrix/client/r0", "/account/3pid/delete")) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("id_server"), idServer); @@ -72,7 +69,7 @@ Unbind3pidFromAccountJob::Unbind3pidFromAccountJob(const QString& medium, const QString& address, const QString& idServer) : BaseJob(HttpVerb::Post, QStringLiteral("Unbind3pidFromAccountJob"), - QStringLiteral("/_matrix/client/r0") % "/account/3pid/unbind") + makePath("/_matrix/client/r0", "/account/3pid/unbind")) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("id_server"), idServer); @@ -85,19 +82,19 @@ Unbind3pidFromAccountJob::Unbind3pidFromAccountJob(const QString& medium, RequestTokenTo3PIDEmailJob::RequestTokenTo3PIDEmailJob( const EmailValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDEmailJob"), - QStringLiteral("/_matrix/client/r0") - % "/account/3pid/email/requestToken", + makePath("/_matrix/client/r0", + "/account/3pid/email/requestToken"), false) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); } RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob( const MsisdnValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDMSISDNJob"), - QStringLiteral("/_matrix/client/r0") - % "/account/3pid/msisdn/requestToken", + makePath("/_matrix/client/r0", + "/account/3pid/msisdn/requestToken"), false) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); } diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h index 1966d533..e436971d 100644 --- a/lib/csapi/administrative_contact.h +++ b/lib/csapi/administrative_contact.h @@ -93,14 +93,14 @@ struct JsonObjectConverter<GetAccount3PIDsJob::ThirdPartyIdentifier> { * * Adds contact information to the user's account. * - * This endpoint is deprecated in favour of the more specific ``/3pid/add`` - * and ``/3pid/bind`` endpoints. + * 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. + * **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: @@ -144,7 +144,8 @@ struct JsonObjectConverter<Post3PIDsJob::ThreePidCredentials> { /*! \brief Adds contact information to the user's account. * - * This API endpoint uses the `User-Interactive Authentication API`_. + * This API endpoint uses the [User-Interactive Authentication + * API](/client-server-api/#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 @@ -206,7 +207,7 @@ public: * 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`` + * 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. */ @@ -222,9 +223,9 @@ public: * * \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``. + * 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, @@ -233,8 +234,8 @@ public: // 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`` + /// 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. @@ -249,7 +250,7 @@ public: * 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`` + * 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. */ @@ -265,9 +266,9 @@ public: * * \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``. + * 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, @@ -276,8 +277,8 @@ public: // 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 + /// 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 @@ -293,7 +294,9 @@ public: * 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 + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken) + * endpoint. The homeserver should validate * the email itself, either by sending a validation email itself or by using * a service it has control over. */ @@ -307,9 +310,11 @@ public: * 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. + * the + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken) + * 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); @@ -331,7 +336,9 @@ public: * 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 + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken) + * 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. */ @@ -345,9 +352,11 @@ public: * 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. + * the + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken) + * 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); diff --git a/lib/csapi/appservice_room_directory.cpp b/lib/csapi/appservice_room_directory.cpp index e8ec55bf..40d784c6 100644 --- a/lib/csapi/appservice_room_directory.cpp +++ b/lib/csapi/appservice_room_directory.cpp @@ -4,16 +4,14 @@ #include "appservice_room_directory.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; -UpdateAppserviceRoomDirectoryVsibilityJob::UpdateAppserviceRoomDirectoryVsibilityJob( +UpdateAppserviceRoomDirectoryVisibilityJob::UpdateAppserviceRoomDirectoryVisibilityJob( const QString& networkId, const QString& roomId, const QString& visibility) : BaseJob(HttpVerb::Put, - QStringLiteral("UpdateAppserviceRoomDirectoryVsibilityJob"), - QStringLiteral("/_matrix/client/r0") - % "/directory/list/appservice/" % networkId % "/" % roomId) + QStringLiteral("UpdateAppserviceRoomDirectoryVisibilityJob"), + makePath("/_matrix/client/r0", "/directory/list/appservice/", + networkId, "/", roomId)) { QJsonObject _data; addParam<>(_data, QStringLiteral("visibility"), visibility); diff --git a/lib/csapi/appservice_room_directory.h b/lib/csapi/appservice_room_directory.h index 3fa02a07..56a69592 100644 --- a/lib/csapi/appservice_room_directory.h +++ b/lib/csapi/appservice_room_directory.h @@ -17,11 +17,11 @@ namespace Quotient { * 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. + * 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 { +class UpdateAppserviceRoomDirectoryVisibilityJob : public BaseJob { public: /*! \brief Updates a room's visibility in the application service's room * directory. @@ -38,9 +38,9 @@ public: * Whether the room should be visible (public) in the directory * or not (private). */ - explicit UpdateAppserviceRoomDirectoryVsibilityJob(const QString& networkId, - const QString& roomId, - const QString& visibility); + explicit UpdateAppserviceRoomDirectoryVisibilityJob( + const QString& networkId, const QString& roomId, + const QString& visibility); }; } // namespace Quotient diff --git a/lib/csapi/banning.cpp b/lib/csapi/banning.cpp index 8a8ec664..472128bb 100644 --- a/lib/csapi/banning.cpp +++ b/lib/csapi/banning.cpp @@ -4,14 +4,12 @@ #include "banning.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; BanJob::BanJob(const QString& roomId, const QString& userId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("BanJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/ban") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/ban")) { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); @@ -19,12 +17,13 @@ BanJob::BanJob(const QString& roomId, const QString& userId, setRequestData(std::move(_data)); } -UnbanJob::UnbanJob(const QString& roomId, const QString& userId) +UnbanJob::UnbanJob(const QString& roomId, const QString& userId, + const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("UnbanJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/unban") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/unban")) { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); + addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); setRequestData(std::move(_data)); } diff --git a/lib/csapi/banning.h b/lib/csapi/banning.h index 37ae91ee..7a9697d3 100644 --- a/lib/csapi/banning.h +++ b/lib/csapi/banning.h @@ -30,7 +30,8 @@ public: * * \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. + * `reason` on the target's updated + * [`m.room.member`](/client-server-api/#mroommember) event. */ explicit BanJob(const QString& roomId, const QString& userId, const QString& reason = {}); @@ -54,8 +55,13 @@ public: * * \param userId * The fully qualified user ID of the user being unbanned. + * + * \param reason + * Optional reason to be included as the `reason` on the subsequent + * membership event. */ - explicit UnbanJob(const QString& roomId, const QString& userId); + explicit UnbanJob(const QString& roomId, const QString& userId, + const QString& reason = {}); }; } // namespace Quotient diff --git a/lib/csapi/capabilities.cpp b/lib/csapi/capabilities.cpp index 33a53cad..bc21e462 100644 --- a/lib/csapi/capabilities.cpp +++ b/lib/csapi/capabilities.cpp @@ -4,20 +4,17 @@ #include "capabilities.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl GetCapabilitiesJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/capabilities"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/r0", "/capabilities")); } GetCapabilitiesJob::GetCapabilitiesJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetCapabilitiesJob"), - QStringLiteral("/_matrix/client/r0") % "/capabilities") + makePath("/_matrix/client/r0", "/capabilities")) { addExpectedKey("capabilities"); } diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp index 7ae89739..6d1e38b6 100644 --- a/lib/csapi/content-repo.cpp +++ b/lib/csapi/content-repo.cpp @@ -4,13 +4,11 @@ #include "content-repo.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; auto queryToUploadContent(const QString& filename) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("filename"), filename); return _q; } @@ -18,17 +16,17 @@ auto queryToUploadContent(const QString& filename) UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename, const QString& contentType) : BaseJob(HttpVerb::Post, QStringLiteral("UploadContentJob"), - QStringLiteral("/_matrix/media/r0") % "/upload", + makePath("/_matrix/media/r0", "/upload"), queryToUploadContent(filename)) { setRequestHeader("Content-Type", contentType.toLatin1()); - setRequestData(Data(content)); + setRequestData(RequestData(content)); addExpectedKey("content_uri"); } auto queryToGetContent(bool allowRemote) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("allow_remote"), allowRemote); return _q; } @@ -37,17 +35,16 @@ QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, bool allowRemote) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/media/r0") - % "/download/" % serverName % "/" - % mediaId, + makePath("/_matrix/media/r0", "/download/", + serverName, "/", mediaId), queryToGetContent(allowRemote)); } GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId, bool allowRemote) : BaseJob(HttpVerb::Get, QStringLiteral("GetContentJob"), - QStringLiteral("/_matrix/media/r0") % "/download/" % serverName - % "/" % mediaId, + makePath("/_matrix/media/r0", "/download/", serverName, "/", + mediaId), queryToGetContent(allowRemote), {}, false) { setExpectedContentTypes({ "*/*" }); @@ -55,7 +52,7 @@ GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId, auto queryToGetContentOverrideName(bool allowRemote) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("allow_remote"), allowRemote); return _q; } @@ -67,9 +64,9 @@ QUrl GetContentOverrideNameJob::makeRequestUrl(QUrl baseUrl, bool allowRemote) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/media/r0") - % "/download/" % serverName % "/" - % mediaId % "/" % fileName, + makePath("/_matrix/media/r0", "/download/", + serverName, "/", mediaId, "/", + fileName), queryToGetContentOverrideName(allowRemote)); } @@ -78,8 +75,8 @@ GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName, const QString& fileName, bool allowRemote) : BaseJob(HttpVerb::Get, QStringLiteral("GetContentOverrideNameJob"), - QStringLiteral("/_matrix/media/r0") % "/download/" % serverName - % "/" % mediaId % "/" % fileName, + makePath("/_matrix/media/r0", "/download/", serverName, "/", + mediaId, "/", fileName), queryToGetContentOverrideName(allowRemote), {}, false) { setExpectedContentTypes({ "*/*" }); @@ -88,7 +85,7 @@ GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName, auto queryToGetContentThumbnail(int width, int height, const QString& method, bool allowRemote) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("width"), width); addParam<>(_q, QStringLiteral("height"), height); addParam<IfNotEmpty>(_q, QStringLiteral("method"), method); @@ -104,8 +101,7 @@ QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl, { return BaseJob::makeRequestUrl( std::move(baseUrl), - QStringLiteral("/_matrix/media/r0") % "/thumbnail/" % serverName % "/" - % mediaId, + makePath("/_matrix/media/r0", "/thumbnail/", serverName, "/", mediaId), queryToGetContentThumbnail(width, height, method, allowRemote)); } @@ -114,45 +110,44 @@ GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName, int height, const QString& method, bool allowRemote) : BaseJob(HttpVerb::Get, QStringLiteral("GetContentThumbnailJob"), - QStringLiteral("/_matrix/media/r0") % "/thumbnail/" % serverName - % "/" % mediaId, + makePath("/_matrix/media/r0", "/thumbnail/", serverName, "/", + mediaId), queryToGetContentThumbnail(width, height, method, allowRemote), {}, false) { setExpectedContentTypes({ "image/jpeg", "image/png" }); } -auto queryToGetUrlPreview(const QString& url, Omittable<qint64> ts) +auto queryToGetUrlPreview(const QUrl& url, Omittable<qint64> ts) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("url"), url); addParam<IfNotEmpty>(_q, QStringLiteral("ts"), ts); return _q; } -QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QString& url, +QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QUrl& url, Omittable<qint64> ts) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/media/r0") - % "/preview_url", + makePath("/_matrix/media/r0", + "/preview_url"), queryToGetUrlPreview(url, ts)); } -GetUrlPreviewJob::GetUrlPreviewJob(const QString& url, Omittable<qint64> ts) +GetUrlPreviewJob::GetUrlPreviewJob(const QUrl& url, Omittable<qint64> ts) : BaseJob(HttpVerb::Get, QStringLiteral("GetUrlPreviewJob"), - QStringLiteral("/_matrix/media/r0") % "/preview_url", + makePath("/_matrix/media/r0", "/preview_url"), queryToGetUrlPreview(url, ts)) {} QUrl GetConfigJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/media/r0") - % "/config"); + makePath("/_matrix/media/r0", "/config")); } GetConfigJob::GetConfigJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetConfigJob"), - QStringLiteral("/_matrix/media/r0") % "/config") + makePath("/_matrix/media/r0", "/config")) {} diff --git a/lib/csapi/content-repo.h b/lib/csapi/content-repo.h index ed67485c..28409f5c 100644 --- a/lib/csapi/content-repo.h +++ b/lib/csapi/content-repo.h @@ -32,11 +32,9 @@ public: // Result properties - /// The `MXC URI`_ to the uploaded content. - QString contentUri() const - { - return loadFromJson<QString>("content_uri"_ls); - } + /// The [MXC URI](/client-server-api/#matrix-content-mxc-uris) to the + /// uploaded content. + QUrl contentUri() const { return loadFromJson<QUrl>("content_uri"_ls); } }; /*! \brief Download content from the content repository. @@ -47,10 +45,10 @@ public: /*! \brief Download content from the content repository. * * \param serverName - * The server name from the ``mxc://`` URI (the authoritory component) + * The server name from the `mxc://` URI (the authoritory component) * * \param mediaId - * The media ID from the ``mxc://`` URI (the path component) + * 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 @@ -95,13 +93,13 @@ public: * name * * \param serverName - * The server name from the ``mxc://`` URI (the authoritory component) + * The server name from the `mxc://` URI (the authoritory component) * * \param mediaId - * The media ID from the ``mxc://`` URI (the path component) + * The media ID from the `mxc://` URI (the path component) * * \param fileName - * A filename to give in the ``Content-Disposition`` header. + * A filename to give in the `Content-Disposition` header. * * \param allowRemote * Indicates to the server that it should not attempt to fetch the media @@ -127,7 +125,7 @@ public: /// 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 + /// The `fileName` requested or the name of the file that was previously /// uploaded, if set. QString contentDisposition() const { @@ -141,17 +139,18 @@ public: /*! \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. + * See the [Thumbnails](/client-server-api/#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) + * The server name from the `mxc://` URI (the authoritory component) * * \param mediaId - * The media ID from the ``mxc://`` URI (the path component) + * The media ID from the `mxc://` URI (the path component) * * \param width * The *desired* width of the thumbnail. The actual thumbnail may be @@ -162,8 +161,8 @@ public: * larger than the size specified. * * \param method - * The desired resizing method. See the `thumbnailing <#thumbnails>`_ - * section for more information. + * The desired resizing method. See the + * [Thumbnails](/client-server-api/#thumbnails) section for more information. * * \param allowRemote * Indicates to the server that it should not attempt to fetch @@ -199,11 +198,11 @@ public: * 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. + * **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: @@ -217,14 +216,14 @@ public: * return a newer version if it does not have the requested version * available. */ - explicit GetUrlPreviewJob(const QString& url, Omittable<qint64> ts = none); + explicit GetUrlPreviewJob(const QUrl& 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, + static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& url, Omittable<qint64> ts = none); // Result properties @@ -235,8 +234,9 @@ public: 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); } + /// An [MXC URI](/client-server-api/#matrix-content-mxc-uris) to the image. + /// Omitted if there is no image. + QUrl ogImage() const { return loadFromJson<QUrl>("og:image"_ls); } }; /*! \brief Get the configuration for the content repository. diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp index a94f9951..9aaef87f 100644 --- a/lib/csapi/create_room.cpp +++ b/lib/csapi/create_room.cpp @@ -4,8 +4,6 @@ #include "create_room.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; CreateRoomJob::CreateRoomJob(const QString& visibility, @@ -18,7 +16,7 @@ CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& preset, Omittable<bool> isDirect, const QJsonObject& powerLevelContentOverride) : BaseJob(HttpVerb::Post, QStringLiteral("CreateRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/createRoom") + makePath("/_matrix/client/r0", "/createRoom")) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("visibility"), visibility); diff --git a/lib/csapi/create_room.h b/lib/csapi/create_room.h index 6a718ff4..81dfbffc 100644 --- a/lib/csapi/create_room.h +++ b/lib/csapi/create_room.h @@ -16,47 +16,42 @@ namespace Quotient { * 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 + * 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 + * 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 + * 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. + * 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. + * 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 + * 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). + * 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``). + * 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 + * 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``. + * `creation_content`. */ class CreateRoomJob : public BaseJob { public: @@ -68,50 +63,44 @@ public: /// 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 + /// 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 + /// 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 + /// 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. + /// 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. + /// 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 + /// 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`` + /// 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``). + /// 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 + /// 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``. + /// `creation_content`. struct Invite3pid { /// The hostname+port of the identity server which should be used for /// third party identifier lookups. @@ -121,7 +110,7 @@ public: /// r0.5-compatible clients and this specification version. QString idAccessToken; /// The kind of address being passed in the address field, for example - /// ``email``. + /// `email`. QString medium; /// The invitee's third party identifier. QString address; @@ -133,50 +122,44 @@ public: /// 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 + /// 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 + /// 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 + /// 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. + /// 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. + /// 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 + /// 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`` + /// 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``). + /// 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 + /// 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``. + /// `creation_content`. struct StateEvent { /// The type of event to send. QString type; @@ -191,12 +174,12 @@ public: /*! \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 + * 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``. + * `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 @@ -204,20 +187,20 @@ public: * 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``. + * 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 + * 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``. + * Events for more information on `m.room.name`. * * \param topic - * If this is included, an ``m.room.topic`` event will be sent + * 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``. + * Events for more information on `m.room.topic`. * * \param invite * A list of user IDs to invite to the room. This will tell the @@ -230,14 +213,14 @@ public: * \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. + * 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. + * Extra keys, such as `m.federate`, to be added to the content + * of the [`m.room.create`](client-server-api/#mroomcreate) 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 @@ -245,28 +228,30 @@ public: * 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. + * 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``. + * 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. + * 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](/client-server-api/#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. + * [`m.room.power_levels`](client-server-api/#mroompower_levels) + * event content prior to it being sent to the room. Defaults to + * overriding nothing. */ explicit CreateRoomJob(const QString& visibility = {}, const QString& roomAliasName = {}, diff --git a/lib/csapi/cross_signing.cpp b/lib/csapi/cross_signing.cpp new file mode 100644 index 00000000..1fa0e949 --- /dev/null +++ b/lib/csapi/cross_signing.cpp @@ -0,0 +1,31 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "cross_signing.h" + +using namespace Quotient; + +UploadCrossSigningKeysJob::UploadCrossSigningKeysJob( + const Omittable<CrossSigningKey>& masterKey, + const Omittable<CrossSigningKey>& selfSigningKey, + const Omittable<CrossSigningKey>& userSigningKey) + : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningKeysJob"), + makePath("/_matrix/client/r0", "/keys/device_signing/upload")) +{ + QJsonObject _data; + addParam<IfNotEmpty>(_data, QStringLiteral("master_key"), masterKey); + addParam<IfNotEmpty>(_data, QStringLiteral("self_signing_key"), + selfSigningKey); + addParam<IfNotEmpty>(_data, QStringLiteral("user_signing_key"), + userSigningKey); + setRequestData(std::move(_data)); +} + +UploadCrossSigningSignaturesJob::UploadCrossSigningSignaturesJob( + const QHash<QString, QHash<QString, QJsonObject>>& signatures) + : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningSignaturesJob"), + makePath("/_matrix/client/r0", "/keys/signatures/upload")) +{ + setRequestData(RequestData(toJson(signatures))); +} diff --git a/lib/csapi/cross_signing.h b/lib/csapi/cross_signing.h new file mode 100644 index 00000000..2ab65e06 --- /dev/null +++ b/lib/csapi/cross_signing.h @@ -0,0 +1,72 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "csapi/definitions/cross_signing_key.h" + +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Upload cross-signing keys. + * + * Publishes cross-signing keys for the user. + * + * This API endpoint uses the [User-Interactive Authentication + * API](/client-server-api/#user-interactive-authentication-api). + */ +class UploadCrossSigningKeysJob : public BaseJob { +public: + /*! \brief Upload cross-signing keys. + * + * \param masterKey + * Optional. The user\'s master key. + * + * \param selfSigningKey + * Optional. The user\'s self-signing key. Must be signed by + * the accompanying master key, or by the user\'s most recently + * uploaded master key if no master key is included in the + * request. + * + * \param userSigningKey + * Optional. The user\'s user-signing key. Must be signed by + * the accompanying master key, or by the user\'s most recently + * uploaded master key if no master key is included in the + * request. + */ + explicit UploadCrossSigningKeysJob( + const Omittable<CrossSigningKey>& masterKey = none, + const Omittable<CrossSigningKey>& selfSigningKey = none, + const Omittable<CrossSigningKey>& userSigningKey = none); +}; + +/*! \brief Upload cross-signing signatures. + * + * Publishes cross-signing signatures for the user. The request body is a + * map from user ID to key ID to signed JSON object. + */ +class UploadCrossSigningSignaturesJob : public BaseJob { +public: + /*! \brief Upload cross-signing signatures. + * + * \param signatures + * The signatures to be published. + */ + explicit UploadCrossSigningSignaturesJob( + const QHash<QString, QHash<QString, QJsonObject>>& signatures = {}); + + // Result properties + + /// A map from user ID to key ID to an error for any signatures + /// that failed. If a signature was invalid, the `errcode` will + /// be set to `M_INVALID_SIGNATURE`. + QHash<QString, QHash<QString, QJsonObject>> failures() const + { + return loadFromJson<QHash<QString, QHash<QString, QJsonObject>>>( + "failures"_ls); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/cross_signing_key.h b/lib/csapi/definitions/cross_signing_key.h new file mode 100644 index 00000000..0cec8161 --- /dev/null +++ b/lib/csapi/definitions/cross_signing_key.h @@ -0,0 +1,47 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +namespace Quotient { +/// Cross signing key +struct CrossSigningKey { + /// The ID of the user the key belongs to. + QString userId; + + /// What the key is used for. + QStringList usage; + + /// The public key. The object must have exactly one property, whose name + /// is in the form `<algorithm>:<unpadded_base64_public_key>`, and whose + /// value is the unpadded base64 public key. + QHash<QString, QString> keys; + + /// Signatures of the key, calculated using the process described at + /// [Signing JSON](/appendices/#signing-json). Optional for the master key. + /// Other keys must be signed by the user\'s master key. + QJsonObject signatures; +}; + +template <> +struct JsonObjectConverter<CrossSigningKey> { + static void dumpTo(QJsonObject& jo, const CrossSigningKey& pod) + { + addParam<>(jo, QStringLiteral("user_id"), pod.userId); + addParam<>(jo, QStringLiteral("usage"), pod.usage); + addParam<>(jo, QStringLiteral("keys"), pod.keys); + addParam<IfNotEmpty>(jo, QStringLiteral("signatures"), pod.signatures); + } + static void fillFrom(const QJsonObject& jo, CrossSigningKey& pod) + { + fromJson(jo.value("user_id"_ls), pod.userId); + fromJson(jo.value("usage"_ls), pod.usage); + fromJson(jo.value("keys"_ls), pod.keys); + fromJson(jo.value("signatures"_ls), pod.signatures); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/definitions/device_keys.h b/lib/csapi/definitions/device_keys.h index 3065f218..84ecefae 100644 --- a/lib/csapi/definitions/device_keys.h +++ b/lib/csapi/definitions/device_keys.h @@ -21,15 +21,15 @@ struct DeviceKeys { QStringList algorithms; /// Public identity keys. The names of the properties should be in the - /// format ``<algorithm>:<device_id>``. The keys themselves should be + /// 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. + /// `<algorithm>:<device_id>` to the signature. /// - /// The signature is calculated using the process described at `Signing - /// JSON`_. + /// The signature is calculated using the process described at [Signing + /// JSON](/appendices/#signing-json). QHash<QString, QHash<QString, QString>> signatures; }; diff --git a/lib/csapi/definitions/event_filter.h b/lib/csapi/definitions/event_filter.h index 67497412..c55d4f92 100644 --- a/lib/csapi/definitions/event_filter.h +++ b/lib/csapi/definitions/event_filter.h @@ -14,13 +14,13 @@ struct EventFilter { /// 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. + /// 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. + /// 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 @@ -28,7 +28,7 @@ struct EventFilter { 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 + /// types are included. A `'*'` can be used as a wildcard to match any /// sequence of characters. QStringList types; }; diff --git a/lib/csapi/definitions/openid_token.h b/lib/csapi/definitions/openid_token.h index 5e68c376..3c447321 100644 --- a/lib/csapi/definitions/openid_token.h +++ b/lib/csapi/definitions/openid_token.h @@ -11,10 +11,10 @@ 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. + /// API `GET /openid/userinfo` to verify the user's identity. QString accessToken; - /// The string ``Bearer``. + /// The string `Bearer`. QString tokenType; /// The homeserver domain the consumer should use when attempting to diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h index 8f30e607..2938b4ec 100644 --- a/lib/csapi/definitions/public_rooms_response.h +++ b/lib/csapi/definitions/public_rooms_response.h @@ -36,7 +36,13 @@ struct PublicRoomsChunk { bool guestCanJoin; /// The URL for the room's avatar, if one is set. - QString avatarUrl; + QUrl avatarUrl; + + /// The room's join rule. When not present, the room is assumed to + /// be `public`. Note that rooms with `invite` join rules are not + /// expected here, but rooms with `knock` rules are given their + /// near-public nature. + QString joinRule; }; template <> @@ -54,6 +60,7 @@ struct JsonObjectConverter<PublicRoomsChunk> { addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable); addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin); addParam<IfNotEmpty>(jo, QStringLiteral("avatar_url"), pod.avatarUrl); + addParam<IfNotEmpty>(jo, QStringLiteral("join_rule"), pod.joinRule); } static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod) { @@ -66,6 +73,7 @@ struct JsonObjectConverter<PublicRoomsChunk> { 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); + fromJson(jo.value("join_rule"_ls), pod.joinRule); } }; diff --git a/lib/csapi/definitions/push_condition.h b/lib/csapi/definitions/push_condition.h index a6decf1b..ce66d075 100644 --- a/lib/csapi/definitions/push_condition.h +++ b/lib/csapi/definitions/push_condition.h @@ -9,26 +9,27 @@ namespace Quotient { struct PushCondition { - /// The kind of condition to apply. See `conditions <#conditions>`_ for - /// more information on the allowed kinds and how they work. + /// The kind of condition to apply. See + /// [conditions](/client-server-api/#conditions) for more information on the + /// allowed kinds and how they work. QString kind; - /// Required for ``event_match`` conditions. The dot-separated field of the + /// Required for `event_match` conditions. The dot-separated field of the /// event to match. /// - /// Required for ``sender_notification_permission`` conditions. The field in + /// 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``. + /// 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 + /// 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 + /// 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 ==. diff --git a/lib/csapi/definitions/push_rule.h b/lib/csapi/definitions/push_rule.h index 43749bae..135537c1 100644 --- a/lib/csapi/definitions/push_rule.h +++ b/lib/csapi/definitions/push_rule.h @@ -25,10 +25,10 @@ struct PushRule { /// 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. + /// applicable to `underride` and `override` rules. QVector<PushCondition> conditions; - /// The glob-style pattern to match against. Only applicable to ``content`` + /// The glob-style pattern to match against. Only applicable to `content` /// rules. QString pattern; }; diff --git a/lib/csapi/definitions/request_email_validation.h b/lib/csapi/definitions/request_email_validation.h index ab34862e..b1781e27 100644 --- a/lib/csapi/definitions/request_email_validation.h +++ b/lib/csapi/definitions/request_email_validation.h @@ -16,15 +16,14 @@ struct EmailValidationData : RequestEmailValidation { /// 3PID verification. /// /// This parameter is deprecated with a plan to be removed in a future - /// specification version for ``/account/password`` and ``/register`` - /// requests. + /// 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. + /// Required if an `id_server` is supplied. QString idAccessToken; }; diff --git a/lib/csapi/definitions/request_msisdn_validation.h b/lib/csapi/definitions/request_msisdn_validation.h index 8539cd98..4600b48c 100644 --- a/lib/csapi/definitions/request_msisdn_validation.h +++ b/lib/csapi/definitions/request_msisdn_validation.h @@ -16,15 +16,14 @@ struct MsisdnValidationData : RequestMsisdnValidation { /// 3PID verification. /// /// This parameter is deprecated with a plan to be removed in a future - /// specification version for ``/account/password`` and ``/register`` - /// requests. + /// 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. + /// Required if an `id_server` is supplied. QString idAccessToken; }; diff --git a/lib/csapi/definitions/request_token_response.h b/lib/csapi/definitions/request_token_response.h index 00222839..d5fbbadb 100644 --- a/lib/csapi/definitions/request_token_response.h +++ b/lib/csapi/definitions/request_token_response.h @@ -10,22 +10,22 @@ 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 + /// 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 + /// 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 + /// advertises this specification version in the `/versions` response /// (ie: r0.5.0). - QString submitUrl; + QUrl submitUrl; }; template <> diff --git a/lib/csapi/definitions/room_event_filter.h b/lib/csapi/definitions/room_event_filter.h index 11e87fde..91caf667 100644 --- a/lib/csapi/definitions/room_event_filter.h +++ b/lib/csapi/definitions/room_event_filter.h @@ -11,30 +11,31 @@ 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``. + /// If `true`, enables lazy-loading of membership events. See + /// [Lazy-loading room + /// members](/client-server-api/#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``. + /// 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](/client-server-api/#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. + /// `'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. + /// 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; }; diff --git a/lib/csapi/definitions/sync_filter.h b/lib/csapi/definitions/sync_filter.h index 9c8f08d5..62e17962 100644 --- a/lib/csapi/definitions/sync_filter.h +++ b/lib/csapi/definitions/sync_filter.h @@ -14,13 +14,13 @@ namespace Quotient { 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`` + /// `'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`` + /// 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 diff --git a/lib/csapi/definitions/third_party_signed.h b/lib/csapi/definitions/third_party_signed.h index 526545d0..7097bda4 100644 --- a/lib/csapi/definitions/third_party_signed.h +++ b/lib/csapi/definitions/third_party_signed.h @@ -7,7 +7,7 @@ #include "converters.h" namespace Quotient { -/// A signature of an ``m.third_party_invite`` token to prove that this user +/// 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. diff --git a/lib/csapi/definitions/user_identifier.h b/lib/csapi/definitions/user_identifier.h index dadf6f97..cb585a6a 100644 --- a/lib/csapi/definitions/user_identifier.h +++ b/lib/csapi/definitions/user_identifier.h @@ -9,8 +9,9 @@ namespace Quotient { /// Identification information for a user struct UserIdentifier { - /// The type of identification. See `Identifier types`_ for supported - /// values and additional property descriptions. + /// The type of identification. See [Identifier + /// types](/client-server-api/#identifier-types) for supported values and + /// additional property descriptions. QString type; /// Identification information for a user diff --git a/lib/csapi/definitions/wellknown/homeserver.h b/lib/csapi/definitions/wellknown/homeserver.h index 5cfaca24..b7db4182 100644 --- a/lib/csapi/definitions/wellknown/homeserver.h +++ b/lib/csapi/definitions/wellknown/homeserver.h @@ -10,7 +10,7 @@ namespace Quotient { /// Used by clients to discover homeserver information. struct HomeserverInformation { /// The base URL for the homeserver for client-server connections. - QString baseUrl; + QUrl baseUrl; }; template <> diff --git a/lib/csapi/definitions/wellknown/identity_server.h b/lib/csapi/definitions/wellknown/identity_server.h index 3bd07bd1..885e3d34 100644 --- a/lib/csapi/definitions/wellknown/identity_server.h +++ b/lib/csapi/definitions/wellknown/identity_server.h @@ -10,7 +10,7 @@ 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; + QUrl baseUrl; }; template <> diff --git a/lib/csapi/device_management.cpp b/lib/csapi/device_management.cpp index eac9a545..da6dbc76 100644 --- a/lib/csapi/device_management.cpp +++ b/lib/csapi/device_management.cpp @@ -4,38 +4,35 @@ #include "device_management.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl GetDevicesJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/devices"); + makePath("/_matrix/client/r0", "/devices")); } GetDevicesJob::GetDevicesJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetDevicesJob"), - QStringLiteral("/_matrix/client/r0") % "/devices") + makePath("/_matrix/client/r0", "/devices")) {} QUrl GetDeviceJob::makeRequestUrl(QUrl baseUrl, const QString& deviceId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/devices/" % deviceId); + makePath("/_matrix/client/r0", "/devices/", + deviceId)); } GetDeviceJob::GetDeviceJob(const QString& deviceId) : BaseJob(HttpVerb::Get, QStringLiteral("GetDeviceJob"), - QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId) + makePath("/_matrix/client/r0", "/devices/", deviceId)) {} UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId, const QString& displayName) : BaseJob(HttpVerb::Put, QStringLiteral("UpdateDeviceJob"), - QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId) + makePath("/_matrix/client/r0", "/devices/", deviceId)) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("display_name"), displayName); @@ -45,7 +42,7 @@ UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId, DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId, const Omittable<AuthenticationData>& auth) : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), - QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId) + makePath("/_matrix/client/r0", "/devices/", deviceId)) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); @@ -55,7 +52,7 @@ DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId, DeleteDevicesJob::DeleteDevicesJob(const QStringList& devices, const Omittable<AuthenticationData>& auth) : BaseJob(HttpVerb::Post, QStringLiteral("DeleteDevicesJob"), - QStringLiteral("/_matrix/client/r0") % "/delete_devices") + makePath("/_matrix/client/r0", "/delete_devices")) { QJsonObject _data; addParam<>(_data, QStringLiteral("devices"), devices); diff --git a/lib/csapi/device_management.h b/lib/csapi/device_management.h index 47dc7ec8..7fb69873 100644 --- a/lib/csapi/device_management.h +++ b/lib/csapi/device_management.h @@ -83,7 +83,8 @@ public: /*! \brief Delete a device * - * This API endpoint uses the `User-Interactive Authentication API`_. + * This API endpoint uses the [User-Interactive Authentication + * API](/client-server-api/#user-interactive-authentication-api). * * Deletes the given device, and invalidates any access token associated with it. */ @@ -104,7 +105,8 @@ public: /*! \brief Bulk deletion of devices * - * This API endpoint uses the `User-Interactive Authentication API`_. + * This API endpoint uses the [User-Interactive Authentication + * API](/client-server-api/#user-interactive-authentication-api). * * Deletes the given devices, and invalidates any access token associated with * them. diff --git a/lib/csapi/directory.cpp b/lib/csapi/directory.cpp index 25ea82e2..b351b4ef 100644 --- a/lib/csapi/directory.cpp +++ b/lib/csapi/directory.cpp @@ -4,14 +4,11 @@ #include "directory.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId) : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomAliasJob"), - QStringLiteral("/_matrix/client/r0") % "/directory/room/" - % roomAlias) + makePath("/_matrix/client/r0", "/directory/room/", roomAlias)) { QJsonObject _data; addParam<>(_data, QStringLiteral("room_id"), roomId); @@ -21,41 +18,38 @@ SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId QUrl GetRoomIdByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/directory/room/" % roomAlias); + makePath("/_matrix/client/r0", + "/directory/room/", roomAlias)); } GetRoomIdByAliasJob::GetRoomIdByAliasJob(const QString& roomAlias) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomIdByAliasJob"), - QStringLiteral("/_matrix/client/r0") % "/directory/room/" - % roomAlias, + makePath("/_matrix/client/r0", "/directory/room/", roomAlias), false) {} QUrl DeleteRoomAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/directory/room/" % roomAlias); + makePath("/_matrix/client/r0", + "/directory/room/", roomAlias)); } DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias) : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomAliasJob"), - QStringLiteral("/_matrix/client/r0") % "/directory/room/" - % roomAlias) + makePath("/_matrix/client/r0", "/directory/room/", roomAlias)) {} QUrl GetLocalAliasesJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/aliases"); + makePath("/_matrix/client/r0", "/rooms/", + roomId, "/aliases")); } GetLocalAliasesJob::GetLocalAliasesJob(const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetLocalAliasesJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/aliases") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/aliases")) { addExpectedKey("aliases"); } diff --git a/lib/csapi/directory.h b/lib/csapi/directory.h index 9b109aad..93a31595 100644 --- a/lib/csapi/directory.h +++ b/lib/csapi/directory.h @@ -68,13 +68,13 @@ public: * 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. + * **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. + * update the `m.room.canonical_alias` event. */ class DeleteRoomAliasJob : public BaseJob { public: @@ -99,18 +99,18 @@ public: * 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 + * 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. + * **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: diff --git a/lib/csapi/event_context.cpp b/lib/csapi/event_context.cpp index d2a5f522..877838e2 100644 --- a/lib/csapi/event_context.cpp +++ b/lib/csapi/event_context.cpp @@ -4,13 +4,11 @@ #include "event_context.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; auto queryToGetEventContext(Omittable<int> limit, const QString& filter) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); addParam<IfNotEmpty>(_q, QStringLiteral("filter"), filter); return _q; @@ -22,9 +20,8 @@ QUrl GetEventContextJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& filter) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/context/" - % eventId, + makePath("/_matrix/client/r0", "/rooms/", + roomId, "/context/", eventId), queryToGetEventContext(limit, filter)); } @@ -33,7 +30,7 @@ GetEventContextJob::GetEventContextJob(const QString& roomId, Omittable<int> limit, const QString& filter) : BaseJob(HttpVerb::Get, QStringLiteral("GetEventContextJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/context/" % eventId, + makePath("/_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 d82d16ab..4e50edf3 100644 --- a/lib/csapi/event_context.h +++ b/lib/csapi/event_context.h @@ -16,8 +16,8 @@ namespace Quotient { * 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. + * [Lazy-loading room members](/client-server-api/#lazy-loading-room-members) + * for more information. */ class GetEventContextJob : public BaseJob { public: @@ -33,13 +33,13 @@ public: * 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 + * 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. + * See [Filtering](/client-server-api/#filtering) for more information. */ explicit GetEventContextJob(const QString& roomId, const QString& eventId, Omittable<int> limit = none, diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp index bb3a893f..38c68be7 100644 --- a/lib/csapi/filter.cpp +++ b/lib/csapi/filter.cpp @@ -4,16 +4,13 @@ #include "filter.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; DefineFilterJob::DefineFilterJob(const QString& userId, const Filter& filter) : BaseJob(HttpVerb::Post, QStringLiteral("DefineFilterJob"), - QStringLiteral("/_matrix/client/r0") % "/user/" % userId - % "/filter") + makePath("/_matrix/client/r0", "/user/", userId, "/filter")) { - setRequestData(Data(toJson(filter))); + setRequestData(RequestData(toJson(filter))); addExpectedKey("filter_id"); } @@ -21,12 +18,12 @@ QUrl GetFilterJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& filterId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") % "/user/" - % userId % "/filter/" % filterId); + makePath("/_matrix/client/r0", "/user/", + userId, "/filter/", filterId)); } GetFilterJob::GetFilterJob(const QString& userId, const QString& filterId) : BaseJob(HttpVerb::Get, QStringLiteral("GetFilterJob"), - QStringLiteral("/_matrix/client/r0") % "/user/" % userId - % "/filter/" % filterId) + makePath("/_matrix/client/r0", "/user/", userId, "/filter/", + filterId)) {} diff --git a/lib/csapi/filter.h b/lib/csapi/filter.h index f07b489c..01bec36b 100644 --- a/lib/csapi/filter.h +++ b/lib/csapi/filter.h @@ -32,7 +32,7 @@ public: // Result properties /// The ID of the filter that was created. Cannot start - /// with a ``{`` as this character is used to determine + /// 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); } diff --git a/lib/csapi/inviting.cpp b/lib/csapi/inviting.cpp index 01620f9e..39d24611 100644 --- a/lib/csapi/inviting.cpp +++ b/lib/csapi/inviting.cpp @@ -4,16 +4,15 @@ #include "inviting.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; -InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId) +InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId, + const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("InviteUserJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/invite") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/invite")) { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); + addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); setRequestData(std::move(_data)); } diff --git a/lib/csapi/inviting.h b/lib/csapi/inviting.h index 59a61b89..eb13cc95 100644 --- a/lib/csapi/inviting.h +++ b/lib/csapi/inviting.h @@ -10,12 +10,11 @@ namespace Quotient { /*! \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`_. + * [third party invites + * section](/client-server-api/#post_matrixclientr0roomsroomidinvite-1). * * This API invites a user to participate in a particular room. * They do not start participating in the room until they actually join the @@ -25,9 +24,7 @@ namespace Quotient { * 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`_ + * `m.room.member` event to the room. */ class InviteUserJob : public BaseJob { public: @@ -38,8 +35,13 @@ public: * * \param userId * The fully qualified user ID of the invitee. + * + * \param reason + * Optional reason to be included as the `reason` on the subsequent + * membership event. */ - explicit InviteUserJob(const QString& roomId, const QString& userId); + explicit InviteUserJob(const QString& roomId, const QString& userId, + const QString& reason = {}); }; } // namespace Quotient diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp index 4761e949..373c1c6a 100644 --- a/lib/csapi/joining.cpp +++ b/lib/csapi/joining.cpp @@ -4,39 +4,41 @@ #include "joining.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; JoinRoomByIdJob::JoinRoomByIdJob( - const QString& roomId, const Omittable<ThirdPartySigned>& thirdPartySigned) + const QString& roomId, const Omittable<ThirdPartySigned>& thirdPartySigned, + const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomByIdJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/join") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/join")) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("third_party_signed"), thirdPartySigned); + addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); setRequestData(std::move(_data)); addExpectedKey("room_id"); } auto queryToJoinRoom(const QStringList& serverName) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("server_name"), serverName); return _q; } JoinRoomJob::JoinRoomJob(const QString& roomIdOrAlias, const QStringList& serverName, - const Omittable<ThirdPartySigned>& thirdPartySigned) + const Omittable<ThirdPartySigned>& thirdPartySigned, + const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/join/" % roomIdOrAlias, + makePath("/_matrix/client/r0", "/join/", roomIdOrAlias), queryToJoinRoom(serverName)) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("third_party_signed"), thirdPartySigned); + addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); setRequestData(std::move(_data)); addExpectedKey("room_id"); } diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h index dd936f92..d0199b11 100644 --- a/lib/csapi/joining.h +++ b/lib/csapi/joining.h @@ -13,7 +13,7 @@ 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.* + * `/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 @@ -21,7 +21,9 @@ namespace Quotient { * 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. + * response of the + * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and + * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs. */ class JoinRoomByIdJob : public BaseJob { public: @@ -32,12 +34,17 @@ public: * * \param thirdPartySigned * If supplied, the homeserver must verify that it matches a pending - * ``m.room.third_party_invite`` event in the room, and perform + * `m.room.third_party_invite` event in the room, and perform * key validity checking if required by the event. + * + * \param reason + * Optional reason to be included as the `reason` on the subsequent + * membership event. */ explicit JoinRoomByIdJob( const QString& roomId, - const Omittable<ThirdPartySigned>& thirdPartySigned = none); + const Omittable<ThirdPartySigned>& thirdPartySigned = none, + const QString& reason = {}); // Result properties @@ -48,7 +55,7 @@ public: /*! \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``. + * `/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 @@ -56,7 +63,9 @@ public: * 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. + * response of the + * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and + * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs. */ class JoinRoomJob : public BaseJob { public: @@ -70,13 +79,18 @@ public: * 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 + * 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. + * + * \param reason + * Optional reason to be included as the `reason` on the subsequent + * membership event. */ explicit JoinRoomJob( const QString& roomIdOrAlias, const QStringList& serverName = {}, - const Omittable<ThirdPartySigned>& thirdPartySigned = none); + const Omittable<ThirdPartySigned>& thirdPartySigned = none, + const QString& reason = {}); // Result properties diff --git a/lib/csapi/keys.cpp b/lib/csapi/keys.cpp index 34ab47c9..d6bd2fab 100644 --- a/lib/csapi/keys.cpp +++ b/lib/csapi/keys.cpp @@ -4,14 +4,12 @@ #include "keys.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; UploadKeysJob::UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys, const QHash<QString, QVariant>& oneTimeKeys) : BaseJob(HttpVerb::Post, QStringLiteral("UploadKeysJob"), - QStringLiteral("/_matrix/client/r0") % "/keys/upload") + makePath("/_matrix/client/r0", "/keys/upload")) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("device_keys"), deviceKeys); @@ -23,7 +21,7 @@ UploadKeysJob::UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys, QueryKeysJob::QueryKeysJob(const QHash<QString, QStringList>& deviceKeys, Omittable<int> timeout, const QString& token) : BaseJob(HttpVerb::Post, QStringLiteral("QueryKeysJob"), - QStringLiteral("/_matrix/client/r0") % "/keys/query") + makePath("/_matrix/client/r0", "/keys/query")) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout); @@ -36,7 +34,7 @@ ClaimKeysJob::ClaimKeysJob( const QHash<QString, QHash<QString, QString>>& oneTimeKeys, Omittable<int> timeout) : BaseJob(HttpVerb::Post, QStringLiteral("ClaimKeysJob"), - QStringLiteral("/_matrix/client/r0") % "/keys/claim") + makePath("/_matrix/client/r0", "/keys/claim")) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout); @@ -47,7 +45,7 @@ ClaimKeysJob::ClaimKeysJob( auto queryToGetKeysChanges(const QString& from, const QString& to) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("from"), from); addParam<>(_q, QStringLiteral("to"), to); return _q; @@ -57,13 +55,13 @@ QUrl GetKeysChangesJob::makeRequestUrl(QUrl baseUrl, const QString& from, const QString& to) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/keys/changes", + makePath("/_matrix/client/r0", + "/keys/changes"), queryToGetKeysChanges(from, to)); } GetKeysChangesJob::GetKeysChangesJob(const QString& from, const QString& to) : BaseJob(HttpVerb::Get, QStringLiteral("GetKeysChangesJob"), - QStringLiteral("/_matrix/client/r0") % "/keys/changes", + makePath("/_matrix/client/r0", "/keys/changes"), queryToGetKeysChanges(from, to)) {} diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h index 8f6c8cc9..7db09e8d 100644 --- a/lib/csapi/keys.h +++ b/lib/csapi/keys.h @@ -4,6 +4,7 @@ #pragma once +#include "csapi/definitions/cross_signing_key.h" #include "csapi/definitions/device_keys.h" #include "jobs/basejob.h" @@ -25,8 +26,8 @@ public: * \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>`_. + * `<algorithm>:<key_id>`. The format of the key is determined + * by the [key algorithm](/client-server-api/#key-algorithms). * * May be absent if no new one-time keys are required. */ @@ -98,7 +99,7 @@ public: /// /// 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. + /// user or device is missing from the `device_keys` result. QHash<QString, QJsonObject> failures() const { return loadFromJson<QHash<QString, QJsonObject>>("failures"_ls); @@ -107,13 +108,45 @@ public: /// 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`` + /// `/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); } + + /// Information on the master cross-signing keys of the queried users. + /// A map from user ID, to master key information. For each key, the + /// information returned will be the same as uploaded via + /// `/keys/device_signing/upload`, along with the signatures + /// uploaded via `/keys/signatures/upload` that the requesting user + /// is allowed to see. + QHash<QString, CrossSigningKey> masterKeys() const + { + return loadFromJson<QHash<QString, CrossSigningKey>>("master_keys"_ls); + } + + /// Information on the self-signing keys of the queried users. A map + /// from user ID, to self-signing key information. For each key, the + /// information returned will be the same as uploaded via + /// `/keys/device_signing/upload`. + QHash<QString, CrossSigningKey> selfSigningKeys() const + { + return loadFromJson<QHash<QString, CrossSigningKey>>( + "self_signing_keys"_ls); + } + + /// Information on the user-signing key of the user making the + /// request, if they queried their own device information. A map + /// from user ID, to user-signing key information. The + /// information returned will be the same as uploaded via + /// `/keys/device_signing/upload`. + QHash<QString, CrossSigningKey> userSigningKeys() const + { + return loadFromJson<QHash<QString, CrossSigningKey>>( + "user_signing_keys"_ls); + } }; template <> @@ -163,18 +196,17 @@ public: /// /// 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. + /// user or device is missing from the `one_time_keys` result. QHash<QString, QJsonObject> failures() const { return loadFromJson<QHash<QString, QJsonObject>>("failures"_ls); } /// 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. + /// map from devices to a map from `<algorithm>:<key_id>` to the key object. /// - /// See the `key algorithms <#key-algorithms>`_ section for information - /// on the Key Object format. + /// See the [key algorithms](/client-server-api/#key-algorithms) section for + /// information on the Key Object format. QHash<QString, QHash<QString, QVariant>> oneTimeKeys() const { return loadFromJson<QHash<QString, QHash<QString, QVariant>>>( @@ -190,26 +222,28 @@ public: * 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* + * membership state `join`); *and* * * added new device identity keys or removed an existing device with - * identity keys, between ``from`` and ``to``. + * 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 + * The desired start point of the list. Should be the `next_batch` field + * from a response to an earlier call to + * [`/sync`](/client-server-api/#get_matrixclientr0sync). 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. + * The desired end point of the list. Should be the `next_batch` + * field from a recent call to + * [`/sync`](/client-server-api/#get_matrixclientr0sync) - 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); diff --git a/lib/csapi/kicking.cpp b/lib/csapi/kicking.cpp index 7de5ce01..433e592c 100644 --- a/lib/csapi/kicking.cpp +++ b/lib/csapi/kicking.cpp @@ -4,14 +4,12 @@ #include "kicking.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; KickJob::KickJob(const QString& roomId, const QString& userId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("KickJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/kick") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/kick")) { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); diff --git a/lib/csapi/kicking.h b/lib/csapi/kicking.h index 2645a54f..11018368 100644 --- a/lib/csapi/kicking.h +++ b/lib/csapi/kicking.h @@ -15,10 +15,10 @@ namespace Quotient { * 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 + * 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>``. + * `/rooms/<room id>/state/m.room.member/<user id>`. */ class KickJob : public BaseJob { public: @@ -32,7 +32,8 @@ public: * * \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. + * `reason` on the target's updated + * [`m.room.member`](/client-server-api/#mroommember) event. */ explicit KickJob(const QString& roomId, const QString& userId, const QString& reason = {}); diff --git a/lib/csapi/knocking.cpp b/lib/csapi/knocking.cpp new file mode 100644 index 00000000..73e13e6e --- /dev/null +++ b/lib/csapi/knocking.cpp @@ -0,0 +1,26 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "knocking.h" + +using namespace Quotient; + +auto queryToKnockRoom(const QStringList& serverName) +{ + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("server_name"), serverName); + return _q; +} + +KnockRoomJob::KnockRoomJob(const QString& roomIdOrAlias, + const QStringList& serverName, const QString& reason) + : BaseJob(HttpVerb::Post, QStringLiteral("KnockRoomJob"), + makePath("/_matrix/client/r0", "/knock/", roomIdOrAlias), + queryToKnockRoom(serverName)) +{ + QJsonObject _data; + addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); + setRequestData(std::move(_data)); + addExpectedKey("room_id"); +} diff --git a/lib/csapi/knocking.h b/lib/csapi/knocking.h new file mode 100644 index 00000000..1108cb64 --- /dev/null +++ b/lib/csapi/knocking.h @@ -0,0 +1,55 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Knock on a room, requesting permission to join. + * + * *Note that this API takes either a room ID or alias, unlike other membership + * APIs.* + * + * This API "knocks" on the room to ask for permission to join, if the user + * is allowed to knock on the room. Acceptance of the knock happens out of + * band from this API, meaning that the client will have to watch for updates + * regarding the acceptance/rejection of the knock. + * + * If the room history settings allow, the user will still be able to see + * history of the room while being in the "knock" state. The user will have + * to accept the invitation to join the room (acceptance of knock) to see + * messages reliably. See the `/join` endpoints for more information about + * history visibility to the user. + * + * The knock will appear as an entry in the response of the + * [`/sync`](/client-server-api/#get_matrixclientr0sync) API. + */ +class KnockRoomJob : public BaseJob { +public: + /*! \brief Knock on a room, requesting permission to join. + * + * \param roomIdOrAlias + * The room identifier or alias to knock upon. + * + * \param serverName + * The servers to attempt to knock on the room through. One of the servers + * must be participating in the room. + * + * \param reason + * Optional reason to be included as the `reason` on the subsequent + * membership event. + */ + explicit KnockRoomJob(const QString& roomIdOrAlias, + const QStringList& serverName = {}, + const QString& reason = {}); + + // Result properties + + /// The knocked room ID. + QString roomId() const { return loadFromJson<QString>("room_id"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/leaving.cpp b/lib/csapi/leaving.cpp index 8bd170bf..0e5386be 100644 --- a/lib/csapi/leaving.cpp +++ b/lib/csapi/leaving.cpp @@ -4,32 +4,25 @@ #include "leaving.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; -QUrl LeaveRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) +LeaveRoomJob::LeaveRoomJob(const QString& roomId, const QString& reason) + : BaseJob(HttpVerb::Post, QStringLiteral("LeaveRoomJob"), + makePath("/_matrix/client/r0", "/rooms/", roomId, "/leave")) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/leave"); + QJsonObject _data; + addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); + setRequestData(std::move(_data)); } -LeaveRoomJob::LeaveRoomJob(const QString& roomId) - : 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), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/forget"); + makePath("/_matrix/client/r0", "/rooms/", + roomId, "/forget")); } ForgetRoomJob::ForgetRoomJob(const QString& roomId) : BaseJob(HttpVerb::Post, QStringLiteral("ForgetRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/forget") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/forget")) {} diff --git a/lib/csapi/leaving.h b/lib/csapi/leaving.h index 1bea7e41..2e402d16 100644 --- a/lib/csapi/leaving.h +++ b/lib/csapi/leaving.h @@ -28,15 +28,12 @@ public: * * \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. + * \param reason + * Optional reason to be included as the `reason` on the subsequent + * membership event. */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); + explicit LeaveRoomJob(const QString& roomId, const QString& reason = {}); }; /*! \brief Stop the requesting user remembering about a particular room. diff --git a/lib/csapi/list_joined_rooms.cpp b/lib/csapi/list_joined_rooms.cpp index 8d7e267f..22ba04da 100644 --- a/lib/csapi/list_joined_rooms.cpp +++ b/lib/csapi/list_joined_rooms.cpp @@ -4,20 +4,17 @@ #include "list_joined_rooms.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl GetJoinedRoomsJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/joined_rooms"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/r0", "/joined_rooms")); } GetJoinedRoomsJob::GetJoinedRoomsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedRoomsJob"), - QStringLiteral("/_matrix/client/r0") % "/joined_rooms") + makePath("/_matrix/client/r0", "/joined_rooms")) { addExpectedKey("joined_rooms"); } diff --git a/lib/csapi/list_joined_rooms.h b/lib/csapi/list_joined_rooms.h index 1034aa7b..59a24a49 100644 --- a/lib/csapi/list_joined_rooms.h +++ b/lib/csapi/list_joined_rooms.h @@ -26,7 +26,7 @@ public: // Result properties - /// The ID of each room in which the user has ``joined`` membership. + /// The ID of each room in which the user has `joined` membership. QStringList joinedRooms() const { return loadFromJson<QStringList>("joined_rooms"_ls); diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp index 415d816c..25f8da5c 100644 --- a/lib/csapi/list_public_rooms.cpp +++ b/lib/csapi/list_public_rooms.cpp @@ -4,31 +4,27 @@ #include "list_public_rooms.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl GetRoomVisibilityOnDirectoryJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/directory/list/room/" % roomId); + makePath("/_matrix/client/r0", + "/directory/list/room/", roomId)); } GetRoomVisibilityOnDirectoryJob::GetRoomVisibilityOnDirectoryJob( const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomVisibilityOnDirectoryJob"), - QStringLiteral("/_matrix/client/r0") % "/directory/list/room/" - % roomId, + makePath("/_matrix/client/r0", "/directory/list/room/", roomId), false) {} SetRoomVisibilityOnDirectoryJob::SetRoomVisibilityOnDirectoryJob( const QString& roomId, const QString& visibility) : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomVisibilityOnDirectoryJob"), - QStringLiteral("/_matrix/client/r0") % "/directory/list/room/" - % roomId) + makePath("/_matrix/client/r0", "/directory/list/room/", roomId)) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("visibility"), visibility); @@ -38,7 +34,7 @@ SetRoomVisibilityOnDirectoryJob::SetRoomVisibilityOnDirectoryJob( auto queryToGetPublicRooms(Omittable<int> limit, const QString& since, const QString& server) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); addParam<IfNotEmpty>(_q, QStringLiteral("since"), since); addParam<IfNotEmpty>(_q, QStringLiteral("server"), server); @@ -50,15 +46,15 @@ QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, Omittable<int> limit, const QString& server) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/publicRooms", + makePath("/_matrix/client/r0", + "/publicRooms"), queryToGetPublicRooms(limit, since, server)); } GetPublicRoomsJob::GetPublicRoomsJob(Omittable<int> limit, const QString& since, const QString& server) : BaseJob(HttpVerb::Get, QStringLiteral("GetPublicRoomsJob"), - QStringLiteral("/_matrix/client/r0") % "/publicRooms", + makePath("/_matrix/client/r0", "/publicRooms"), queryToGetPublicRooms(limit, since, server), {}, false) { addExpectedKey("chunk"); @@ -66,7 +62,7 @@ GetPublicRoomsJob::GetPublicRoomsJob(Omittable<int> limit, const QString& since, auto queryToQueryPublicRooms(const QString& server) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("server"), server); return _q; } @@ -78,7 +74,7 @@ QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, Omittable<bool> includeAllNetworks, const QString& thirdPartyInstanceId) : BaseJob(HttpVerb::Post, QStringLiteral("QueryPublicRoomsJob"), - QStringLiteral("/_matrix/client/r0") % "/publicRooms", + makePath("/_matrix/client/r0", "/publicRooms"), queryToQueryPublicRooms(server)) { QJsonObject _data; diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h index b0cb79d2..963c8b56 100644 --- a/lib/csapi/list_public_rooms.h +++ b/lib/csapi/list_public_rooms.h @@ -170,7 +170,7 @@ public: * * \param thirdPartyInstanceId * The specific third party network/protocol to request from the - * homeserver. Can only be used if ``include_all_networks`` is false. + * homeserver. Can only be used if `include_all_networks` is false. */ explicit QueryPublicRoomsJob(const QString& server = {}, Omittable<int> limit = none, diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp index a5bac9ea..71fd93c5 100644 --- a/lib/csapi/login.cpp +++ b/lib/csapi/login.cpp @@ -4,20 +4,17 @@ #include "login.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl GetLoginFlowsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/login"); + makePath("/_matrix/client/r0", "/login")); } GetLoginFlowsJob::GetLoginFlowsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetLoginFlowsJob"), - QStringLiteral("/_matrix/client/r0") % "/login", false) + makePath("/_matrix/client/r0", "/login"), false) {} LoginJob::LoginJob(const QString& type, @@ -26,7 +23,7 @@ LoginJob::LoginJob(const QString& type, const QString& deviceId, const QString& initialDeviceDisplayName) : BaseJob(HttpVerb::Post, QStringLiteral("LoginJob"), - QStringLiteral("/_matrix/client/r0") % "/login", false) + makePath("/_matrix/client/r0", "/login"), false) { QJsonObject _data; addParam<>(_data, QStringLiteral("type"), type); diff --git a/lib/csapi/login.h b/lib/csapi/login.h index a406fc79..b35db1eb 100644 --- a/lib/csapi/login.h +++ b/lib/csapi/login.h @@ -14,17 +14,17 @@ namespace Quotient { /*! \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. + * 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 + /// 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 + /// The login type. This is supplied as the `type` when /// logging in. QString type; }; @@ -64,13 +64,14 @@ struct JsonObjectConverter<GetLoginFlowsJob::LoginFlow> { * 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 + * 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`` + * 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`_. + * [Relationship between access tokens and + * devices](/client-server-api/#relationship-between-access-tokens-and-devices). */ class LoginJob : public BaseJob { public: @@ -83,30 +84,33 @@ public: * 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 + * 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`` + * 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`_. + * [Relationship between access tokens and + * devices](/client-server-api/#relationship-between-access-tokens-and-devices). * * \param password - * Required when ``type`` is ``m.login.password``. The user's + * 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. + * 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. + * known client device, a new device will be created. The given + * device ID must not be the same as a + * [cross-signing](/client-server-api/#cross-signing) key ID. + * 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. + * if `device_id` corresponds to a known device. */ explicit LoginJob(const QString& type, const Omittable<UserIdentifier>& identifier = none, @@ -130,8 +134,8 @@ public: /// 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. + /// `user_id` (by splitting at the first colon) if they require + /// it. Note also that `homeserver` is not spelt this way. QString homeServer() const { return loadFromJson<QString>("home_server"_ls); diff --git a/lib/csapi/logout.cpp b/lib/csapi/logout.cpp index 9583b8ec..e8083e31 100644 --- a/lib/csapi/logout.cpp +++ b/lib/csapi/logout.cpp @@ -4,30 +4,26 @@ #include "logout.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl LogoutJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/logout"); + makePath("/_matrix/client/r0", "/logout")); } LogoutJob::LogoutJob() : BaseJob(HttpVerb::Post, QStringLiteral("LogoutJob"), - QStringLiteral("/_matrix/client/r0") % "/logout") + makePath("/_matrix/client/r0", "/logout")) {} QUrl LogoutAllJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/logout/all"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/r0", "/logout/all")); } LogoutAllJob::LogoutAllJob() : BaseJob(HttpVerb::Post, QStringLiteral("LogoutAllJob"), - QStringLiteral("/_matrix/client/r0") % "/logout/all") + makePath("/_matrix/client/r0", "/logout/all")) {} diff --git a/lib/csapi/logout.h b/lib/csapi/logout.h index 78f14e40..2e4c2692 100644 --- a/lib/csapi/logout.h +++ b/lib/csapi/logout.h @@ -12,7 +12,8 @@ namespace Quotient { * * 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. + * [Device keys](/client-server-api/#device-keys) for the device are deleted + * alongside the device. */ class LogoutJob : public BaseJob { public: @@ -31,10 +32,12 @@ public: * * 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. + * devices for the user are also deleted. [Device + * keys](/client-server-api/#device-keys) for the device are deleted alongside + * the device. * - * This endpoint does not use the `User-Interactive Authentication API`_ because + * This endpoint does not use the [User-Interactive Authentication + * API](/client-server-api/#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 diff --git a/lib/csapi/message_pagination.cpp b/lib/csapi/message_pagination.cpp index 855c051f..1a93b75b 100644 --- a/lib/csapi/message_pagination.cpp +++ b/lib/csapi/message_pagination.cpp @@ -4,15 +4,13 @@ #include "message_pagination.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; auto queryToGetRoomEvents(const QString& from, const QString& to, const QString& dir, Omittable<int> limit, const QString& filter) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("from"), from); addParam<IfNotEmpty>(_q, QStringLiteral("to"), to); addParam<>(_q, QStringLiteral("dir"), dir); @@ -28,7 +26,7 @@ QUrl GetRoomEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, { return BaseJob::makeRequestUrl( std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/messages", + makePath("/_matrix/client/r0", "/rooms/", roomId, "/messages"), queryToGetRoomEvents(from, to, dir, limit, filter)); } @@ -36,7 +34,6 @@ 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", + makePath("/_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 286d4237..363e4d99 100644 --- a/lib/csapi/message_pagination.h +++ b/lib/csapi/message_pagination.h @@ -15,8 +15,8 @@ namespace Quotient { * 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. + * [Lazy-loading room members](/client-server-api/#lazy-loading-room-members) + * for more information. */ class GetRoomEventsJob : public BaseJob { public: @@ -27,8 +27,8 @@ public: * * \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 + * 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 @@ -36,8 +36,8 @@ public: * * \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 + * 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 @@ -64,25 +64,25 @@ public: // Result properties - /// The token the pagination starts from. If ``dir=b`` this will be - /// the token supplied in ``from``. + /// 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); } - /// The token the pagination ends at. If ``dir=b`` this token should + /// 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); } - /// 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. + /// 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); } - /// 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``. + /// 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 + /// 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. diff --git a/lib/csapi/notifications.cpp b/lib/csapi/notifications.cpp index a479d500..1e523c6f 100644 --- a/lib/csapi/notifications.cpp +++ b/lib/csapi/notifications.cpp @@ -4,14 +4,12 @@ #include "notifications.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; auto queryToGetNotifications(const QString& from, Omittable<int> limit, const QString& only) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); addParam<IfNotEmpty>(_q, QStringLiteral("only"), only); @@ -23,8 +21,8 @@ QUrl GetNotificationsJob::makeRequestUrl(QUrl baseUrl, const QString& from, const QString& only) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/notifications", + makePath("/_matrix/client/r0", + "/notifications"), queryToGetNotifications(from, limit, only)); } @@ -32,7 +30,7 @@ GetNotificationsJob::GetNotificationsJob(const QString& from, Omittable<int> limit, const QString& only) : BaseJob(HttpVerb::Get, QStringLiteral("GetNotificationsJob"), - QStringLiteral("/_matrix/client/r0") % "/notifications", + makePath("/_matrix/client/r0", "/notifications"), queryToGetNotifications(from, limit, only)) { addExpectedKey("notifications"); diff --git a/lib/csapi/notifications.h b/lib/csapi/notifications.h index ff499c7a..0c38fe6b 100644 --- a/lib/csapi/notifications.h +++ b/lib/csapi/notifications.h @@ -22,7 +22,7 @@ public: /// 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`_. + /// See [Push Rules: API](/client-server-api/#push-rules-api). QVector<QVariant> actions; /// The Event object for the event that triggered the notification. EventPtr event; @@ -35,7 +35,7 @@ public: QString roomId; /// The unix timestamp at which the event notification was sent, /// in milliseconds. - int ts; + qint64 ts; }; // Construction/destruction @@ -49,7 +49,7 @@ public: * Limit on the number of events to return in this request. * * \param only - * Allows basic filtering of events returned. Supply ``highlight`` + * Allows basic filtering of events returned. Supply `highlight` * to return only events where the notification had the highlight * tweak set. */ @@ -68,8 +68,8 @@ public: // Result properties - /// The token to supply in the ``from`` param of the next - /// ``/notifications`` request in order to request more + /// 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); } diff --git a/lib/csapi/openid.cpp b/lib/csapi/openid.cpp index 3941e9c0..5c93a2d7 100644 --- a/lib/csapi/openid.cpp +++ b/lib/csapi/openid.cpp @@ -4,15 +4,13 @@ #include "openid.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; RequestOpenIdTokenJob::RequestOpenIdTokenJob(const QString& userId, const QJsonObject& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestOpenIdTokenJob"), - QStringLiteral("/_matrix/client/r0") % "/user/" % userId - % "/openid/request_token") + makePath("/_matrix/client/r0", "/user/", userId, + "/openid/request_token")) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); } diff --git a/lib/csapi/openid.h b/lib/csapi/openid.h index efb5f623..0be39c8c 100644 --- a/lib/csapi/openid.h +++ b/lib/csapi/openid.h @@ -18,7 +18,7 @@ namespace Quotient { * 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 + * be used to request another OpenID access token or call `/sync`, for * example. */ class RequestOpenIdTokenJob : public BaseJob { @@ -38,9 +38,10 @@ public: // 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, + /// 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()); } }; diff --git a/lib/csapi/peeking_events.cpp b/lib/csapi/peeking_events.cpp index 70a5b6f3..eb5d22fa 100644 --- a/lib/csapi/peeking_events.cpp +++ b/lib/csapi/peeking_events.cpp @@ -4,14 +4,12 @@ #include "peeking_events.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; auto queryToPeekEvents(const QString& from, Omittable<int> timeout, const QString& roomId) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); addParam<IfNotEmpty>(_q, QStringLiteral("timeout"), timeout); addParam<IfNotEmpty>(_q, QStringLiteral("room_id"), roomId); @@ -22,14 +20,13 @@ QUrl PeekEventsJob::makeRequestUrl(QUrl baseUrl, const QString& from, Omittable<int> timeout, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/events", + makePath("/_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", + makePath("/_matrix/client/r0", "/events"), queryToPeekEvents(from, timeout, roomId)) {} diff --git a/lib/csapi/peeking_events.h b/lib/csapi/peeking_events.h index cecd9f2d..885ff340 100644 --- a/lib/csapi/peeking_events.h +++ b/lib/csapi/peeking_events.h @@ -13,12 +13,12 @@ namespace Quotient { * * 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. + * the `timeout` is reached. * - * This API is the same as the normal ``/events`` endpoint, but can be + * 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 + * 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. */ @@ -51,12 +51,12 @@ public: // Result properties - /// A token which correlates to the first value in ``chunk``. This - /// is usually the same token supplied to ``from=``. + /// 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 last value in ``chunk``. This - /// token should be used in the next request to ``/events``. + /// 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); } /// An array of events. diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp index 58d0d157..4f77c466 100644 --- a/lib/csapi/presence.cpp +++ b/lib/csapi/presence.cpp @@ -4,15 +4,12 @@ #include "presence.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence, const QString& statusMsg) : BaseJob(HttpVerb::Put, QStringLiteral("SetPresenceJob"), - QStringLiteral("/_matrix/client/r0") % "/presence/" % userId - % "/status") + makePath("/_matrix/client/r0", "/presence/", userId, "/status")) { QJsonObject _data; addParam<>(_data, QStringLiteral("presence"), presence); @@ -23,14 +20,13 @@ SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence, QUrl GetPresenceJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/presence/" % userId % "/status"); + makePath("/_matrix/client/r0", "/presence/", + userId, "/status")); } GetPresenceJob::GetPresenceJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetPresenceJob"), - QStringLiteral("/_matrix/client/r0") % "/presence/" % userId - % "/status") + makePath("/_matrix/client/r0", "/presence/", userId, "/status")) { addExpectedKey("presence"); } diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h index a885bf4f..4ab50e25 100644 --- a/lib/csapi/presence.h +++ b/lib/csapi/presence.h @@ -12,7 +12,7 @@ namespace Quotient { * * 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 + * not need to specify the `last_active_ago` field. You cannot set the * presence state of another user. */ class SetPresenceJob : public BaseJob { diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp index 8436b8e6..64ac84ca 100644 --- a/lib/csapi/profile.cpp +++ b/lib/csapi/profile.cpp @@ -4,15 +4,12 @@ #include "profile.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; SetDisplayNameJob::SetDisplayNameJob(const QString& userId, const QString& displayname) : BaseJob(HttpVerb::Put, QStringLiteral("SetDisplayNameJob"), - QStringLiteral("/_matrix/client/r0") % "/profile/" % userId - % "/displayname") + makePath("/_matrix/client/r0", "/profile/", userId, "/displayname")) { QJsonObject _data; addParam<>(_data, QStringLiteral("displayname"), displayname); @@ -22,21 +19,19 @@ SetDisplayNameJob::SetDisplayNameJob(const QString& userId, QUrl GetDisplayNameJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/profile/" % userId % "/displayname"); + makePath("/_matrix/client/r0", "/profile/", + userId, "/displayname")); } GetDisplayNameJob::GetDisplayNameJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetDisplayNameJob"), - QStringLiteral("/_matrix/client/r0") % "/profile/" % userId - % "/displayname", + makePath("/_matrix/client/r0", "/profile/", userId, "/displayname"), false) {} -SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QString& avatarUrl) +SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl) : BaseJob(HttpVerb::Put, QStringLiteral("SetAvatarUrlJob"), - QStringLiteral("/_matrix/client/r0") % "/profile/" % userId - % "/avatar_url") + makePath("/_matrix/client/r0", "/profile/", userId, "/avatar_url")) { QJsonObject _data; addParam<>(_data, QStringLiteral("avatar_url"), avatarUrl); @@ -46,25 +41,24 @@ SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QString& avatarUrl QUrl GetAvatarUrlJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/profile/" % userId % "/avatar_url"); + makePath("/_matrix/client/r0", "/profile/", + userId, "/avatar_url")); } GetAvatarUrlJob::GetAvatarUrlJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetAvatarUrlJob"), - QStringLiteral("/_matrix/client/r0") % "/profile/" % userId - % "/avatar_url", + makePath("/_matrix/client/r0", "/profile/", userId, "/avatar_url"), false) {} QUrl GetUserProfileJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/profile/" % userId); + makePath("/_matrix/client/r0", "/profile/", + userId)); } GetUserProfileJob::GetUserProfileJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetUserProfileJob"), - QStringLiteral("/_matrix/client/r0") % "/profile/" % userId, false) + makePath("/_matrix/client/r0", "/profile/", userId), false) {} diff --git a/lib/csapi/profile.h b/lib/csapi/profile.h index 3858fab2..7f9c9e95 100644 --- a/lib/csapi/profile.h +++ b/lib/csapi/profile.h @@ -11,7 +11,7 @@ namespace Quotient { /*! \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``. + * set this user's display name, e.g. you need to have their `access_token`. */ class SetDisplayNameJob : public BaseJob { public: @@ -61,7 +61,7 @@ public: /*! \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``. + * set this user's avatar URL, e.g. you need to have their `access_token`. */ class SetAvatarUrlJob : public BaseJob { public: @@ -73,7 +73,7 @@ public: * \param avatarUrl * The new avatar URL for this user. */ - explicit SetAvatarUrlJob(const QString& userId, const QString& avatarUrl); + explicit SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl); }; /*! \brief Get the user's avatar URL. @@ -101,7 +101,7 @@ public: // Result properties /// The user's avatar URL if they have set one, otherwise not present. - QString avatarUrl() const { return loadFromJson<QString>("avatar_url"_ls); } + QUrl avatarUrl() const { return loadFromJson<QUrl>("avatar_url"_ls); } }; /*! \brief Get this user's profile information. @@ -109,7 +109,7 @@ public: * 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``. + * limited to `displayname` or `avatar_url`. */ class GetUserProfileJob : public BaseJob { public: @@ -130,7 +130,7 @@ public: // Result properties /// The user's avatar URL if they have set one, otherwise not present. - QString avatarUrl() const { return loadFromJson<QString>("avatar_url"_ls); } + QUrl avatarUrl() const { return loadFromJson<QUrl>("avatar_url"_ls); } /// The user's display name if they have set one, otherwise not present. QString displayname() const diff --git a/lib/csapi/pusher.cpp b/lib/csapi/pusher.cpp index 028022c5..ef4b3767 100644 --- a/lib/csapi/pusher.cpp +++ b/lib/csapi/pusher.cpp @@ -4,20 +4,17 @@ #include "pusher.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl GetPushersJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/pushers"); + makePath("/_matrix/client/r0", "/pushers")); } GetPushersJob::GetPushersJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetPushersJob"), - QStringLiteral("/_matrix/client/r0") % "/pushers") + makePath("/_matrix/client/r0", "/pushers")) {} PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, @@ -26,7 +23,7 @@ PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, const QString& lang, const PusherData& data, const QString& profileTag, Omittable<bool> append) : BaseJob(HttpVerb::Post, QStringLiteral("PostPusherJob"), - QStringLiteral("/_matrix/client/r0") % "/pushers/set") + makePath("/_matrix/client/r0", "/pushers/set")) { QJsonObject _data; addParam<>(_data, QStringLiteral("pushkey"), pushkey); diff --git a/lib/csapi/pusher.h b/lib/csapi/pusher.h index ae0050d2..622b0df6 100644 --- a/lib/csapi/pusher.h +++ b/lib/csapi/pusher.h @@ -19,9 +19,9 @@ public: /// A dictionary of information for the pusher implementation /// itself. struct PusherData { - /// Required if ``kind`` is ``http``. The URL to use to send + /// Required if `kind` is `http`. The URL to use to send /// notifications to. - QString url; + QUrl url; /// The format to use when sending notifications to the Push /// Gateway. QString format; @@ -29,11 +29,11 @@ public: /// Gets all currently active pushers for the authenticated user. struct Pusher { - /// This is a unique identifier for this pusher. See ``/set`` for + /// 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 + /// The kind of pusher. `"http"` is a pusher that /// sends HTTP pokes. QString kind; /// This is a reverse-DNS style identifier for the application. @@ -104,27 +104,27 @@ struct JsonObjectConverter<GetPushersJob::Pusher> { /*! \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. + * This endpoint allows the creation, modification and deletion of + * [pushers](/client-server-api/#push-notifications) 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`` + /// 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 + /// 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; + /// `/_matrix/push/v1/notify`. + QUrl url; /// The format to send notifications in to Push Gateways if the - /// ``kind`` is ``http``. The details about what fields 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'. + /// [Push Gateway Specification](/push-gateway-api/). Currently the only + /// format available is 'event_id_only'. QString format; }; @@ -140,13 +140,13 @@ public: * client has no such concept, use any unique identifier. * Max length, 512 bytes. * - * If the ``kind`` is ``"email"``, this is the email address to + * 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. + * 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. @@ -154,7 +154,7 @@ public: * different platform versions get different app identifiers. * Max length, 64 chars. * - * If the ``kind`` is ``"email"``, this is ``"m.email"``. + * If the `kind` is `"email"`, this is `"m.email"`. * * \param appDisplayName * A string that will allow the user to identify what application @@ -170,7 +170,7 @@ public: * * \param data * A dictionary of information for the pusher implementation - * itself. If ``kind`` is ``http``, this should contain ``url`` + * itself. If `kind` is `http`, this should contain `url` * which is the URL to use to send notifications to. * * \param profileTag @@ -182,7 +182,7 @@ public: * 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``. + * users. The default is `false`. */ explicit PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, diff --git a/lib/csapi/pushrules.cpp b/lib/csapi/pushrules.cpp index 86165744..0d840788 100644 --- a/lib/csapi/pushrules.cpp +++ b/lib/csapi/pushrules.cpp @@ -4,20 +4,17 @@ #include "pushrules.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl GetPushRulesJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/pushrules"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/r0", "/pushrules")); } GetPushRulesJob::GetPushRulesJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRulesJob"), - QStringLiteral("/_matrix/client/r0") % "/pushrules") + makePath("/_matrix/client/r0", "/pushrules")) { addExpectedKey("global"); } @@ -26,16 +23,15 @@ QUrl GetPushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/pushrules/" % scope % "/" % kind - % "/" % ruleId); + makePath("/_matrix/client/r0", "/pushrules/", + scope, "/", kind, "/", ruleId)); } GetPushRuleJob::GetPushRuleJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleJob"), - QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" - % kind % "/" % ruleId) + makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + "/", ruleId)) {} QUrl DeletePushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, @@ -43,21 +39,20 @@ QUrl DeletePushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/pushrules/" % scope % "/" % kind - % "/" % ruleId); + makePath("/_matrix/client/r0", "/pushrules/", + scope, "/", kind, "/", ruleId)); } DeletePushRuleJob::DeletePushRuleJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Delete, QStringLiteral("DeletePushRuleJob"), - QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" - % kind % "/" % ruleId) + makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + "/", ruleId)) {} auto queryToSetPushRule(const QString& before, const QString& after) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("before"), before); addParam<IfNotEmpty>(_q, QStringLiteral("after"), after); return _q; @@ -70,8 +65,8 @@ SetPushRuleJob::SetPushRuleJob(const QString& scope, const QString& kind, const QVector<PushCondition>& conditions, const QString& pattern) : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleJob"), - QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" - % kind % "/" % ruleId, + makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + "/", ruleId), queryToSetPushRule(before, after)) { QJsonObject _data; @@ -86,17 +81,17 @@ QUrl IsPushRuleEnabledJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/pushrules/" % scope % "/" % kind - % "/" % ruleId % "/enabled"); + makePath("/_matrix/client/r0", "/pushrules/", + scope, "/", kind, "/", ruleId, + "/enabled")); } 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") + makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + "/", ruleId, "/enabled")) { addExpectedKey("enabled"); } @@ -105,8 +100,8 @@ 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") + makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + "/", ruleId, "/enabled")) { QJsonObject _data; addParam<>(_data, QStringLiteral("enabled"), enabled); @@ -118,17 +113,17 @@ QUrl GetPushRuleActionsJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/pushrules/" % scope % "/" % kind - % "/" % ruleId % "/actions"); + makePath("/_matrix/client/r0", "/pushrules/", + scope, "/", kind, "/", ruleId, + "/actions")); } 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") + makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + "/", ruleId, "/actions")) { addExpectedKey("actions"); } @@ -138,8 +133,8 @@ SetPushRuleActionsJob::SetPushRuleActionsJob(const QString& scope, const QString& ruleId, const QVector<QVariant>& actions) : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleActionsJob"), - QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/" - % kind % "/" % ruleId % "/actions") + makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + "/", ruleId, "/actions")) { QJsonObject _data; addParam<>(_data, QStringLiteral("actions"), actions); diff --git a/lib/csapi/pushrules.h b/lib/csapi/pushrules.h index 1c6d5c2d..a5eb48f0 100644 --- a/lib/csapi/pushrules.h +++ b/lib/csapi/pushrules.h @@ -15,9 +15,9 @@ namespace Quotient { /*! \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. + * 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: @@ -49,7 +49,7 @@ public: /*! \brief Retrieve a push rule. * * \param scope - * ``global`` to specify global rules. + * `global` to specify global rules. * * \param kind * The kind of rule @@ -71,7 +71,7 @@ public: // 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. + /// rule itself such as the rule's `actions` and `conditions` if set. PushRule pushRule() const { return fromJson<PushRule>(jsonData()); } }; @@ -84,7 +84,7 @@ public: /*! \brief Delete a push rule. * * \param scope - * ``global`` to specify global rules. + * `global` to specify global rules. * * \param kind * The kind of rule @@ -117,7 +117,7 @@ public: /*! \brief Add or change a push rule. * * \param scope - * ``global`` to specify global rules. + * `global` to specify global rules. * * \param kind * The kind of rule @@ -129,7 +129,7 @@ public: * 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 + * 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. * @@ -141,10 +141,10 @@ public: * \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. + * always matches. Only applicable to `underride` and `override` rules. * * \param pattern - * Only applicable to ``content`` rules. The glob-style pattern to match + * Only applicable to `content` rules. The glob-style pattern to match * against. */ explicit SetPushRuleJob(const QString& scope, const QString& kind, @@ -165,8 +165,8 @@ 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``. + * Either `global` or `device/<profile_tag>` to specify global + * rules or device rules for the given `profile_tag`. * * \param kind * The kind of rule @@ -200,7 +200,7 @@ public: /*! \brief Enable or disable a push rule. * * \param scope - * ``global`` to specify global rules. + * `global` to specify global rules. * * \param kind * The kind of rule @@ -224,8 +224,8 @@ 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``. + * Either `global` or `device/<profile_tag>` to specify global + * rules or device rules for the given `profile_tag`. * * \param kind * The kind of rule @@ -263,7 +263,7 @@ public: /*! \brief Set the actions for a push rule. * * \param scope - * ``global`` to specify global rules. + * `global` to specify global rules. * * \param kind * The kind of rule diff --git a/lib/csapi/read_markers.cpp b/lib/csapi/read_markers.cpp index 39e4d148..f2edb71e 100644 --- a/lib/csapi/read_markers.cpp +++ b/lib/csapi/read_markers.cpp @@ -4,16 +4,13 @@ #include "read_markers.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; SetReadMarkerJob::SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, const QString& mRead) : BaseJob(HttpVerb::Post, QStringLiteral("SetReadMarkerJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/read_markers") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/read_markers")) { QJsonObject _data; addParam<>(_data, QStringLiteral("m.fully_read"), mFullyRead); diff --git a/lib/csapi/read_markers.h b/lib/csapi/read_markers.h index 0e122c63..00a2aa0d 100644 --- a/lib/csapi/read_markers.h +++ b/lib/csapi/read_markers.h @@ -26,7 +26,7 @@ public: * * \param mRead * The event ID to set the read receipt location at. This is - * equivalent to calling ``/receipt/m.read/$elsewhere:example.org`` + * 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, diff --git a/lib/csapi/receipts.cpp b/lib/csapi/receipts.cpp index 00d1c28a..401c3bfe 100644 --- a/lib/csapi/receipts.cpp +++ b/lib/csapi/receipts.cpp @@ -4,16 +4,14 @@ #include "receipts.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; 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) + makePath("/_matrix/client/r0", "/rooms/", roomId, "/receipt/", + receiptType, "/", eventId)) { - setRequestData(Data(toJson(receipt))); + setRequestData(RequestData(toJson(receipt))); } diff --git a/lib/csapi/receipts.h b/lib/csapi/receipts.h index 1fac0acf..7ac093cd 100644 --- a/lib/csapi/receipts.h +++ b/lib/csapi/receipts.h @@ -27,8 +27,8 @@ public: * 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. + * 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, diff --git a/lib/csapi/redaction.cpp b/lib/csapi/redaction.cpp index 91497064..acf1b0e4 100644 --- a/lib/csapi/redaction.cpp +++ b/lib/csapi/redaction.cpp @@ -4,15 +4,13 @@ #include "redaction.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; 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) + makePath("/_matrix/client/r0", "/rooms/", roomId, "/redact/", + eventId, "/", txnId)) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); diff --git a/lib/csapi/redaction.h b/lib/csapi/redaction.h index 541e433a..f0db9f9f 100644 --- a/lib/csapi/redaction.h +++ b/lib/csapi/redaction.h @@ -15,9 +15,12 @@ namespace Quotient { * * 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. + * Any user with a power level greater than or equal to the `m.room.redaction` + * event power level may send redaction events in the room. If the user's power + * level greater is also greater than or equal to the `redact` power level + * of the room, the user may redact events sent by other users. + * + * Server administrators may redact events sent by users on their server. */ class RedactEventJob : public BaseJob { public: diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp index 33f61265..153abcee 100644 --- a/lib/csapi/registration.cpp +++ b/lib/csapi/registration.cpp @@ -4,13 +4,11 @@ #include "registration.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; auto queryToRegister(const QString& kind) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("kind"), kind); return _q; } @@ -22,7 +20,7 @@ RegisterJob::RegisterJob(const QString& kind, const QString& initialDeviceDisplayName, Omittable<bool> inhibitLogin) : BaseJob(HttpVerb::Post, QStringLiteral("RegisterJob"), - QStringLiteral("/_matrix/client/r0") % "/register", + makePath("/_matrix/client/r0", "/register"), queryToRegister(kind), {}, false) { QJsonObject _data; @@ -40,28 +38,26 @@ RegisterJob::RegisterJob(const QString& kind, RequestTokenToRegisterEmailJob::RequestTokenToRegisterEmailJob( const EmailValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterEmailJob"), - QStringLiteral("/_matrix/client/r0") - % "/register/email/requestToken", + makePath("/_matrix/client/r0", "/register/email/requestToken"), false) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); } RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob( const MsisdnValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterMSISDNJob"), - QStringLiteral("/_matrix/client/r0") - % "/register/msisdn/requestToken", + makePath("/_matrix/client/r0", "/register/msisdn/requestToken"), false) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); } ChangePasswordJob::ChangePasswordJob(const QString& newPassword, bool logoutDevices, const Omittable<AuthenticationData>& auth) : BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), - QStringLiteral("/_matrix/client/r0") % "/account/password") + makePath("/_matrix/client/r0", "/account/password")) { QJsonObject _data; addParam<>(_data, QStringLiteral("new_password"), newPassword); @@ -74,28 +70,28 @@ RequestTokenToResetPasswordEmailJob::RequestTokenToResetPasswordEmailJob( const EmailValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToResetPasswordEmailJob"), - QStringLiteral("/_matrix/client/r0") - % "/account/password/email/requestToken", + makePath("/_matrix/client/r0", + "/account/password/email/requestToken"), false) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); } RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob( const MsisdnValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToResetPasswordMSISDNJob"), - QStringLiteral("/_matrix/client/r0") - % "/account/password/msisdn/requestToken", + makePath("/_matrix/client/r0", + "/account/password/msisdn/requestToken"), false) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); } DeactivateAccountJob::DeactivateAccountJob( const Omittable<AuthenticationData>& auth, const QString& idServer) : BaseJob(HttpVerb::Post, QStringLiteral("DeactivateAccountJob"), - QStringLiteral("/_matrix/client/r0") % "/account/deactivate") + makePath("/_matrix/client/r0", "/account/deactivate")) { QJsonObject _data; addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); @@ -106,7 +102,7 @@ DeactivateAccountJob::DeactivateAccountJob( auto queryToCheckUsernameAvailability(const QString& username) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("username"), username); return _q; } @@ -115,13 +111,13 @@ QUrl CheckUsernameAvailabilityJob::makeRequestUrl(QUrl baseUrl, const QString& username) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/register/available", + makePath("/_matrix/client/r0", + "/register/available"), queryToCheckUsernameAvailability(username)); } CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(const QString& username) : BaseJob(HttpVerb::Get, QStringLiteral("CheckUsernameAvailabilityJob"), - QStringLiteral("/_matrix/client/r0") % "/register/available", + makePath("/_matrix/client/r0", "/register/available"), queryToCheckUsernameAvailability(username), {}, false) {} diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h index 6864fd47..c1614f20 100644 --- a/lib/csapi/registration.h +++ b/lib/csapi/registration.h @@ -15,8 +15,9 @@ 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. + * This API endpoint uses the [User-Interactive Authentication + * API](/client-server-api/#user-interactive-authentication-api), except in the + * cases where a guest account is being registered. * * Register for an account on this homeserver. * @@ -31,45 +32,46 @@ namespace Quotient { * 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 + * 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 + * `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``. + * `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``. + * `user_id` matches the provided `username`. * - * The returned access token must be associated with the ``device_id`` + * 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`_. + * [Relationship between access tokens and + * devices](/client-server-api/#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 + * 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>`_. + * [Matrix specification](/appendices/#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``. + * 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. + * authenticate the `register` call itself. * * \param username * The basis for the localpart of the desired Matrix ID. If omitted, @@ -85,10 +87,10 @@ public: * * \param initialDeviceDisplayName * A display name to assign to the newly-created device. Ignored - * if ``device_id`` corresponds to a known device. + * if `device_id` corresponds to a known device. * * \param inhibitLogin - * If true, an ``access_token`` and ``device_id`` should not be + * If true, an `access_token` and `device_id` should not be * returned from this call, therefore preventing an automatic * login. Defaults to false. */ @@ -105,12 +107,12 @@ public: /// 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 <../appendices.html#user-identifiers>`_. + /// the [Matrix specification](/appendices/#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. + /// Required if the `inhibit_login` option is false. QString accessToken() const { return loadFromJson<QString>("access_token"_ls); @@ -120,8 +122,8 @@ public: /// 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. + /// `user_id` (by splitting at the first colon) if they require + /// it. Note also that `homeserver` is not spelt this way. QString homeServer() const { return loadFromJson<QString>("home_server"_ls); @@ -129,7 +131,7 @@ public: /// 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. + /// Required if the `inhibit_login` option is false. QString deviceId() const { return loadFromJson<QString>("device_id"_ls); } }; @@ -201,9 +203,9 @@ public: * * 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. + * This API endpoint uses the [User-Interactive Authentication + * API](/client-server-api/#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. @@ -224,8 +226,8 @@ public: * Whether the user's other access tokens, and their associated devices, * should be revoked if the request succeeds. * - * When ``false``, the server can still take advantage of `the soft logout - * method <#soft-logout>`_ for the user's remaining devices. + * When `false`, the server can still take advantage of the [soft logout + * method](/client-server-api/#soft-logout) for the user's remaining devices. * * \param auth * Additional authentication information for the user-interactive @@ -242,23 +244,18 @@ public: * 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. + * `/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 + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken) + * 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. + * `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: @@ -269,24 +266,18 @@ public: * 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. + * `/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 + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken) + * 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. + * `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); @@ -305,22 +296,18 @@ public: * 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. + * `/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 + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken) + * 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. + * `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: @@ -331,23 +318,18 @@ public: * 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. + * `/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 + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken) + * 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. + * `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); @@ -366,7 +348,8 @@ public: * Deactivate the user's account, removing all ability for the user to * login again. * - * This API endpoint uses the `User-Interactive Authentication API`_. + * This API endpoint uses the [User-Interactive Authentication + * API](/client-server-api/#user-interactive-authentication-api). * * An access token should be submitted to this endpoint if the client has * an active session. @@ -374,7 +357,7 @@ public: * 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`` + * 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. */ @@ -388,11 +371,11 @@ public: * * \param idServer * The identity server to unbind all of the user's 3PIDs from. - * If not provided, the homeserver MUST use the ``id_server`` + * 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``. + * 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 = {}); @@ -400,12 +383,12 @@ public: // 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 + /// 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 + /// `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 + /// must be `success` if the homeserver has no identifiers to unbind /// for the user. QString idServerUnbindResult() const { @@ -447,7 +430,7 @@ public: // Result properties /// A flag to indicate that the username is available. This should always - /// be ``true`` when the server replies with 200 OK. + /// be `true` when the server replies with 200 OK. Omittable<bool> available() const { return loadFromJson<Omittable<bool>>("available"_ls); diff --git a/lib/csapi/report_content.cpp b/lib/csapi/report_content.cpp index 0a41625f..0a76d5b8 100644 --- a/lib/csapi/report_content.cpp +++ b/lib/csapi/report_content.cpp @@ -4,18 +4,16 @@ #include "report_content.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; ReportContentJob::ReportContentJob(const QString& roomId, const QString& eventId, - int score, const QString& reason) + Omittable<int> score, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("ReportContentJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/report/" % eventId) + makePath("/_matrix/client/r0", "/rooms/", roomId, "/report/", + eventId)) { QJsonObject _data; - addParam<>(_data, QStringLiteral("score"), score); - addParam<>(_data, QStringLiteral("reason"), reason); + addParam<IfNotEmpty>(_data, QStringLiteral("score"), score); + addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); setRequestData(std::move(_data)); } diff --git a/lib/csapi/report_content.h b/lib/csapi/report_content.h index 375e1829..e401c2e1 100644 --- a/lib/csapi/report_content.h +++ b/lib/csapi/report_content.h @@ -31,7 +31,8 @@ public: * The reason the content is being reported. May be blank. */ explicit ReportContentJob(const QString& roomId, const QString& eventId, - int score, const QString& reason); + Omittable<int> score = none, + const QString& reason = {}); }; } // namespace Quotient diff --git a/lib/csapi/room_send.cpp b/lib/csapi/room_send.cpp index 63986c56..f80f9300 100644 --- a/lib/csapi/room_send.cpp +++ b/lib/csapi/room_send.cpp @@ -4,16 +4,14 @@ #include "room_send.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; 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) + makePath("/_matrix/client/r0", "/rooms/", roomId, "/send/", + eventType, "/", txnId)) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); addExpectedKey("event_id"); } diff --git a/lib/csapi/room_send.h b/lib/csapi/room_send.h index 39460aca..96f5beca 100644 --- a/lib/csapi/room_send.h +++ b/lib/csapi/room_send.h @@ -16,7 +16,7 @@ namespace Quotient { * * 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. + * [Room Events](/client-server-api/#room-events) for the m. event specification. */ class SendMessageJob : public BaseJob { public: @@ -40,7 +40,8 @@ public: * * 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. + * [Room Events](/client-server-api/#room-events) for the m. event + * specification. */ explicit SendMessageJob(const QString& roomId, const QString& eventType, const QString& txnId, const QJsonObject& body = {}); diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp index e18108ac..f6d2e6ec 100644 --- a/lib/csapi/room_state.cpp +++ b/lib/csapi/room_state.cpp @@ -4,8 +4,6 @@ #include "room_state.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId, @@ -13,9 +11,9 @@ SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId, const QString& stateKey, const QJsonObject& body) : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomStateWithKeyJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/state/" % eventType % "/" % stateKey) + makePath("/_matrix/client/r0", "/rooms/", roomId, "/state/", + eventType, "/", stateKey)) { - setRequestData(Data(toJson(body))); + setRequestData(RequestData(toJson(body))); addExpectedKey("event_id"); } diff --git a/lib/csapi/room_state.h b/lib/csapi/room_state.h index 447605ff..f95af223 100644 --- a/lib/csapi/room_state.h +++ b/lib/csapi/room_state.h @@ -10,22 +10,20 @@ namespace Quotient { /*! \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 + * 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. + * 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. + * [Room Events](/client-server-api/#room-events) for the `m.` event + * specification. * - * If the event type being sent is ``m.room.canonical_alias`` servers + * 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 @@ -46,22 +44,20 @@ public: * 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 + * 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. + * 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. + * [Room Events](/client-server-api/#room-events) for the `m.` event + * specification. * - * If the event type being sent is ``m.room.canonical_alias`` servers + * 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 diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp index e3791b08..d4129cfb 100644 --- a/lib/csapi/room_upgrades.cpp +++ b/lib/csapi/room_upgrades.cpp @@ -4,14 +4,11 @@ #include "room_upgrades.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; UpgradeRoomJob::UpgradeRoomJob(const QString& roomId, const QString& newVersion) : BaseJob(HttpVerb::Post, QStringLiteral("UpgradeRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/upgrade") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/upgrade")) { QJsonObject _data; addParam<>(_data, QStringLiteral("new_version"), newVersion); diff --git a/lib/csapi/rooms.cpp b/lib/csapi/rooms.cpp index 724d941f..5310aa32 100644 --- a/lib/csapi/rooms.cpp +++ b/lib/csapi/rooms.cpp @@ -4,24 +4,21 @@ #include "rooms.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl GetOneRoomEventJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/event/" - % eventId); + makePath("/_matrix/client/r0", "/rooms/", + roomId, "/event/", eventId)); } GetOneRoomEventJob::GetOneRoomEventJob(const QString& roomId, const QString& eventId) : BaseJob(HttpVerb::Get, QStringLiteral("GetOneRoomEventJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/event/" % eventId) + makePath("/_matrix/client/r0", "/rooms/", roomId, "/event/", + eventId)) {} QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, @@ -29,36 +26,35 @@ QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& stateKey) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/state/" - % eventType % "/" % stateKey); + makePath("/_matrix/client/r0", "/rooms/", + roomId, "/state/", eventType, "/", + stateKey)); } 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) + makePath("/_matrix/client/r0", "/rooms/", roomId, "/state/", + eventType, "/", stateKey)) {} QUrl GetRoomStateJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/state"); + makePath("/_matrix/client/r0", "/rooms/", + roomId, "/state")); } GetRoomStateJob::GetRoomStateJob(const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/state") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/state")) {} auto queryToGetMembersByRoom(const QString& at, const QString& membership, const QString& notMembership) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("at"), at); addParam<IfNotEmpty>(_q, QStringLiteral("membership"), membership); addParam<IfNotEmpty>(_q, QStringLiteral("not_membership"), notMembership); @@ -72,7 +68,7 @@ QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, { return BaseJob::makeRequestUrl( std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/members", + makePath("/_matrix/client/r0", "/rooms/", roomId, "/members"), queryToGetMembersByRoom(at, membership, notMembership)); } @@ -81,8 +77,7 @@ GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId, const QString& membership, const QString& notMembership) : BaseJob(HttpVerb::Get, QStringLiteral("GetMembersByRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/members", + makePath("/_matrix/client/r0", "/rooms/", roomId, "/members"), queryToGetMembersByRoom(at, membership, notMembership)) {} @@ -90,12 +85,12 @@ QUrl GetJoinedMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/joined_members"); + makePath("/_matrix/client/r0", "/rooms/", + roomId, "/joined_members")); } GetJoinedMembersByRoomJob::GetJoinedMembersByRoomJob(const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedMembersByRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/joined_members") + makePath("/_matrix/client/r0", "/rooms/", roomId, + "/joined_members")) {} diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index f0bfa349..2620582b 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -12,7 +12,7 @@ namespace Quotient { /*! \brief Get a single event by event ID. * - * Get a single event based on ``roomId/eventId``. You must have permission to + * 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 { @@ -43,9 +43,6 @@ public: /*! \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 @@ -118,16 +115,15 @@ public: * * \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 + * 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 + * 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``. + * 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 @@ -162,7 +158,7 @@ public: * 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 + * respond than `/members` as it can be implemented more efficiently on the * server. */ class GetJoinedMembersByRoomJob : public BaseJob { @@ -173,13 +169,13 @@ public: /// 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 + /// 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; + QUrl avatarUrl; }; // Construction/destruction diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp index 5649d52a..295dd1cc 100644 --- a/lib/csapi/search.cpp +++ b/lib/csapi/search.cpp @@ -4,13 +4,11 @@ #include "search.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; auto queryToSearch(const QString& nextBatch) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("next_batch"), nextBatch); return _q; } @@ -18,7 +16,7 @@ auto queryToSearch(const QString& nextBatch) SearchJob::SearchJob(const Categories& searchCategories, const QString& nextBatch) : BaseJob(HttpVerb::Post, QStringLiteral("SearchJob"), - QStringLiteral("/_matrix/client/r0") % "/search", + makePath("/_matrix/client/r0", "/search"), queryToSearch(nextBatch)) { QJsonObject _data; diff --git a/lib/csapi/search.h b/lib/csapi/search.h index c009ded6..3d02752a 100644 --- a/lib/csapi/search.h +++ b/lib/csapi/search.h @@ -23,15 +23,15 @@ public: /// returned are included in the response. struct IncludeEventContext { /// How many events before the result are - /// returned. By default, this is ``5``. + /// returned. By default, this is `5`. Omittable<int> beforeLimit; /// How many events after the result are - /// returned. By default, this is ``5``. + /// 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``. + /// By default, this is `false`. Omittable<bool> includeProfile; }; @@ -54,10 +54,10 @@ public: QString searchTerm; /// The keys to search. Defaults to all. QStringList keys; - /// This takes a `filter`_. + /// This takes a [filter](/client-server-api/#filtering). RoomEventFilter filter; /// The order in which to search for results. - /// By default, this is ``"rank"``. + /// By default, this is `"rank"`. QString orderBy; /// Configures whether any context for the events /// returned are included in the response. @@ -81,7 +81,7 @@ public: /// Performs a full text search across different categories. QString displayname; /// Performs a full text search across different categories. - QString avatarUrl; + QUrl avatarUrl; }; /// Context for result, if requested. @@ -93,7 +93,7 @@ public: /// The historic profile information of the /// users that sent the events returned. /// - /// The ``string`` key is the user ID for which + /// The `string` key is the user ID for which /// the profile belongs to. QHash<QString, UserProfile> profileInfo; /// Events just before the result. @@ -139,15 +139,15 @@ public: 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``. + /// `include_state` key set with a value of `true`. /// - /// The ``string`` key is the room ID for which the ``State - /// Event`` array belongs to. + /// The `string` key is the room ID for which the `State + /// Event` array belongs to. UnorderedMap<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: + /// 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 @@ -172,7 +172,7 @@ public: * * \param nextBatch * The point to return events from. If given, this should be a - * ``next_batch`` result from a previous call to this endpoint. + * `next_batch` result from a previous call to this endpoint. */ explicit SearchJob(const Categories& searchCategories, const QString& nextBatch = {}); diff --git a/lib/csapi/sso_login_redirect.cpp b/lib/csapi/sso_login_redirect.cpp index 85a18560..871d6ff6 100644 --- a/lib/csapi/sso_login_redirect.cpp +++ b/lib/csapi/sso_login_redirect.cpp @@ -4,13 +4,11 @@ #include "sso_login_redirect.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; auto queryToRedirectToSSO(const QString& redirectUrl) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("redirectUrl"), redirectUrl); return _q; } @@ -18,13 +16,36 @@ auto queryToRedirectToSSO(const QString& redirectUrl) QUrl RedirectToSSOJob::makeRequestUrl(QUrl baseUrl, const QString& redirectUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/login/sso/redirect", + makePath("/_matrix/client/r0", + "/login/sso/redirect"), queryToRedirectToSSO(redirectUrl)); } RedirectToSSOJob::RedirectToSSOJob(const QString& redirectUrl) : BaseJob(HttpVerb::Get, QStringLiteral("RedirectToSSOJob"), - QStringLiteral("/_matrix/client/r0") % "/login/sso/redirect", + makePath("/_matrix/client/r0", "/login/sso/redirect"), queryToRedirectToSSO(redirectUrl), {}, false) {} + +auto queryToRedirectToIdP(const QString& redirectUrl) +{ + QUrlQuery _q; + addParam<>(_q, QStringLiteral("redirectUrl"), redirectUrl); + return _q; +} + +QUrl RedirectToIdPJob::makeRequestUrl(QUrl baseUrl, const QString& idpId, + const QString& redirectUrl) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + makePath("/_matrix/client/r0", + "/login/sso/redirect/", idpId), + queryToRedirectToIdP(redirectUrl)); +} + +RedirectToIdPJob::RedirectToIdPJob(const QString& idpId, + const QString& redirectUrl) + : BaseJob(HttpVerb::Get, QStringLiteral("RedirectToIdPJob"), + makePath("/_matrix/client/r0", "/login/sso/redirect/", idpId), + queryToRedirectToIdP(redirectUrl), {}, false) +{} diff --git a/lib/csapi/sso_login_redirect.h b/lib/csapi/sso_login_redirect.h index d6330e38..ade1eb7d 100644 --- a/lib/csapi/sso_login_redirect.h +++ b/lib/csapi/sso_login_redirect.h @@ -13,7 +13,9 @@ namespace Quotient { * 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. + * The server MUST respond with an HTTP redirect to the SSO interface, + * or present a page which lets the user select an IdP to continue + * with in the event multiple are supported by the server. */ class RedirectToSSOJob : public BaseJob { public: @@ -33,4 +35,36 @@ public: static QUrl makeRequestUrl(QUrl baseUrl, const QString& redirectUrl); }; +/*! \brief Redirect the user's browser to the SSO interface for an IdP. + * + * This endpoint is the same as `/login/sso/redirect`, though with an + * IdP ID from the original `identity_providers` array to inform the + * server of which IdP the client/user would like to continue with. + * + * The server MUST respond with an HTTP redirect to the SSO interface + * for that IdP. + */ +class RedirectToIdPJob : public BaseJob { +public: + /*! \brief Redirect the user's browser to the SSO interface for an IdP. + * + * \param idpId + * The `id` of the IdP from the `m.login.sso` `identity_providers` + * array denoting the user's selection. + * + * \param redirectUrl + * URI to which the user will be redirected after the homeserver has + * authenticated the user with SSO. + */ + explicit RedirectToIdPJob(const QString& idpId, const QString& redirectUrl); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for RedirectToIdPJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& idpId, + const QString& redirectUrl); +}; + } // namespace Quotient diff --git a/lib/csapi/tags.cpp b/lib/csapi/tags.cpp index dc22dc18..f717de6e 100644 --- a/lib/csapi/tags.cpp +++ b/lib/csapi/tags.cpp @@ -4,30 +4,28 @@ #include "tags.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl GetRoomTagsJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") % "/user/" - % userId % "/rooms/" % roomId % "/tags"); + makePath("/_matrix/client/r0", "/user/", + userId, "/rooms/", roomId, "/tags")); } GetRoomTagsJob::GetRoomTagsJob(const QString& userId, const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomTagsJob"), - QStringLiteral("/_matrix/client/r0") % "/user/" % userId - % "/rooms/" % roomId % "/tags") + makePath("/_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) + makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + roomId, "/tags/", tag)) { QJsonObject _data; fillJson(_data, additionalProperties); @@ -39,14 +37,14 @@ QUrl DeleteRoomTagJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& tag) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/user/" % userId % "/rooms/" % roomId - % "/tags/" % tag); + makePath("/_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) + makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + roomId, "/tags/", tag)) {} diff --git a/lib/csapi/tags.h b/lib/csapi/tags.h index a815d9b3..a854531a 100644 --- a/lib/csapi/tags.h +++ b/lib/csapi/tags.h @@ -18,7 +18,7 @@ public: /// List the tags set by a user on a room. struct Tag { - /// A number in a range ``[0,1]`` describing a relative + /// 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. @@ -83,7 +83,7 @@ public: * The tag to add. * * \param order - * A number in a range ``[0,1]`` describing a relative + * A number in a range `[0,1]` describing a relative * position of the room under the given tag. * * \param additionalProperties diff --git a/lib/csapi/third_party_lookup.cpp b/lib/csapi/third_party_lookup.cpp index baf1fab5..4c930668 100644 --- a/lib/csapi/third_party_lookup.cpp +++ b/lib/csapi/third_party_lookup.cpp @@ -4,39 +4,36 @@ #include "third_party_lookup.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl GetProtocolsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/thirdparty/protocols"); + makePath("/_matrix/client/r0", + "/thirdparty/protocols")); } GetProtocolsJob::GetProtocolsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolsJob"), - QStringLiteral("/_matrix/client/r0") % "/thirdparty/protocols") + makePath("/_matrix/client/r0", "/thirdparty/protocols")) {} QUrl GetProtocolMetadataJob::makeRequestUrl(QUrl baseUrl, const QString& protocol) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/thirdparty/protocol/" % protocol); + makePath("/_matrix/client/r0", + "/thirdparty/protocol/", protocol)); } GetProtocolMetadataJob::GetProtocolMetadataJob(const QString& protocol) : BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolMetadataJob"), - QStringLiteral("/_matrix/client/r0") % "/thirdparty/protocol/" - % protocol) + makePath("/_matrix/client/r0", "/thirdparty/protocol/", protocol)) {} auto queryToQueryLocationByProtocol(const QString& searchFields) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("searchFields"), searchFields); return _q; } @@ -46,22 +43,21 @@ QUrl QueryLocationByProtocolJob::makeRequestUrl(QUrl baseUrl, const QString& searchFields) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/thirdparty/location/" % protocol, + makePath("/_matrix/client/r0", + "/thirdparty/location/", protocol), queryToQueryLocationByProtocol(searchFields)); } QueryLocationByProtocolJob::QueryLocationByProtocolJob( const QString& protocol, const QString& searchFields) : BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByProtocolJob"), - QStringLiteral("/_matrix/client/r0") % "/thirdparty/location/" - % protocol, + makePath("/_matrix/client/r0", "/thirdparty/location/", protocol), queryToQueryLocationByProtocol(searchFields)) {} auto queryToQueryUserByProtocol(const QString& fields) { - BaseJob::Query _q; + QUrlQuery _q; addParam<IfNotEmpty>(_q, QStringLiteral("fields..."), fields); return _q; } @@ -71,22 +67,21 @@ QUrl QueryUserByProtocolJob::makeRequestUrl(QUrl baseUrl, const QString& fields) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/thirdparty/user/" % protocol, + makePath("/_matrix/client/r0", + "/thirdparty/user/", protocol), queryToQueryUserByProtocol(fields)); } QueryUserByProtocolJob::QueryUserByProtocolJob(const QString& protocol, const QString& fields) : BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByProtocolJob"), - QStringLiteral("/_matrix/client/r0") % "/thirdparty/user/" - % protocol, + makePath("/_matrix/client/r0", "/thirdparty/user/", protocol), queryToQueryUserByProtocol(fields)) {} auto queryToQueryLocationByAlias(const QString& alias) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("alias"), alias); return _q; } @@ -94,20 +89,20 @@ auto queryToQueryLocationByAlias(const QString& alias) QUrl QueryLocationByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& alias) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/thirdparty/location", + makePath("/_matrix/client/r0", + "/thirdparty/location"), queryToQueryLocationByAlias(alias)); } QueryLocationByAliasJob::QueryLocationByAliasJob(const QString& alias) : BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByAliasJob"), - QStringLiteral("/_matrix/client/r0") % "/thirdparty/location", + makePath("/_matrix/client/r0", "/thirdparty/location"), queryToQueryLocationByAlias(alias)) {} auto queryToQueryUserByID(const QString& userid) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("userid"), userid); return _q; } @@ -115,13 +110,13 @@ auto queryToQueryUserByID(const QString& userid) QUrl QueryUserByIDJob::makeRequestUrl(QUrl baseUrl, const QString& userid) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/thirdparty/user", + makePath("/_matrix/client/r0", + "/thirdparty/user"), queryToQueryUserByID(userid)); } QueryUserByIDJob::QueryUserByIDJob(const QString& userid) : BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByIDJob"), - QStringLiteral("/_matrix/client/r0") % "/thirdparty/user", + makePath("/_matrix/client/r0", "/thirdparty/user"), queryToQueryUserByID(userid)) {} diff --git a/lib/csapi/third_party_membership.cpp b/lib/csapi/third_party_membership.cpp index fda772d2..59275e41 100644 --- a/lib/csapi/third_party_membership.cpp +++ b/lib/csapi/third_party_membership.cpp @@ -4,16 +4,13 @@ #include "third_party_membership.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; 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") + makePath("/_matrix/client/r0", "/rooms/", roomId, "/invite")) { QJsonObject _data; addParam<>(_data, QStringLiteral("id_server"), idServer); diff --git a/lib/csapi/third_party_membership.h b/lib/csapi/third_party_membership.h index 55cab370..a424678f 100644 --- a/lib/csapi/third_party_membership.h +++ b/lib/csapi/third_party_membership.h @@ -10,14 +10,13 @@ namespace Quotient { /*! \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`_. + * the* [joining rooms + * section](/client-server-api/#post_matrixclientr0roomsroomidinvite). * * This API invites a user to participate in a particular room. * They do not start participating in the room until they actually join the @@ -27,7 +26,7 @@ namespace Quotient { * 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`` + * 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 @@ -35,7 +34,7 @@ namespace Quotient { * 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, + * 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 @@ -51,9 +50,7 @@ namespace Quotient { * - 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`_ + * append a `m.room.third_party_invite` event to the room. */ class InviteBy3PIDJob : public BaseJob { public: @@ -73,7 +70,7 @@ public: * * \param medium * The kind of address being passed in the address field, for example - * ``email``. + * `email`. * * \param address * The invitee's third party identifier. diff --git a/lib/csapi/to_device.cpp b/lib/csapi/to_device.cpp index 28c4115a..628e8314 100644 --- a/lib/csapi/to_device.cpp +++ b/lib/csapi/to_device.cpp @@ -4,18 +4,16 @@ #include "to_device.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; 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) + makePath("/_matrix/client/r0", "/sendToDevice/", eventType, "/", + txnId)) { QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("messages"), messages); + addParam<>(_data, QStringLiteral("messages"), messages); setRequestData(std::move(_data)); } diff --git a/lib/csapi/to_device.h b/lib/csapi/to_device.h index f5d69d65..7a237195 100644 --- a/lib/csapi/to_device.h +++ b/lib/csapi/to_device.h @@ -32,7 +32,7 @@ public: */ explicit SendToDeviceJob( const QString& eventType, const QString& txnId, - const QHash<QString, QHash<QString, QJsonObject>>& messages = {}); + const QHash<QString, QHash<QString, QJsonObject>>& messages); }; } // namespace Quotient diff --git a/lib/csapi/typing.cpp b/lib/csapi/typing.cpp index 8e214053..c9673118 100644 --- a/lib/csapi/typing.cpp +++ b/lib/csapi/typing.cpp @@ -4,15 +4,13 @@ #include "typing.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; 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) + makePath("/_matrix/client/r0", "/rooms/", roomId, "/typing/", + userId)) { QJsonObject _data; addParam<>(_data, QStringLiteral("typing"), typing); diff --git a/lib/csapi/typing.h b/lib/csapi/typing.h index 2c953949..64a310d0 100644 --- a/lib/csapi/typing.h +++ b/lib/csapi/typing.h @@ -11,8 +11,8 @@ namespace Quotient { /*! \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 + * 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 { @@ -26,7 +26,7 @@ public: * The room in which the user is typing. * * \param typing - * Whether the user is typing or not. If ``false``, the ``timeout`` + * Whether the user is typing or not. If `false`, the `timeout` * key can be omitted. * * \param timeout diff --git a/lib/csapi/users.cpp b/lib/csapi/users.cpp index a0279d7e..48b727f0 100644 --- a/lib/csapi/users.cpp +++ b/lib/csapi/users.cpp @@ -4,14 +4,12 @@ #include "users.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; SearchUserDirectoryJob::SearchUserDirectoryJob(const QString& searchTerm, Omittable<int> limit) : BaseJob(HttpVerb::Post, QStringLiteral("SearchUserDirectoryJob"), - QStringLiteral("/_matrix/client/r0") % "/user_directory/search") + makePath("/_matrix/client/r0", "/user_directory/search")) { QJsonObject _data; addParam<>(_data, QStringLiteral("search_term"), searchTerm); diff --git a/lib/csapi/users.h b/lib/csapi/users.h index 6fc26f57..ec186592 100644 --- a/lib/csapi/users.h +++ b/lib/csapi/users.h @@ -19,7 +19,7 @@ namespace Quotient { * * 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. + * `Accept-Language` header provided in the request, if present. */ class SearchUserDirectoryJob : public BaseJob { public: @@ -34,14 +34,14 @@ public: /// /// 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. + /// `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; + QUrl avatarUrl; }; // Construction/destruction diff --git a/lib/csapi/versions.cpp b/lib/csapi/versions.cpp index 9003e27f..a1efc33e 100644 --- a/lib/csapi/versions.cpp +++ b/lib/csapi/versions.cpp @@ -4,20 +4,17 @@ #include "versions.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl GetVersionsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client") - % "/versions"); + makePath("/_matrix/client", "/versions")); } GetVersionsJob::GetVersionsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetVersionsJob"), - QStringLiteral("/_matrix/client") % "/versions", false) + makePath("/_matrix/client", "/versions"), false) { addExpectedKey("versions"); } diff --git a/lib/csapi/versions.h b/lib/csapi/versions.h index 828a7eb9..896e2ea9 100644 --- a/lib/csapi/versions.h +++ b/lib/csapi/versions.h @@ -12,14 +12,14 @@ namespace Quotient { * * Gets the versions of the specification supported by the server. * - * Values will take the form ``rX.Y.Z``. + * 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``. + * 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 + * 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 diff --git a/lib/csapi/voip.cpp b/lib/csapi/voip.cpp index 43170057..c748ad94 100644 --- a/lib/csapi/voip.cpp +++ b/lib/csapi/voip.cpp @@ -4,18 +4,15 @@ #include "voip.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl GetTurnServerJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/voip/turnServer"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/r0", "/voip/turnServer")); } GetTurnServerJob::GetTurnServerJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetTurnServerJob"), - QStringLiteral("/_matrix/client/r0") % "/voip/turnServer") + makePath("/_matrix/client/r0", "/voip/turnServer")) {} diff --git a/lib/csapi/wellknown.cpp b/lib/csapi/wellknown.cpp index 1aa0a90b..0b441279 100644 --- a/lib/csapi/wellknown.cpp +++ b/lib/csapi/wellknown.cpp @@ -4,18 +4,15 @@ #include "wellknown.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl GetWellknownJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/.well-known") - % "/matrix/client"); + makePath("/.well-known", "/matrix/client")); } GetWellknownJob::GetWellknownJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetWellknownJob"), - QStringLiteral("/.well-known") % "/matrix/client", false) + makePath("/.well-known", "/matrix/client"), false) {} diff --git a/lib/csapi/wellknown.h b/lib/csapi/wellknown.h index b21d9fc7..c707d232 100644 --- a/lib/csapi/wellknown.h +++ b/lib/csapi/wellknown.h @@ -14,7 +14,7 @@ namespace Quotient { * * 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 + * e.g. `com.example.myapp.property`. This ensures property names are * suitably namespaced for each application and reduces the risk of * clashes. * diff --git a/lib/csapi/whoami.cpp b/lib/csapi/whoami.cpp index 73f0298e..ed8a9817 100644 --- a/lib/csapi/whoami.cpp +++ b/lib/csapi/whoami.cpp @@ -4,20 +4,17 @@ #include "whoami.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; QUrl GetTokenOwnerJob::makeRequestUrl(QUrl baseUrl) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/account/whoami"); + return BaseJob::makeRequestUrl( + std::move(baseUrl), makePath("/_matrix/client/r0", "/account/whoami")); } GetTokenOwnerJob::GetTokenOwnerJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetTokenOwnerJob"), - QStringLiteral("/_matrix/client/r0") % "/account/whoami") + makePath("/_matrix/client/r0", "/account/whoami")) { addExpectedKey("user_id"); } diff --git a/lib/csapi/whoami.h b/lib/csapi/whoami.h index af8f1e8a..319f82c5 100644 --- a/lib/csapi/whoami.h +++ b/lib/csapi/whoami.h @@ -14,8 +14,8 @@ namespace Quotient { * * 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`` + * 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. */ @@ -33,8 +33,14 @@ public: // Result properties - /// The user id that owns the access token. + /// The user ID that owns the access token. QString userId() const { return loadFromJson<QString>("user_id"_ls); } + + /// Device ID associated with the access token. If no device + /// is associated with the access token (such as in the case + /// of application services) then this field can be omitted. + /// Otherwise this is required. + QString deviceId() const { return loadFromJson<QString>("device_id"_ls); } }; } // namespace Quotient diff --git a/lib/eventitem.h b/lib/eventitem.h index 1986ba77..0ab1a01d 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -9,11 +9,10 @@ #include <utility> namespace Quotient { -class StateEventBase; -class EventStatus { - Q_GADGET -public: +namespace EventStatus { + Q_NAMESPACE + /** Special marks an event can assume * * This is used to hint at a special status of some events in UI. @@ -31,9 +30,8 @@ public: 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) -}; + Q_ENUM_NS(Code) +} // namespace EventStatus class EventItemBase { public: @@ -106,7 +104,6 @@ inline const CallEventBase* EventItemBase::viewAs<CallEventBase>() const } class PendingEventItem : public EventItemBase { - Q_GADGET public: using EventItemBase::EventItemBase; @@ -148,4 +145,3 @@ inline QDebug& operator<<(QDebug& d, const TimelineItem& ti) return d; } } // namespace Quotient -Q_DECLARE_METATYPE(Quotient::EventStatus) diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index 8cea0ec8..9cf77be3 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -4,7 +4,6 @@ #pragma once #include "event.h" -#include "eventcontent.h" namespace Quotient { constexpr const char* FavouriteTag = "m.favourite"; @@ -16,12 +15,12 @@ struct TagRecord { order_type order; - TagRecord(order_type order = none) : order(std::move(order)) {} + TagRecord(order_type order = none) : order(order) {} bool operator<(const TagRecord& other) const { // Per The Spec, rooms with no order should be after those with order, - // against optional<>::operator<() convention. + // against std::optional<>::operator<() convention. return order && (!other.order || *order < *other.order); } }; @@ -55,15 +54,15 @@ using TagsMap = QHash<QString, TagRecord>; public: \ using content_type = _ContentType; \ DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name(QJsonObject obj) : Event(typeId(), std::move(obj)) {} \ - explicit _Name(_ContentType content) \ + explicit _Name(const QJsonObject& obj) : Event(typeId(), obj) {} \ + explicit _Name(const content_type& content) \ : Event(typeId(), matrixTypeId(), \ - QJsonObject { { QStringLiteral(#_ContentKey), \ - toJson(std::move(content)) } }) \ + QJsonObject { \ + { QStringLiteral(#_ContentKey), toJson(content) } }) \ {} \ auto _ContentKey() const \ { \ - return content<content_type>(#_ContentKey##_ls); \ + return contentPart<content_type>(#_ContentKey##_ls); \ } \ }; \ REGISTER_EVENT_TYPE(_Name) \ diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h index 6132cb44..4c01c941 100644 --- a/lib/events/callanswerevent.h +++ b/lib/events/callanswerevent.h @@ -19,11 +19,11 @@ public: int lifetime() const { - return content<int>("lifetime"_ls); + return contentPart<int>("lifetime"_ls); } // FIXME: Omittable<>? QString sdp() const { - return contentJson()["answer"_ls].toObject().value("sdp"_ls).toString(); + return contentPart<QJsonObject>("answer"_ls).value("sdp"_ls).toString(); } }; diff --git a/lib/events/callcandidatesevent.cpp b/lib/events/callcandidatesevent.cpp deleted file mode 100644 index b87c8e9b..00000000 --- a/lib/events/callcandidatesevent.cpp +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Marius Gripsgard <marius@ubports.com> -// SPDX-FileCopyrightText: 2018 Josip Delic <delijati@googlemail.com> -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "callcandidatesevent.h" - -/* -m.call.candidates -{ - "age": 242352, - "content": { - "call_id": "12345", - "candidates": [ - { - "candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156 -43670 typ host generation 0", "sdpMLineIndex": 0, "sdpMid": "audio" - } - ], - "version": 0 - }, - "event_id": "$WLGTSEFSEF:localhost", - "origin_server_ts": 1431961217939, - "room_id": "!Cuyf34gef24t:localhost", - "sender": "@example:localhost", - "type": "m.call.candidates" -} -*/ diff --git a/lib/events/callcandidatesevent.h b/lib/events/callcandidatesevent.h index c2ccac3b..74c38f2c 100644 --- a/lib/events/callcandidatesevent.h +++ b/lib/events/callcandidatesevent.h @@ -25,17 +25,17 @@ public: QJsonArray candidates() const { - return content<QJsonArray>("candidates"_ls); + return contentPart<QJsonArray>("candidates"_ls); } QString callId() const { - return content<QString>("call_id"); + return contentPart<QString>("call_id"); } int version() const { - return content<int>("version"); + return contentPart<int>("version"); } }; diff --git a/lib/events/callhangupevent.cpp b/lib/events/callhangupevent.cpp deleted file mode 100644 index 43bc4db0..00000000 --- a/lib/events/callhangupevent.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/****************************************************************************** - * SPDX-FileCopyrightText: 2017 Marius Gripsgard <marius@ubports.com> - * SPDX-FileCopyrightText: 2018 Josip Delic <delijati@googlemail.com> - * - * SPDX-License-Identifier: LGPL-2.1-or-later - */ - -#include "callhangupevent.h" - -/* -m.call.hangup -{ - "age": 242352, - "content": { - "call_id": "12345", - "version": 0 - }, - "event_id": "$WLGTSEFSEF:localhost", - "origin_server_ts": 1431961217939, - "room_id": "!Cuyf34gef24t:localhost", - "sender": "@example:localhost", - "type": "m.call.hangup" -} -*/ - -using namespace Quotient; - -CallHangupEvent::CallHangupEvent(const QJsonObject& obj) - : CallEventBase(typeId(), obj) -{ - qCDebug(EVENTS) << "Call Hangup event"; -} - -CallHangupEvent::CallHangupEvent(const QString& callId) - : CallEventBase(typeId(), matrixTypeId(), callId, 0) -{} diff --git a/lib/events/callhangupevent.h b/lib/events/callhangupevent.h index 24382ac2..f3f82833 100644 --- a/lib/events/callhangupevent.h +++ b/lib/events/callhangupevent.h @@ -11,8 +11,12 @@ class CallHangupEvent : public CallEventBase { public: DEFINE_EVENT_TYPEID("m.call.hangup", CallHangupEvent) - explicit CallHangupEvent(const QJsonObject& obj); - explicit CallHangupEvent(const QString& callId); + explicit CallHangupEvent(const QJsonObject& obj) + : CallEventBase(typeId(), obj) + {} + explicit CallHangupEvent(const QString& callId) + : CallEventBase(typeId(), matrixTypeId(), callId, 0) + {} }; REGISTER_EVENT_TYPE(CallHangupEvent) diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h index d3454c4f..80b7d651 100644 --- a/lib/events/callinviteevent.h +++ b/lib/events/callinviteevent.h @@ -18,11 +18,11 @@ public: int lifetime() const { - return content<int>("lifetime"_ls); + return contentPart<int>("lifetime"_ls); } // FIXME: Omittable<>? QString sdp() const { - return contentJson()["offer"_ls].toObject().value("sdp"_ls).toString(); + return contentPart<QJsonObject>("offer"_ls).value("sdp"_ls).toString(); } }; diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index eb7123eb..de89a7c6 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -7,7 +7,6 @@ #include "roomevent.h" namespace Quotient { -class Room; /* * While the specification states: * @@ -27,7 +26,6 @@ class Room; * 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) @@ -43,7 +41,7 @@ public: QString algorithm() const { - QString algo = content<QString>(AlgorithmKeyL); + QString algo = contentPart<QString>(AlgorithmKeyL); if (!SupportedAlgorithms.contains(algo)) { qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo << "is not supported"; @@ -52,17 +50,17 @@ public: } QByteArray ciphertext() const { - return content<QString>(CiphertextKeyL).toLatin1(); + return contentPart<QString>(CiphertextKeyL).toLatin1(); } QJsonObject ciphertext(const QString& identityKey) const { - return content<QJsonObject>(CiphertextKeyL).value(identityKey).toObject(); + return contentPart<QJsonObject>(CiphertextKeyL).value(identityKey).toObject(); } - QString senderKey() const { return content<QString>(SenderKeyKeyL); } + QString senderKey() const { return contentPart<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); } + QString deviceId() const { return contentPart<QString>(DeviceIdKeyL); } + QString sessionId() const { return contentPart<QString>(SessionIdKeyL); } }; REGISTER_EVENT_TYPE(EncryptedEvent) diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h new file mode 100644 index 00000000..24ac9de1 --- /dev/null +++ b/lib/events/encryptedfile.h @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPl-2.1-or-later + +#pragma once + +#include "converters.h" + +namespace Quotient { +/** + * JSON Web Key object as specified in + * https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes + * The only currently relevant member is `k`, the rest needs to be set to the defaults specified in the spec. + */ +struct JWK +{ + Q_GADGET + Q_PROPERTY(QString kty MEMBER kty CONSTANT) + Q_PROPERTY(QStringList keyOps MEMBER keyOps CONSTANT) + Q_PROPERTY(QString alg MEMBER alg CONSTANT) + Q_PROPERTY(QString k MEMBER k CONSTANT) + Q_PROPERTY(bool ext MEMBER ext CONSTANT) + +public: + QString kty; + QStringList keyOps; + QString alg; + QString k; + bool ext; +}; + +struct EncryptedFile +{ + Q_GADGET + Q_PROPERTY(QUrl url MEMBER url CONSTANT) + Q_PROPERTY(JWK key MEMBER key CONSTANT) + Q_PROPERTY(QString iv MEMBER iv CONSTANT) + Q_PROPERTY(QHash<QString, QString> hashes MEMBER hashes CONSTANT) + Q_PROPERTY(QString v MEMBER v CONSTANT) + +public: + QUrl url; + JWK key; + QString iv; + QHash<QString, QString> hashes; + QString v; +}; + +template <> +struct JsonObjectConverter<EncryptedFile> { + static void dumpTo(QJsonObject& jo, const EncryptedFile& pod) + { + addParam<>(jo, QStringLiteral("url"), pod.url); + addParam<>(jo, QStringLiteral("key"), pod.key); + addParam<>(jo, QStringLiteral("iv"), pod.iv); + addParam<>(jo, QStringLiteral("hashes"), pod.hashes); + addParam<>(jo, QStringLiteral("v"), pod.v); + } + static void fillFrom(const QJsonObject& jo, EncryptedFile& pod) + { + fromJson(jo.value("url"_ls), pod.url); + fromJson(jo.value("key"_ls), pod.key); + fromJson(jo.value("iv"_ls), pod.iv); + fromJson(jo.value("hashes"_ls), pod.hashes); + fromJson(jo.value("v"_ls), pod.v); + } +}; + +template <> +struct JsonObjectConverter<JWK> { + static void dumpTo(QJsonObject& jo, const JWK& pod) + { + addParam<>(jo, QStringLiteral("kty"), pod.kty); + addParam<>(jo, QStringLiteral("key_ops"), pod.keyOps); + addParam<>(jo, QStringLiteral("alg"), pod.alg); + addParam<>(jo, QStringLiteral("k"), pod.k); + addParam<>(jo, QStringLiteral("ext"), pod.ext); + } + static void fillFrom(const QJsonObject& jo, JWK& pod) + { + fromJson(jo.value("kty"_ls), pod.kty); + fromJson(jo.value("key_ops"_ls), pod.keyOps); + fromJson(jo.value("alg"_ls), pod.alg); + fromJson(jo.value("k"_ls), pod.k); + fromJson(jo.value("ext"_ls), pod.ext); + } +}; +} // namespace Quotient diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index 490a5e8a..aa05a96e 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -39,6 +39,14 @@ EncryptionEventContent::EncryptionEventContent(const QJsonObject& json) , rotationPeriodMsgs(json[RotationPeriodMsgsKeyL].toInt(100)) {} +EncryptionEventContent::EncryptionEventContent(EncryptionType et) + : encryption(et) +{ + if(encryption != Undefined) { + algorithm = encryptionStrings[encryption]; + } +} + void EncryptionEventContent::fillJson(QJsonObject* o) const { Q_ASSERT(o); diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index f9bbab12..14439fcc 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -12,9 +12,7 @@ class EncryptionEventContent : public EventContent::Base { public: enum EncryptionType : size_t { MegolmV1AesSha2 = 0, Undefined }; - explicit EncryptionEventContent(EncryptionType et = Undefined) - : encryption(et) - {} + explicit EncryptionEventContent(EncryptionType et = Undefined); explicit EncryptionEventContent(const QJsonObject& json); EncryptionType encryption; @@ -40,6 +38,7 @@ public: // default value : StateEvent(typeId(), obj) {} + EncryptionEvent(EncryptionEvent&&) = delete; template <typename... ArgTs> EncryptionEvent(ArgTs&&... contentArgs) : StateEvent(typeId(), matrixTypeId(), QString(), diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 3d66ab55..96be717c 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -46,14 +46,11 @@ QString Event::matrixType() const { return fullJson()[TypeKeyL].toString(); } 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 f8f8311d..8f62872d 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -5,6 +5,7 @@ #include "converters.h" #include "logging.h" +#include "function_traits.h" namespace Quotient { // === event_ptr_tt<> and type casting facilities === @@ -28,31 +29,30 @@ inline TargetEventT* weakPtrCast(const event_ptr_tt<EventT>& ptr) // === 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 SenderKey = QStringLiteral("sender"); -static const auto RoomIdKey = QStringLiteral("room_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 SenderKeyL = "sender"_ls; -static const auto RoomIdKeyL = "room_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; +constexpr auto TypeKeyL = "type"_ls; +constexpr auto BodyKeyL = "body"_ls; +constexpr auto ContentKeyL = "content"_ls; +constexpr auto EventIdKeyL = "event_id"_ls; +constexpr auto SenderKeyL = "sender"_ls; +constexpr auto RoomIdKeyL = "room_id"_ls; +constexpr auto UnsignedKeyL = "unsigned"_ls; +constexpr auto RedactedCauseKeyL = "redacted_because"_ls; +constexpr auto PrevContentKeyL = "prev_content"_ls; +constexpr auto StateKeyKeyL = "state_key"_ls; +const QString TypeKey { TypeKeyL }; +const QString BodyKey { BodyKeyL }; +const QString ContentKey { ContentKeyL }; +const QString EventIdKey { EventIdKeyL }; +const QString SenderKey { SenderKeyL }; +const QString RoomIdKey { RoomIdKeyL }; +const QString UnsignedKey { UnsignedKeyL }; +const QString StateKeyKey { StateKeyKeyL }; /// Make a minimal correct Matrix event JSON -template <typename StrT> -inline QJsonObject basicEventJson(StrT matrixType, const QJsonObject& content) +inline QJsonObject basicEventJson(const QString& matrixType, + const QJsonObject& content) { - return { { TypeKey, std::forward<StrT>(matrixType) }, - { ContentKey, content } }; + return { { TypeKey, matrixType }, { ContentKey, content } }; } // === Event types and event types registry === @@ -76,8 +76,7 @@ public: private: EventTypeRegistry() = default; - Q_DISABLE_COPY(EventTypeRegistry) - DISABLE_MOVE(EventTypeRegistry) + Q_DISABLE_COPY_MOVE(EventTypeRegistry) static EventTypeRegistry& get() { @@ -111,97 +110,90 @@ inline event_type_t typeId() inline event_type_t unknownEventTypeId() { return typeId<void>(); } -// === EventFactory === +// === Event creation facilities === -/** Create an event of arbitrary type from its arguments */ +//! 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) - { - 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() +namespace _impl { + template <class EventT, class BaseEventT> + event_ptr_tt<BaseEventT> makeIfMatches(const QJsonObject& json, + const QString& matrixType) { - using inner_factory_tt = std::function<event_ptr_tt<BaseEventT>( - const QJsonObject&, const QString&)>; - static std::vector<inner_factory_tt> _factories {}; - return _factories; + return QLatin1String(EventT::matrixTypeId()) == matrixType + ? makeEvent<EventT>(json) + : nullptr; } -}; -/** 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 -} + //! \brief A family of event factories to create events from CS API responses + //! + //! Each of these factories, as instantiated by event base types (Event, + //! RoomEvent etc.) is capable of producing an event object derived from + //! \p BaseEventT, using the JSON payload and the event type passed to its + //! make() method. Don't use these directly to make events; use loadEvent() + //! overloads as the frontend for these. Never instantiate new factories + //! outside of base event classes. + //! \sa loadEvent, setupFactory, Event::factory, RoomEvent::factory, + //! StateEventBase::factory + template <typename BaseEventT> + class EventFactory + : private std::vector<event_ptr_tt<BaseEventT> (*)(const QJsonObject&, + const QString&)> { + // Actual makeIfMatches specialisations will differ in the first + // template parameter but that doesn't affect the function type + public: + explicit EventFactory(const char* name = "") + : name(name) + { + static auto yetToBeConstructed = true; + Q_ASSERT(yetToBeConstructed); + if (!yetToBeConstructed) // For Release builds that pass Q_ASSERT + qCritical(EVENTS) + << "Another EventFactory for the same base type is being " + "created - event creation logic will be splintered"; + yetToBeConstructed = false; + } + EventFactory(const EventFactory&) = delete; + + //! \brief Add a method to create events of a given type + //! + //! Adds a standard factory method (makeIfMatches) for \p EventT so that + //! event objects of this type can be created dynamically by loadEvent. + //! The caller is responsible for ensuring this method is called only + //! once per type. + //! \sa makeIfMatches, loadEvent, Quotient::loadEvent + template <class EventT> + bool addMethod() + { + this->emplace_back(&makeIfMatches<EventT, BaseEventT>); + qDebug(EVENTS) << "Added factory method for" + << EventT::matrixTypeId() << "events;" << this->size() + << "methods in the" << name << "chain by now"; + return true; + } + + auto loadEvent(const QJsonObject& json, const QString& matrixType) + { + for (const auto& f : *this) + if (auto e = f(json, matrixType)) + return e; + return makeEvent<BaseEventT>(unknownEventTypeId(), json); + } + + const char* const name; + }; +} // namespace _impl // === 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>; + static inline _impl::EventFactory<Event> factory { "Event" }; explicit Event(Type type, const QJsonObject& json); explicit Event(Type type, event_mtype_t matrixType, @@ -213,7 +205,10 @@ public: Type type() const { return _type; } QString matrixType() const; + [[deprecated("Use fullJson() and stringify it with QJsonDocument::toJson() " + "or by other means")]] QByteArray originalJson() const; + [[deprecated("Use fullJson() instead")]] // QJsonObject originalJsonObject() const { return fullJson(); } const QJsonObject& fullJson() const { return _json; } @@ -222,19 +217,30 @@ public: // a "content" object; but since its structure is different for // different types, we're implementing it per-event type. + // NB: const return types below are meant to catch accidental attempts + // to change event JSON (e.g., consider contentJson()["inexistentKey"]). + const QJsonObject contentJson() const; - const QJsonObject unsignedJson() const; + + template <typename T = QJsonValue, typename KeyT> + const T contentPart(KeyT&& key) const + { + return fromJson<T>(contentJson()[std::forward<KeyT>(key)]); + } template <typename T> + [[deprecated("Use contentPart() to get a part of the event content")]] // T content(const QString& key) const { - return fromJson<T>(contentJson()[key]); + return contentPart<T>(key); } - template <typename T> - T content(QLatin1String key) const + const QJsonObject unsignedJson() const; + + template <typename T = QJsonValue, typename KeyT> + const T unsignedPart(KeyT&& key) const { - return fromJson<T>(contentJson()[key]); + return fromJson<T>(unsignedJson()[std::forward<KeyT>(key)]); } friend QDebug operator<<(QDebug dbg, const Event& e) @@ -262,7 +268,7 @@ template <typename EventT> using EventsArray = std::vector<event_ptr_tt<EventT>>; using Events = EventsArray<Event>; -// === Macros used with event class definitions === +// === Facilities for event class definitions === // This macro should be used in a public section of an event class to // provide matrixTypeId() and typeId(). @@ -274,14 +280,28 @@ using Events = EventsArray<Event>; // 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>(); \ - } \ +#define REGISTER_EVENT_TYPE(_Type) \ + [[maybe_unused]] inline const auto _factoryAdded##_Type = \ + _Type::factory.addMethod<_Type>(); \ // End of macro -// === is<>(), eventCast<>() and visit<>() === +// === Event loading === +// (see also event_loader.h) + +//! \brief Point of customisation to dynamically load events +//! +//! The default specialisation of this calls BaseEventT::factory and if that +//! fails (i.e. returns nullptr) creates an unknown event of BaseEventT. +//! Other specialisations may reuse other factories, add validations common to +//! BaseEventT, and so on +template <class BaseEventT> +event_ptr_tt<BaseEventT> doLoadEvent(const QJsonObject& json, + const QString& matrixType) +{ + return BaseEventT::factory.loadEvent(json, matrixType); +} + +// === is<>(), eventCast<>() and switchOnType<>() === template <class EventT> inline bool is(const Event& e) @@ -303,12 +323,12 @@ inline auto eventCast(const BasePtrT& eptr) : nullptr; } -// A single generic catch-all visitor +// A trivial generic catch-all "switch" template <class BaseEventT, typename FnT> -inline auto visit(const BaseEventT& event, FnT&& visitor) - -> decltype(visitor(event)) +inline auto switchOnType(const BaseEventT& event, FnT&& fn) + -> decltype(fn(event)) { - return visitor(event); + return fn(event); } namespace _impl { @@ -319,52 +339,60 @@ namespace _impl { && !std::is_same_v<BaseT, std::decay_t<fn_arg_t<FnT>>>; } -// A single type-specific void visitor +// A trivial type-specific "switch" for a void function template <class BaseT, typename FnT> -inline auto visit(const BaseT& event, FnT&& visitor) +inline auto switchOnType(const BaseT& event, FnT&& fn) -> std::enable_if_t<_impl::needs_downcast<BaseT, FnT> && std::is_void_v<fn_return_t<FnT>>> { using event_type = fn_arg_t<FnT>; if (is<std::decay_t<event_type>>(event)) - visitor(static_cast<event_type>(event)); + fn(static_cast<event_type>(event)); } -// A single type-specific non-void visitor with an optional default value -// non-voidness is guarded by defaultValue type +// A trivial type-specific "switch" for non-void functions with an optional +// default value; non-voidness is guarded by defaultValue type template <class BaseT, typename FnT> -inline auto visit(const BaseT& event, FnT&& visitor, - fn_return_t<FnT>&& defaultValue = {}) +inline auto switchOnType(const BaseT& event, FnT&& fn, + fn_return_t<FnT>&& defaultValue = {}) -> std::enable_if_t<_impl::needs_downcast<BaseT, FnT>, fn_return_t<FnT>> { 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); + return fn(static_cast<event_type>(event)); + return std::move(defaultValue); } -// A chain of 2 or more visitors +// A switch for a chain of 2 or more functions template <class BaseT, typename FnT1, typename FnT2, typename... FnTs> -inline std::common_type_t<fn_return_t<FnT1>, fn_return_t<FnT2>> visit( - const BaseT& event, FnT1&& visitor1, FnT2&& visitor2, - FnTs&&... visitors) +inline std::common_type_t<fn_return_t<FnT1>, fn_return_t<FnT2>> +switchOnType(const BaseT& event, FnT1&& fn1, FnT2&& fn2, FnTs&&... fns) { 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)...); + return fn1(static_cast<event_type1&>(event)); + return switchOnType(event, std::forward<FnT2>(fn2), + std::forward<FnTs>(fns)...); +} + +template <class BaseT, typename... FnTs> +[[deprecated("The new name for visit() is switchOnType()")]] // +inline std::common_type_t<fn_return_t<FnTs>...> +visit(const BaseT& event, FnTs&&... fns) +{ + return switchOnType(event, std::forward<FnTs>(fns)...); } -// A facility overload that calls void-returning visit() on each event + // A facility overload that calls void-returning switchOnType() on each event // over a range of event pointers +// TODO: replace with ranges::for_each once all standard libraries have it template <typename RangeT, typename... FnTs> -inline auto visitEach(RangeT&& events, FnTs&&... visitors) +inline auto visitEach(RangeT&& events, FnTs&&... fns) -> std::enable_if_t<std::is_void_v< - decltype(visit(**begin(events), std::forward<FnTs>(visitors)...))>> + decltype(switchOnType(**begin(events), std::forward<FnTs>(fns)...))>> { for (auto&& evtPtr: events) - visit(*evtPtr, std::forward<FnTs>(visitors)...); + switchOnType(*evtPtr, std::forward<FnTs>(fns)...); } } // namespace Quotient Q_DECLARE_METATYPE(Quotient::Event*) diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index b249b160..22878d4c 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -5,10 +5,13 @@ #include "converters.h" #include "util.h" +#include "logging.h" #include <QtCore/QMimeDatabase> +#include <QtCore/QFileInfo> using namespace Quotient::EventContent; +using std::move; QJsonObject Base::toJson() const { @@ -17,23 +20,44 @@ QJsonObject Base::toJson() const return o; } -FileInfo::FileInfo(const QUrl& u, qint64 payloadSize, const QMimeType& mimeType, - const QString& originalFilename) +FileInfo::FileInfo(const QFileInfo &fi) + : mimeType(QMimeDatabase().mimeTypeForFile(fi)) + , url(QUrl::fromLocalFile(fi.filePath())) + , payloadSize(fi.size()) + , originalName(fi.fileName()) +{ + Q_ASSERT(fi.isFile()); +} + +FileInfo::FileInfo(QUrl u, qint64 payloadSize, const QMimeType& mimeType, + Omittable<EncryptedFile> file, QString originalFilename) : mimeType(mimeType) - , url(u) + , url(move(u)) , payloadSize(payloadSize) - , originalName(originalFilename) -{} + , originalName(move(originalFilename)) + , file(file) +{ + if (!isValid()) + qCWarning(MESSAGES) + << "To client developers: using FileInfo(QUrl, qint64, ...) " + "constructor for non-mxc resources is deprecated since Quotient " + "0.7; for local resources, use FileInfo(QFileInfo) instead"; +} -FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename) +FileInfo::FileInfo(QUrl mxcUrl, const QJsonObject& infoJson, + const Omittable<EncryptedFile> &file, + QString originalFilename) : originalInfoJson(infoJson) , mimeType( QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString())) - , url(u) + , url(move(mxcUrl)) , payloadSize(fromJson<qint64>(infoJson["size"_ls])) - , originalName(originalFilename) + , originalName(move(originalFilename)) + , file(file) { + if(url.isEmpty() && file.has_value()) { + url = file->url; + } if (!mimeType.isValid()) mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); } @@ -53,14 +77,20 @@ void FileInfo::fillInfoJson(QJsonObject* infoJson) const infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); } -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 QFileInfo& fi, QSize imageSize) + : FileInfo(fi), imageSize(imageSize) +{} + +ImageInfo::ImageInfo(const QUrl& mxcUrl, qint64 fileSize, const QMimeType& type, + QSize imageSize, const Omittable<EncryptedFile> &file, const QString& originalFilename) + : FileInfo(mxcUrl, fileSize, type, file, originalFilename) + , imageSize(imageSize) {} -ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson, +ImageInfo::ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson, + const Omittable<EncryptedFile> &file, const QString& originalFilename) - : FileInfo(u, infoJson, originalFilename) + : FileInfo(mxcUrl, infoJson, file, originalFilename) , imageSize(infoJson["w"_ls].toInt(), infoJson["h"_ls].toInt()) {} @@ -73,9 +103,10 @@ void ImageInfo::fillInfoJson(QJsonObject* infoJson) const infoJson->insert(QStringLiteral("h"), imageSize.height()); } -Thumbnail::Thumbnail(const QJsonObject& infoJson) +Thumbnail::Thumbnail(const QJsonObject& infoJson, const Omittable<EncryptedFile> &file) : ImageInfo(QUrl(infoJson["thumbnail_url"_ls].toString()), - infoJson["thumbnail_info"_ls].toObject()) + infoJson["thumbnail_info"_ls].toObject(), + file) {} void Thumbnail::fillInfoJson(QJsonObject* infoJson) const diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 60d1f7b7..f609a603 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -12,6 +12,10 @@ #include <QtCore/QUrl> #include <QtCore/QMetaType> +#include "encryptedfile.h" + +class QFileInfo; + namespace Quotient { namespace EventContent { /** @@ -47,13 +51,14 @@ namespace EventContent { // but specific aggregation structure is altered. See doc comments to // each type for the list of available attributes. - // A quick classes inheritance structure follows: + // A quick classes inheritance structure follows (the definitions are + // spread across eventcontent.h and roommessageevent.h): // FileInfo - // FileContent : UrlBasedContent<FileInfo, Thumbnail> - // AudioContent : UrlBasedContent<FileInfo, Duration> + // FileContent : UrlWithThumbnailContent<FileInfo> + // AudioContent : PlayableContent<UrlBasedContent<FileInfo>> // ImageInfo : FileInfo + imageSize attribute - // ImageContent : UrlBasedContent<ImageInfo, Thumbnail> - // VideoContent : UrlBasedContent<ImageInfo, Thumbnail, Duration> + // ImageContent : UrlWithThumbnailContent<ImageInfo> + // VideoContent : PlayableContent<UrlWithThumbnailContent<ImageInfo>> /** * A base/mixin class for structures representing an "info" object for @@ -73,11 +78,15 @@ namespace EventContent { */ class FileInfo { public: - explicit FileInfo(const QUrl& u, qint64 payloadSize = -1, + FileInfo() = default; + explicit FileInfo(const QFileInfo& fi); + explicit FileInfo(QUrl mxcUrl, qint64 payloadSize = -1, const QMimeType& mimeType = {}, - const QString& originalFilename = {}); - FileInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename = {}); + Omittable<EncryptedFile> file = none, + QString originalFilename = {}); + FileInfo(QUrl mxcUrl, const QJsonObject& infoJson, + const Omittable<EncryptedFile> &file, + QString originalFilename = {}); bool isValid() const; @@ -98,6 +107,7 @@ namespace EventContent { QUrl url; qint64 payloadSize; QString originalName; + Omittable<EncryptedFile> file = none; }; template <typename InfoT> @@ -113,10 +123,14 @@ namespace EventContent { */ class ImageInfo : public FileInfo { public: - explicit ImageInfo(const QUrl& u, qint64 fileSize = -1, - QMimeType mimeType = {}, const QSize& imageSize = {}, + ImageInfo() = default; + explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {}); + explicit ImageInfo(const QUrl& mxcUrl, qint64 fileSize = -1, + const QMimeType& type = {}, QSize imageSize = {}, + const Omittable<EncryptedFile> &file = none, const QString& originalFilename = {}); - ImageInfo(const QUrl& u, const QJsonObject& infoJson, + ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson, + const Omittable<EncryptedFile> &encryptedFile, const QString& originalFilename = {}); void fillInfoJson(QJsonObject* infoJson) const; @@ -134,8 +148,8 @@ namespace EventContent { */ class Thumbnail : public ImageInfo { public: - Thumbnail() : ImageInfo(QUrl()) {} // To allow empty thumbnails - Thumbnail(const QJsonObject& infoJson); + Thumbnail() = default; // Allow empty thumbnails + Thumbnail(const QJsonObject& infoJson, const Omittable<EncryptedFile> &file = none); Thumbnail(const ImageInfo& info) : ImageInfo(info) {} using ImageInfo::ImageInfo; @@ -148,13 +162,13 @@ namespace EventContent { 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: + explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {} using Base::Base; }; @@ -175,7 +189,7 @@ namespace EventContent { explicit UrlBasedContent(const QJsonObject& json) : TypedBase(json) , InfoT(QUrl(json["url"].toString()), json["info"].toObject(), - json["filename"].toString()) + fromJson<Omittable<EncryptedFile>>(json["file"]), json["filename"].toString()) { // A small hack to facilitate links creation in QML. originalJson.insert("mediaId", InfoT::mediaId()); @@ -189,7 +203,11 @@ namespace EventContent { void fillJson(QJsonObject* json) const override { Q_ASSERT(json); - json->insert("url", InfoT::url.toString()); + if (!InfoT::file.has_value()) { + json->insert("url", InfoT::url.toString()); + } else { + json->insert("file", Quotient::toJson(*InfoT::file)); + } if (!InfoT::originalName.isEmpty()) json->insert("filename", InfoT::originalName); json->insert("info", toInfoJson<InfoT>(*this)); diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index 978668f2..fe624d70 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -6,16 +6,6 @@ #include "stateevent.h" namespace Quotient { -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); - } -} // namespace _impl /*! Create an event with proper type from a JSON object * @@ -26,7 +16,7 @@ namespace _impl { template <typename BaseEventT> inline event_ptr_tt<BaseEventT> loadEvent(const QJsonObject& fullJson) { - return _impl::loadEvent<BaseEventT>(fullJson, fullJson[TypeKeyL].toString()); + return doLoadEvent<BaseEventT>(fullJson, fullJson[TypeKeyL].toString()); } /*! Create an event from a type string and content JSON @@ -39,8 +29,8 @@ template <typename BaseEventT> inline event_ptr_tt<BaseEventT> loadEvent(const QString& matrixType, const QJsonObject& content) { - return _impl::loadEvent<BaseEventT>(basicEventJson(matrixType, content), - matrixType); + return doLoadEvent<BaseEventT>(basicEventJson(matrixType, content), + matrixType); } /*! Create a state event from a type string, content JSON and state key @@ -53,7 +43,7 @@ inline StateEventPtr loadStateEvent(const QString& matrixType, const QJsonObject& content, const QString& stateKey = {}) { - return _impl::loadEvent<StateEventBase>( + return doLoadEvent<StateEventBase>( basicStateEventJson(matrixType, content, stateKey), matrixType); } diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h index 777905f2..5a2b98c4 100644 --- a/lib/events/reactionevent.h +++ b/lib/events/reactionevent.h @@ -47,7 +47,7 @@ public: explicit ReactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) {} EventRelation relation() const { - return content<EventRelation>(QStringLiteral("m.relates_to")); + return contentPart<EventRelation>("m.relates_to"_ls); } }; REGISTER_EVENT_TYPE(ReactionEvent) diff --git a/lib/events/receiptevent.cpp b/lib/events/receiptevent.cpp index 4185d92d..72dbf2e3 100644 --- a/lib/events/receiptevent.cpp +++ b/lib/events/receiptevent.cpp @@ -25,6 +25,27 @@ Example of a Receipt Event: using namespace Quotient; +// The library loads the event-ids-to-receipts JSON map into a vector because +// map lookups are not used and vectors are massively faster. Same goes for +// de-/serialization of ReceiptsForEvent::receipts. +// (XXX: would this be generally preferred across CS API JSON maps?..) +QJsonObject toJson(const EventsWithReceipts& ewrs) +{ + QJsonObject json; + for (const auto& e : ewrs) { + QJsonObject receiptsJson; + for (const auto& r : e.receipts) + receiptsJson.insert(r.userId, + QJsonObject { { "ts"_ls, toJson(r.timestamp) } }); + json.insert(e.evtId, QJsonObject { { "m.read"_ls, receiptsJson } }); + } + return json; +} + +ReceiptEvent::ReceiptEvent(const EventsWithReceipts &ewrs) + : Event(typeId(), matrixTypeId(), toJson(ewrs)) +{} + EventsWithReceipts ReceiptEvent::eventsWithReceipts() const { EventsWithReceipts result; @@ -39,14 +60,14 @@ EventsWithReceipts ReceiptEvent::eventsWithReceipts() const } const auto reads = eventIt.value().toObject().value("m.read"_ls).toObject(); - QVector<Receipt> receipts; - receipts.reserve(reads.size()); + QVector<UserTimestamp> usersAtEvent; + usersAtEvent.reserve(reads.size()); for (auto userIt = reads.begin(); userIt != reads.end(); ++userIt) { const auto user = userIt.value().toObject(); - receipts.push_back( + usersAtEvent.push_back( { userIt.key(), fromJson<QDateTime>(user["ts"_ls]) }); } - result.push_back({ eventIt.key(), std::move(receipts) }); + result.push_back({ eventIt.key(), std::move(usersAtEvent) }); } return result; } diff --git a/lib/events/receiptevent.h b/lib/events/receiptevent.h index 4feec9ea..9683deef 100644 --- a/lib/events/receiptevent.h +++ b/lib/events/receiptevent.h @@ -9,19 +9,20 @@ #include <QtCore/QVector> namespace Quotient { -struct Receipt { +struct UserTimestamp { QString userId; QDateTime timestamp; }; struct ReceiptsForEvent { QString evtId; - QVector<Receipt> receipts; + QVector<UserTimestamp> receipts; }; using EventsWithReceipts = QVector<ReceiptsForEvent>; class ReceiptEvent : public Event { public: DEFINE_EVENT_TYPEID("m.receipt", ReceiptEvent) + explicit ReceiptEvent(const EventsWithReceipts& ewrs); explicit ReceiptEvent(const QJsonObject& obj) : Event(typeId(), obj) {} EventsWithReceipts eventsWithReceipts() const; diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h index ed560331..be20bf52 100644 --- a/lib/events/redactionevent.h +++ b/lib/events/redactionevent.h @@ -17,7 +17,7 @@ public: { return fullJson()["redacts"_ls].toString(); } - QString reason() const { return contentJson()["reason"_ls].toString(); } + QString reason() const { return contentPart<QString>("reason"_ls); } }; REGISTER_EVENT_TYPE(RedactionEvent) } // namespace Quotient diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index a4257895..8618ba31 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -20,12 +20,12 @@ public: : StateEvent(typeId(), matrixTypeId(), QString(), avatar) {} // A replica of EventContent::ImageInfo constructor - explicit RoomAvatarEvent(const QUrl& u, qint64 fileSize = -1, + explicit RoomAvatarEvent(const QUrl& mxcUrl, qint64 fileSize = -1, QMimeType mimeType = {}, const QSize& imageSize = {}, const QString& originalFilename = {}) : RoomAvatarEvent(EventContent::ImageContent { - u, fileSize, mimeType, imageSize, originalFilename }) + mxcUrl, fileSize, mimeType, imageSize, none, originalFilename }) {} QUrl url() const { return content().url; } diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp index 6558bade..bb6de648 100644 --- a/lib/events/roomcreateevent.cpp +++ b/lib/events/roomcreateevent.cpp @@ -5,19 +5,35 @@ using namespace Quotient; +template <> +struct Quotient::JsonConverter<RoomType> { + static RoomType load(const QJsonValue& jv) + { + const auto& roomTypeString = jv.toString(); + for (auto it = RoomTypeStrings.begin(); it != RoomTypeStrings.end(); + ++it) + if (roomTypeString == *it) + return RoomType(it - RoomTypeStrings.begin()); + + if (!roomTypeString.isEmpty()) + qCWarning(EVENTS) << "Unknown Room Type: " << roomTypeString; + return RoomType::Undefined; + } +}; + bool RoomCreateEvent::isFederated() const { - return fromJson<bool>(contentJson()["m.federate"_ls]); + return contentPart<bool>("m.federate"_ls); } QString RoomCreateEvent::version() const { - return fromJson<QString>(contentJson()["room_version"_ls]); + return contentPart<QString>("room_version"_ls); } RoomCreateEvent::Predecessor RoomCreateEvent::predecessor() const { - const auto predJson = contentJson()["predecessor"_ls].toObject(); + const auto predJson = contentPart<QJsonObject>("predecessor"_ls); return { fromJson<QString>(predJson[RoomIdKeyL]), fromJson<QString>(predJson[EventIdKeyL]) }; } @@ -26,3 +42,8 @@ bool RoomCreateEvent::isUpgrade() const { return contentJson().contains("predecessor"_ls); } + +RoomType RoomCreateEvent::roomType() const +{ + return contentPart<RoomType>("type"_ls); +} diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h index 05e623ed..b3ad287c 100644 --- a/lib/events/roomcreateevent.h +++ b/lib/events/roomcreateevent.h @@ -4,6 +4,7 @@ #pragma once #include "stateevent.h" +#include "quotient_common.h" namespace Quotient { class RoomCreateEvent : public StateEventBase { @@ -24,6 +25,7 @@ public: QString version() const; Predecessor predecessor() const; bool isUpgrade() const; + RoomType roomType() const; }; REGISTER_EVENT_TYPE(RoomCreateEvent) } // namespace Quotient diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 4fec9d2b..b728e0bf 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -9,9 +9,6 @@ using namespace Quotient; -[[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) @@ -19,7 +16,7 @@ RoomEvent::RoomEvent(Type type, event_mtype_t matrixType, RoomEvent::RoomEvent(Type type, const QJsonObject& json) : Event(type, json) { - if (const auto redaction = unsignedJson()[RedactedCauseKeyL]; + if (const auto redaction = unsignedPart(RedactedCauseKeyL); redaction.isObject()) _redactedBecause = makeEvent<RedactionEvent>(redaction.toObject()); } @@ -45,14 +42,14 @@ QString RoomEvent::senderId() const bool RoomEvent::isReplaced() const { - return unsignedJson()["m.relations"_ls].toObject().contains("m.replace"); + return unsignedPart<QJsonObject>("m.relations"_ls).contains("m.replace"); } QString RoomEvent::replacedBy() const { // clang-format off - return unsignedJson()["m.relations"_ls].toObject() - .value("m.replace").toObject() + return unsignedPart<QJsonObject>("m.relations"_ls) + .value("m.replace"_ls).toObject() .value(EventIdKeyL).toString(); // clang-format on } @@ -64,7 +61,7 @@ QString RoomEvent::redactionReason() const QString RoomEvent::transactionId() const { - return unsignedJson()["transaction_id"_ls].toString(); + return unsignedPart<QString>("transaction_id"_ls); } QString RoomEvent::stateKey() const diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index fea509c0..8be58481 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -12,16 +12,8 @@ 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>; + static inline _impl::EventFactory<RoomEvent> factory { "RoomEvent" }; // RedactionEvent is an incomplete type here so we cannot inline // constructors and destructors and we cannot use 'using'. @@ -32,11 +24,12 @@ public: QString id() const; QDateTime originTimestamp() const; - [[deprecated("Use originTimestamp()")]] QDateTime timestamp() const { - return originTimestamp(); - } QString roomId() const; QString senderId() const; + //! \brief Determine whether the event has been replaced + //! + //! \return true if this event has been overridden by another event + //! with `"rel_type": "m.replace"`; false otherwise bool isReplaced() const; QString replacedBy() const; bool isRedacted() const { return bool(_redactedBecause); } @@ -48,28 +41,23 @@ public: QString transactionId() const; QString stateKey() const; + //! \brief Fill the pending event object with the room id void setRoomId(const QString& roomId); + //! \brief Fill the pending event object with the sender id 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() - */ + //! \brief Fill the pending event object with the transaction id + //! \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 - */ + //! \brief Add an event id to locally created events after they are sent + //! + //! When a new event is created locally, it has no id; the homeserver + //! assigns it once the event is sent. 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 about the change; there's no signal or + //! callback for that in RoomEvent. void addId(const QString& newId); protected: @@ -90,8 +78,8 @@ public: ~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); } + QString callId() const { return contentPart<QString>("call_id"_ls); } + int version() const { return contentPart<int>("version"_ls); } }; } // namespace Quotient Q_DECLARE_METATYPE(Quotient::RoomEvent*) diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index 14e80324..d021fbec 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -13,10 +13,10 @@ public: explicit RoomKeyEvent(const QJsonObject& obj); - QString algorithm() const { return content<QString>("algorithm"_ls); } - QString roomId() const { return content<QString>(RoomIdKeyL); } - QString sessionId() const { return content<QString>("session_id"_ls); } - QString sessionKey() const { return content<QString>("session_key"_ls); } + QString algorithm() const { return contentPart<QString>("algorithm"_ls); } + QString roomId() const { return contentPart<QString>(RoomIdKeyL); } + QString sessionId() const { return contentPart<QString>("session_id"_ls); } + QString sessionKey() const { return contentPart<QString>("session_key"_ls); } }; REGISTER_EVENT_TYPE(RoomKeyEvent) } // namespace Quotient diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index 9634ca3a..3141f6b5 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -7,27 +7,26 @@ #include "converters.h" #include "logging.h" -#include <array> - -static const std::array<QString, 5> membershipStrings = { - { QStringLiteral("invite"), QStringLiteral("join"), QStringLiteral("knock"), - QStringLiteral("leave"), QStringLiteral("ban") } -}; +#include <QtCore/QtAlgorithms> namespace Quotient { template <> -struct JsonConverter<MembershipType> { - static MembershipType load(const QJsonValue& jv) +struct JsonConverter<Membership> { + static Membership 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()); - - if (!membershipString.isEmpty()) - qCWarning(EVENTS) << "Unknown MembershipType: " << membershipString; - return MembershipType::Undefined; + const auto& ms = jv.toString(); + if (ms.isEmpty()) + { + qCWarning(EVENTS) << "Empty membership state"; + return Membership::Invalid; + } + const auto it = + std::find(MembershipStrings.begin(), MembershipStrings.end(), ms); + if (it != MembershipStrings.end()) + return Membership(1U << (it - MembershipStrings.begin())); + + qCWarning(EVENTS) << "Unknown Membership value: " << ms; + return Membership::Invalid; } }; } // namespace Quotient @@ -35,7 +34,7 @@ struct JsonConverter<MembershipType> { using namespace Quotient; MemberEventContent::MemberEventContent(const QJsonObject& json) - : membership(fromJson<MembershipType>(json["membership"_ls])) + : membership(fromJson<Membership>(json["membership"_ls])) , isDirect(json["is_direct"_ls].toBool()) , displayName(fromJson<Omittable<QString>>(json["displayname"_ls])) , avatarUrl(fromJson<Omittable<QString>>(json["avatar_url"_ls])) @@ -48,10 +47,10 @@ MemberEventContent::MemberEventContent(const QJsonObject& json) void MemberEventContent::fillJson(QJsonObject* o) const { Q_ASSERT(o); - Q_ASSERT_X(membership != MembershipType::Undefined, __FUNCTION__, - "The key 'membership' must be explicit in MemberEventContent"); - if (membership != MembershipType::Undefined) - o->insert(QStringLiteral("membership"), membershipStrings[membership]); + if (membership != Membership::Invalid) + o->insert(QStringLiteral("membership"), + MembershipStrings[qCountTrailingZeroBits( + std::underlying_type_t<Membership>(membership))]); if (displayName) o->insert(QStringLiteral("displayname"), *displayName); if (avatarUrl && avatarUrl->isValid()) @@ -67,51 +66,49 @@ bool RoomMemberEvent::changesMembership() const bool RoomMemberEvent::isInvite() const { - return membership() == MembershipType::Invite && changesMembership(); + return membership() == Membership::Invite && changesMembership(); } bool RoomMemberEvent::isRejectedInvite() const { - return membership() == MembershipType::Leave && prevContent() - && prevContent()->membership == MembershipType::Invite; + return membership() == Membership::Leave && prevContent() + && prevContent()->membership == Membership::Invite; } bool RoomMemberEvent::isJoin() const { - return membership() == MembershipType::Join && changesMembership(); + return membership() == Membership::Join && changesMembership(); } bool RoomMemberEvent::isLeave() const { - return membership() == MembershipType::Leave && prevContent() + return membership() == Membership::Leave && prevContent() && prevContent()->membership != membership() - && prevContent()->membership != MembershipType::Ban - && prevContent()->membership != MembershipType::Invite; + && prevContent()->membership != Membership::Ban + && prevContent()->membership != Membership::Invite; } bool RoomMemberEvent::isBan() const { - return membership() == MembershipType::Ban && changesMembership(); + return membership() == Membership::Ban && changesMembership(); } bool RoomMemberEvent::isUnban() const { - return membership() == MembershipType::Leave && prevContent() - && prevContent()->membership == MembershipType::Ban; + return membership() == Membership::Leave && prevContent() + && prevContent()->membership == Membership::Ban; } bool RoomMemberEvent::isRename() const { - auto prevName = prevContent() && prevContent()->displayName - ? *prevContent()->displayName - : QString(); - return newDisplayName() != prevName; + return prevContent() && prevContent()->displayName + ? newDisplayName() != *prevContent()->displayName + : newDisplayName().has_value(); } bool RoomMemberEvent::isAvatarUpdate() const { - auto prevAvatarUrl = prevContent() && prevContent()->avatarUrl - ? *prevContent()->avatarUrl - : QUrl(); - return newAvatarUrl() != prevAvatarUrl; + return prevContent() && prevContent()->avatarUrl + ? newAvatarUrl() != *prevContent()->avatarUrl + : newAvatarUrl().has_value(); } diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index f2fbe689..0fb464d4 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -7,23 +7,21 @@ #include "eventcontent.h" #include "stateevent.h" +#include "quotient_common.h" namespace Quotient { class MemberEventContent : public EventContent::Base { public: - enum MembershipType : unsigned char { - Invite = 0, - Join, - Knock, - Leave, - Ban, - Undefined - }; + using MembershipType + [[deprecated("Use Quotient::Membership instead")]] = Membership; - explicit MemberEventContent(MembershipType mt = Join) : membership(mt) {} + explicit MemberEventContent(Membership ms = Membership::Join) + : membership(ms) + {} explicit MemberEventContent(const QJsonObject& json); - MembershipType membership; + Membership membership; + /// (Only for invites) Whether the invite is to a direct chat bool isDirect = false; Omittable<QString> displayName; Omittable<QUrl> avatarUrl; @@ -33,15 +31,15 @@ protected: void fillJson(QJsonObject* o) const override; }; -using MembershipType = MemberEventContent::MembershipType; +using MembershipType [[deprecated("Use Membership instead")]] = Membership; class RoomMemberEvent : public StateEvent<MemberEventContent> { Q_GADGET public: DEFINE_EVENT_TYPEID("m.room.member", RoomMemberEvent) - using MembershipType = MemberEventContent::MembershipType; - Q_ENUM(MembershipType) + using MembershipType + [[deprecated("Use Quotient::Membership instead")]] = Membership; explicit RoomMemberEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} @@ -51,21 +49,20 @@ public: 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 - */ + //! \brief 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 have a specialisation of + //! EventFactory\<> and be constructible with unknownTypeId() instead of + //! its genuine id. Don't use directly. + //! \sa EventFactory, loadEvent, GetMembersByRoomJob RoomMemberEvent(Type type, const QJsonObject& fullJson) : StateEvent(type, fullJson) {} - MembershipType membership() const { return content().membership; } + Membership membership() const { return content().membership; } QString userId() const { return stateKey(); } bool isDirect() const { return content().isDirect; } Omittable<QString> newDisplayName() const { return content().displayName; } @@ -91,14 +88,13 @@ public: }; template <> -class EventFactory<RoomMemberEvent> { -public: - static event_ptr_tt<RoomMemberEvent> make(const QJsonObject& json, - const QString&) - { +inline event_ptr_tt<RoomMemberEvent> +doLoadEvent<RoomMemberEvent>(const QJsonObject& json, const QString& matrixType) +{ + if (matrixType == QLatin1String(RoomMemberEvent::matrixTypeId())) return makeEvent<RoomMemberEvent>(json); - } -}; + return makeEvent<RoomMemberEvent>(unknownEventTypeId(), json); +} REGISTER_EVENT_TYPE(RoomMemberEvent) } // namespace Quotient diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 31c0fd9e..2b7b4166 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -10,7 +10,9 @@ #include <QtCore/QFileInfo> #include <QtCore/QMimeDatabase> #include <QtGui/QImageReader> -#include <QtMultimedia/QMediaResource> +#if QT_VERSION_MAJOR < 6 +# include <QtMultimedia/QMediaResource> +#endif using namespace Quotient; using namespace EventContent; @@ -133,6 +135,7 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody, MsgType msgType, : RoomMessageEvent(plainBody, msgTypeToJson(msgType), content) {} +#if QT_VERSION_MAJOR < 6 TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) { auto filePath = file.absoluteFilePath(); @@ -142,21 +145,21 @@ TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) auto mimeTypeName = mimeType.name(); if (mimeTypeName.startsWith("image/")) return new ImageContent(localUrl, file.size(), mimeType, - QImageReader(filePath).size(), + QImageReader(filePath).size(), none, file.fileName()); // duration can only be obtained asynchronously and can only be reliably // done by starting to play the file. Left for a future implementation. if (mimeTypeName.startsWith("video/")) return new VideoContent(localUrl, file.size(), mimeType, - QMediaResource(localUrl).resolution(), + QMediaResource(localUrl).resolution(), none, file.fileName()); if (mimeTypeName.startsWith("audio/")) - return new AudioContent(localUrl, file.size(), mimeType, + return new AudioContent(localUrl, file.size(), mimeType, none, file.fileName()); } - return new FileContent(localUrl, file.size(), mimeType, file.fileName()); + return new FileContent(localUrl, file.size(), mimeType, none, file.fileName()); } RoomMessageEvent::RoomMessageEvent(const QString& plainBody, @@ -166,6 +169,7 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody, : rawMsgTypeForFile(file), contentFromFile(file, asGenericFile)) {} +#endif RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj), _content(nullptr) @@ -200,12 +204,12 @@ RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const QString RoomMessageEvent::rawMsgtype() const { - return contentJson()[MsgTypeKeyL].toString(); + return contentPart<QString>(MsgTypeKeyL); } QString RoomMessageEvent::plainBody() const { - return contentJson()[BodyKeyL].toString(); + return contentPart<QString>(BodyKeyL); } QMimeType RoomMessageEvent::mimeType() const diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 8303ce4e..56597ddc 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -18,10 +18,6 @@ namespace MessageEventContent = EventContent; // Back-compatibility */ 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) @@ -42,8 +38,12 @@ public: explicit RoomMessageEvent(const QString& plainBody, MsgType msgType = MsgType::Text, EventContent::TypedBase* content = nullptr); +#if QT_VERSION_MAJOR < 6 + [[deprecated("Create an EventContent object on the client side" + " and pass it to other constructors")]] // explicit RoomMessageEvent(const QString& plainBody, const QFileInfo& file, bool asGenericFile = false); +#endif explicit RoomMessageEvent(const QJsonObject& obj); MsgType msgtype() const; @@ -58,9 +58,26 @@ public: _content.data()); } QMimeType mimeType() const; + //! \brief Determine whether the message has text content + //! + //! \return true, if the message type is one of m.text, m.notice, m.emote, + //! or the message type is unspecified (in which case plainBody() + //! can still be examined); false otherwise bool hasTextContent() const; + //! \brief Determine whether the message has a file/attachment + //! + //! \return true, if the message has a data structure corresponding to + //! a file (such as m.file or m.audio); false otherwise bool hasFileContent() const; + //! \brief Determine whether the message has a thumbnail + //! + //! \return true, if the message has a data structure corresponding to + //! a thumbnail (the message type may be one for visual content, + //! such as m.image, or generic binary content, i.e. m.file); + //! false otherwise bool hasThumbnail() const; + //! \brief Obtain id of an event replaced by the current one + //! \sa RoomEvent::isReplaced, RoomEvent::replacedBy QString replacedEvent() const; static QString rawMsgTypeForUrl(const QUrl& url); diff --git a/lib/events/roomtombstoneevent.cpp b/lib/events/roomtombstoneevent.cpp index 080d269c..2c3492d6 100644 --- a/lib/events/roomtombstoneevent.cpp +++ b/lib/events/roomtombstoneevent.cpp @@ -7,10 +7,10 @@ using namespace Quotient; QString RoomTombstoneEvent::serverMessage() const { - return fromJson<QString>(contentJson()["body"_ls]); + return contentPart<QString>("body"_ls); } QString RoomTombstoneEvent::successorRoomId() const { - return fromJson<QString>(contentJson()["replacement_room"_ls]); + return contentPart<QString>("replacement_room"_ls); } diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index c977cb6e..13597979 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -8,8 +8,7 @@ namespace Quotient { namespace EventContent { template <typename T> - class SimpleContent { - public: + struct SimpleContent { using value_type = T; // The constructor is templated to enable perfect forwarding @@ -25,11 +24,8 @@ namespace EventContent { return { { key, Quotient::toJson(value) } }; } - public: T value; - - protected: - QString key; + const QString key; }; } // namespace EventContent @@ -57,19 +53,16 @@ DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name) DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic) DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages", QStringList, pinnedEvents) -class RoomAliasesEvent - : public StateEvent<EventContent::SimpleContent<QStringList>> { +class [[deprecated( + "m.room.aliases events are deprecated by the Matrix spec; use" + " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases")]] // +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 42fc9054..e53d47d4 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -5,20 +5,13 @@ 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. -[[maybe_unused]] static auto stateEventTypeInitialised = - RoomEvent::factory_t::addMethod( - [](const QJsonObject& json, const QString& matrixType) -> StateEventPtr { - if (!json.contains(StateKeyKeyL)) - return nullptr; - - if (auto e = StateEventBase::factory_t::make(json, matrixType)) - return e; - - return makeEvent<StateEventBase>(unknownEventTypeId(), json); - }); +StateEventBase::StateEventBase(Type type, const QJsonObject& json) + : RoomEvent(json.contains(StateKeyKeyL) ? type : unknownEventTypeId(), json) +{ + if (Event::type() == unknownEventTypeId() && !json.contains(StateKeyKeyL)) + qWarning(EVENTS) << "Attempt to create a state event with no stateKey -" + "forcing the event type to unknown to avoid damage"; +} StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, const QString& stateKey, @@ -28,22 +21,22 @@ StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, bool StateEventBase::repeatsState() const { - const auto prevContentJson = unsignedJson().value(PrevContentKeyL); + const auto prevContentJson = unsignedPart(PrevContentKeyL); return fullJson().value(ContentKeyL) == prevContentJson; } QString StateEventBase::replacedState() const { - return unsignedJson().value("replaces_state"_ls).toString(); + return unsignedPart<QString>("replaces_state"_ls); } void StateEventBase::dumpTo(QDebug dbg) const { if (!stateKey().isEmpty()) dbg << '<' << stateKey() << "> "; - if (unsignedJson().contains(PrevContentKeyL)) - dbg << QJsonDocument(unsignedJson()[PrevContentKeyL].toObject()) - .toJson(QJsonDocument::Compact) + if (const auto prevContentJson = unsignedPart<QJsonObject>(PrevContentKeyL); + !prevContentJson.isEmpty()) + dbg << QJsonDocument(prevContentJson).toJson(QJsonDocument::Compact) << " -> "; RoomEvent::dumpTo(dbg); } diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 1415f709..c37965aa 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -18,13 +18,10 @@ inline QJsonObject basicStateEventJson(const QString& matrixTypeId, } class StateEventBase : public RoomEvent { - Q_GADGET - Q_PROPERTY(QString stateKey READ stateKey CONSTANT) public: - using factory_t = EventFactory<StateEventBase>; + static inline _impl::EventFactory<StateEventBase> factory { "StateEvent" }; - StateEventBase(Type type, const QJsonObject& json) : RoomEvent(type, json) - {} + StateEventBase(Type type, const QJsonObject& json); StateEventBase(Type type, event_mtype_t matrixType, const QString& stateKey = {}, const QJsonObject& contentJson = {}); @@ -39,6 +36,22 @@ public: using StateEventPtr = event_ptr_tt<StateEventBase>; using StateEvents = EventsArray<StateEventBase>; +//! \brief Override RoomEvent factory with that from StateEventBase if JSON has +//! stateKey +//! +//! This means in particular that an event with a type known to RoomEvent but +//! having stateKey set (even to an empty value) will be treated as a state +//! event and most likely end up as unknown (consider, e.g., m.room.message +//! that has stateKey set). +template <> +inline RoomEventPtr doLoadEvent(const QJsonObject& json, + const QString& matrixType) +{ + if (json.contains(StateKeyKeyL)) + return StateEventBase::factory.loadEvent(json, matrixType); + return RoomEvent::factory.loadEvent(json, matrixType); +} + template <> inline bool is<StateEventBase>(const Event& e) { @@ -100,10 +113,6 @@ public: 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; diff --git a/lib/events/stickerevent.cpp b/lib/events/stickerevent.cpp index ea4dff3f..628fd154 100644 --- a/lib/events/stickerevent.cpp +++ b/lib/events/stickerevent.cpp @@ -12,7 +12,7 @@ StickerEvent::StickerEvent(const QJsonObject &obj) QString StickerEvent::body() const { - return content<QString>("body"_ls); + return contentPart<QString>("body"_ls); } const EventContent::ImageContent &StickerEvent::image() const diff --git a/lib/events/typingevent.cpp b/lib/events/typingevent.cpp index e97e978f..7e5d7ee6 100644 --- a/lib/events/typingevent.cpp +++ b/lib/events/typingevent.cpp @@ -7,5 +7,5 @@ using namespace Quotient; QStringList TypingEvent::users() const { - return fromJson<QStringList>(contentJson()["user_ids"_ls]); + return contentPart<QStringList>("user_ids"_ls); } diff --git a/lib/eventstats.cpp b/lib/eventstats.cpp new file mode 100644 index 00000000..9fa7f5ff --- /dev/null +++ b/lib/eventstats.cpp @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2021 Quotient contributors +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "eventstats.h" + +using namespace Quotient; + +EventStats EventStats::fromRange(const Room* room, const Room::rev_iter_t& from, + const Room::rev_iter_t& to, + const EventStats& init) +{ + Q_ASSERT(to <= room->historyEdge()); + Q_ASSERT(from >= Room::rev_iter_t(room->syncEdge())); + Q_ASSERT(from <= to); + QElapsedTimer et; + et.start(); + const auto result = + accumulate(from, to, init, + [room](EventStats acc, const TimelineItem& ti) { + acc.notableCount += room->isEventNotable(ti); + acc.highlightCount += room->notificationFor(ti).type + == Notification::Highlight; + return acc; + }); + if (et.nsecsElapsed() > profilerMinNsecs() / 10) + qCDebug(PROFILER).nospace() + << "Event statistics collection over index range [" << from->index() + << "," << (to - 1)->index() << "] took " << et; + return result; +} + +EventStats EventStats::fromMarker(const Room* room, + const EventStats::marker_t& marker) +{ + const auto s = fromRange(room, marker_t(room->syncEdge()), marker, + { 0, 0, marker == room->historyEdge() }); + Q_ASSERT(s.isValidFor(room, marker)); + return s; +} + +EventStats EventStats::fromCachedCounters(Omittable<int> notableCount, + Omittable<int> highlightCount) +{ + const auto hCount = std::max(0, highlightCount.value_or(0)); + if (!notableCount.has_value()) + return { 0, hCount, true }; + auto nCount = notableCount.value_or(0); + return { std::max(0, nCount), hCount, nCount != -1 }; +} + +bool EventStats::updateOnMarkerMove(const Room* room, const marker_t& oldMarker, + const marker_t& newMarker) +{ + if (newMarker == oldMarker) + return false; + + // Double-check consistency between the old marker and the old stats + Q_ASSERT(isValidFor(room, oldMarker)); + Q_ASSERT(oldMarker > newMarker); + + // A bit of optimisation: only calculate the difference if the marker moved + // less than half the remaining timeline ahead; otherwise, recalculation + // over the remaining timeline will very likely be faster. + if (oldMarker != room->historyEdge() + && oldMarker - newMarker < newMarker - marker_t(room->syncEdge())) { + const auto removedStats = fromRange(room, newMarker, oldMarker); + Q_ASSERT(notableCount >= removedStats.notableCount + && highlightCount >= removedStats.highlightCount); + notableCount -= removedStats.notableCount; + highlightCount -= removedStats.highlightCount; + return removedStats.notableCount > 0 || removedStats.highlightCount > 0; + } + + const auto newStats = EventStats::fromMarker(room, newMarker); + if (!isEstimate && newStats == *this) + return false; + *this = newStats; + return true; +} + +bool EventStats::isValidFor(const Room* room, const marker_t& marker) const +{ + const auto markerAtHistoryEdge = marker == room->historyEdge(); + // Either markerAtHistoryEdge and isEstimate are in the same state, or it's + // a special case of no notable events and the marker at history edge + // (then isEstimate can assume any value). + return markerAtHistoryEdge == isEstimate + || (markerAtHistoryEdge && notableCount == 0); +} + +QDebug Quotient::operator<<(QDebug dbg, const EventStats& es) +{ + QDebugStateSaver _(dbg); + dbg.nospace() << es.notableCount << '/' << es.highlightCount; + if (es.isEstimate) + dbg << " (estimated)"; + return dbg; +} diff --git a/lib/eventstats.h b/lib/eventstats.h new file mode 100644 index 00000000..77c661a7 --- /dev/null +++ b/lib/eventstats.h @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2021 Quotient contributors +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "room.h" + +namespace Quotient { + +//! \brief Counters of unread events and highlights with a precision flag +//! +//! This structure contains a static snapshot with values of unread counters +//! returned by Room::partiallyReadStats and Room::unreadStats (properties +//! or methods). +//! +//! \note It's just a simple grouping of counters and is not automatically +//! updated from the room as subsequent syncs arrive. +//! \sa Room::unreadStats, Room::partiallyReadStats, Room::isEventNotable +struct EventStats { + Q_GADGET + Q_PROPERTY(qsizetype notableCount MEMBER notableCount CONSTANT) + Q_PROPERTY(qsizetype highlightCount MEMBER highlightCount CONSTANT) + Q_PROPERTY(bool isEstimate MEMBER isEstimate CONSTANT) +public: + //! The number of "notable" events in an events range + //! \sa Room::isEventNotable + qsizetype notableCount = 0; + qsizetype highlightCount = 0; + //! \brief Whether the counter values above are exact + //! + //! This is false when the end marker (m.read receipt or m.fully_read) used + //! to collect the stats points to an event loaded locally and the counters + //! can therefore be calculated exactly using the locally available segment + //! of the timeline; true when the marker points to an event outside of + //! the local timeline (in which case the estimation is made basing on + //! the data supplied by the homeserver as well as counters saved from + //! the previous run of the client). + bool isEstimate = true; + + // TODO: replace with = default once C++20 becomes a requirement on clients + bool operator==(const EventStats& rhs) const + { + return notableCount == rhs.notableCount + && highlightCount == rhs.highlightCount + && isEstimate == rhs.isEstimate; + } + bool operator!=(const EventStats& rhs) const { return !operator==(rhs); } + + //! \brief Check whether the event statistics are empty + //! + //! Empty statistics have notable and highlight counters of zero and + //! isEstimate set to false. + Q_INVOKABLE bool empty() const + { + return notableCount == 0 && !isEstimate && highlightCount == 0; + } + + using marker_t = Room::rev_iter_t; + + //! \brief Build event statistics on a range of events + //! + //! This is a factory that returns an EventStats instance with counts of + //! notable and highlighted events between \p from and \p to reverse + //! timeline iterators; the \p init parameter allows to override + //! the initial statistics object and start from other values. + static EventStats fromRange(const Room* room, const marker_t& from, + const marker_t& to, + const EventStats& init = { 0, 0, false }); + + //! \brief Build event statistics on a range from sync edge to marker + //! + //! This is mainly a shortcut for \code + //! <tt>fromRange(room, marker_t(room->syncEdge()), marker)</tt> + //! \endcode except that it also sets isEstimate to true if (and only if) + //! <tt>to == room->historyEdge()</tt>. + static EventStats fromMarker(const Room* room, const marker_t& marker); + + //! \brief Loads a statistics object from the cached counters + //! + //! Sets isEstimate to `true` unless both notableCount and highlightCount + //! are equal to -1. + static EventStats fromCachedCounters(Omittable<int> notableCount, + Omittable<int> highlightCount = none); + + //! \brief Update statistics when a read marker moves down the timeline + //! + //! Removes events between oldMarker and newMarker from statistics + //! calculation if \p oldMarker points to an existing event in the timeline, + //! or recalculates the statistics entirely if \p oldMarker points + //! to <tt>room->historyEdge()</tt>. Always results in exact statistics + //! (<tt>isEstimate == false</tt>. + //! \param oldMarker Must point correspond to the _current_ statistics + //! isEstimate state, i.e. it should point to + //! <tt>room->historyEdge()</tt> if <tt>isEstimate == true</tt>, or + //! to a valid position within the timeline otherwise + //! \param newMarker Must point to a valid position in the timeline (not to + //! <tt>room->historyEdge()</tt> that is equal to or closer to + //! the sync edge than \p oldMarker + //! \return true if either notableCount or highlightCount changed, or if + //! the statistics was completely recalculated; false otherwise + bool updateOnMarkerMove(const Room* room, const marker_t& oldMarker, + const marker_t& newMarker); + + //! \brief Validate the statistics object against the given marker + //! + //! Checks whether the statistics object data are valid for a given marker. + //! No stats recalculation takes place, only isEstimate and zero-ness + //! of notableCount are checked. + bool isValidFor(const Room* room, const marker_t& marker) const; +}; + +QDebug operator<<(QDebug dbg, const EventStats& es); + +} diff --git a/lib/function_traits.cpp b/lib/function_traits.cpp new file mode 100644 index 00000000..20bcf30e --- /dev/null +++ b/lib/function_traits.cpp @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "function_traits.h" + +// Tests for function_traits<> + +using namespace Quotient; + +int f_(); +static_assert(std::is_same_v<fn_return_t<decltype(f_)>, int>, + "Test fn_return_t<>"); + +void f1_(int, float); +static_assert(std::is_same_v<fn_arg_t<decltype(f1_), 1>, float>, + "Test fn_arg_t<>"); + +struct Fo { + int operator()(); + static constexpr auto l = [] { return 0.0f; }; + bool memFn(); + void constMemFn() const&; + double field; + const double field2; +}; +static_assert(std::is_same_v<fn_return_t<Fo>, int>, + "Test return type of function object"); +static_assert(std::is_same_v<fn_return_t<decltype(Fo::l)>, float>, + "Test return type of lambda"); +static_assert(std::is_same_v<fn_arg_t<decltype(&Fo::memFn)>, Fo>, + "Test first argument type of member function"); +static_assert(std::is_same_v<fn_return_t<decltype(&Fo::memFn)>, bool>, + "Test return type of member function"); +static_assert(std::is_same_v<fn_arg_t<decltype(&Fo::constMemFn)>, const Fo&>, + "Test first argument type of const member function"); +static_assert(std::is_void_v<fn_return_t<decltype(&Fo::constMemFn)>>, + "Test return type of const member function"); +static_assert(std::is_same_v<fn_return_t<decltype(&Fo::field)>, double&>, + "Test return type of a class member"); +static_assert(std::is_same_v<fn_return_t<decltype(&Fo::field2)>, const double&>, + "Test return type of a const class member"); + +struct Fo1 { + void operator()(int); +}; +static_assert(std::is_same_v<fn_arg_t<Fo1>, int>, + "Test fn_arg_t defaulting to first argument"); + +template <typename T> +static void ft(const std::vector<T>&); +static_assert( + std::is_same<fn_arg_t<decltype(ft<double>)>, const std::vector<double>&>(), + "Test function templates"); diff --git a/lib/function_traits.h b/lib/function_traits.h new file mode 100644 index 00000000..83b8e425 --- /dev/null +++ b/lib/function_traits.h @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <functional> + +namespace Quotient { + +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...>; + // See also the comment for wrap_in_function() in qt_connection_util.h + using function_type = std::function<ReturnT(ArgTs...)>; +}; + +namespace _impl { + template <typename AlwaysVoid, typename> + struct fn_object_traits; + + // Specialisation for a lambda function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_object_traits<void, ReturnT (ClassT::*)(ArgTs...)> + : function_traits<ReturnT(ArgTs...)> {}; + + // Specialisation for a const lambda function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_object_traits<void, ReturnT (ClassT::*)(ArgTs...) const> + : function_traits<ReturnT(ArgTs...)> {}; + + // Specialisation for function objects with (non-overloaded) operator() + // (this includes non-generic lambdas) + template <typename T> + struct fn_traits<decltype(void(&T::operator())), T> + : public fn_object_traits<void, decltype(&T::operator())> {}; + + // Specialisation for a member function in a non-functor class + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...)> + : function_traits<ReturnT(ClassT, 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(const ClassT&, ArgTs...)> {}; + + // Specialisation for a constref member function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...) const&> + : function_traits<ReturnT(const ClassT&, ArgTs...)> {}; + + // Specialisation for a prvalue member function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...) &&> + : function_traits<ReturnT(ClassT&&, ArgTs...)> {}; + + // Specialisation for a pointer-to-member + template <typename ReturnT, typename ClassT> + struct fn_traits<void, ReturnT ClassT::*> + : function_traits<ReturnT&(ClassT)> {}; + + // Specialisation for a const pointer-to-member + template <typename ReturnT, typename ClassT> + struct fn_traits<void, const ReturnT ClassT::*> + : function_traits<const ReturnT&(ClassT)> {}; +} // 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>; + +} // namespace Quotient diff --git a/lib/identity/definitions/request_email_validation.h b/lib/identity/definitions/request_email_validation.h index 079da953..87549505 100644 --- a/lib/identity/definitions/request_email_validation.h +++ b/lib/identity/definitions/request_email_validation.h @@ -11,16 +11,16 @@ 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 + /// `[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`` + /// 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 + /// 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 diff --git a/lib/identity/definitions/request_msisdn_validation.h b/lib/identity/definitions/request_msisdn_validation.h index a29fd0de..d2ea463f 100644 --- a/lib/identity/definitions/request_msisdn_validation.h +++ b/lib/identity/definitions/request_msisdn_validation.h @@ -11,20 +11,20 @@ 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 + /// `[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. + /// 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 + /// 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`` + /// 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 diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 48c2996d..971fea7b 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -5,6 +5,7 @@ #include "basejob.h" #include "connectiondata.h" +#include "quotient_common.h" #include <QtCore/QRegularExpression> #include <QtCore/QTimer> @@ -15,8 +16,6 @@ #include <QtNetwork/QNetworkReply> #include <QtNetwork/QNetworkRequest> -#include <array> - using namespace Quotient; using std::chrono::seconds, std::chrono::milliseconds; using namespace std::chrono_literals; @@ -25,7 +24,7 @@ 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; + return httpCode == 410 ? IncorrectRequest : NotFound; switch (httpCode) { case 401: return Unauthorised; @@ -33,19 +32,19 @@ BaseJob::StatusCode BaseJob::Status::fromHttpCode(int httpCode) case 403: case 407: // clang-format on return ContentAccessError; case 404: - return NotFoundError; + return NotFound; // clang-format off case 400: case 405: case 406: case 426: case 428: case 505: // clang-format on case 494: // Unofficial nginx "Request header too large" case 497: // Unofficial nginx "HTTP request sent to HTTPS port" - return IncorrectRequestError; + return IncorrectRequest; case 429: - return TooManyRequestsError; + return TooManyRequests; case 501: case 510: - return RequestNotImplementedError; + return RequestNotImplemented; case 511: - return NetworkAuthRequiredError; + return NetworkAuthRequired; default: return NetworkError; } @@ -63,12 +62,6 @@ QDebug BaseJob::Status::dumpToLog(QDebug dbg) const return dbg << ": " << message; } -template <typename... Ts> -constexpr auto make_array(Ts&&... items) -{ - return std::array<std::common_type_t<Ts...>, sizeof...(Ts)>({items...}); -} - class BaseJob::Private { public: struct JobTimeoutConfig { @@ -78,8 +71,8 @@ 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) + Private(HttpVerb v, QByteArray endpoint, const QUrlQuery& q, + RequestData&& data, bool nt) : verb(v) , apiEndpoint(std::move(endpoint)) , requestQuery(q) @@ -113,10 +106,10 @@ public: // Contents for the network request HttpVerb verb; - QString apiEndpoint; + QByteArray apiEndpoint; QHash<QByteArray, QByteArray> requestHeaders; QUrlQuery requestQuery; - Data requestData; + RequestData requestData; bool needsToken; bool inBackground = false; @@ -173,14 +166,36 @@ public: } }; -BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, +inline bool isHex(QChar c) +{ + return c.isDigit() || (c >= u'A' && c <= u'F') || (c >= u'a' && c <= u'f'); +} + +QByteArray BaseJob::encodeIfParam(const QString& paramPart) +{ + const auto percentIndex = paramPart.indexOf('%'); + if (percentIndex != -1 && paramPart.size() > percentIndex + 2 + && isHex(paramPart[percentIndex + 1]) + && isHex(paramPart[percentIndex + 2])) { + qCWarning(JOBS) + << "Developers, upfront percent-encoding of job parameters is " + "deprecated since libQuotient 0.7; the string involved is" + << paramPart; + return QUrl(paramPart, QUrl::TolerantMode).toEncoded(); + } + return QUrl::toPercentEncoding(paramPart); +} + +BaseJob::BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint, bool needsToken) - : BaseJob(verb, name, endpoint, Query {}, Data {}, needsToken) + : BaseJob(verb, name, std::move(endpoint), QUrlQuery {}, RequestData {}, + 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)) +BaseJob::BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint, + const QUrlQuery& query, RequestData&& data, bool needsToken) + : d(new Private(verb, std::move(endpoint), query, std::move(data), + needsToken)) { setObjectName(name); connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout); @@ -201,13 +216,6 @@ QUrl BaseJob::requestUrl() const { return d->reply ? d->reply->url() : QUrl(); } bool BaseJob::isBackground() const { return d->inBackground; } -const QString& BaseJob::apiEndpoint() const { return d->apiEndpoint; } - -void BaseJob::setApiEndpoint(const QString& apiEndpoint) -{ - d->apiEndpoint = apiEndpoint; -} - const BaseJob::headers_t& BaseJob::requestHeaders() const { return d->requestHeaders; @@ -224,16 +232,19 @@ void BaseJob::setRequestHeaders(const BaseJob::headers_t& headers) d->requestHeaders = headers; } -const QUrlQuery& BaseJob::query() const { return d->requestQuery; } +QUrlQuery BaseJob::query() const { return d->requestQuery; } void BaseJob::setRequestQuery(const QUrlQuery& query) { d->requestQuery = query; } -const BaseJob::Data& BaseJob::requestData() const { return d->requestData; } +const RequestData& BaseJob::requestData() const { return d->requestData; } -void BaseJob::setRequestData(Data&& data) { std::swap(d->requestData, data); } +void BaseJob::setRequestData(RequestData&& data) +{ + std::swap(d->requestData, data); +} const QByteArrayList& BaseJob::expectedContentTypes() const { @@ -263,17 +274,17 @@ const QNetworkReply* BaseJob::reply() const { return d->reply.data(); } QNetworkReply* BaseJob::reply() { return d->reply.data(); } -QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QString& path, +QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QByteArray& encodedPath, const QUrlQuery& query) { - auto pathBase = baseUrl.path(); - // 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); + // Make sure the added path is relative even if it's not (the official + // API definitions have the leading slash though it's not really correct). + const auto pathUrl = + QUrl::fromEncoded(encodedPath.mid(encodedPath.startsWith('/')), + QUrl::StrictMode); + Q_ASSERT_X(pathUrl.isValid(), __FUNCTION__, + qPrintable(pathUrl.errorString())); + baseUrl = baseUrl.resolved(pathUrl); baseUrl.setQuery(query); return baseUrl; } @@ -288,7 +299,8 @@ void BaseJob::Private::sendRequest() req.setRawHeader("Authorization", QByteArray("Bearer ") + connection->accessToken()); req.setAttribute(QNetworkRequest::BackgroundRequestAttribute, inBackground); - req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::NoLessSafeRedirectPolicy); req.setMaximumRedirectsAllowed(10); req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true); req.setAttribute( @@ -353,7 +365,7 @@ void BaseJob::initiate(ConnectionData* connData, bool inBackground) qCCritical(d->logCat) << "Developers, ensure the Connection is valid before using it"; Q_ASSERT(false); - setStatus(IncorrectRequestError, tr("Invalid server connection")); + setStatus(IncorrectRequest, tr("Invalid server connection")); } // The status is no good, finalise QTimer::singleShot(0, this, &BaseJob::finishJob); @@ -403,42 +415,42 @@ BaseJob::Status BaseJob::Private::parseJson() void BaseJob::gotReply() { - setStatus(checkReply(reply())); - - if (status().good() - && d->expectedContentTypes == QByteArrayList { "application/json" }) { + // Defer actually updating the status until it's finalised + auto statusSoFar = checkReply(reply()); + if (statusSoFar.good() + && d->expectedContentTypes == QByteArrayList { "application/json" }) // + { d->rawResponse = reply()->readAll(); - setStatus(d->parseJson()); - if (status().good() && !expectedKeys().empty()) { + statusSoFar = d->parseJson(); + if (statusSoFar.good() && !expectedKeys().empty()) { const auto& responseObject = jsonData(); QByteArrayList missingKeys; for (const auto& k: expectedKeys()) if (!responseObject.contains(k)) missingKeys.push_back(k); if (!missingKeys.empty()) - setStatus(IncorrectResponse, tr("Required JSON keys missing: ") - + missingKeys.join()); + statusSoFar = { IncorrectResponse, + tr("Required JSON keys missing: ") + + missingKeys.join() }; } + setStatus(statusSoFar); 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()) + if (statusSoFar.good()) { setStatus(prepareResult()); - else { - d->rawResponse = reply()->readAll(); - qCDebug(d->logCat).noquote() - << "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); + return; } + + d->rawResponse = reply()->readAll(); + qCDebug(d->logCat).noquote() + << "Error body (truncated if long):" << rawDataSample(500); + setStatus(prepareError(statusSoFar)); } bool checkContentType(const QByteArray& type, const QByteArrayList& patterns) @@ -503,7 +515,7 @@ BaseJob::Status BaseJob::checkReply(const QNetworkReply* reply) const BaseJob::Status BaseJob::prepareResult() { return Success; } -BaseJob::Status BaseJob::prepareError() +BaseJob::Status BaseJob::prepareError(Status currentStatus) { // 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) @@ -513,10 +525,10 @@ BaseJob::Status BaseJob::prepareError() // 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 + // of if's below will fall through retaining the current status) const auto& errorJson = jsonData(); const auto errCode = errorJson.value("errcode"_ls).toString(); - if (error() == TooManyRequestsError || errCode == "M_LIMIT_EXCEEDED") { + if (error() == TooManyRequests || errCode == "M_LIMIT_EXCEEDED") { QString msg = tr("Too many requests"); int64_t retryAfterMs = errorJson.value("retry_after_ms"_ls).toInt(-1); if (retryAfterMs >= 0) @@ -526,16 +538,16 @@ BaseJob::Status BaseJob::prepareError() d->connection->limitRate(milliseconds(retryAfterMs)); - return { TooManyRequestsError, msg }; + return { TooManyRequests, msg }; } if (errCode == "M_CONSENT_NOT_GIVEN") { d->errorUrl = QUrl(errorJson.value("consent_uri"_ls).toString()); - return { UserConsentRequiredError }; + return { UserConsentRequired }; } if (errCode == "M_UNSUPPORTED_ROOM_VERSION" || errCode == "M_INCOMPATIBLE_ROOM_VERSION") - return { UnsupportedRoomVersionError, + return { UnsupportedRoomVersion, errorJson.contains("room_version"_ls) ? tr("Requested room version: %1") .arg(errorJson.value("room_version"_ls).toString()) @@ -548,9 +560,9 @@ BaseJob::Status BaseJob::prepareError() // 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 { currentStatus.code, errorJson.value("error"_ls).toString() }; - return NoError; // Retain the status if the error payload is not recognised + return currentStatus; // The error payload is not recognised } QJsonValue BaseJob::takeValueFromJson(const QString& key) @@ -717,27 +729,27 @@ QString BaseJob::statusCaption() const return tr("Request was abandoned"); case NetworkError: return tr("Network problems"); - case TimeoutError: + case Timeout: return tr("Request timed out"); case Unauthorised: return tr("Unauthorised request"); case ContentAccessError: return tr("Access error"); - case NotFoundError: + case NotFound: return tr("Not found"); - case IncorrectRequestError: + case IncorrectRequest: return tr("Invalid request"); - case IncorrectResponseError: + case IncorrectResponse: return tr("Response could not be parsed"); - case TooManyRequestsError: + case TooManyRequests: return tr("Too many requests"); - case RequestNotImplementedError: + case RequestNotImplemented: return tr("Function not implemented by the server"); - case NetworkAuthRequiredError: + case NetworkAuthRequired: return tr("Network authentication required"); - case UserConsentRequiredError: + case UserConsentRequired: return tr("User consent required"); - case UnsupportedRoomVersionError: + case UnsupportedRoomVersion: return tr("The server does not support the needed room version"); default: return tr("Request failed"); @@ -799,7 +811,7 @@ void BaseJob::abandon() void BaseJob::timeout() { - setStatus(TimeoutError, "The job has timed out"); + setStatus(Timeout, "The job has timed out"); finishJob(); } diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index ca91a781..ddf243ed 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -7,8 +7,10 @@ #include "requestdata.h" #include "../logging.h" #include "../converters.h" +#include "../quotient_common.h" #include <QtCore/QObject> +#include <QtCore/QStringBuilder> class QNetworkReply; class QSslError; @@ -23,7 +25,18 @@ class BaseJob : public QObject { Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT) Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries) Q_PROPERTY(int statusCode READ error NOTIFY statusChanged) + + static QByteArray encodeIfParam(const QString& paramPart); + template <int N> + static inline auto encodeIfParam(const char (&constPart)[N]) + { + return constPart; + } + public: +#define WITH_DEPRECATED_ERROR_VERSION(Recommended) \ + Recommended, DECL_DEPRECATED_ENUMERATOR(Recommended##Error, Recommended) + /*! The status code of a job * * Every job is created in Unprepared status; upon calling prepare() @@ -34,7 +47,7 @@ public: */ enum StatusCode { Success = 0, - NoError = Success, // To be compatible with Qt conventions + NoError = Success, Pending = 1, WarningLevel = 20, //< Warnings have codes starting from this UnexpectedResponseType = 21, @@ -43,28 +56,18 @@ public: Abandoned = 50, //< A tiny period between abandoning and object deletion ErrorLevel = 100, //< Errors have codes starting from this NetworkError = 101, - Timeout, - TimeoutError = Timeout, + WITH_DEPRECATED_ERROR_VERSION(Timeout), Unauthorised, ContentAccessError, - NotFoundError, - IncorrectRequest, - IncorrectRequestError = IncorrectRequest, - IncorrectResponse, - IncorrectResponseError = IncorrectResponse, - JsonParseError //< \deprecated Use IncorrectResponse instead - = IncorrectResponse, - TooManyRequests, - TooManyRequestsError = TooManyRequests, + WITH_DEPRECATED_ERROR_VERSION(NotFound), + WITH_DEPRECATED_ERROR_VERSION(IncorrectRequest), + WITH_DEPRECATED_ERROR_VERSION(IncorrectResponse), + WITH_DEPRECATED_ERROR_VERSION(TooManyRequests), RateLimited = TooManyRequests, - RequestNotImplemented, - RequestNotImplementedError = RequestNotImplemented, - UnsupportedRoomVersion, - UnsupportedRoomVersionError = UnsupportedRoomVersion, - NetworkAuthRequired, - NetworkAuthRequiredError = NetworkAuthRequired, - UserConsentRequired, - UserConsentRequiredError = UserConsentRequired, + WITH_DEPRECATED_ERROR_VERSION(RequestNotImplemented), + WITH_DEPRECATED_ERROR_VERSION(UnsupportedRoomVersion), + WITH_DEPRECATED_ERROR_VERSION(NetworkAuthRequired), + WITH_DEPRECATED_ERROR_VERSION(UserConsentRequired), CannotLeaveRoom, UserDeactivated, FileError, @@ -72,21 +75,19 @@ public: }; 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); - } - }; +#undef WITH_DEPRECATED_ERROR_VERSION - using Data = RequestData; + template <typename... StrTs> + static QByteArray makePath(StrTs&&... parts) + { + return (QByteArray() % ... % encodeIfParam(parts)); + } + + using Data +#ifndef Q_CC_MSVC + Q_DECL_DEPRECATED_X("Use Quotient::RequestData instead") +#endif + = RequestData; /*! * This structure stores the status of a server call job. The status @@ -136,10 +137,11 @@ public: }; public: - BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, + BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint, + bool needsToken = true); + BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint, + const QUrlQuery& query, RequestData&& data = {}, bool needsToken = true); - BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, - const Query& query, Data&& data = {}, bool needsToken = true); QUrl requestUrl() const; bool isBackground() const; @@ -194,7 +196,7 @@ public: * 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... + template <typename T, typename StrT> T loadFromJson(const StrT& keyName, T&& defaultValue = {}) const { const auto& jv = jsonData().value(keyName); @@ -336,16 +338,18 @@ Q_SIGNALS: protected: using headers_t = QHash<QByteArray, QByteArray>; + Q_DECL_DEPRECATED_X("Deprecated due to being unused") const QString& apiEndpoint() const; + Q_DECL_DEPRECATED_X("Deprecated due to being unused") void setApiEndpoint(const QString& apiEndpoint); const headers_t& requestHeaders() const; void setRequestHeader(const headers_t::key_type& headerName, const headers_t::mapped_type& headerValue); void setRequestHeaders(const headers_t& headers); - const QUrlQuery& query() const; + QUrlQuery query() const; void setRequestQuery(const QUrlQuery& query); - const Data& requestData() const; - void setRequestData(Data&& data); + const RequestData& requestData() const; + void setRequestData(RequestData&& data); const QByteArrayList& expectedContentTypes() const; void addExpectedContentType(const QByteArray& contentType); void setExpectedContentTypes(const QByteArrayList& contentTypes); @@ -361,7 +365,7 @@ protected: * 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, + static QUrl makeRequestUrl(QUrl baseUrl, const QByteArray &encodedPath, const QUrlQuery& query = {}); /*! Prepares the job for execution @@ -395,10 +399,12 @@ protected: * 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, + * responses as early as possible and only then process custom errors, * with JSON or non-JSON payload. + * + * \return updated (if necessary) job status */ - virtual Status prepareError(); + virtual Status prepareError(Status currentStatus); /*! \brief Get direct access to the JSON response object in the job * diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp index 7dbf4ab3..6fe8ef26 100644 --- a/lib/jobs/mediathumbnailjob.cpp +++ b/lib/jobs/mediathumbnailjob.cpp @@ -17,13 +17,17 @@ MediaThumbnailJob::MediaThumbnailJob(const QString& serverName, const QString& mediaId, QSize requestedSize) : GetContentThumbnailJob(serverName, mediaId, requestedSize.width(), requestedSize.height(), "scale") -{} +{ + setLoggingCategory(THUMBNAILJOB); +} MediaThumbnailJob::MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize) : MediaThumbnailJob(mxcUri.authority(), mxcUri.path().mid(1), // sans leading '/' requestedSize) -{} +{ + setLoggingCategory(THUMBNAILJOB); +} QImage MediaThumbnailJob::thumbnail() const { return _thumbnail; } diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp index 047e2920..2c001ccc 100644 --- a/lib/jobs/requestdata.cpp +++ b/lib/jobs/requestdata.cpp @@ -3,6 +3,7 @@ #include "requestdata.h" +#include <QtCore/QIODevice> #include <QtCore/QBuffer> #include <QtCore/QByteArray> #include <QtCore/QJsonArray> @@ -31,4 +32,8 @@ RequestData::RequestData(const QJsonObject& jo) : _source(fromJson(jo)) {} RequestData::RequestData(const QJsonArray& ja) : _source(fromJson(ja)) {} +RequestData::RequestData(QIODevice* source) + : _source(std::unique_ptr<QIODevice>(source)) +{} + RequestData::~RequestData() = default; diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h index 4958e0f9..4f05e5ff 100644 --- a/lib/jobs/requestdata.h +++ b/lib/jobs/requestdata.h @@ -24,8 +24,7 @@ public: RequestData(const QByteArray& a = {}); RequestData(const QJsonObject& jo); RequestData(const QJsonArray& ja); - RequestData(QIODevice* source) : _source(std::unique_ptr<QIODevice>(source)) - {} + RequestData(QIODevice* source); RequestData(RequestData&&) = default; RequestData& operator=(RequestData&&) = default; ~RequestData(); @@ -36,5 +35,3 @@ 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 59a34ef3..9b1b46f0 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -10,7 +10,7 @@ static size_t jobId = 0; SyncJob::SyncJob(const QString& since, const QString& filter, int timeout, const QString& presence) : BaseJob(HttpVerb::Get, QStringLiteral("SyncJob-%1").arg(++jobId), - QStringLiteral("_matrix/client/r0/sync")) + "_matrix/client/r0/sync") { setLoggingCategory(SYNCJOB); QUrlQuery query; diff --git a/lib/joinstate.h b/lib/joinstate.h deleted file mode 100644 index 805ce73a..00000000 --- a/lib/joinstate.h +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> -// SPDX-License-Identifier: LGPL-2.1-or-later - -#pragma once - -#include <QtCore/QFlags> - -#include <array> - -namespace Quotient { -enum class JoinState : unsigned int { - Join = 0x1, - Invite = 0x2, - Leave = 0x4, -}; - -Q_DECLARE_FLAGS(JoinStates, JoinState) - -// 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 >>= 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 af229684..15eac69d 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -16,4 +16,6 @@ LOGGING_CATEGORY(EPHEMERAL, "quotient.events.ephemeral") LOGGING_CATEGORY(E2EE, "quotient.e2ee") LOGGING_CATEGORY(JOBS, "quotient.jobs") LOGGING_CATEGORY(SYNCJOB, "quotient.jobs.sync") +LOGGING_CATEGORY(THUMBNAILJOB, "quotient.jobs.thumbnail") +LOGGING_CATEGORY(NETWORK, "quotient.network") LOGGING_CATEGORY(PROFILER, "quotient.profiler") diff --git a/lib/logging.h b/lib/logging.h index 432ed16f..5bf050a9 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -16,6 +16,8 @@ Q_DECLARE_LOGGING_CATEGORY(EPHEMERAL) Q_DECLARE_LOGGING_CATEGORY(E2EE) Q_DECLARE_LOGGING_CATEGORY(JOBS) Q_DECLARE_LOGGING_CATEGORY(SYNCJOB) +Q_DECLARE_LOGGING_CATEGORY(THUMBNAILJOB) +Q_DECLARE_LOGGING_CATEGORY(NETWORK) Q_DECLARE_LOGGING_CATEGORY(PROFILER) namespace Quotient { @@ -35,24 +37,18 @@ using QDebugManip = QDebug (*)(QDebug); */ inline QDebug formatJson(QDebug debug_object) { -#if QT_VERSION < QT_VERSION_CHECK(5, 4, 0) - return debug_object; -#else 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) +//! Suppress full qualification of enums/QFlags when logging +inline QDebug terse(QDebug dbg) { - return qdm(debug_object); + return +#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) + dbg.setVerbosity(0), dbg; +#else + dbg.verbosity(QDebug::MinimumVerbosity); +#endif } inline qint64 profilerMinNsecs() @@ -66,8 +62,19 @@ inline qint64 profilerMinNsecs() * 1000; } } // namespace Quotient -/// \deprecated Use namespace Quotient instead -namespace QMatrixClient = Quotient; + +/** + * @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, Quotient::QDebugManip qdm) +{ + return qdm(debug_object); +} inline QDebug operator<<(QDebug debug_object, const QElapsedTimer& et) { diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp new file mode 100644 index 00000000..0b6643fc --- /dev/null +++ b/lib/mxcreply.cpp @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "mxcreply.h" + +#include "room.h" + +using namespace Quotient; + +class MxcReply::Private +{ +public: + explicit Private(QNetworkReply* r = nullptr) + : m_reply(r) + {} + QNetworkReply* m_reply; +}; + +MxcReply::MxcReply(QNetworkReply* reply) + : d(std::make_unique<Private>(reply)) +{ + reply->setParent(this); + connect(d->m_reply, &QNetworkReply::finished, this, [this]() { + setError(d->m_reply->error(), d->m_reply->errorString()); + setOpenMode(ReadOnly); + Q_EMIT finished(); + }); +} + +MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) + : d(std::make_unique<Private>(reply)) +{ + reply->setParent(this); + connect(d->m_reply, &QNetworkReply::finished, this, [this, room, eventId]() { + setError(d->m_reply->error(), d->m_reply->errorString()); + setOpenMode(ReadOnly); + emit finished(); + }); +} + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) +#define ERROR_SIGNAL errorOccurred +#else +#define ERROR_SIGNAL error +#endif + +MxcReply::MxcReply() +{ + static const auto BadRequestPhrase = tr("Bad Request"); + QMetaObject::invokeMethod(this, [this]() { + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 400); + setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, + BadRequestPhrase); + setError(QNetworkReply::ProtocolInvalidOperationError, + BadRequestPhrase); + setFinished(true); + emit ERROR_SIGNAL(QNetworkReply::ProtocolInvalidOperationError); + emit finished(); + }, Qt::QueuedConnection); +} + +qint64 MxcReply::readData(char *data, qint64 maxSize) +{ + return d->m_reply->read(data, maxSize); +} + +void MxcReply::abort() +{ + d->m_reply->abort(); +} diff --git a/lib/mxcreply.h b/lib/mxcreply.h new file mode 100644 index 00000000..efaf01c6 --- /dev/null +++ b/lib/mxcreply.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <QtNetwork/QNetworkReply> +#include <memory> + +namespace Quotient { +class Room; + +class MxcReply : public QNetworkReply +{ +public: + explicit MxcReply(); + explicit MxcReply(QNetworkReply *reply); + MxcReply(QNetworkReply* reply, Room* room, const QString& eventId); + +public Q_SLOTS: + void abort() override; + +protected: + qint64 readData(char *data, qint64 maxSize) override; + +private: + class Private; + std::unique_ptr<Private> d; +}; +} diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index a94ead34..57618329 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -3,18 +3,43 @@ #include "networkaccessmanager.h" +#include "connection.h" +#include "room.h" +#include "accountregistry.h" +#include "mxcreply.h" + #include <QtCore/QCoreApplication> +#include <QtCore/QThreadStorage> +#include <QtCore/QSettings> #include <QtNetwork/QNetworkReply> using namespace Quotient; class NetworkAccessManager::Private { public: + explicit Private(NetworkAccessManager* q) + : q(q) + {} + + QNetworkReply* createImplRequest(Operation op, + const QNetworkRequest& outerRequest, + Connection* connection) + { + Q_ASSERT(outerRequest.url().scheme() == "mxc"); + QNetworkRequest r(outerRequest); + r.setUrl(QUrl(QStringLiteral("%1/_matrix/media/r0/download/%2") + .arg(connection->homeserver().toString(), + outerRequest.url().authority() + + outerRequest.url().path()))); + return q->createRequest(op, r); + } + + NetworkAccessManager* q; QList<QSslError> ignoredSslErrors; }; NetworkAccessManager::NetworkAccessManager(QObject* parent) - : QNetworkAccessManager(parent), d(std::make_unique<Private>()) + : QNetworkAccessManager(parent), d(std::make_unique<Private>(this)) {} QList<QSslError> NetworkAccessManager::ignoredSslErrors() const @@ -34,7 +59,7 @@ void NetworkAccessManager::clearIgnoredSslErrors() static NetworkAccessManager* createNam() { - auto nam = new NetworkAccessManager(QCoreApplication::instance()); + auto nam = new NetworkAccessManager(); #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) // See #109; in newer Qt, bearer management is deprecated altogether NetworkAccessManager::connect(nam, @@ -47,8 +72,11 @@ static NetworkAccessManager* createNam() NetworkAccessManager* NetworkAccessManager::instance() { - static auto* nam = createNam(); - return nam; + static QThreadStorage<NetworkAccessManager*> storage; + if(!storage.hasLocalData()) { + storage.setLocalData(createNam()); + } + return storage.localData(); } NetworkAccessManager::~NetworkAccessManager() = default; @@ -56,7 +84,49 @@ NetworkAccessManager::~NetworkAccessManager() = default; QNetworkReply* NetworkAccessManager::createRequest( Operation op, const QNetworkRequest& request, QIODevice* outgoingData) { + const auto& mxcUrl = request.url(); + if (mxcUrl.scheme() == "mxc") { + const QUrlQuery query(mxcUrl.query()); + const auto accountId = query.queryItemValue(QStringLiteral("user_id")); + if (accountId.isEmpty()) { + // Using QSettings here because Quotient::NetworkSettings + // doesn't provide multithreading guarantees + static thread_local QSettings s; + if (!s.value("Network/allow_direct_media_requests").toBool()) { + qCWarning(NETWORK) << "No connection specified"; + return new MxcReply(); + } + // TODO: Make the best effort with a direct unauthenticated request + // to the media server + } else { + auto* const connection = AccountRegistry::instance().get(accountId); + if (!connection) { + qCWarning(NETWORK) << "Connection" << accountId << "not found"; + return new MxcReply(); + } + const auto roomId = query.queryItemValue(QStringLiteral("room_id")); + if (!roomId.isEmpty()) { + auto room = connection->room(roomId); + if (!room) { + qCWarning(NETWORK) << "Room" << roomId << "not found"; + return new MxcReply(); + } + return new MxcReply( + d->createImplRequest(op, request, connection), room, + query.queryItemValue(QStringLiteral("event_id"))); + } + return new MxcReply( + d->createImplRequest(op, request, connection)); + } + } auto reply = QNetworkAccessManager::createRequest(op, request, outgoingData); reply->ignoreSslErrors(d->ignoredSslErrors); return reply; } + +QStringList NetworkAccessManager::supportedSchemesImplementation() const +{ + auto schemes = QNetworkAccessManager::supportedSchemesImplementation(); + schemes += QStringLiteral("mxc"); + return schemes; +} diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h index 47729a1b..87bc12a1 100644 --- a/lib/networkaccessmanager.h +++ b/lib/networkaccessmanager.h @@ -8,6 +8,8 @@ #include <memory> namespace Quotient { +class Room; +class Connection; class NetworkAccessManager : public QNetworkAccessManager { Q_OBJECT public: @@ -21,6 +23,9 @@ public: /** Get a pointer to the singleton */ static NetworkAccessManager* instance(); +public Q_SLOTS: + QStringList supportedSchemesImplementation() const; + private: QNetworkReply* createRequest(Operation op, const QNetworkRequest& request, QIODevice* outgoingData = Q_NULLPTR) override; diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index c6fa037a..86593cc8 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -3,7 +3,7 @@ #pragma once -#include "util.h" +#include "function_traits.h" #include <QtCore/QPointer> @@ -19,12 +19,7 @@ namespace _impl { 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>(); -#else auto pc = std::make_unique<QMetaObject::Connection>(); -#endif auto& c = *pc; // Resolve a reference before pc is moved to lambda // Perfect forwarding doesn't work through signal-slot connections - @@ -73,6 +68,17 @@ namespace _impl { }), connType); } + + // 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() + // ...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)); + } } // namespace _impl /*! \brief Create a connection that self-disconnects when its "slot" returns true @@ -90,7 +96,7 @@ 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), + return _impl::connectUntil(sender, signal, context, _impl::wrap_in_function(slot), connType); } @@ -100,8 +106,13 @@ inline auto connectSingleShot(SenderT* sender, SignalT signal, ContextT* context, const FunctorT& slot, Qt::ConnectionType connType = Qt::AutoConnection) { +#if QT_VERSION_MAJOR >= 6 + return QObject::connect(sender, signal, context, slot, + Qt::ConnectionType(connType + | Qt::SingleShotConnection)); +#else return _impl::connectSingleShot( - sender, signal, context, wrap_in_function(slot), connType); + sender, signal, context, _impl::wrap_in_function(slot), connType); } // Specialisation for usual Qt slots passed as pointers-to-members. @@ -114,11 +125,12 @@ inline auto connectSingleShot(SenderT* sender, SignalT signal, { // TODO: when switching to C++20, use std::bind_front() instead return _impl::connectSingleShot(sender, signal, receiver, - wrap_in_function( + _impl::wrap_in_function( [receiver, slot](const ArgTs&... args) { (receiver->*slot)(args...); }), connType); +#endif } /*! \brief A guard pointer that disconnects an interested object upon destruction diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 22fdbe94..0e3e2a40 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -5,18 +5,95 @@ #include <qobjectdefs.h> +#include <array> + +// See https://bugreports.qt.io/browse/QTBUG-82295 - despite the comment that +// Q_FLAG[_NS] "should" be applied to the enum only, Qt doesn't allow to wrap +// a flag type into a QVariant then. The macros below define Q_FLAG[_NS] and on +// top of that add Q_ENUM[_NS]_IMPL which is a part of Q_ENUM() macro that +// enables the metatype data but goes under the moc radar to avoid double +// registration of the same data in the map defined in moc_*.cpp +#define QUO_DECLARE_FLAGS(Flags, Enum) \ + Q_DECLARE_FLAGS(Flags, Enum) \ + Q_ENUM_IMPL(Enum) \ + Q_FLAG(Flags) + +#define QUO_DECLARE_FLAGS_NS(Flags, Enum) \ + Q_DECLARE_FLAGS(Flags, Enum) \ + Q_ENUM_NS_IMPL(Enum) \ + Q_FLAG_NS(Flags) + +#define DECL_DEPRECATED_ENUMERATOR(Deprecated, Recommended) \ + Deprecated Q_DECL_ENUMERATOR_DEPRECATED_X("Use " #Recommended) = Recommended + 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 }; +// std::array {} needs explicit template parameters on macOS because +// Apple stdlib doesn't have deduction guides for std::array. C++20 has +// to_array() but that can't be borrowed, this time because of MSVC: +// https://developercommunity.visualstudio.com/t/vc-ice-p1-initc-line-3652-from-stdto-array/1464038 +// Therefore a simpler (but also slightly more wobbly - it resolves the element +// type using std::common_type<>) make_array facility is implemented here. +template <typename... Ts> +constexpr auto make_array(Ts&&... items) +{ + return std::array<std::common_type_t<Ts...>, sizeof...(items)>( + { std::forward<Ts>(items)... }); +} + +// TODO: code like this should be generated from the CS API definition + +//! \brief Membership states +//! +//! These are used for member events. The names here are case-insensitively +//! equal to state names used on the wire. +//! \sa MemberEventContent, RoomMemberEvent +enum class Membership : unsigned int { + // Specific power-of-2 values (1,2,4,...) are important here as syncdata.cpp + // depends on that, as well as Join being the first in line + Invalid = 0x0, + Join = 0x1, + Leave = 0x2, + Invite = 0x4, + Knock = 0x8, + Ban = 0x10, + Undefined = Invalid +}; +QUO_DECLARE_FLAGS_NS(MembershipMask, Membership) +constexpr inline auto MembershipStrings = make_array( + // The order MUST be the same as the order in the original enum + "join", "leave", "invite", "knock", "ban"); + +//! \brief Local user join-state names +//! +//! This represents a subset of Membership values that may arrive as the local +//! user's state grouping for the sync response. +//! \sa SyncData +enum class JoinState : std::underlying_type_t<Membership> { + Invalid = std::underlying_type_t<Membership>(Membership::Invalid), + Join = std::underlying_type_t<Membership>(Membership::Join), + Leave = std::underlying_type_t<Membership>(Membership::Leave), + Invite = std::underlying_type_t<Membership>(Membership::Invite), + Knock = std::underlying_type_t<Membership>(Membership::Knock), +}; +QUO_DECLARE_FLAGS_NS(JoinStates, JoinState) + +constexpr inline auto JoinStateStrings = make_array( + MembershipStrings[0], MembershipStrings[1], MembershipStrings[2], + MembershipStrings[3] /* same as MembershipStrings, sans "ban" */ +); + +//! \brief Network job running policy flags +//! +//! So far only background/foreground flags are available. +//! \sa Connection::callApi, Connection::run +enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 }; Q_ENUM_NS(RunningPolicy) +//! \brief The result of URI resolution using UriResolver +//! \sa UriResolver enum UriResolveResult : short { StillResolving = -1, UriResolved = 0, @@ -27,6 +104,16 @@ enum UriResolveResult : short { }; Q_ENUM_NS(UriResolveResult) +enum RoomType { + Space, + Undefined, +}; +Q_ENUM_NS(RoomType) + +constexpr inline auto RoomTypeStrings = make_array( + "m.space" +); + } // namespace Quotient -/// \deprecated Use namespace Quotient instead -namespace QMatrixClient = Quotient; +Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::MembershipMask) +Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::JoinStates) diff --git a/lib/room.cpp b/lib/room.cpp index 2a9cc0d8..6854879c 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -15,6 +15,10 @@ #include "e2ee.h" #include "syncdata.h" #include "user.h" +#include "eventstats.h" + +// NB: since Qt 6, moc_room.cpp needs User fully defined +#include "moc_room.cpp" #include "csapi/account-data.h" #include "csapi/banning.h" @@ -51,7 +55,6 @@ #include <QtCore/QDir> #include <QtCore/QHash> -#include <QtCore/QMimeDatabase> #include <QtCore/QPointer> #include <QtCore/QRegularExpression> #include <QtCore/QStringBuilder> // for efficient string concats (operator%) @@ -98,8 +101,8 @@ public: 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 + /// The state of the room at syncEdge() + /// \sa syncEdge QHash<StateEventKey, const StateEventBase*> currentState; /// Servers with aliases for this room except the one of the local user /// \sa Room::remoteAliases @@ -114,19 +117,21 @@ public: QHash<QPair<QString, QString>, RelatedEvents> relations; QString displayname; Avatar avatar; - int highlightCount = 0; - int notificationCount = 0; + QHash<QString, Notification> notifications; + qsizetype serverHighlightCount = 0; + // Starting up with estimate event statistics as there's zero knowledge + // about the timeline. + EventStats partiallyReadStats {}, unreadStats {}; members_map_t membersMap; QList<User*> usersTyping; - QMultiHash<QString, User*> eventIdReadUsers; + QHash<QString, QSet<QString>> eventIdReadUsers; QList<User*> usersInvited; QList<User*> membersLeft; - int unreadMessages = 0; bool displayed = false; QString firstDisplayedEventId; QString lastDisplayedEventId; - QHash<const User*, QString> lastReadEventIds; - QString serverReadMarker; + QHash<QString, ReadReceipt> lastReadReceipts; + QString fullyReadUntilEventId; TagsMap tags; UnorderedMap<QString, EventPtr> accountData; QString prevBatch; @@ -195,6 +200,8 @@ public: /// A point in the timeline corresponding to baseState rev_iter_t timelineBase() const { return q->findInTimeline(-1); } + rev_iter_t historyEdge() const { return timeline.crend(); } + Timeline::const_iterator syncEdge() const { return timeline.cend(); } void getPreviousContent(int limit = 10, const QString &filter = {}); @@ -210,6 +217,7 @@ public: evtKey.second)); qCDebug(STATE) << "A new stub event created for key {" << evtKey.first << evtKey.second << "}"; + qCDebug(STATE) << "Stubbed state size:" << stubbedState.size(); } evt = stubbedState[evtKey].get(); Q_ASSERT(evt); @@ -219,6 +227,16 @@ public: return evt; } + QVector<const StateEventBase*> stateEventsOfType(const QString& evtType) const + { + auto vals = QVector<const StateEventBase*>(); + for (auto it = currentState.cbegin(); it != currentState.cend(); ++it) + if (it.key().first == evtType) + vals.append(it.value()); + + return vals; + } + template <typename EventT> const EventT* getCurrentState(const QString& stateKey = {}) const { @@ -237,39 +255,34 @@ public: // 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; + Changes changes {}; if (!events.empty()) { QElapsedTimer et; et.start(); for (auto&& eptr : events) { const auto& evt = *eptr; Q_ASSERT(evt.isStateEvent()); - auto change = q->processStateEvent(evt); - if (change != NoChange) { + if (auto change = q->processStateEvent(evt); change) { changes |= change; baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr); } } if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) - << "*** Room::Private::updateStateFrom():" << events.size() - << "event(s)," << et; + << "Updated" << q->objectName() << "room state from" + << events.size() << "event(s) in" << et; } return changes; } Changes addNewMessageEvents(RoomEvents&& events); void addHistoricalMessageEvents(RoomEvents&& events); + Changes updateStatsFromSyncData(const SyncRoomData &data, bool fromCache); + void postprocessChanges(Changes changes, bool saveState = true); + /** Move events into the timeline * * Insert events into the timeline, either new or historical. @@ -287,11 +300,12 @@ public: */ 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); + Changes setLastReadReceipt(const QString& userId, rev_iter_t newMarker, + ReadReceipt newReceipt = {}, + bool deferStatsUpdate = false); + Changes setFullyReadMarker(const QString &eventId); + Changes updateStats(const rev_iter_t& from, const rev_iter_t& to); + bool markMessagesAsRead(const rev_iter_t& upToMarker); void getAllMembers(); @@ -303,6 +317,8 @@ public: return sendEvent(makeEvent<EventT>(std::forward<ArgTs>(eventArgs)...)); } + QString doPostFile(RoomEventPtr &&msgEvent, const QUrl &localUrl); + RoomEvent* addAsPending(RoomEventPtr&& event); QString doSendEvent(const RoomEvent* pEvent); @@ -455,7 +471,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) emit baseStateLoaded(); return this == r; // loadedRoomState fires only once per room }); - qCDebug(STATE) << "New" << toCString(initialJoinState) << "Room:" << id; + qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id; } Room::~Room() { delete d; } @@ -535,21 +551,6 @@ QStringList Room::altAliases() const return d->getCurrentState<RoomCanonicalAliasEvent>()->altAliases(); } -QStringList Room::localAliases() const -{ - return d->getCurrentState<RoomAliasesEvent>( - connection()->domain()) - ->aliases(); -} - -QStringList Room::remoteAliases() const -{ - QStringList result; - for (const auto& s : std::as_const(d->aliasServers)) - result += d->getCurrentState<RoomAliasesEvent>(s)->aliases(); - return result; -} - QString Room::canonicalAlias() const { return d->getCurrentState<RoomCanonicalAliasEvent>()->alias(); @@ -574,6 +575,11 @@ QVector< const Quotient::RoomEvent* > Quotient::Room::pinnedEvents() const return pinnedEvents; } +QString Room::displayNameForHtml() const +{ + return displayName().toHtmlEscaped(); +} + void Room::refreshDisplayName() { d->updateDisplayname(); } QString Room::topic() const @@ -593,13 +599,13 @@ QImage Room::avatar(int width, int height) { if (!d->avatar.url().isEmpty()) return d->avatar.get(connection(), width, height, - [=] { emit avatarChanged(); }); + [this] { emit avatarChanged(); }); // Use the first (excluding self) user's avatar for direct chats const auto dcUsers = directChatUsers(); for (auto* u : dcUsers) if (u != localUser()) - return u->avatar(width, height, this, [=] { emit avatarChanged(); }); + return u->avatar(width, height, this, [this] { emit avatarChanged(); }); return {}; } @@ -615,6 +621,16 @@ JoinState Room::memberJoinState(User* user) const : JoinState::Leave; } +Membership Room::memberState(const QString& userId) const +{ + return d->getCurrentState<RoomMemberEvent>(userId)->membership(); +} + +bool Room::isMember(const QString& userId) const +{ + return memberState(userId) == Membership::Join; +} + JoinState Room::joinState() const { return d->joinState; } void Room::setJoinState(JoinState state) @@ -623,156 +639,261 @@ void Room::setJoinState(JoinState state) if (state == oldState) return; d->joinState = state; - qCDebug(STATE) << "Room" << id() << "changed state: " << int(oldState) - << "->" << int(state); - emit changed(Change::JoinStateChange); + qCDebug(STATE) << "Room" << id() << "changed state: " << terse << oldState + << "->" << state; emit joinStateChanged(oldState, state); } -Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId) -{ - auto& storedId = lastReadEventIds[u]; - if (storedId == eventId) - return Change::NoChange; - eventIdReadUsers.remove(storedId, u); - eventIdReadUsers.insert(eventId, u); - swap(storedId, eventId); - emit q->lastReadEventChanged(u); - emit q->readMarkerForUserMoved(u, eventId, storedId); - if (isLocalUser(u)) { - if (storedId != serverReadMarker) - connection->callApi<SetReadMarkerJob>(BackgroundRequest, id, - storedId); - emit q->readMarkerMoved(eventId, storedId); - return Change::ReadMarkerChange; +Room::Changes Room::Private::setLastReadReceipt(const QString& userId, + rev_iter_t newMarker, + ReadReceipt newReceipt, + bool deferStatsUpdate) +{ + if (newMarker == historyEdge() && !newReceipt.eventId.isEmpty()) + newMarker = q->findInTimeline(newReceipt.eventId); + if (newMarker != historyEdge()) { + // Try to auto-promote the read marker over the user's own messages + // (switch to direct iterators for that). + const auto eagerMarker = find_if(newMarker.base(), syncEdge(), + [=](const TimelineItem& ti) { + return ti->senderId() != userId; + }); + // eagerMarker is now just after the desired event for newMarker + if (eagerMarker != newMarker.base()) { + newMarker = rev_iter_t(eagerMarker); + qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << userId + << "to" << *newMarker; + } + // Fill newReceipt with the event (and, if needed, timestamp) from + // eagerMarker + newReceipt.eventId = (eagerMarker - 1)->event()->id(); + if (newReceipt.timestamp.isNull()) + newReceipt.timestamp = QDateTime::currentDateTime(); + } + auto& storedReceipt = + lastReadReceipts[userId]; // clazy:exclude=detaching-member + const auto prevEventId = storedReceipt.eventId; + // Check that either the new marker is actually "newer" than the current one + // or, if both markers are at historyEdge(), event ids are different. + // NB: with reverse iterators, timeline history edge >= sync edge + if (prevEventId == newReceipt.eventId + || newMarker > q->findInTimeline(prevEventId)) + return Change::None; + + // Finally make the change + + Changes changes = Change::Other; + auto oldEventReadUsersIt = + eventIdReadUsers.find(prevEventId); // clazy:exclude=detaching-member + if (oldEventReadUsersIt != eventIdReadUsers.end()) { + oldEventReadUsersIt->remove(userId); + if (oldEventReadUsersIt->isEmpty()) + eventIdReadUsers.erase(oldEventReadUsersIt); + } + eventIdReadUsers[newReceipt.eventId].insert(userId); + storedReceipt = move(newReceipt); + + { + auto dbg = qDebug(EPHEMERAL); // This trick needs qDebug, not qCDebug + dbg << "The new read receipt for" << userId << "is now at"; + if (newMarker == historyEdge()) + dbg << storedReceipt.eventId; + else + dbg << *newMarker; + } + + // TODO: use Room::member() when it becomes a thing and only emit signals + // for actual members, not just any user + const auto member = q->user(userId); + Q_ASSERT(member != nullptr); + if (isLocalUser(member) && !deferStatsUpdate) { + if (unreadStats.updateOnMarkerMove(q, q->findInTimeline(prevEventId), + newMarker)) { + qCDebug(MESSAGES) + << "Updated unread event statistics in" << q->objectName() + << "after moving the local read receipt:" << unreadStats; + changes |= Change::UnreadStats; + } + Q_ASSERT(unreadStats.isValidFor(q, newMarker)); // post-check } - return Change::NoChange; + emit q->lastReadEventChanged(member); + // TODO: remove in 0.8 + if (!isLocalUser(member)) + emit q->readMarkerForUserMoved(member, prevEventId, + storedReceipt.eventId); + return changes; } -void Room::Private::updateUnreadCount(const rev_iter_t& from, - const rev_iter_t& to) +Room::Changes Room::Private::updateStats(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()); - // Catch a special case when the last read event id refers to an event - // that has just arrived. In this case we should recalculate - // unreadMessages and might need to promote the read marker further - // over local-origin messages. - auto readMarker = q->readMarker(); - if (readMarker == timeline.crend() && q->allHistoryLoaded()) - --readMarker; // Read marker not found in the timeline, initialise it - if (readMarker >= from && readMarker < to) { - promoteReadMarker(q->localUser(), readMarker, true); - return; + const auto fullyReadMarker = q->fullyReadMarker(); + auto readReceiptMarker = q->localReadReceiptMarker(); + Changes changes = Change::None; + // Correct the read receipt to never be behind the fully read marker + if (readReceiptMarker > fullyReadMarker + && setLastReadReceipt(connection->userId(), fullyReadMarker, {}, true)) { + changes |= Change::Other; + readReceiptMarker = q->localReadReceiptMarker(); + qCInfo(MESSAGES) << "The local m.read receipt was behind m.fully_read " + "marker - it's now corrected to be at index" + << readReceiptMarker->index(); + } + + if (fullyReadMarker < from) + return Change::None; // What's arrived is already fully read + + // If there's no read marker in the whole room, initialise it + if (fullyReadMarker == historyEdge() && q->allHistoryLoaded()) + return setFullyReadMarker(timeline.front()->id()); + + // Catch a case when the id in the last fully read marker or the local read + // receipt refers to an event that has just arrived. In this case either + // one (unreadStats) or both statistics should be recalculated to get + // an exact number instead of an estimation (see documentation on + // EventStats::isEstimate). For the same reason (switching from the + // estimate to the exact number) this branch forces returning + // Change::UnreadStats and also possibly Change::PartiallyReadStats, even if + // the estimation luckily matched the exact result. + if (readReceiptMarker < to || changes /*i.e. read receipt was corrected*/) { + unreadStats = EventStats::fromMarker(q, readReceiptMarker); + Q_ASSERT(!unreadStats.isEstimate); + qCDebug(MESSAGES).nospace() << "Recalculated unread event statistics in" + << q->objectName() << ": " << unreadStats; + changes |= Change::UnreadStats; + if (fullyReadMarker < to) { + // Add up to unreadStats instead of counting same events again + partiallyReadStats = EventStats::fromRange(q, readReceiptMarker, + q->fullyReadMarker(), + unreadStats); + Q_ASSERT(!partiallyReadStats.isEstimate); + + qCDebug(MESSAGES).nospace() + << "Recalculated partially read event statistics in " + << q->objectName() << ": " << partiallyReadStats; + return changes | Change::PartiallyReadStats; + } } - Q_ASSERT(to <= readMarker); - - 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/quotient-im/libQuotient/wiki/unread_count - if (unreadMessages < 0) - unreadMessages = 0; - - unreadMessages += newUnreadMessages; - 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, - 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 - 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(); - }); + // As of here, at least the fully read marker (but maybe also read receipt) + // points to somewhere beyond the "oldest" message from the arrived batch - + // add up newly arrived messages to the current stats, instead of a complete + // recalculation. + Q_ASSERT(fullyReadMarker >= to); - auto changes = setLastReadEvent(u, (*(eagerMarker - 1))->id()); - if (isLocalUser(u)) { - const auto oldUnreadCount = unreadMessages; - 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; + const auto newStats = EventStats::fromRange(q, from, to); + Q_ASSERT(!newStats.isEstimate); + if (newStats.empty()) + return changes; - // See https://github.com/quotient-im/libQuotient/wiki/unread_count - if (unreadMessages == 0) - unreadMessages = -1; + const auto doAddStats = [this, &changes, newStats](EventStats& s, + const rev_iter_t& marker, + Change c) { + s.notableCount += newStats.notableCount; + s.highlightCount += newStats.highlightCount; + if (!s.isEstimate) + s.isEstimate = marker == historyEdge(); + changes |= c; + }; - if (force || unreadMessages != oldUnreadCount) { - if (unreadMessages == -1) { - qCDebug(MESSAGES) - << "Room" << displayname << "has no more unread messages"; - } else - qCDebug(MESSAGES) << "Room" << displayname << "still has" - << unreadMessages << "unread message(s)"; - emit q->unreadMessagesChanged(q); - changes |= Change::UnreadNotifsChange; - } + doAddStats(partiallyReadStats, fullyReadMarker, Change::PartiallyReadStats); + if (readReceiptMarker >= to) { + // readReceiptMarker < to branch shouldn't have been entered + Q_ASSERT(!changes.testFlag(Change::UnreadStats)); + doAddStats(unreadStats, readReceiptMarker, Change::UnreadStats); } + qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained" << newStats + << "notable/highlighted event(s); total statistics:" + << partiallyReadStats << "since the fully read marker," + << unreadStats << "since read receipt"; + + // Check invariants + Q_ASSERT(partiallyReadStats.isValidFor(q, fullyReadMarker)); + Q_ASSERT(unreadStats.isValidFor(q, readReceiptMarker)); return changes; } -Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker) +Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) { - const auto prevMarker = q->readMarker(); - auto changes = promoteReadMarker(q->localUser(), upToMarker); - if (prevMarker != upToMarker) - qCDebug(MESSAGES) << "Marked messages as read until" << *q->readMarker(); + if (fullyReadUntilEventId == eventId) + return Change::None; - // 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>(BackgroundRequest, - id, QStringLiteral("m.read"), - QUrl::toPercentEncoding( - (*upToMarker)->id())); - break; + const auto prevReadMarker = q->fullyReadMarker(); + const auto newReadMarker = q->findInTimeline(eventId); + if (newReadMarker > prevReadMarker) + return Change::None; + + const auto prevFullyReadId = std::exchange(fullyReadUntilEventId, eventId); + qCDebug(MESSAGES) << "Fully read marker in" << q->objectName() // + << "set to" << fullyReadUntilEventId; + + QT_IGNORE_DEPRECATIONS(Changes changes = Change::ReadMarker|Change::Other;) + if (const auto rm = q->fullyReadMarker(); rm != historyEdge()) { + // Pull read receipt if it's behind, and update statistics + changes |= setLastReadReceipt(connection->userId(), rm); + if (partiallyReadStats.updateOnMarkerMove(q, prevReadMarker, rm)) { + changes |= Change::PartiallyReadStats; + qCDebug(MESSAGES) + << "Updated partially read event statistics in" + << q->objectName() + << "after moving m.fully_read marker: " << partiallyReadStats; } + Q_ASSERT(partiallyReadStats.isValidFor(q, rm)); // post-check } + emit q->fullyReadMarkerMoved(prevFullyReadId, fullyReadUntilEventId); + // TODO: Remove in 0.8 + emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId); return changes; } -void Room::markMessagesAsRead(QString uptoEventId) +void Room::setReadReceipt(const QString& atEventId) +{ + if (const auto changes = d->setLastReadReceipt(localUser()->id(), + historyEdge(), + { atEventId })) { + connection()->callApi<PostReceiptJob>(BackgroundRequest, id(), + QStringLiteral("m.read"), + QUrl::toPercentEncoding(atEventId)); + d->postprocessChanges(changes); + } else + qCDebug(EPHEMERAL) << "The new read receipt for" << localUser()->id() + << "in" << objectName() + << "is at or behind the old one, skipping"; +} + +bool Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker) +{ + if (upToMarker == q->historyEdge()) + qCWarning(MESSAGES) << "Cannot mark an unknown event in" + << q->objectName() << "as fully read"; + else if (const auto changes = setFullyReadMarker(upToMarker->event()->id())) { + // The assumption below is that if a read receipt was sent on a newer + // event, the homeserver will keep it there instead of reverting to + // m.fully_read + connection->callApi<SetReadMarkerJob>(BackgroundRequest, id, + fullyReadUntilEventId, + fullyReadUntilEventId); + postprocessChanges(changes); + return true; + } else + qCDebug(MESSAGES) << "Event" << *upToMarker << "in" << q->objectName() + << "is behind the current fully read marker at" + << *q->fullyReadMarker() + << "- won't move fully read marker back in timeline"; + return false; +} + +void Room::markMessagesAsRead(const QString& uptoEventId) { d->markMessagesAsRead(findInTimeline(uptoEventId)); } void Room::markAllMessagesAsRead() { - if (!d->timeline.empty()) - d->markMessagesAsRead(d->timeline.crbegin()); + d->markMessagesAsRead(d->timeline.crbegin()); } bool Room::canSwitchVersions() const @@ -789,18 +910,44 @@ bool Room::canSwitchVersions() const return true; } -bool Room::hasUnreadMessages() const { return unreadCount() >= 0; } +bool Room::isEventNotable(const TimelineItem &ti) const +{ + const auto& evt = *ti; + const auto* rme = ti.viewAs<RoomMessageEvent>(); + return !evt.isRedacted() + && (is<RoomTopicEvent>(evt) || is<RoomNameEvent>(evt) + || is<RoomAvatarEvent>(evt) || is<RoomTombstoneEvent>(evt) + || (rme && rme->msgtype() != MessageEventType::Notice + && rme->replacedEvent().isEmpty())) + && evt.senderId() != localUser()->id(); +} + +Notification Room::notificationFor(const TimelineItem &ti) const +{ + return d->notifications.value(ti->id()); +} -int Room::unreadCount() const { return d->unreadMessages; } +Notification Room::checkForNotifications(const TimelineItem &ti) +{ + return { Notification::None }; +} -Room::rev_iter_t Room::historyEdge() const { return d->timeline.crend(); } +bool Room::hasUnreadMessages() const { return !d->partiallyReadStats.empty(); } -Room::Timeline::const_iterator Room::syncEdge() const +int countFromStats(const EventStats& s) { - return d->timeline.cend(); + return s.empty() ? -1 : int(s.notableCount); } -Room::rev_iter_t Room::timelineEdge() const { return historyEdge(); } +int Room::unreadCount() const { return countFromStats(partiallyReadStats()); } + +EventStats Room::partiallyReadStats() const { return d->partiallyReadStats; } + +EventStats Room::unreadStats() const { return d->unreadStats; } + +Room::rev_iter_t Room::historyEdge() const { return d->historyEdge(); } + +Room::Timeline::const_iterator Room::syncEdge() const { return d->syncEdge(); } TimelineItem::index_t Room::minTimelineIndex() const { @@ -820,7 +967,7 @@ bool Room::isValidIndex(TimelineItem::index_t timelineIndex) const Room::rev_iter_t Room::findInTimeline(TimelineItem::index_t index) const { - return timelineEdge() + return historyEdge() - (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0); } @@ -872,18 +1019,17 @@ void Room::Private::getAllMembers() allMembersJob = connection->callApi<GetMembersByRoomJob>( id, connection->nextBatchToken(), "join"); auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1; - connect(allMembersJob, &BaseJob::success, q, [=] { + connect(allMembersJob, &BaseJob::success, q, [this, nextIndex] { 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()) for (auto it = q->findInTimeline(nextIndex).base(); - it != timeline.cend(); ++it) + it != syncEdge(); ++it) if (is<RoomMemberEvent>(**it)) roomChanges |= q->processStateEvent(**it); - if (roomChanges & MembersChange) - emit q->memberListChanged(); + postprocessChanges(roomChanges); emit q->allMembersLoaded(); }); } @@ -897,11 +1043,8 @@ void Room::setDisplayed(bool displayed) d->displayed = displayed; emit displayedChanged(displayed); - if (displayed) { - resetHighlightCount(); - resetNotificationCount(); + if (displayed) d->getAllMembers(); - } } QString Room::firstDisplayedEventId() const { return d->firstDisplayedEventId; } @@ -916,6 +1059,11 @@ void Room::setFirstDisplayedEventId(const QString& eventId) if (d->firstDisplayedEventId == eventId) return; + if (!eventId.isEmpty() && findInTimeline(eventId) == historyEdge()) + qCWarning(MESSAGES) + << eventId + << "is marked as first displayed but doesn't seem to be loaded"; + d->firstDisplayedEventId = eventId; emit firstDisplayedEventChanged(); } @@ -938,6 +1086,12 @@ void Room::setLastDisplayedEventId(const QString& eventId) if (d->lastDisplayedEventId == eventId) return; + const auto marker = findInTimeline(eventId); + if (!eventId.isEmpty() && marker == historyEdge()) + qCWarning(MESSAGES) + << eventId + << "is marked as last displayed but doesn't seem to be loaded"; + d->lastDisplayedEventId = eventId; emit lastDisplayedEventChanged(); } @@ -951,38 +1105,70 @@ void Room::setLastDisplayedEvent(TimelineItem::index_t index) Room::rev_iter_t Room::readMarker(const User* user) const { Q_ASSERT(user); - return findInTimeline(d->lastReadEventIds.value(user)); + return findInTimeline(lastReadReceipt(user->id()).eventId); } -Room::rev_iter_t Room::readMarker() const { return readMarker(localUser()); } +Room::rev_iter_t Room::readMarker() const { return fullyReadMarker(); } + +QString Room::readMarkerEventId() const { return lastFullyReadEventId(); } + +ReadReceipt Room::lastReadReceipt(const QString& userId) const +{ + return d->lastReadReceipts.value(userId); +} -QString Room::readMarkerEventId() const +ReadReceipt Room::lastLocalReadReceipt() const { - return d->lastReadEventIds.value(localUser()); + return d->lastReadReceipts.value(localUser()->id()); } -QList<User*> Room::usersAtEventId(const QString& eventId) +Room::rev_iter_t Room::localReadReceiptMarker() const { - return d->eventIdReadUsers.values(eventId); + return findInTimeline(lastLocalReadReceipt().eventId); } -int Room::notificationCount() const { return d->notificationCount; } +QString Room::lastFullyReadEventId() const { return d->fullyReadUntilEventId; } + +Room::rev_iter_t Room::fullyReadMarker() const +{ + return findInTimeline(d->fullyReadUntilEventId); +} + +QSet<QString> Room::userIdsAtEvent(const QString& eventId) +{ + return d->eventIdReadUsers.value(eventId); +} + +QSet<User*> Room::usersAtEventId(const QString& eventId) +{ + const auto& userIds = d->eventIdReadUsers.value(eventId); + QSet<User*> users; + users.reserve(userIds.size()); + for (const auto& uId : userIds) + users.insert(user(uId)); + return users; +} + +qsizetype Room::notificationCount() const +{ + return d->unreadStats.notableCount; +} void Room::resetNotificationCount() { - if (d->notificationCount == 0) + if (d->unreadStats.notableCount == 0) return; - d->notificationCount = 0; + d->unreadStats.notableCount = 0; emit notificationCountChanged(); } -int Room::highlightCount() const { return d->highlightCount; } +qsizetype Room::highlightCount() const { return d->serverHighlightCount; } void Room::resetHighlightCount() { - if (d->highlightCount == 0) + if (d->serverHighlightCount == 0) return; - d->highlightCount = 0; + d->serverHighlightCount = 0; emit highlightCountChanged(); } @@ -1120,6 +1306,17 @@ QList<User*> Room::directChatUsers() const return connection()->directChatUsers(this); } +QUrl Room::makeMediaUrl(const QString& eventId, const QUrl& mxcUrl) const +{ + auto url = connection()->makeMediaUrl(mxcUrl); + QUrlQuery q(url.query()); + Q_ASSERT(q.hasQueryItem("user_id")); + q.addQueryItem("room_id", id()); + q.addQueryItem("event_id", eventId); + url.setQuery(q); + return url; +} + QString safeFileName(QString rawName) { return rawName.replace(QRegularExpression("[/\\<>|\"*?:]"), "_"); @@ -1173,9 +1370,8 @@ QUrl Room::urlToThumbnail(const QString& eventId) const if (event->hasThumbnail()) { auto* thumbnail = event->content()->thumbnailInfo(); Q_ASSERT(thumbnail != nullptr); - return MediaThumbnailJob::makeRequestUrl(connection()->homeserver(), - thumbnail->url, - thumbnail->imageSize); + return connection()->getUrlForApi<MediaThumbnailJob>( + thumbnail->url, thumbnail->imageSize); } qCDebug(MAIN) << "Event" << eventId << "has no thumbnail"; return {}; @@ -1186,8 +1382,7 @@ QUrl Room::urlToDownload(const QString& eventId) const if (auto* event = d->getEventWithFile(eventId)) { auto* fileInfo = event->content()->fileInfo(); Q_ASSERT(fileInfo != nullptr); - return DownloadFileJob::makeRequestUrl(connection()->homeserver(), - fileInfo->url); + return connection()->getUrlForApi<DownloadFileJob>(fileInfo->url); } return {}; } @@ -1250,7 +1445,7 @@ QList<User*> Room::membersLeft() const { return d->membersLeft; } QList<User*> Room::users() const { return d->membersMap.values(); } -[[deprecated]] QStringList Room::memberNames() const +QStringList Room::memberNames() const { return safeMemberNames(); } @@ -1275,8 +1470,6 @@ QStringList Room::htmlSafeMemberNames() const return res; } -int Room::memberCount() const { return d->membersMap.size(); } - int Room::timelineSize() const { return int(d->timeline.size()); } bool Room::usesEncryption() const @@ -1290,6 +1483,17 @@ const StateEventBase* Room::getCurrentState(const QString& evtType, return d->getCurrentState({ evtType, stateKey }); } +const QVector<const StateEventBase*> +Room::stateEventsOfType(const QString& evtType) const +{ + return d->stateEventsOfType(evtType); +} + +const QHash<StateEventKey, const StateEventBase*>& Room::currentState() const +{ + return d->currentState; +} + RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) { #ifndef Quotient_E2EE_ENABLED @@ -1353,11 +1557,10 @@ GetRoomEventsJob* Room::eventsHistoryJob() const { return d->eventsHistoryJob; } Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) { if (!summary.merge(newSummary)) - return Change::NoChange; + return Change::None; qCDebug(STATE).nospace().noquote() << "Updated room summary for " << q->objectName() << ": " << summary; - emit q->memberListChanged(); - return Change::SummaryChange; + return Change::Summary; } void Room::Private::insertMemberIntoMap(User* u) @@ -1435,7 +1638,8 @@ void Room::Private::removeMemberFromMap(User* u) inline auto makeErrorStr(const Event& e, QByteArray msg) { - return msg.append("; event dump follows:\n").append(e.originalJson()); + return msg.append("; event dump follows:\n") + .append(QJsonDocument(e.fullJson()).toJson()); } Room::Timeline::size_type @@ -1461,11 +1665,12 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events, !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 - timeline.emplace_back(move(e), ++index); + const auto& ti = placement == Older + ? timeline.emplace_front(move(e), --index) + : timeline.emplace_back(move(e), ++index); eventsIndex.insert(eId, index); + if (auto n = q->checkForNotifications(ti); n.type != Notification::None) + notifications.insert(e->id(), n); Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId); } const auto insertedSize = (index - baseIndex) * placement; @@ -1523,7 +1728,7 @@ QString Room::disambiguatedMemberName(const QString& mxId) const QString Room::safeMemberName(const QString& userId) const { - return sanitized(roomMembername(userId)); + return sanitized(disambiguatedMemberName(userId)); } QString Room::htmlSafeMemberName(const QString& userId) const @@ -1540,63 +1745,132 @@ QUrl Room::memberAvatarUrl(const QString &mxId) const : QUrl(); } +Room::Changes Room::Private::updateStatsFromSyncData(const SyncRoomData& data, + bool fromCache) +{ + Changes changes {}; + if (fromCache) { + // Initial load of cached statistics + partiallyReadStats = + EventStats::fromCachedCounters(data.partiallyReadCount); + unreadStats = EventStats::fromCachedCounters(data.unreadCount, + data.highlightCount); + // Migrate from lib 0.6: -1 in the old unread counter overrides 0 + // (which loads to an estimate) in notification_count. Next caching will + // save -1 in both places, completing the migration. + if (data.unreadCount == 0 && data.partiallyReadCount == -1) + unreadStats.isEstimate = false; + changes |= Change::PartiallyReadStats | Change::UnreadStats; + qCDebug(MESSAGES) << "Loaded" << q->objectName() + << "event statistics from cache:" << partiallyReadStats + << "since m.fully_read," << unreadStats + << "since m.read"; + } else if (timeline.empty()) { + // In absence of actual events use statistics from the homeserver + if (merge(unreadStats.notableCount, data.unreadCount)) + changes |= Change::PartiallyReadStats; + if (merge(unreadStats.highlightCount, data.highlightCount)) + changes |= Change::UnreadStats; + unreadStats.isEstimate = !data.unreadCount.has_value() + || *data.unreadCount > 0; + qCDebug(MESSAGES) + << "Using server-side unread event statistics while the" + << q->objectName() << "timeline is empty:" << unreadStats; + } + bool correctedStats = false; + if (unreadStats.highlightCount > partiallyReadStats.highlightCount) { + correctedStats = true; + partiallyReadStats.highlightCount = unreadStats.highlightCount; + partiallyReadStats.isEstimate |= unreadStats.isEstimate; + } + if (unreadStats.notableCount > partiallyReadStats.notableCount) { + correctedStats = true; + partiallyReadStats.notableCount = unreadStats.notableCount; + partiallyReadStats.isEstimate |= unreadStats.isEstimate; + } + if (!unreadStats.isEstimate && partiallyReadStats.isEstimate) { + correctedStats = true; + partiallyReadStats.isEstimate = true; + } + if (correctedStats) + qCDebug(MESSAGES) << "Partially read event statistics in" + << q->objectName() << "were adjusted to" + << partiallyReadStats + << "to be consistent with the m.read receipt"; + Q_ASSERT(partiallyReadStats.isValidFor(q, q->fullyReadMarker())); + Q_ASSERT(unreadStats.isValidFor(q, q->localReadReceiptMarker())); + + // TODO: Once the library learns to count highlights, drop + // serverHighlightCount and only use the server-side counter when + // the timeline is empty (see the code above). + if (merge(serverHighlightCount, data.highlightCount)) { + qCDebug(MESSAGES) << "Updated highlights number in" << q->objectName() + << "to" << serverHighlightCount; + changes |= Change::Highlights; + } + return changes; +} + void Room::updateData(SyncRoomData&& data, bool fromCache) { if (d->prevBatch.isEmpty()) d->prevBatch = data.timelinePrevBatch; setJoinState(data.joinState); - Changes roomChanges = Change::NoChange; - QElapsedTimer et; - et.start(); + Changes roomChanges {}; + // The order of calculation is important - don't merge the lines! + roomChanges |= d->updateStateFrom(data.state); + roomChanges |= d->setSummary(move(data.summary)); + roomChanges |= d->addNewMessageEvents(move(data.timeline)); + + for (auto&& ephemeralEvent : data.ephemeral) + roomChanges |= processEphemeralEvent(move(ephemeralEvent)); + for (auto&& event : data.accountData) roomChanges |= processAccountDataEvent(move(event)); - roomChanges |= d->updateStateFrom(data.state); + roomChanges |= d->updateStatsFromSyncData(data, fromCache); - 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; - } - if (roomChanges & TopicChange) + if (roomChanges & Change::Topic) emit topicChanged(); - if (roomChanges & (NameChange | AliasesChange)) + if (roomChanges & (Change::Name | Change::Aliases)) emit namesChanged(this); - if (roomChanges & MembersChange) - emit memberListChanged(); + d->postprocessChanges(roomChanges, !fromCache); +} - roomChanges |= d->setSummary(move(data.summary)); +void Room::Private::postprocessChanges(Changes changes, bool saveState) +{ + if (!changes) + return; - for (auto&& ephemeralEvent : data.ephemeral) - roomChanges |= processEphemeralEvent(move(ephemeralEvent)); + if (changes & Change::Members) + emit q->memberListChanged(); - // 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; - emit unreadMessagesChanged(this); - } + if (changes + & (Change::Name | Change::Aliases | Change::Members | Change::Summary)) + updateDisplayname(); - if (data.highlightCount != d->highlightCount) { - d->highlightCount = data.highlightCount; - emit highlightCountChanged(); - } - if (data.notificationCount != d->notificationCount) { - d->notificationCount = data.notificationCount; - emit notificationCountChanged(); - } - if (roomChanges != Change::NoChange) { - d->updateDisplayname(); - emit changed(roomChanges); - if (!fromCache) - connection()->saveRoomState(this); + if (changes & Change::PartiallyReadStats) { + emit q->unreadMessagesChanged(q); // TODO: remove in 0.8 + emit q->partiallyReadStatsChanged(); } + + if (changes & Change::UnreadStats) + emit q->unreadStatsChanged(); + + if (changes & Change::Highlights) + emit q->highlightCountChanged(); + + qCDebug(MAIN) << terse << changes << "= hex" << +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + Qt:: +#endif + hex << uint(changes) << "in" << q->objectName(); + emit q->changed(changes); + if (saveState) + connection->saveRoomState(q); } RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) @@ -1716,6 +1990,12 @@ QString Room::retryMessage(const QString& txnId) return d->doSendEvent(it->event()); } +// Lambda defers actual tr() invocation to the moment when translations are +// initialised +const auto FileTransferCancelledMsg = [] { + return Room::tr("File transfer cancelled"); +}; + void Room::discardMessage(const QString& txnId) { auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), @@ -1730,7 +2010,7 @@ void Room::discardMessage(const QString& txnId) if (isJobPending(transferIt->job)) { transferIt->status = FileTransferInfo::Cancelled; transferIt->job->abandon(); - emit fileTransferFailed(txnId, tr("File upload cancelled")); + emit fileTransferFailed(txnId, FileTransferCancelledMsg()); } else if (transferIt->status == FileTransferInfo::Completed) { qCWarning(MAIN) << "File for transaction" << txnId @@ -1770,57 +2050,80 @@ QString Room::postReaction(const QString& eventId, const QString& key) return d->sendEvent<ReactionEvent>(EventRelation::annotate(eventId, key)); } -QString Room::postFile(const QString& plainText, const QUrl& localPath, - bool asGenericFile) +QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl) { - QFileInfo localFile { localPath.toLocalFile() }; - Q_ASSERT(localFile.isFile()); - - const auto txnId = - d->addAsPending( - makeEvent<RoomMessageEvent>(plainText, localFile, asGenericFile)) - ->transactionId(); + const auto txnId = addAsPending(move(msgEvent))->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); + q->uploadFile(txnId, localUrl); // 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"; - } + const auto& transferJob = fileTransfers.value(txnId).job; + connect(q, &Room::fileTransferCompleted, transferJob, + [this, txnId](const QString& tId, const QUrl&, const QUrl& mxcUri) { + if (tId != txnId) + return; + + const auto it = q->findPendingEvent(txnId); + if (it != unsyncedEvents.end()) { + it->setFileUploaded(mxcUri); + emit q->pendingEventChanged( + int(it - unsyncedEvents.begin())); + 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"; } }); - 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(); - } - } + connect(q, &Room::fileTransferFailed, transferJob, + [this, txnId](const QString& tId) { + if (tId != txnId) + return; + + const auto it = q->findPendingEvent(txnId); + if (it == unsyncedEvents.end()) + return; + + const auto idx = int(it - unsyncedEvents.begin()); + emit q->pendingEventAboutToDiscard(idx); + // See #286 on why `it` may not be valid here. + unsyncedEvents.erase(unsyncedEvents.begin() + idx); + emit q->pendingEventDiscarded(); }); return txnId; } +QString Room::postFile(const QString& plainText, + EventContent::TypedBase* content) +{ + Q_ASSERT(content != nullptr && content->fileInfo() != nullptr); + const auto* const fileInfo = content->fileInfo(); + Q_ASSERT(fileInfo != nullptr); + QFileInfo localFile { fileInfo->url.toLocalFile() }; + Q_ASSERT(localFile.isFile()); + + return d->doPostFile( + makeEvent<RoomMessageEvent>( + plainText, RoomMessageEvent::rawMsgTypeForFile(localFile), content), + fileInfo->url); +} + +#if QT_VERSION_MAJOR < 6 +QString Room::postFile(const QString& plainText, const QUrl& localPath, + bool asGenericFile) +{ + QFileInfo localFile { localPath.toLocalFile() }; + Q_ASSERT(localFile.isFile()); + return d->doPostFile(makeEvent<RoomMessageEvent>(plainText, localFile, + asGenericFile), + localPath); +} +#endif + QString Room::postEvent(RoomEvent* event) { return d->sendEvent(RoomEventPtr(event)); @@ -1943,7 +2246,7 @@ void Room::Private::getPreviousContent(int limit, const QString &filter) eventsHistoryJob = connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit, filter); emit q->eventsHistoryJobChanged(); - connect(eventsHistoryJob, &BaseJob::success, q, [=] { + connect(eventsHistoryJob, &BaseJob::success, q, [this] { prevBatch = eventsHistoryJob->end(); addHistoricalMessageEvents(eventsHistoryJob->chunk()); }); @@ -2068,16 +2371,16 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) void Room::cancelFileTransfer(const QString& id) { - const auto it = d->fileTransfers.constFind(id); - if (it == d->fileTransfers.cend()) { + const auto it = d->fileTransfers.find(id); + if (it == d->fileTransfers.end()) { qCWarning(MAIN) << "No information on file transfer" << id << "in room" << d->id; return; } if (isJobPending(it->job)) it->job->abandon(); - d->fileTransfers.remove(id); - emit fileTransferCancelled(id); + it->status = FileTransferInfo::Cancelled; + emit fileTransferFailed(id, FileTransferCancelledMsg()); } void Room::Private::dropDuplicateEvents(RoomEvents& events) const @@ -2114,7 +2417,7 @@ void Room::Private::dropDuplicateEvents(RoomEvents& events) const RoomEventPtr makeRedacted(const RoomEvent& target, const RedactionEvent& redaction) { - auto originalJson = target.originalJsonObject(); + auto originalJson = target.fullJson(); // clang-format off static const QStringList keepKeys { EventIdKey, TypeKey, RoomIdKey, SenderKey, StateKeyKey, @@ -2125,15 +2428,14 @@ RoomEventPtr makeRedacted(const RoomEvent& target, QStringLiteral("membership") }; // clang-format on - std::vector<std::pair<event_type_t, QStringList>> keepContentKeysMap { + static const std::pair<event_type_t, 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") } } + QStringLiteral("users"), QStringLiteral("users_default") } } // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } } // , { RoomHistoryVisibility::typeId(), // { QStringLiteral("history_visibility") } } @@ -2145,9 +2447,9 @@ RoomEventPtr makeRedacted(const RoomEvent& target, ++it; } auto keepContentKeys = - find_if(keepContentKeysMap.begin(), keepContentKeysMap.end(), + find_if(begin(keepContentKeysMap), end(keepContentKeysMap), [&target](const auto& t) { return target.type() == t.first; }); - if (keepContentKeys == keepContentKeysMap.end()) { + if (keepContentKeys == end(keepContentKeysMap)) { originalJson.remove(ContentKeyL); originalJson.remove(PrevContentKeyL); } else { @@ -2161,7 +2463,7 @@ RoomEventPtr makeRedacted(const RoomEvent& target, originalJson.insert(ContentKey, content); } auto unsignedData = originalJson.take(UnsignedKeyL).toObject(); - unsignedData[RedactedCauseKeyL] = redaction.originalJsonObject(); + unsignedData[RedactedCauseKeyL] = redaction.fullJson(); originalJson.insert(QStringLiteral("unsigned"), unsignedData); return loadEvent<RoomEvent>(originalJson); @@ -2226,8 +2528,13 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) RoomEventPtr makeReplaced(const RoomEvent& target, const RoomMessageEvent& replacement) { - auto originalJson = target.originalJsonObject(); - originalJson[ContentKeyL] = replacement.contentJson().value("m.new_content"_ls); + const auto& targetReply = target.contentPart<QJsonObject>("m.relates_to"); + auto newContent = replacement.contentPart<QJsonObject>("m.new_content"_ls); + if (!targetReply.empty()) { + newContent["m.relates_to"] = targetReply; + } + auto originalJson = target.fullJson(); + originalJson[ContentKeyL] = newContent; auto unsignedData = originalJson.take(UnsignedKeyL).toObject(); auto relations = unsignedData.take("m.relations"_ls).toObject(); @@ -2287,8 +2594,10 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) { dropDuplicateEvents(events); if (events.empty()) - return Change::NoChange; + return Change::None; + QElapsedTimer et; + et.start(); { // Pre-process redactions and edits so that events that get // redacted/replaced in the same batch landed in the timeline already @@ -2338,7 +2647,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) // clients historically expect. This may eventually change though if we // postulate that the current state is only current between syncs but not // within a sync. - Changes roomChanges = Change::NoChange; + Changes roomChanges {}; for (const auto& eptr : events) roomChanges |= q->processStateEvent(*eptr); @@ -2356,7 +2665,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) emit q->aboutToAddNewMessages(eventsSpan); auto insertedSize = moveEventsToTimeline(eventsSpan, Newer); totalInserted += insertedSize; - auto firstInserted = timeline.cend() - insertedSize; + auto firstInserted = syncEdge() - insertedSize; q->onAddNewTimelineEvents(firstInserted); emit q->addedMessages(firstInserted->index(), timeline.back().index()); @@ -2386,20 +2695,20 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) unsyncedEvents.erase(unsyncedEvents.begin() + pendingEvtIdx); if (auto insertedSize = moveEventsToTimeline({ remoteEcho, it }, Newer)) { totalInserted += insertedSize; - q->onAddNewTimelineEvents(timeline.cend() - insertedSize); + q->onAddNewTimelineEvents(syncEdge() - insertedSize); } emit q->pendingEventMerged(); } // Events merged and transferred from `events` to `timeline` now. - const auto from = timeline.cend() - totalInserted; + const auto from = syncEdge() - totalInserted; if (q->supportsCalls()) - for (auto it = from; it != timeline.cend(); ++it) + for (auto it = from; it != syncEdge(); ++it) if (const auto* evt = it->viewAs<CallEventBase>()) emit q->callEvent(q, evt); if (totalInserted > 0) { - for (auto it = from; it != timeline.cend(); ++it) { + for (auto it = from; it != syncEdge(); ++it) { if (const auto* reaction = it->viewAs<ReactionEvent>()) { const auto& relation = reaction->relation(); relations[{ relation.eventId, relation.type }] << reaction; @@ -2411,28 +2720,22 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) << 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 - // the same author's events newly arrived. Others will need explicit - // read receipts from the server (or, for the local user, - // markMessagesAsRead() invocation) to promote their read markers over - // the new message events. - 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); - } - } + roomChanges |= updateStats(timeline.crbegin(), rev_iter_t(from)); - updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); - roomChanges |= Change::UnreadNotifsChange; + // If the local user's message(s) is/are first in the batch + // and the fully read marker was right before it, promote + // the fully read marker to the same event as the read receipt. + const auto& firstWriterId = (*from)->senderId(); + if (firstWriterId == connection->userId() + && q->fullyReadMarker().base() == from) + roomChanges |= + setFullyReadMarker(q->lastReadReceipt(firstWriterId).eventId); } Q_ASSERT(timeline.size() == timelineSize + totalInserted); + if (totalInserted > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "Added" << totalInserted << "new event(s) to" + << q->objectName() << "in" << et; return roomChanges; } @@ -2446,6 +2749,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) if (events.empty()) return; + Changes changes {}; // In case of lazy-loading new members may be loaded with historical // messages. Also, the cache doesn't store events with empty content; // so when such events show up in the timeline they should be properly @@ -2454,39 +2758,40 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) const auto& e = *eptr; if (e.isStateEvent() && !currentState.contains({ e.matrixType(), e.stateKey() })) { - q->processStateEvent(e); + changes |= q->processStateEvent(e); } } emit q->aboutToAddHistoricalMessages(events); const auto insertedSize = moveEventsToTimeline(events, Older); - const auto from = timeline.crend() - insertedSize; + const auto from = historyEdge() - insertedSize; 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()); - for (auto it = from; it != timeline.crend(); ++it) { + for (auto it = from; it != historyEdge(); ++it) { if (const auto* reaction = it->viewAs<ReactionEvent>()) { const auto& relation = reaction->relation(); relations[{ relation.eventId, relation.type }] << reaction; emit q->updatedEvent(relation.eventId); } } - if (from <= q->readMarker()) - updateUnreadCount(from, timeline.crend()); - Q_ASSERT(timeline.size() == timelineSize + insertedSize); if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::addHistoricalMessageEvents():" - << insertedSize << "event(s)," << et; + qCDebug(PROFILER) << "Added" << insertedSize << "historical event(s) to" + << q->objectName() << "in" << et; + + changes |= updateStats(from, historyEdge()); + if (changes) + postprocessChanges(changes); } Room::Changes Room::processStateEvent(const RoomEvent& e) { if (!e.isStateEvent()) - return NoChange; + return Change::None; // Find a value (create an empty one if necessary) and get a reference // to it. Can't use getCurrentState<>() because it (creates and) returns @@ -2495,7 +2800,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) auto& curStateEvent = d->currentState[{ e.matrixType(), e.stateKey() }]; // Prepare for the state change // clang-format off - const bool proceed = visit(e + const bool proceed = switchOnType(e , [this, curStateEvent](const RoomMemberEvent& rme) { // clang-format on auto* oldRme = static_cast<const RoomMemberEvent*>(curStateEvent); @@ -2506,16 +2811,16 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return false; // Stay low and hope for the best... } const auto prevMembership = oldRme ? oldRme->membership() - : MembershipType::Leave; + : Membership::Leave; switch (prevMembership) { - case MembershipType::Invite: + case Membership::Invite: if (rme.membership() != prevMembership) { d->usersInvited.removeOne(u); Q_ASSERT(!d->usersInvited.contains(u)); } break; - case MembershipType::Join: - if (rme.membership() == MembershipType::Join) { + case Membership::Join: + if (rme.membership() == Membership::Join) { // rename/avatar change or no-op if (rme.newDisplayName()) { emit memberAboutToRename(u, *rme.newDisplayName()); @@ -2529,7 +2834,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return false; } } else { - if (rme.membership() == MembershipType::Invite) + if (rme.membership() == Membership::Invite) qCWarning(MAIN) << "Membership change from Join to Invite:" << rme; // whatever the new membership, it's no more Join @@ -2537,16 +2842,16 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) emit userRemoved(u); } break; - case MembershipType::Ban: - case MembershipType::Knock: - case MembershipType::Leave: - if (rme.membership() == MembershipType::Invite - || rme.membership() == MembershipType::Join) { + case Membership::Ban: + case Membership::Knock: + case Membership::Leave: + if (rme.membership() == Membership::Invite + || rme.membership() == Membership::Join) { d->membersLeft.removeOne(u); Q_ASSERT(!d->membersLeft.contains(u)); } break; - case MembershipType::Undefined: + case Membership::Undefined: ; // A warning will be dropped in the post-processing block below } return true; @@ -2573,8 +2878,11 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) } , true); // By default, go forward with the state change // clang-format on - if (!proceed) - return NoChange; + if (!proceed) { + if (!curStateEvent) // Remove the empty placeholder if one was created + d->currentState.remove({ e.matrixType(), e.stateKey() }); + return Change::None; + } // Change the state const auto* const oldStateEvent = @@ -2590,15 +2898,15 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) // Update internal structures as per the change and work out the return value // clang-format off - const auto result = visit(e + const auto result = switchOnType(e , [] (const RoomNameEvent&) { - return NameChange; + return Change::Name; } , [this, oldStateEvent] (const RoomCanonicalAliasEvent& cae) { // clang-format on setObjectName(cae.alias().isEmpty() ? d->id : cae.alias()); const auto* oldCae = - static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent); + static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent); QStringList previousAltAliases {}; if (oldCae) { previousAltAliases = oldCae->altAliases(); @@ -2610,8 +2918,9 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) if (!cae.alias().isEmpty()) newAliases.push_front(cae.alias()); - connection()->updateRoomAliases(id(), previousAltAliases, newAliases); - return AliasesChange; + connection()->updateRoomAliases(id(), previousAltAliases, + newAliases); + return Change::Aliases; // clang-format off } , [this] (const RoomPinnedEvent&) { @@ -2619,12 +2928,12 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return OtherChange; } , [] (const RoomTopicEvent&) { - return TopicChange; + return Change::Topic; } , [this] (const RoomAvatarEvent& evt) { if (d->avatar.updateUrl(evt.url())) emit avatarChanged(); - return AvatarChange; + return Change::Avatar; } , [this,oldStateEvent] (const RoomMemberEvent& evt) { // clang-format on @@ -2633,10 +2942,10 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) static_cast<const RoomMemberEvent*>(oldStateEvent); const auto prevMembership = oldMemberEvent ? oldMemberEvent->membership() - : MembershipType::Leave; + : Membership::Leave; switch (evt.membership()) { - case MembershipType::Join: - if (prevMembership != MembershipType::Join) { + case Membership::Join: + if (prevMembership != Membership::Join) { d->insertMemberIntoMap(u); emit userAdded(u); } else { @@ -2648,29 +2957,29 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) emit memberAvatarChanged(u); } break; - case MembershipType::Invite: + case Membership::Invite: if (!d->usersInvited.contains(u)) d->usersInvited.push_back(u); if (u == localUser() && evt.isDirect()) connection()->addToDirectChats(this, user(evt.senderId())); break; - case MembershipType::Knock: - case MembershipType::Ban: - case MembershipType::Leave: + case Membership::Knock: + case Membership::Ban: + case Membership::Leave: if (!d->membersLeft.contains(u)) d->membersLeft.append(u); break; - case MembershipType::Undefined: + case Membership::Undefined: qCWarning(MEMBERS) << "Ignored undefined membership type"; } - return MembersChange; + return Change::Members; // clang-format off } , [this] (const EncryptionEvent&) { // As encryption can only be switched on once, emit the signal here // instead of aggregating and emitting in updateData() emit encryption(); - return OtherChange; + return Change::Other; } , [this] (const RoomTombstoneEvent& evt) { const auto successorId = evt.successorRoomId(); @@ -2686,30 +2995,31 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return true; }); - return OtherChange; + return Change::Other; // clang-format off } - , OtherChange); + , Change::Other); // clang-format on - Q_ASSERT(result != NoChange); + Q_ASSERT(result != Change::None); return result; } Room::Changes Room::processEphemeralEvent(EventPtr&& event) { - Changes changes = NoChange; + Changes changes {}; QElapsedTimer et; et.start(); if (auto* evt = eventCast<TypingEvent>(event)) { d->usersTyping.clear(); - for (const auto& userId : evt->users()) { - auto u = user(userId); - if (memberJoinState(u) == JoinState::Join) - d->usersTyping.append(u); - } + d->usersTyping.reserve(evt->users().size()); // Assume all are members + for (const auto& userId : evt->users()) + if (isMember(userId)) + d->usersTyping.append(user(userId)); + if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::processEphemeralEvent(typing):" - << evt->users().size() << "users," << et; + qCDebug(PROFILER) + << "Processing typing events from" << evt->users().size() + << "user(s) in" << objectName() << "took" << et; emit typingChanged(); } if (auto* evt = eventCast<ReceiptEvent>(event)) { @@ -2717,67 +3027,54 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) const auto& eventsWithReceipts = evt->eventsWithReceipts(); for (const auto& p : eventsWithReceipts) { totalReceipts += p.receipts.size(); - { - if (p.receipts.size() == 1) - 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"; - } const auto newMarker = findInTimeline(p.evtId); - 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 { - qCDebug(EPHEMERAL) << "Event" << p.evtId - << "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) { - if (r.userId == connection()->userId()) - continue; // FIXME, #185 - auto u = user(r.userId); - if (memberJoinState(u) == JoinState::Join - && readMarker(u) == timelineEdge()) - changes |= d->setLastReadEvent(u, p.evtId); - } - } + if (newMarker == historyEdge()) + qCDebug(EPHEMERAL) + << "Event" << p.evtId + << "is not found; saving read receipt(s) 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 because read receipts + // are not supposed to move backwards. Otherwise, blindly store + // the event id for this user and update the read marker when/if + // the event is fetched later on. + const auto updatedCount = std::count_if( + p.receipts.cbegin(), p.receipts.cend(), + [this, &changes, &newMarker, &evtId = p.evtId](const auto& r) { + const auto change = + d->setLastReadReceipt(r.userId, newMarker, + { evtId, r.timestamp }); + changes |= change; + return change & Change::Any; + }); + + if (p.receipts.size() > 1) + qCDebug(EPHEMERAL) << p.evtId << "marked as read for" + << updatedCount << "user(s)"; + if (updatedCount < p.receipts.size()) + qCDebug(EPHEMERAL) << p.receipts.size() - updatedCount + << "receipts were skipped"; } if (eventsWithReceipts.size() > 3 || totalReceipts > 10 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) - << "*** Room::processEphemeralEvent(receipts):" - << eventsWithReceipts.size() << "event(s) with" - << totalReceipts << "receipt(s)," << et; + qCDebug(PROFILER) << "Processing" << totalReceipts + << "receipt(s) on" << eventsWithReceipts.size() + << "event(s) in" << objectName() << "took" << et; } return changes; } Room::Changes Room::processAccountDataEvent(EventPtr&& event) { - Changes changes = NoChange; + Changes changes {}; if (auto* evt = eventCast<TagEvent>(event)) { d->setTags(evt->tags()); - changes |= Change::TagsChange; + changes |= Change::Tags; } - if (auto* evt = eventCast<ReadMarkerEvent>(event)) { - auto readEventId = evt->event_id(); - 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); - } + if (auto* evt = eventCast<const ReadMarkerEvent>(event)) + changes |= d->setFullyReadMarker(evt->event_id()); + // For all account data events auto& currentData = d->accountData[event->matrixType()]; // A polymorphic event-specific comparison might be a bit more @@ -2788,9 +3085,12 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) qCDebug(STATE) << "Updated account data of type" << currentData->matrixType(); emit accountDataChanged(currentData->matrixType()); - return Change::AccountDataChange; + // TODO: Drop AccountDataChange in 0.8 + // NB: GCC (at least 10) only accepts QT_IGNORE_DEPRECATIONS around + // a statement, not within a statement + QT_IGNORE_DEPRECATIONS(changes |= Change::AccountData | Change::Other;) } - return Change::NoChange; + return changes; } template <typename ContT> @@ -2958,19 +3258,28 @@ QJsonObject Room::Private::toJson() const { QStringLiteral("events"), accountDataEvents } }); } - QJsonObject unreadNotifObj { { SyncRoomData::UnreadCountKey, - unreadMessages } }; - - if (highlightCount > 0) - unreadNotifObj.insert(QStringLiteral("highlight_count"), highlightCount); - if (notificationCount > 0) - unreadNotifObj.insert(QStringLiteral("notification_count"), - notificationCount); - - result.insert(QStringLiteral("unread_notifications"), unreadNotifObj); + if (const auto& readReceipt = q->lastReadReceipt(connection->userId()); + !readReceipt.eventId.isEmpty()) // + { + result.insert( + QStringLiteral("ephemeral"), + QJsonObject { + { QStringLiteral("events"), + QJsonArray { ReceiptEvent({ { readReceipt.eventId, + { { connection->userId(), + readReceipt.timestamp } } } }) + .fullJson() } } }); + } + + result.insert(UnreadNotificationsKey, + QJsonObject { { PartiallyReadCountKey, + countFromStats(partiallyReadStats) }, + { HighlightCountKey, serverHighlightCount } }); + result.insert(NewUnreadCountKey, countFromStats(unreadStats)); if (et.elapsed() > 30) - qCDebug(PROFILER) << "Room::toJson() for" << displayname << "took" << et; + qCDebug(PROFILER) << "Room::toJson() for" << q->objectName() << "took" + << et; return result; } @@ -2984,12 +3293,25 @@ bool MemberSorter::operator()(User* u1, User* u2) const return operator()(u1, room->disambiguatedMemberName(u2->id())); } -bool MemberSorter::operator()(User* u1, const QString& u2name) const +bool MemberSorter::operator()(User* u1, QStringView u2name) const { auto n1 = room->disambiguatedMemberName(u1->id()); if (n1.startsWith('@')) n1.remove(0, 1); - auto n2 = u2name.midRef(u2name.startsWith('@') ? 1 : 0); + const auto n2 = u2name.mid(u2name.startsWith('@') ? 1 : 0) +#if QT_VERSION_MAJOR < 6 + .toString() // Qt 5 doesn't have QStringView::localeAwareCompare +#endif + ; return n1.localeAwareCompare(n2) < 0; } + +void Room::activateEncryption() +{ + if(usesEncryption()) { + qCWarning(E2EE) << "Room" << objectName() << "is already encrypted"; + return; + } + setState<EncryptionEvent>(EncryptionEventContent::MegolmV1AesSha2); +} @@ -11,7 +11,7 @@ #include "connection.h" #include "eventitem.h" -#include "joinstate.h" +#include "quotient_common.h" #include "csapi/message_pagination.h" @@ -71,6 +71,45 @@ public: bool failed() const { return status == Failed; } }; +//! \brief Data structure for a room member's read receipt +//! \sa Room::lastReadReceipt +class ReadReceipt { + Q_GADGET + Q_PROPERTY(QString eventId MEMBER eventId CONSTANT) + Q_PROPERTY(QDateTime timestamp MEMBER timestamp CONSTANT) +public: + QString eventId; + QDateTime timestamp = {}; + + bool operator==(const ReadReceipt& other) const + { + return eventId == other.eventId && timestamp == other.timestamp; + } + bool operator!=(const ReadReceipt& other) const + { + return !operator==(other); + } +}; +inline void swap(ReadReceipt& lhs, ReadReceipt& rhs) +{ + swap(lhs.eventId, rhs.eventId); + swap(lhs.timestamp, rhs.timestamp); +} + +struct EventStats; + +struct Notification +{ + enum Type { None = 0, Basic, Highlight }; + Q_ENUM(Notification) + + Type type = None; + +private: + Q_GADGET + Q_PROPERTY(Type type MEMBER type CONSTANT) +}; + class Room : public QObject { Q_OBJECT Q_PROPERTY(Connection* connection READ connection CONSTANT) @@ -87,6 +126,7 @@ class Room : public QObject { Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged) Q_PROPERTY(QStringList pinnedEventIds READ pinnedEventIds WRITE setPinnedEvents NOTIFY pinnedEventsChanged) + Q_PROPERTY(QString displayNameForHtml READ displayNameForHtml NOTIFY displaynameChanged) Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) @@ -95,7 +135,6 @@ class Room : public QObject { Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages) Q_PROPERTY(QStringList memberNames READ safeMemberNames 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) @@ -106,21 +145,28 @@ class Room : public QObject { setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged) Q_PROPERTY(QString lastDisplayedEventId READ lastDisplayedEventId WRITE setLastDisplayedEventId NOTIFY lastDisplayedEventChanged) - + //! \deprecated since 0.7 Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerMoved) + Q_PROPERTY(QString lastFullyReadEventId READ lastFullyReadEventId WRITE + markMessagesAsRead NOTIFY fullyReadMarkerMoved) + //! \deprecated since 0.7 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) + partiallyReadStatsChanged STORED false) + //! \deprecated since 0.7 + Q_PROPERTY(int unreadCount READ unreadCount NOTIFY partiallyReadStatsChanged + STORED false) + Q_PROPERTY(qsizetype highlightCount READ highlightCount + NOTIFY highlightCountChanged) + Q_PROPERTY(qsizetype notificationCount READ notificationCount + NOTIFY notificationCountChanged) + Q_PROPERTY(EventStats partiallyReadStats READ partiallyReadStats NOTIFY partiallyReadStatsChanged) + Q_PROPERTY(EventStats unreadStats READ unreadStats NOTIFY unreadStatsChanged) 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(bool isFavourite READ isFavourite NOTIFY tagsChanged STORED false) + Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged STORED false) Q_PROPERTY(GetRoomEventsJob* eventsHistoryJob READ eventsHistoryJob NOTIFY eventsHistoryJobChanged) @@ -132,26 +178,49 @@ public: 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 + //! \brief Room changes that can be tracked using Room::changed() signal + //! + //! This enumeration lists kinds of changes that can be tracked with + //! a "cumulative" changed() signal instead of using individual signals for + //! each change. Specific enumerators mention these individual signals. + //! \sa changed + enum class Change : uint { + None = 0x0, //< No changes occurred in the room + Name = 0x1, //< \sa namesChanged, displaynameChanged + Aliases = 0x2, //< \sa namesChanged, displaynameChanged + CanonicalAlias = Aliases, + Topic = 0x4, //< \sa topicChanged + PartiallyReadStats = 0x8, //< \sa partiallyReadStatsChanged + DECL_DEPRECATED_ENUMERATOR(UnreadNotifs, PartiallyReadStats), + Avatar = 0x10, //< \sa avatarChanged + JoinState = 0x20, //< \sa joinStateChanged + Tags = 0x40, //< \sa tagsChanged + //! \sa userAdded, userRemoved, memberRenamed, memberListChanged, + //! displaynameChanged + Members = 0x80, + UnreadStats = 0x100, //< \sa unreadStatsChanged + AccountData Q_DECL_ENUMERATOR_DEPRECATED_X( + "Change::AccountData will be merged into Change::Other in 0.8") = + 0x200, + Summary = 0x400, //< \sa summaryChanged, displaynameChanged + ReadMarker Q_DECL_ENUMERATOR_DEPRECATED_X( + "Change::ReadMarker will be merged into Change::Other in 0.8") = + 0x800, + Highlights = 0x1000, //< \sa highlightCountChanged + //! A catch-all value that covers changes not listed above (such as + //! encryption turned on or the room having been upgraded), as well as + //! changes in the room state that the library is not aware of (e.g., + //! custom state events) and m.read/m.fully_read position changes. + //! \sa encryptionChanged, upgraded, accountDataChanged + Other = 0x8000, + //! This is intended to test a Change/Changes value for non-emptiness; + //! adding <tt>& Change::Any</tt> has the same meaning as + //! !testFlag(Change::None) or adding <tt>!= Change::None</tt> + //! \note testFlag(Change::Any) tests that _all_ bits are on and + //! will always return false. + Any = 0xFFFF }; - Q_DECLARE_FLAGS(Changes, Change) - Q_FLAG(Changes) + QUO_DECLARE_FLAGS(Changes, Change) Room(Connection* connection, QString id, JoinState initialJoinState); ~Room() override; @@ -181,21 +250,15 @@ public: 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; + //! Get a list of both canonical and alternative aliases QStringList aliases() const; QString displayName() const; QStringList pinnedEventIds() const; // Returns events available locally, use pinnedEventIds() for full list QVector<const RoomEvent*> pinnedEvents() const; + QString displayNameForHtml() const; QString topic() const; QString avatarMediaId() const; QUrl avatarUrl() const; @@ -205,12 +268,10 @@ public: QList<User*> membersLeft() const; Q_INVOKABLE QList<Quotient::User*> users() const; - [[deprecated("Use safeMemberNames() or htmlSafeMemberNames() instead")]] + Q_DECL_DEPRECATED_X("Use safeMemberNames() or htmlSafeMemberNames() instead") // QStringList memberNames() const; QStringList safeMemberNames() const; QStringList htmlSafeMemberNames() const; - [[deprecated("Use joinedCount(), invitedCount(), totalMemberCount()")]] - int memberCount() const; int timelineSize() const; bool usesEncryption() const; RoomEventPtr decryptMessage(const EncryptedEvent& encryptedEvent); @@ -255,24 +316,27 @@ public: * * \return Join if the user is a room member; Leave otherwise */ + Q_DECL_DEPRECATED_X("Use isMember() instead") Q_INVOKABLE Quotient::JoinState memberJoinState(Quotient::User* user) const; + //! \brief Check the join state of a given user in this room + //! + //! \return the given user's state with respect to the room + Q_INVOKABLE Quotient::Membership memberState(const QString& userId) const; + + //! Check whether a user with the given id is a member of the room + Q_INVOKABLE bool isMember(const QString& userId) const; + //! \brief Get a display name (without disambiguation) for the given member //! //! \sa safeMemberName, htmlSafeMemberName Q_INVOKABLE QString memberName(const QString& mxId) const; - /*! - * \brief Get a disambiguated name for the given user in the room context - * - * \deprecated use safeMemberName() instead - */ + //! \brief Get a disambiguated name for the given user in the room context + Q_DECL_DEPRECATED_X("Use safeMemberName() instead") Q_INVOKABLE QString roomMembername(const Quotient::User* u) const; - /*! - * \brief Get a disambiguated name for a user with this id in the room context - * - * \deprecated use safeMemberName() instead - */ + //! \brief Get a disambiguated name for a user with this id in the room + Q_DECL_DEPRECATED_X("Use safeMemberName() instead") Q_INVOKABLE QString roomMembername(const QString& userId) const; /*! @@ -319,8 +383,6 @@ public: * 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 @@ -337,9 +399,13 @@ public: const char* relType) const; const RoomCreateEvent* creation() const - { return getCurrentState<RoomCreateEvent>(); } + { + return getCurrentState<RoomCreateEvent>(); + } const RoomTombstoneEvent* tombstone() const - { return getCurrentState<RoomTombstoneEvent>(); } + { + return getCurrentState<RoomTombstoneEvent>(); + } bool displayed() const; /// Mark the room as currently displayed to the user @@ -359,44 +425,223 @@ public: void setLastDisplayedEventId(const QString& eventId); void setLastDisplayedEvent(TimelineItem::index_t index); + //! \brief Obtain a read receipt of any user + //! \deprecated Use lastReadReceipt or fullyReadMarker instead. + //! + //! Historically, readMarker was returning a "converged" read marker + //! representing both the read receipt and the fully read marker, as + //! Quotient managed them together. Since 0.6.8, a single-argument call of + //! readMarker returns the last read receipt position (for any room member) + //! and a call without arguments returns the last _fully read_ position, + //! to provide access to both positions separately while maintaining API + //! stability guarantees. 0.7 has separate methods to return read receipts + //! and the fully read marker - use them instead. + //! \sa lastReadReceipt + [[deprecated("Use lastReadReceipt() to get m.read receipt or" + " fullyReadMarker() to get m.fully_read marker")]] // rev_iter_t readMarker(const User* user) const; + //! \brief Obtain the local user's fully-read marker + //! \deprecated Use fullyReadMarker instead + //! + //! See the documentation for the single-argument overload. + //! \sa fullyReadMarker + [[deprecated("Use localReadReceiptMarker() or fullyReadMarker()")]] // rev_iter_t readMarker() const; + //! \brief Get the event id for the local user's fully-read marker + //! \deprecated Use lastFullyReadEventId instead + //! + //! See the readMarker documentation + [[deprecated("Use lastReadReceipt() to get m.read receipt or" + " lastFullyReadEventId() to get an event id that" + " m.fully_read marker points to")]] // 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 + //! \brief Get the latest read receipt from a user + //! + //! The user id must be valid. A read receipt with an empty event id + //! is returned if the user id is valid but there was no read receipt + //! from them. + //! \sa usersAtEventId + ReadReceipt lastReadReceipt(const QString& userId) const; + + //! \brief Get the latest read receipt from the local user + //! + //! This is a shortcut for <tt>lastReadReceipt(localUserId)</tt>. + //! \sa lastReadReceipt + ReadReceipt lastLocalReadReceipt() const; + + //! \brief Find the timeline item the local read receipt is at + //! + //! This is a shortcut for \code + //! room->findInTimeline(room->lastLocalReadReceipt().eventId); + //! \endcode + rev_iter_t localReadReceiptMarker() const; + + //! \brief Get the latest event id marked as fully read + //! + //! This can be either the event id pointed to by the actual latest + //! m.fully_read event, or the latest event id marked locally as fully read + //! if markMessagesAsRead or markAllMessagesAsRead has been called and + //! the homeserver didn't return an updated m.fully_read event yet. + //! \sa markMessagesAsRead, markAllMessagesAsRead, fullyReadMarker + QString lastFullyReadEventId() const; + + //! \brief Get the iterator to the latest timeline item marked as fully read + //! + //! This method calls findInTimeline on the result of lastFullyReadEventId. + //! If the fully read marker turns out to be outside the timeline (because + //! the event marked as fully read is too far back in the history) the + //! returned value will be equal to historyEdge. + //! + //! Be sure to read the caveats on iterators returned by findInTimeline. + //! \sa lastFullyReadEventId, findInTimeline + rev_iter_t fullyReadMarker() const; + + //! \brief Get users whose latest read receipts point to the event + //! + //! This method is for cases when you need to show users who have read + //! an event. Calling it on inexistent or empty event id will return + //! an empty set. + //! \note The returned list may contain ids resolving to users that are + //! not loaded as room members yet (in particular, if members are not + //! yet lazy-loaded). For now this merely means that the user's + //! room-specific name and avatar will not be there; but generally + //! it's recommended to ensure that all room members are loaded + //! before operating on the result of this function. + //! \sa lastReadReceipt, allMembersLoaded + QSet<QString> userIdsAtEvent(const QString& eventId); + + [[deprecated("Use userIdsAtEvent instead")]] + QSet<User*> usersAtEventId(const QString& eventId); + + //! \brief Mark the event with uptoEventId as fully read + //! + //! Marks the event with the specified id as fully read locally and also + //! sends an update to m.fully_read account data to the server either + //! for this message or, if it's from the local user, for + //! the nearest non-local message before. uptoEventId must point to a known + //! event in the timeline; the method will do nothing if the event is behind + //! the current m.fully_read marker or is not loaded, to prevent + //! accidentally trying to move the marker back in the timeline. + //! \sa markAllMessagesAsRead, fullyReadMarker + Q_INVOKABLE void markMessagesAsRead(const QString& uptoEventId); + + //! \brief Determine whether an event should be counted as unread + //! + //! The criteria of including an event in unread counters are described in + //! [MSC2654](https://github.com/matrix-org/matrix-doc/pull/2654); according + //! to these, the event should be counted as unread (or, in libQuotient + //! parlance, is "notable") if it is: + //! - either + //! - a message event that is not m.notice, or + //! - a state event with type being one of: + //! `m.room.topic`, `m.room.name`, `m.room.avatar`, `m.room.tombstone`; + //! - neither redacted, nor an edit (redactions cause the redacted event + //! to stop being notable, while edits are not notable themselves while + //! the original event usually is); + //! - from a non-local user (events from other devices of the local + //! user are not notable). + //! \sa partiallyReadStats, unreadStats + virtual bool isEventNotable(const TimelineItem& ti) const; + + //! \brief Get notification details for an event + //! + //! This allows to get details on the kind of notification that should + //! generated for \p evt. + Notification notificationFor(const TimelineItem& ti) const; + + //! \brief Get event statistics since the fully read marker + //! + //! This call returns a structure containing: + //! - the number of notable unread events since the fully read marker; + //! depending on the fully read marker state with respect to the local + //! timeline, this number may be either exact or estimated + //! (see EventStats::isEstimate); + //! - the number of highlights (TODO). + //! + //! Note that this is different from the unread count defined by MSC2654 + //! and from the notification/highlight numbers defined by the spec in that + //! it counts events since the fully read marker, not since the last + //! read receipt position. + //! + //! As E2EE is not supported in the library, the returned result will always + //! be an estimate (<tt>isEstimate == true</tt>) for encrypted rooms; + //! moreover, since the library doesn't know how to tackle push rules yet + //! the number of highlights returned here will always be zero (there's no + //! good substitute for that now). + //! + //! \sa isEventNotable, fullyReadMarker, unreadStats, EventStats + EventStats partiallyReadStats() const; + + //! \brief Get event statistics since the last read receipt + //! + //! This call returns a structure that contains the following three numbers, + //! all counted on the timeline segment between the event pointed to by + //! the m.fully_read marker and the sync edge: + //! - the number of unread events - depending on the read receipt state + //! with respect to the local timeline, this number may be either precise + //! or estimated (see EventStats::isEstimate); + //! - the number of highlights (TODO). + //! + //! As E2EE is not supported in the library, the returned result will always + //! be an estimate (<tt>isEstimate == true</tt>) for encrypted rooms; + //! moreover, since the library doesn't know how to tackle push rules yet + //! the number of highlights returned here will always be zero - use + //! highlightCount() for now. + //! + //! \sa isEventNotable, lastLocalReadReceipt, partiallyReadStats, + //! highlightCount + EventStats unreadStats() const; + + [[deprecated( + "Use partiallyReadStats/unreadStats() and EventStats::empty()")]] 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. - */ + //! \brief Get the number of notable events since the fully read marker + //! + //! \deprecated Since 0.7 there are two ways to count unread events: since + //! the fully read marker (used by libQuotient pre-0.7) and since the last + //! read receipt (as used by most of Matrix ecosystem, including the spec + //! and MSCs). This function currently returns a value derived from + //! partiallyReadStats() for compatibility with libQuotient 0.6; it will be + //! removed due to ambiguity. Use unreadStats() to obtain the spec-compliant + //! count of unread events and the highlight count; partiallyReadStats() to + //! obtain the unread events count since the fully read marker. + //! + //! \return -1 (_not 0_) when all messages are known to have been fully read, + //! i.e. the fully read marker points to _the latest notable_ event + //! loaded in the local timeline (which may be different from + //! the latest event in the local timeline as that might not be + //! notable); + //! 0 when there may be unread messages but the current local + //! timeline doesn't have any notable ones (often but not always + //! because it's entirely empty yet); + //! a positive integer when there is (or estimated to be) a number + //! of unread notable events as described above. + //! + //! \sa partiallyReadStats, unreadStats + [[deprecated("Use partiallyReadStats() or unreadStats() instead")]] // int unreadCount() const; - Q_INVOKABLE int notificationCount() const; + //! \brief Get the number of notifications since the last read receipt + //! + //! This is the same as <tt>unreadStats().notableCount</tt>. + //! + //! \sa unreadStats, lastLocalReadReceipt + qsizetype notificationCount() const; + + //! \deprecated Use setReadReceipt() to drive changes in notification count Q_INVOKABLE void resetNotificationCount(); - Q_INVOKABLE int highlightCount() const; + + //! \brief Get the number of highlights since the last read receipt + //! + //! As of 0.7, this is defined by the homeserver as Quotient doesn't process + //! push rules. + //! + //! \sa unreadStats, lastLocalReadReceipt + qsizetype highlightCount() const; + + //! \deprecated Use setReadReceipt() to drive changes in highlightCount Q_INVOKABLE void resetHighlightCount(); /** Check whether the room has account data of the given type @@ -466,6 +711,9 @@ public: /// Get the list of users this room is a direct chat with QList<User*> directChatUsers() const; + Q_INVOKABLE QUrl makeMediaUrl(const QString& eventId, + const QUrl &mxcUrl) const; + Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId) const; Q_INVOKABLE QUrl urlToDownload(const QString& eventId) const; @@ -514,6 +762,19 @@ public: Q_INVOKABLE const Quotient::StateEventBase* getCurrentState(const QString& evtType, const QString& stateKey = {}) const; + /// Get all state events in the room. + /*! This method returns all known state events that have occured in + * the room, as a mapping from the event type and state key to value. + */ + const QHash<StateEventKey, const StateEventBase*>& currentState() const; + + /// Get all state events in the room of a certain type. + /*! This method returns all known state events that have occured in + * the room of the given type. + */ + Q_INVOKABLE const QVector<const StateEventBase*> + stateEventsOfType(const QString& evtType) 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. @@ -554,8 +815,13 @@ public Q_SLOTS: 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, EventContent::TypedBase* content); +#if QT_VERSION_MAJOR < 6 + Q_DECL_DEPRECATED_X("Use postFile(QString, MessageEventType, EventContent)") // QString postFile(const QString& plainText, const QUrl& localPath, bool asGenericFile = false); +#endif /** Post a pre-created room message event * * Takes ownership of the event, deleting it once the matching one @@ -594,7 +860,12 @@ public Q_SLOTS: void downloadFile(const QString& eventId, const QUrl& localFilename = {}); void cancelFileTransfer(const QString& id); - /// Mark all messages in the room as read + //! \brief Set a given event as last read and post a read receipt on it + //! + //! Does nothing if the event is behind the current read receipt. + //! \sa lastReadReceipt, markMessagesAsRead, markAllMessagesAsRead + void setReadReceipt(const QString& atEventId); + //! Put the fully-read marker at the latest message in the room void markAllMessagesAsRead(); /// Switch the room's version (aka upgrade) @@ -608,6 +879,12 @@ public Q_SLOTS: void answerCall(const QString& callId, const QString& sdp); void hangupCall(const QString& callId); + /** + * Activates encryption for this room. + * Warning: Cannot be undone + */ + void activateEncryption(); + Q_SIGNALS: /// Initial set of state events has been loaded /** @@ -619,11 +896,11 @@ Q_SIGNALS: */ void baseStateLoaded(); void eventsHistoryJobChanged(); - void aboutToAddHistoricalMessages(RoomEventsRange events); - void aboutToAddNewMessages(RoomEventsRange events); + void aboutToAddHistoricalMessages(Quotient::RoomEventsRange events); + void aboutToAddNewMessages(Quotient::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); + void pendingEventAboutToAdd(Quotient::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 @@ -692,17 +969,26 @@ Q_SIGNALS: Quotient::JoinState newState); void typingChanged(); - void highlightCountChanged(); - void notificationCountChanged(); + void highlightCountChanged(); //< \sa highlightCount + void notificationCountChanged(); //< \sa notificationCount void displayedChanged(bool displayed); void firstDisplayedEventChanged(); void lastDisplayedEventChanged(); + //! The event that m.read receipt points to has changed + //! \sa lastReadReceipt void lastReadEventChanged(Quotient::User* user); + void fullyReadMarkerMoved(QString fromEventId, QString toEventId); + //! \deprecated since 0.7 - use fullyReadMarkerMoved void readMarkerMoved(QString fromEventId, QString toEventId); + //! \deprecated since 0.7 - use lastReadEventChanged void readMarkerForUserMoved(Quotient::User* user, QString fromEventId, QString toEventId); + //! \deprecated since 0.7 - use either partiallyReadStatsChanged + //! or unreadStatsChanged void unreadMessagesChanged(Quotient::Room* room); + void partiallyReadStatsChanged(); + void unreadStatsChanged(); void accountDataAboutToChange(QString type); void accountDataChanged(QString type); @@ -717,7 +1003,8 @@ Q_SIGNALS: 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); + // fileTransferCancelled() is no more here; use fileTransferFailed() and + // check the transfer status instead void callEvent(Quotient::Room* room, const Quotient::RoomEvent* event); @@ -743,6 +1030,7 @@ protected: {} virtual QJsonObject toJson() const; virtual void updateData(SyncRoomData&& data, bool fromCache = false); + virtual Notification checkForNotifications(const TimelineItem& ti); private: friend class Connection; @@ -761,7 +1049,7 @@ public: explicit MemberSorter(const Room* r) : room(r) {} bool operator()(User* u1, User* u2) const; - bool operator()(User* u1, const QString& u2name) const; + bool operator()(User* u1, QStringView u2name) const; template <typename ContT, typename ValT> typename ContT::size_type lowerBoundIndex(const ContT& c, const ValT& v) const @@ -774,4 +1062,5 @@ private: }; } // namespace Quotient Q_DECLARE_METATYPE(Quotient::FileTransferInfo) +Q_DECLARE_METATYPE(Quotient::ReadReceipt) Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::Room::Changes) diff --git a/lib/settings.cpp b/lib/settings.cpp index 703f4320..ed9082b0 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -21,7 +21,9 @@ void Settings::setLegacyNames(const QString& organizationName, Settings::Settings(QObject* parent) : QSettings(parent) { +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) setIniCodec("UTF-8"); +#endif } void Settings::setValue(const QString& key, const QVariant& value) @@ -124,19 +126,6 @@ void AccountSettings::setHomeserver(const QUrl& url) QString AccountSettings::userId() const { return group().section('/', -1); } -QString AccountSettings::accessToken() const -{ - return value(AccessTokenKey).toString(); -} - -void AccountSettings::setAccessToken(const QString& accessToken) -{ - qCWarning(MAIN) << "Saving access_token to QSettings is insecure." - " Developers, do it manually or contribute to share " - "QtKeychain logic to libQuotient."; - setValue(AccessTokenKey, accessToken); -} - void AccountSettings::clearAccessToken() { legacySettings.remove(AccessTokenKey); diff --git a/lib/settings.h b/lib/settings.h index 84c54802..efd0d714 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -78,9 +78,8 @@ protected: class SettingsGroup : public Settings { public: - template <typename... ArgTs> - explicit SettingsGroup(QString path, ArgTs&&... qsettingsArgs) - : Settings(std::forward<ArgTs>(qsettingsArgs)...) + explicit SettingsGroup(QString path, QObject* parent = nullptr) + : Settings(parent) , groupPath(std::move(path)) {} @@ -131,15 +130,11 @@ class AccountSettings : public SettingsGroup { 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)...) + explicit AccountSettings(const QString& accountId, QObject* parent = nullptr) + : SettingsGroup("Accounts/" + accountId, parent) {} QString userId() const; @@ -147,11 +142,7 @@ public: 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_DECL_DEPRECATED_X("Access tokens are not stored in QSettings any more") Q_INVOKABLE void clearAccessToken(); QByteArray encryptionAccountPickle(); diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index adcba5cd..396e77eb 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -10,9 +10,6 @@ using namespace Quotient; -const QString SyncRoomData::UnreadCountKey = - QStringLiteral("x-quotient.unread_count"); - bool RoomSummary::isEmpty() const { return !joinedMemberCount && !invitedMemberCount && !heroes; @@ -64,23 +61,23 @@ inline EventsArrayT load(const QJsonObject& batches, StrT keyName) return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls)); } -SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, - const QJsonObject& room_) - : roomId(roomId_) - , joinState(joinState_) - , summary(fromJson<RoomSummary>(room_["summary"_ls])) - , state(load<StateEvents>(room_, joinState == JoinState::Invite +SyncRoomData::SyncRoomData(QString roomId_, JoinState joinState, + const QJsonObject& roomJson) + : roomId(std::move(roomId_)) + , joinState(joinState) + , summary(fromJson<RoomSummary>(roomJson["summary"_ls])) + , state(load<StateEvents>(roomJson, joinState == JoinState::Invite ? "invite_state"_ls : "state"_ls)) { switch (joinState) { case JoinState::Join: - ephemeral = load<Events>(room_, "ephemeral"_ls); + ephemeral = load<Events>(roomJson, "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(); + accountData = load<Events>(roomJson, "account_data"_ls); + timeline = load<RoomEvents>(roomJson, "timeline"_ls); + const auto timelineJson = roomJson.value("timeline"_ls).toObject(); timelineLimited = timelineJson.value("limited"_ls).toBool(); timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString(); @@ -89,21 +86,24 @@ SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, default: /* nothing on top of state */; } - const auto unreadJson = room_.value("unread_notifications"_ls).toObject(); - unreadCount = unreadJson.value(UnreadCountKey).toInt(-2); - highlightCount = unreadJson.value("highlight_count"_ls).toInt(); - notificationCount = unreadJson.value("notification_count"_ls).toInt(); - if (highlightCount > 0 || notificationCount > 0) - qCDebug(SYNCJOB) << "Room" << roomId_ - << "has highlights:" << highlightCount - << "and notifications:" << notificationCount; + const auto unreadJson = roomJson.value(UnreadNotificationsKey).toObject(); + + fromJson(unreadJson.value(PartiallyReadCountKey), partiallyReadCount); + if (!partiallyReadCount.has_value()) + fromJson(unreadJson.value("x-quotient.unread_count"_ls), + partiallyReadCount); + + fromJson(roomJson.value(NewUnreadCountKey), unreadCount); + if (!unreadCount.has_value()) + fromJson(unreadJson.value("notification_count"_ls), unreadCount); + fromJson(unreadJson.value(HighlightCountKey), highlightCount); } SyncData::SyncData(const QString& cacheFileName) { QFileInfo cacheFileInfo { cacheFileName }; auto json = loadJson(cacheFileName); - auto requiredVersion = std::get<0>(cacheVersion()); + auto requiredVersion = MajorCacheVersion; auto actualVersion = json.value("cache_version"_ls).toObject().value("major"_ls).toInt(); if (actualVersion == requiredVersion) @@ -128,6 +128,11 @@ Events&& SyncData::takeAccountData() { return std::move(accountData); } Events&& SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); } +std::pair<int, int> SyncData::cacheVersion() +{ + return { MajorCacheVersion, 2 }; +} + QJsonObject SyncData::loadJson(const QString& fileName) { QFile roomFile { fileName }; @@ -171,13 +176,14 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) 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) { + // This assumes that MemberState values go over powers of 2: 1,2,4,... + const auto joinState = JoinState(1U << i); 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())); + roomData.reserve(roomData.size() + static_cast<size_t>(rs.size())); for (auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) { auto roomJson = roomIt->isObject() @@ -187,7 +193,7 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) unresolvedRoomIds.push_back(roomIt.key()); continue; } - roomData.emplace_back(roomIt.key(), JoinState(ii), roomJson); + roomData.emplace_back(roomIt.key(), joinState, roomJson); const auto& r = roomData.back(); totalEvents += r.state.size() + r.ephemeral.size() + r.accountData.size() + r.timeline.size(); diff --git a/lib/syncdata.h b/lib/syncdata.h index e69bac17..36d2e0bf 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -3,11 +3,17 @@ #pragma once -#include "joinstate.h" +#include "quotient_common.h" #include "events/stateevent.h" namespace Quotient { + +constexpr auto UnreadNotificationsKey = "unread_notifications"_ls; +constexpr auto PartiallyReadCountKey = "x-quotient.since_fully_read_count"_ls; +constexpr auto NewUnreadCountKey = "org.matrix.msc2654.unread_count"_ls; +constexpr auto HighlightCountKey = "highlight_count"_ls; + /// Room summary, as defined in MSC688 /** * Every member of this structure is an Omittable; as per the MSC, only @@ -29,7 +35,6 @@ struct RoomSummary { }; QDebug operator<<(QDebug dbg, const RoomSummary& rs); - template <> struct JsonObjectConverter<RoomSummary> { static void dumpTo(QJsonObject& jo, const RoomSummary& rs); @@ -48,16 +53,14 @@ public: bool timelineLimited; QString timelinePrevBatch; - int unreadCount; - int highlightCount; - int notificationCount; + Omittable<int> partiallyReadCount; + Omittable<int> unreadCount; + Omittable<int> highlightCount; - SyncRoomData(const QString& roomId, JoinState joinState_, - const QJsonObject& room_); + SyncRoomData(QString roomId, JoinState joinState, + const QJsonObject& roomJson); SyncRoomData(SyncRoomData&&) = default; SyncRoomData& operator=(SyncRoomData&&) = default; - - static const QString UnreadCountKey; }; // QVector cannot work with non-copyable objects, std::vector can. @@ -87,7 +90,8 @@ public: QStringList unresolvedRooms() const { return unresolvedRoomIds; } - static std::pair<int, int> cacheVersion() { return { 11, 0 }; } + static constexpr int MajorCacheVersion = 11; + static std::pair<int, int> cacheVersion(); static QString fileNameForRoom(QString roomId); private: diff --git a/lib/uri.cpp b/lib/uri.cpp index 291bfcae..c8843dda 100644 --- a/lib/uri.cpp +++ b/lib/uri.cpp @@ -70,7 +70,7 @@ static QString pathSegment(const QUrl& url, int which) encodedPath(url).section('/', which, which).toUtf8()); } -static auto decodeFragmentPart(const QStringRef& part) +static auto decodeFragmentPart(QStringView part) { return QUrl::fromPercentEncoding(part.toLatin1()).toUtf8(); } @@ -98,7 +98,7 @@ Uri::Uri(QUrl url) : QUrl(std::move(url)) 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('/'); + const auto& splitPath = urlPath.split('/'); switch (splitPath.size()) { case 2: break; @@ -128,9 +128,9 @@ Uri::Uri(QUrl url) : QUrl(std::move(url)) // 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")) }; + *this = Uri { decodeFragmentPart(m.capturedView(u"main")), + decodeFragmentPart(m.capturedView(u"sec")), + decodeFragmentPart(m.capturedView(u"query")) }; } } @@ -186,14 +186,18 @@ QString Uri::primaryId() const if (primaryType_ == Empty || primaryType_ == Invalid) return {}; - const auto& idStem = pathSegment(*this, 1); - return idStem.isEmpty() ? idStem : primaryType_ + idStem; + auto idStem = pathSegment(*this, 1); + if (!idStem.isEmpty()) + idStem.push_front(char(primaryType_)); + return idStem; } QString Uri::secondaryId() const { - const auto& idStem = pathSegment(*this, 3); - return idStem.isEmpty() ? idStem : secondaryType() + idStem; + auto idStem = pathSegment(*this, 3); + if (!idStem.isEmpty()) + idStem.push_front(char(secondaryType())); + return idStem; } static const auto ActionKey = QStringLiteral("action"); diff --git a/lib/uriresolver.cpp b/lib/uriresolver.cpp index 287e0552..681e3842 100644 --- a/lib/uriresolver.cpp +++ b/lib/uriresolver.cpp @@ -8,6 +8,8 @@ using namespace Quotient; +UriResolverBase::~UriResolverBase() = default; + UriResolveResult UriResolverBase::visitResource(Connection* account, const Uri& uri) { diff --git a/lib/uriresolver.h b/lib/uriresolver.h index f290e58b..ff97324a 100644 --- a/lib/uriresolver.h +++ b/lib/uriresolver.h @@ -42,23 +42,29 @@ public: UriResolveResult visitResource(Connection* account, const Uri& uri); protected: + virtual ~UriResolverBase() = 0; + /// 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) + virtual UriResolveResult visitUser(User* user [[maybe_unused]], + const QString& action [[maybe_unused]]) { 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) {} + virtual void visitRoom(Room* room [[maybe_unused]], + const QString& eventId [[maybe_unused]]) + {} /// 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 = {}) + virtual void joinRoom(Connection* account [[maybe_unused]], + const QString& roomAliasOrId [[maybe_unused]], + const QStringList& viaServers [[maybe_unused]] = {}) {} /// Called by visitResource() when the passed URI has `type() == NonMatrix` /*! @@ -67,7 +73,10 @@ protected: * `return QDesktopServices::openUrl(url);` but it's strongly advised to * ask for a user confirmation beforehand. */ - virtual bool visitNonMatrix(const QUrl& url) { return false; } + virtual bool visitNonMatrix(const QUrl& url [[maybe_unused]]) + { + return false; + } }; /*! \brief Resolve the resource and invoke an action on it, via function objects diff --git a/lib/user.cpp b/lib/user.cpp index 7933c5d9..7da71dba 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -65,7 +65,8 @@ User::~User() = default; void User::load() { - auto *profileJob = connection()->callApi<GetUserProfileJob>(id()); + auto* profileJob = + connection()->callApi<GetUserProfileJob>(id()); connect(profileJob, &BaseJob::result, this, [this, profileJob] { d->defaultName = profileJob->displayname(); d->defaultAvatar = Avatar(QUrl(profileJob->avatarUrl())); @@ -81,7 +82,7 @@ bool User::isGuest() const 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()); + Q_ASSERT(it != d->id.cend()); return *it == ':'; } @@ -92,8 +93,6 @@ QString User::name(const Room* room) const return room ? room->memberName(id()) : d->defaultName; } -QString User::rawName(const Room* room) const { return name(room); } - void User::rename(const QString& newName) { const auto actualNewName = sanitized(newName); @@ -121,11 +120,11 @@ void User::rename(const QString& newName, const Room* r) rename(newName); return; } - Q_ASSERT_X(r->memberJoinState(this) == JoinState::Join, __FUNCTION__, + // #481: take the current state and update it with the new name + auto evtC = r->getCurrentState<RoomMemberEvent>(id())->content(); + Q_ASSERT_X(evtC.membership == Membership::Join, __FUNCTION__, "Attempt to rename a user that's not a room member"); - const auto actualNewName = sanitized(newName); - MemberEventContent evtC; - evtC.displayName = actualNewName; + evtC.displayName = sanitized(newName); r->setState<RoomMemberEvent>(id(), move(evtC)); // The state will be updated locally after it arrives with sync } @@ -134,17 +133,17 @@ template <typename SourceT> inline bool User::doSetAvatar(SourceT&& source) { return d->defaultAvatar.upload( - connection(), source, [this](const QString& contentUri) { + connection(), source, [this](const QUrl& contentUri) { auto* j = connection()->callApi<SetAvatarUrlJob>(id(), contentUri); connect(j, &BaseJob::success, this, - [this, newUrl = QUrl(contentUri)] { - if (newUrl == d->defaultAvatar.url()) { - d->defaultAvatar.updateUrl(newUrl); + [this, contentUri] { + if (contentUri == d->defaultAvatar.url()) { + d->defaultAvatar.updateUrl(contentUri); emit defaultAvatarChanged(); } else qCWarning(MAIN) << "User" << id() << "already has avatar URL set to" - << newUrl.toDisplayString(); + << contentUri.toDisplayString(); }); }); } @@ -161,7 +160,7 @@ bool User::setAvatar(QIODevice* source) void User::removeAvatar() { - connection()->callApi<SetAvatarUrlJob>(id(), ""); + connection()->callApi<SetAvatarUrlJob>(id(), QUrl()); } void User::requestDirectChat() { connection()->requestDirectChat(this); } @@ -184,8 +183,6 @@ QString User::fullName(const Room* room) const return displayName.isEmpty() ? id() : (displayName % " (" % id() % ')'); } -QString User::bridged() const { return {}; } - const Avatar& User::avatarObject(const Room* room) const { if (!room) @@ -196,18 +193,18 @@ const Avatar& User::avatarObject(const Room* room) const return d->otherAvatars.try_emplace(mediaId, url).first->second; } -QImage User::avatar(int dimension, const Room* room) +QImage User::avatar(int dimension, const Room* room) const { return avatar(dimension, dimension, room); } -QImage User::avatar(int width, int height, const Room* room) +QImage User::avatar(int width, int height, const Room* room) const { return avatar(width, height, room, [] {}); } QImage User::avatar(int width, int height, const Room* room, - const Avatar::get_callback_t& callback) + const Avatar::get_callback_t& callback) const { return avatarObject(room).get(connection(), width, height, callback); } @@ -22,7 +22,6 @@ class User : public QObject { Q_PROPERTY(QString name READ name NOTIFY defaultNameChanged) Q_PROPERTY(QString displayName READ displayname NOTIFY defaultNameChanged STORED false) Q_PROPERTY(QString fullName READ fullName NOTIFY defaultNameChanged STORED false) - Q_PROPERTY(QString bridgeName READ bridged NOTIFY defaultNameChanged STORED false) Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY defaultAvatarChanged STORED false) Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY defaultAvatarChanged) public: @@ -40,18 +39,10 @@ public: * 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 + * \sa displayName */ 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. @@ -70,13 +61,6 @@ public: */ 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 @@ -99,11 +83,11 @@ public: */ const Avatar& avatarObject(const Room* room = nullptr) const; Q_INVOKABLE QImage avatar(int dimension, - const Quotient::Room* room = nullptr); + const Quotient::Room* room = nullptr) const; Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight, - const Quotient::Room* room = nullptr); + const Quotient::Room* room = nullptr) const; QImage avatar(int width, int height, const Room* room, - const Avatar::get_callback_t& callback); + const Avatar::get_callback_t& callback) const; QString avatarMediaId(const Room* room = nullptr) const; QUrl avatarUrl(const Room* room = nullptr) const; diff --git a/lib/util.cpp b/lib/util.cpp index 904bfd5a..03ebf325 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -14,9 +14,6 @@ static const auto RegExpOptions = QRegularExpression::CaseInsensitiveOption -#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 @@ -33,7 +30,7 @@ void Quotient::linkifyUrls(QString& htmlEscapedText) // comma or dot static const QRegularExpression FullUrlRegExp( QStringLiteral( - R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet|matrix):(//)?)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"), + R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp):(//)?\w|(magnet|matrix):)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"), RegExpOptions); // email address: // [word chars, dots or dashes]@[word chars, dots or dashes].[word chars] @@ -44,7 +41,7 @@ void Quotient::linkifyUrls(QString& htmlEscapedText) // 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})?))"), + R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"), RegExpOptions); Q_ASSERT(FullUrlRegExp.isValid() && EmailAddressRegExp.isValid() && MxIdRegExp.isValid()); @@ -119,34 +116,22 @@ QString Quotient::serverPart(const QString& mxId) return parser.match(mxId).captured(1); } -// Tests for function_traits<> - -using namespace Quotient; - -int f_(); -static_assert(std::is_same<fn_return_t<decltype(f_)>, int>::value, - "Test fn_return_t<>"); - -void f1_(int, QString); -static_assert(std::is_same<fn_arg_t<decltype(f1_), 1>, QString>::value, - "Test fn_arg_t<>"); - -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(std::is_same<fn_arg_t<Fo1>, int>(), - "Test fn_arg_t defaulting to first argument"); - -template <typename T> -static QString ft(T&&); -static_assert(std::is_same<fn_arg_t<decltype(ft<QString>)>, QString&&>(), - "Test function templates"); +QString Quotient::versionString() +{ + return QStringLiteral(Quotient_VERSION_STRING); +} + +int Quotient::majorVersion() +{ + return Quotient_VERSION_MAJOR; +} + +int Quotient::minorVersion() +{ + return Quotient_VERSION_MINOR; +} + +int Quotient::patchVersion() +{ + return Quotient_VERSION_PATCH; +} @@ -12,10 +12,30 @@ #include <unordered_map> #include <optional> -// 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; +#ifndef Q_DISABLE_MOVE +// Q_DISABLE_MOVE was introduced in Q_VERSION_CHECK(5,13,0) +# define Q_DISABLE_MOVE(_ClassName) \ + _ClassName(_ClassName&&) Q_DECL_EQ_DELETE; \ + _ClassName& operator=(_ClassName&&) Q_DECL_EQ_DELETE; +#endif + +#ifndef Q_DISABLE_COPY_MOVE +#define Q_DISABLE_COPY_MOVE(Class) \ + Q_DISABLE_COPY(Class) \ + Q_DISABLE_MOVE(Class) +#endif + +#define DISABLE_MOVE(_ClassName) \ +static_assert(false, "Use Q_DISABLE_MOVE instead; Quotient enables it across all used versions of Qt"); + +#ifndef QT_IGNORE_DEPRECATIONS +// QT_IGNORE_DEPRECATIONS was introduced in Q_VERSION_CHECK(5,15,0) +# define QT_IGNORE_DEPRECATIONS(statement) \ + QT_WARNING_PUSH \ + QT_WARNING_DISABLE_DEPRECATED \ + statement \ + QT_WARNING_POP +#endif namespace Quotient { /// An equivalent of std::hash for QTypes to enable std::unordered_map<QType, ...> @@ -30,6 +50,13 @@ struct HashQ { template <typename KeyT, typename ValT> using UnorderedMap = std::unordered_map<KeyT, ValT, HashQ<KeyT>>; +namespace _impl { + template <typename TT> + constexpr inline auto IsOmittableValue = false; + template <typename TT> + constexpr inline auto IsOmittable = IsOmittableValue<std::decay_t<TT>>; +} + constexpr auto none = std::nullopt; /** `std::optional` with tweaks @@ -107,18 +134,18 @@ public: 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 + //! 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> + -> std::enable_if_t<std::is_convertible_v<T1, T>, bool> { if (!other || (this->has_value() && **this == *other)) return false; - *this = other; + emplace(*other); return true; } @@ -131,72 +158,38 @@ public: const value_type& operator*() const& { return base_type::operator*(); } value_type& operator*() && { return base_type::operator*(); } }; +template <typename T> +Omittable(T&&) -> Omittable<T>; namespace _impl { - template <typename AlwaysVoid, typename> - struct fn_traits {}; + template <typename T> + constexpr inline auto IsOmittableValue<Omittable<T>> = true; } -/// 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...)>; -}; +template <typename T1, typename T2> +inline auto merge(Omittable<T1>& lhs, T2&& rhs) +{ + return lhs.merge(std::forward<T2>(rhs)); +} -namespace _impl { - // Specialisation for function objects with (non-overloaded) operator() - // (this includes non-generic lambdas) - template <typename T> - 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) +//! \brief Merge the value from an Omittable +//! This is an adaptation of Omittable::merge() to the case when the value +//! on the left hand side is not an Omittable. +//! \return true if \p rhs is not omitted and the \p lhs value was different, +//! in other words, if \p lhs has changed; +//! false otherwise +template <typename T1, typename T2> +inline auto merge(T1& lhs, const Omittable<T2>& rhs) + -> std::enable_if_t<!_impl::IsOmittable<T1> + && std::is_convertible_v<T2, T1>, bool> { - return typename function_traits<FnT>::function_type(std::forward<FnT>(f)); + if (!rhs || lhs == *rhs) + return false; + lhs = *rhs; + return true; } -inline auto operator"" _ls(const char* s, std::size_t size) +inline constexpr auto operator"" _ls(const char* s, std::size_t size) { return QLatin1String(s, int(size)); } @@ -283,4 +276,9 @@ qreal stringToHueF(const QString& s); /** Extract the serverpart from MXID */ QString serverPart(const QString& mxId); + +QString versionString(); +int majorVersion(); +int minorVersion(); +int patchVersion(); } // namespace Quotient |